Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/quick-forks-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@fuzdev/fuz_ui': minor
---

deps: upgrade fuz_css with oklch

- `Dialog`'s backdrop dim reads fuz_css's shared `--backdrop_color`
variable (was `--dialog_bg`), so one theme variable retints every
backdrop, native and component alike.
- `ThemeRoot` warns in dev when a single-scheme theme arrives without its
`scheme_mirror` - resolve with `resolve_theme_stance` before rendering.
- Contrast becomes a modifier, matching fuz_css's registry rework:
`ThemeState` gains `contrast` (the selected modifier, persisted under
`fuz:contrast`) and `composed_theme` (the base theme with the modifier
composed over it via `compose_themes`), which `ThemeRoot` now renders.
`ThemeInput` grows a contrast select fed by fuz_css's
`contrast_modifiers` (override or hide with its `contrast_modifiers`
prop) - without it, pickers would lose low/high contrast entirely now
that the pair left `default_themes`. Both light up when the fuz_css dep
ships the new APIs; against older releases they no-op.
- Utility classes follow fuz_css's property-first palette naming:
`palette_X_NN` → `color_X_NN` (the bare `.palette_a`-`.palette_j`
component conventions are unchanged).
16 changes: 16 additions & 0 deletions .changeset/radio-menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@fuzdev/fuz_ui': minor
---

feat: `RadioMenu`, and the theme inputs use fuz_css's `palette_a` convention

`RadioMenu` is a horizontal menu of joined `menuitemradio` buttons with one
selected, generic over the option type: `options`, a bindable `value`, a
`label` function (also the default `title` and list key), optional `title`
and `key` overrides, `onselect`, and a `children` snippet for custom button
content. `ColorSchemeInput` is now a thin `RadioMenu` over `color_schemes`;
its markup and styles are unchanged.

`ColorSchemeInput` and `ThemeInput` buttons take the `palette_a` class -
`color_a` was the bare component convention before fuz_css's palette
rename and no longer resolves.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ consumes are listed under [Library and API generation](#library-and-api-generati
### Forms and inputs

- `HueInput`, `ColorSchemeInput`, `ThemeInput` - theme controls
- `RadioMenu` - a joined-button `menuitemradio` row, the shape `ColorSchemeInput` is built on

### Icons and media

Expand Down
58 changes: 10 additions & 48 deletions src/lib/ColorSchemeInput.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<script lang="ts">
import { swallow } from '@fuzdev/fuz_util/dom.ts';
import { color_schemes, type ColorScheme } from '@fuzdev/fuz_css/theme.ts';
import type { SvelteHTMLElements } from 'svelte/elements';

import { theme_state_context } from './theme_state.svelte.ts';
import RadioMenu from './RadioMenu.svelte';

const get_theme_state = theme_state_context.get();

Expand All @@ -15,50 +15,12 @@
} = $props();
</script>

<!-- TODO maybe support menubar aria
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/menubar_role -->
<menu {...rest} class="color-scheme-control unstyled {rest.class}">
{#each color_schemes as color_scheme (color_scheme)}
{@const selected = color_scheme === value.color_scheme}
<button
type="button"
class={['color-scheme color_a', { selected }]}
role="menuitemradio"
title={selected
? `${color_scheme} color scheme is selected`
: `select ${color_scheme} color scheme`}
aria-checked={selected}
onclick={(e) => {
swallow(e);
value.color_scheme = color_scheme;
}}
>
<div class="content">{color_scheme}</div>
</button>
{/each}
</menu>

<style>
.color-scheme-control {
display: flex;
flex-direction: row;
justify-content: center;
}
.content {
display: flex;
align-items: center;
justify-content: center;
padding: 0 var(--space_lg);
}
.color-scheme {
border-radius: 0;
}
.color-scheme:first-child {
border-top-left-radius: var(--border_radius, var(--border_radius_md));
border-bottom-left-radius: var(--border_radius, var(--border_radius_md));
}
.color-scheme:last-child {
border-top-right-radius: var(--border_radius, var(--border_radius_md));
border-bottom-right-radius: var(--border_radius, var(--border_radius_md));
}
</style>
<RadioMenu
{...rest}
class={['color-scheme-control', rest.class]}
options={color_schemes}
bind:value={value.color_scheme}
label={(color_scheme) => color_scheme}
title={(color_scheme, selected) =>
selected ? `${color_scheme} color scheme is selected` : `select ${color_scheme} color scheme`}
/>
2 changes: 1 addition & 1 deletion src/lib/DocsFooter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
border: var(--border_width) double var(--border_color);
}
.logo a:hover {
--border_color: var(--color_a_50);
--border_color: var(--palette_a_50);
}
.root-url {
/* TODO messy */
Expand Down
24 changes: 8 additions & 16 deletions src/lib/HueInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@

<style>
.hue-input {
--outline_color: hsl(var(--hue) 50% 60%);
--outline_color: oklch(0.65 0.13 var(--hue));
/* TODO @many figure these variables out so they're easily customized (similar pattern in a lot of places) */
/* --thumb_background_color: hsl(var(--hue) 50% 50%); */
/* --thumb_background_color: oklch(0.6 0.13 var(--hue)); */
}
.preview {
background-color: hsl(var(--hue) 50% 50%);
background-color: oklch(0.6 0.13 var(--hue));
/* mirror the `min-height: var(--input_height)` that fuz_css gives the sibling inputs,
so the preview tracks their height even when `--input_height` is customized */
min-height: var(--input_height);
Expand All @@ -88,22 +88,14 @@
}
.minimap-wrapper {
padding: 0 var(--input_padding_x);
background: hsl(0 50% 50%) 0%;
}
.minimap {
/* sweep the OKLCH hue circle at constant lightness/chroma so the strip
matches what the hue knobs actually produce */
background: linear-gradient(
90deg,
hsl(0 50% 50%) 0%,
hsl(36 50% 50%) 10%,
hsl(72 50% 50%) 20%,
hsl(108 50% 50%) 30%,
hsl(144 50% 50%) 40%,
hsl(180 50% 50%) 50%,
hsl(216 50% 50%) 60%,
hsl(252 50% 50%) 70%,
hsl(288 50% 50%) 80%,
hsl(324 50% 50%) 90%,
hsl(360 50% 50%) 100%
90deg in oklch longer hue,
oklch(0.6 0.13 0) 0%,
oklch(0.6 0.13 360) 100%
);
height: var(--space_lg);
}
Expand Down
8 changes: 4 additions & 4 deletions src/lib/LibraryDetail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -282,16 +282,16 @@ with file-type coloring. Uses svelte-docinfo's file type predicates for module c
}
/* TODO probably add variables/classes for these */
.ts {
--link_color: var(--color_a_50);
--link_color: var(--palette_a_50);
}
.svelte {
--link_color: var(--color_h_50);
--link_color: var(--palette_h_50);
}
.css {
--link_color: var(--color_b_50);
--link_color: var(--palette_b_50);
}
.json {
--link_color: var(--color_f_50);
--link_color: var(--palette_f_50);
}
/* TODO extract */
.declarations {
Expand Down
96 changes: 96 additions & 0 deletions src/lib/RadioMenu.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<script lang="ts" generics="T">
import { swallow } from '@fuzdev/fuz_util/dom.ts';
import type { Snippet } from 'svelte';
import type { SvelteHTMLElements } from 'svelte/elements';

/**
* A horizontal menu of joined `menuitemradio` buttons with one selected -
* the shape `ColorSchemeInput` takes, and any other few-way pick where the
* options should sit one glance apart rather than behind a `select`.
*
* @module
*/

let {
options,
value = $bindable(),
onselect,
label,
title,
key = (option: T): unknown => label(option),
children,
...rest
}: SvelteHTMLElements['menu'] & {
options: Array<T>;
/** The selected option, compared by identity; bindable. */
value: T;
/** Called after a click selects an option. */
onselect?: (option: T) => void;
/** The display text of an option - also the default `title` and `key` source. */
label: (option: T) => string;
/** The button title, defaulting to "select X" / "X is selected" from `label`. */
title?: (option: T, selected: boolean) => string;
/** Keys the options list; defaults to the label. */
key?: (option: T) => unknown;
/** Custom button content in place of the label text. */
children?: Snippet<[option: T, selected: boolean]>;
} = $props();

const title_of = (option: T, selected: boolean): string =>
title
? title(option, selected)
: selected
? `${label(option)} is selected`
: `select ${label(option)}`;
</script>

<menu {...rest} class={['radio-menu unstyled', rest.class]}>
{#each options as option (key(option))}
{@const selected = option === value}
<button
type="button"
class={['radio-menu-item palette_a', { selected }]}
role="menuitemradio"
title={title_of(option, selected)}
aria-checked={selected}
onclick={(e) => {
swallow(e);
value = option;
onselect?.(option);
}}
>
<div class="content">
{#if children}
{@render children(option, selected)}
{:else}
{label(option)}
{/if}
</div>
</button>
{/each}
</menu>

<style>
.radio-menu {
display: flex;
flex-direction: row;
justify-content: center;
}
.content {
display: flex;
align-items: center;
justify-content: center;
padding: 0 var(--space_lg);
}
.radio-menu-item {
border-radius: 0;
}
.radio-menu-item:first-child {
border-top-left-radius: var(--border_radius, var(--border_radius_md));
border-bottom-left-radius: var(--border_radius, var(--border_radius_md));
}
.radio-menu-item:last-child {
border-top-right-radius: var(--border_radius, var(--border_radius_md));
border-bottom-right-radius: var(--border_radius, var(--border_radius_md));
}
</style>
20 changes: 10 additions & 10 deletions src/lib/Spiders.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@

const {
spiders = [
'var(--color_a_50)',
'var(--color_b_50)',
'var(--color_c_50)',
'var(--color_d_50)',
'var(--color_e_50)',
'var(--color_f_50)',
'var(--color_g_50)',
'var(--color_h_50)',
'var(--color_i_50)',
'var(--color_j_50)'
'var(--palette_a_50)',
'var(--palette_b_50)',
'var(--palette_c_50)',
'var(--palette_d_50)',
'var(--palette_e_50)',
'var(--palette_f_50)',
'var(--palette_g_50)',
'var(--palette_h_50)',
'var(--palette_i_50)',
'var(--palette_j_50)'
],
seed = minute_of_day(),
random = create_random_alea(seed),
Expand Down
2 changes: 1 addition & 1 deletion src/lib/ThemeInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<li class="row" role="none">
<button
type="button"
class={['theme-button color_a', { selected }]}
class={['theme-button palette_a', { selected }]}
role="menuitemradio"
aria-label="{theme.name} theme"
aria-checked={selected}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ export interface AlertStatusOptions {

export const alert_status_options: Record<AlertStatus, AlertStatusOptions> = {
inform: { color: 'var(--text_70)', icon: '✻' },
help: { color: 'var(--color_b_50)', icon: '➺' },
error: { color: 'var(--color_c_50)', icon: '!?' }
help: { color: 'var(--palette_b_50)', icon: '➺' },
error: { color: 'var(--palette_c_50)', icon: '!?' }
};
24 changes: 13 additions & 11 deletions src/lib/logos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ import type { SvgData } from './svg.ts';

/*

Logo colors are the midpoint between the light and dark versions of color variant 5:
Logo colors are frozen brand values — originally derived as the midpoint
between the light and dark mid-stops of fuz_css's pre-OKLCH palette
(today's `--palette_X_50`), and deliberately not re-derived since:

```css
--color_a: #6199d1;
--color_b: #3db33d;
--color_c: #d93636;
--color_d: #8866cc;
--color_e: #d8bc31;
--color_f: #84522a; // TODO this one isn't the midpoint bc fuz_css's color needs tweaking, too light in dark mode
--color_g: #e55d95;
--color_h: #f4672f;
--color_i: #36e2e2;
#6199d1 (blue, a)
#3db33d (green, b)
#d93636 (red, c)
#8866cc (purple, d)
#d8bc31 (yellow, e)
#84522a (brown, f)
#e55d95 (pink, g)
#f4672f (orange, h)
#36e2e2 (cyan, i)
```
*/

Expand Down Expand Up @@ -159,7 +161,7 @@ export const logo_fuz_util = {
} satisfies SvgData;

export const logo_tsv = {
label: 'a pixelated orange quill drawing a straight line',
label: 'a pixelated orange quill drawing a precise line',
fill: '#f4672f',
paths: [
{
Expand Down
2 changes: 1 addition & 1 deletion src/lib/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const project_items: Array<ProjectItem> = [
{name: 'fuz_app', url: 'https://app.fuz.dev/', logo: logo_fuz_app, description: '[pre-alpha] fullstack app library', glyph: '🗝', color_class: 'color_j_50'},
{name: 'fuz_css', url: 'https://css.fuz.dev/', logo: logo_fuz_css, description: 'semantic-first CSS framework and design system', glyph: '🌿', color_class: 'color_b_50'},
{name: 'fuz_ui', url: 'https://ui.fuz.dev/', logo: logo_fuz_ui, description: 'Svelte UI library', glyph: '🧶', color_class: 'color_c_50'},
{name: 'fuz_template', url: 'https://template.fuz.dev/', logo: logo_fuz_template, description: 'a static web app and Node library template with TypeScript, Svelte, SvelteKit, Vite, esbuild, Gro, and Fuz', glyph: '❄', color_class: 'color_h_50'},
{name: 'fuz_template', url: 'https://template.fuz.dev/', logo: logo_fuz_template, description: 'a web app template with TypeScript + SvelteKit + optional Rust for the fuz-stack', glyph: '❄', color_class: 'color_h_50'},
{name: 'fuz_blog', url: 'https://blog.fuz.dev/', logo: logo_fuz_blog, description: 'blog software from scratch with SvelteKit', glyph: '🖊️', color_class: 'color_e_50'},
{name: 'fuz_code', url: 'https://code.fuz.dev/', logo: logo_fuz_code, description: 'syntax styling utilities and components for TypeScript, Svelte, Markdown, and more', glyph: '🎨', color_class: 'color_g_50'},
{name: 'fuz_mastodon', url: 'https://mastodon.fuz.dev/', logo: logo_fuz_mastodon, description: 'Mastodon components and helpers for Svelte, SvelteKit, and Fuz', glyph: '🦣', color_class: 'color_d_50'},
Expand Down
4 changes: 1 addition & 3 deletions src/routes/ThemeForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@

const new_theme: Theme = $derived({ name: new_name, variables: new_variables });

const code = $derived(
render_theme_style(new_theme, { empty_default_theme: false, specificity: 1 })
);
const code = $derived(render_theme_style(new_theme, { empty_default_theme: false }));

const light_count = $derived(new_variables.reduce((c, v) => (v.light ? c + 1 : c), 0));
const dark_count = $derived(new_variables.reduce((c, v) => (v.dark ? c + 1 : c), 0));
Expand Down
Loading
Loading