feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build:
- New <Button> compound (morfo + soma + sema + eidos): 6 variants
(solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional
icon/endIcon snippets, loading state with built-in Spinner, asChild
Slot pattern, intent-driven sema (commit-action with fromProp:intent).
Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE):
- context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group:
full per-part Morfo contracts, reference-parity tables, ARIA contract
tables, keyboard tables, composition controls.
- color-field, time-field, time-picker, time-range-picker, color-picker,
month-grid, year-grid: same canonical treatment; per-part data/aria/
keyboard iteration of raw morfo, reference parity vs react-aria/
ark-ui/Chakra/bits-ui/Mantine.
Picker recipe polish:
- PickerShell.Clear/Cancel/Close refactored to use <Button size="xs">
(affects 5 pickers — date / date-range / time / time-range / color).
- TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14
/ 32 -> 28), per-channel accent (hour=primary, minute=secondary,
second=neutral; end variant muted), ruler off by default, hour ruler
cleaned to 0/6/12/18/23.
- ColorPicker: typography token --_color-picker-trigger-font-size that
scales with data-size (10/12/14px); autoFormat default true; Area
recipe override for [data-x-channel='hue'] with rainbow gradient +
saturation/brightness Y overlays.
- ValueText eidos wrapper: fix children snippet bug that suppressed
soma's displayValue fallback.
- date-field.css: segment editing affordance (cursor + hover + active
ring) via new --_date-field-accent-element token across 8 color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
import { Soma } from '../../core/soma.svelte';
|
refactor(soma): route FieldProvider through field/internals barrel + register DRP view sub-parts (audit Round 3 §3 #23 + P4 follow-up)
Two related housekeeping items in one commit:
**1. FieldProvider via barrel (audit Round 3 §3 #23)**
37 consumer files across soma imported `FieldProvider` from the deep
path `'../field/field-provider.svelte'`, mirroring the same pattern
that motivated `internals.ts` indirection on the 5 pickers earlier.
Adds `src/uix/soma/components/field/internals.ts` re-exporting
`FieldProvider` and exposes it from the barrel `exports.ts`. The 37
imports become the canonical `from '../field'`. Consistency only —
no behavioral change.
**2. DateRangePicker view sub-parts (P4 follow-up)**
The MonthView/YearView soma parts created in commit `e93c0c73` stamp
internal layout data-attrs (`-header`, `-prev`, `-heading`, `-next`,
`-grid`, `-cell`) that weren't declared in the morfo, surfacing as 12
hardcoded-data-attr violations once the contract suite caught up.
Declares the 12 sub-parts in the morfo with the canonical kebab
prefix (`month-view-*`, `year-view-*`). Each entry minimal (data: [],
aria: []) except `*-prev` / `*-next` which stamp `type='button'` and
`*-cell` which surfaces the `data-range-{start,end,in-range}`
classification states used by the eidos range tinting CSS.
Test result: 2393/2399 passing — back to the canonical 6 fails
(Words + cookie infra). The 13 transient regressions are closed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
import { FieldProvider } from '../field';
|
feat(eidos): Button + 9 demo rebuilds + picker recipe polish
Build:
- New <Button> compound (morfo + soma + sema + eidos): 6 variants
(solid/soft/surface/outline/ghost/plain), 8 colors, 5 sizes, optional
icon/endIcon snippets, loading state with built-in Spinner, asChild
Slot pattern, intent-driven sema (commit-action with fromProp:intent).
Demo rebuilds (canonical depth per DEMO_AUTHORING_GUIDE):
- context-menu, dropdown-menu, pin-input, alert-dialog, toggle-group:
full per-part Morfo contracts, reference-parity tables, ARIA contract
tables, keyboard tables, composition controls.
- color-field, time-field, time-picker, time-range-picker, color-picker,
month-grid, year-grid: same canonical treatment; per-part data/aria/
keyboard iteration of raw morfo, reference parity vs react-aria/
ark-ui/Chakra/bits-ui/Mantine.
Picker recipe polish:
- PickerShell.Clear/Cancel/Close refactored to use <Button size="xs">
(affects 5 pickers — date / date-range / time / time-range / color).
- TimePicker + TimeRangePicker: popover condensed (min-width 18rem -> 14
/ 32 -> 28), per-channel accent (hour=primary, minute=secondary,
second=neutral; end variant muted), ruler off by default, hour ruler
cleaned to 0/6/12/18/23.
- ColorPicker: typography token --_color-picker-trigger-font-size that
scales with data-size (10/12/14px); autoFormat default true; Area
recipe override for [data-x-channel='hue'] with rainbow gradient +
saturation/brightness Y overlays.
- ValueText eidos wrapper: fix children snippet bug that suppressed
soma's displayValue fallback.
- date-field.css: segment editing affordance (cursor + hover + active
ring) via new --_date-field-accent-element token across 8 color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
import { buttonMorfo } from '../../../morfo/components/button';
|
|
|
|
|
import type { OptsFromProps } from '../../provider';
|
|
|
|
|
import type { OnChangeFn, SomaMouseEvent } from '../../types';
|
|
|
|
|
import type { ButtonProps, ButtonIntent } from './types';
|
|
|
|
|
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
|
|
|
|
|
|
|
|
|
|
// ── Provider ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Provider's reactive contract derived from `ButtonProps`.
|
|
|
|
|
*/
|
|
|
|
|
type ButtonOpts = OptsFromProps<
|
|
|
|
|
ButtonProps,
|
|
|
|
|
| 'intent'
|
|
|
|
|
| 'color'
|
|
|
|
|
| 'type'
|
|
|
|
|
| 'disabled'
|
|
|
|
|
| 'loading'
|
|
|
|
|
| 'loadingText'
|
|
|
|
|
| 'spinnerPlacement'
|
|
|
|
|
| 'aria-label'
|
|
|
|
|
| 'onPress',
|
|
|
|
|
never
|
|
|
|
|
>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Button provider — runtime-direct.
|
|
|
|
|
*
|
|
|
|
|
* Owns the press → `commit-action` flow. Sequence is `pre` so the
|
|
|
|
|
* perceptual signal lands at the moment of the click; the consumer's
|
|
|
|
|
* onclick may then run async work without blocking it.
|
|
|
|
|
*/
|
|
|
|
|
export class ButtonProvider {
|
|
|
|
|
static create(opts: ButtonOpts) {
|
|
|
|
|
return new ButtonProvider(opts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
readonly opts: ButtonOpts;
|
|
|
|
|
readonly field = FieldProvider.get();
|
|
|
|
|
readonly runtime: SomaRuntime;
|
|
|
|
|
readonly runtimePart: SomaRuntimePart;
|
|
|
|
|
readonly soma: Soma;
|
|
|
|
|
|
|
|
|
|
private constructor(opts: ButtonOpts) {
|
|
|
|
|
this.opts = opts;
|
|
|
|
|
|
|
|
|
|
this.soma = Soma.require();
|
|
|
|
|
this.runtime = this.soma.runtime(buttonMorfo, {
|
|
|
|
|
states: {
|
|
|
|
|
loading: () => opts.loading.current ?? false
|
|
|
|
|
},
|
|
|
|
|
props: {
|
|
|
|
|
disabled: () => this.isDisabled,
|
|
|
|
|
loading: () => opts.loading.current ?? false,
|
|
|
|
|
intent: () => opts.intent.current ?? 'neutral',
|
|
|
|
|
type: () => opts.type.current ?? 'button',
|
|
|
|
|
/**
|
|
|
|
|
* Resolved visual color. Doctrine: aesthetic must not
|
|
|
|
|
* contradict semantic. When intent is evaluative
|
|
|
|
|
* (affirm/fulfill/risk/threat/loss), it WINS over any
|
|
|
|
|
* hierarchy override. When intent is neutral, `color` may
|
|
|
|
|
* promote the visual to primary/secondary; if neither,
|
|
|
|
|
* default 'primary' (Button's default is promoted because
|
|
|
|
|
* actions are typically the focal point of their surface).
|
|
|
|
|
*/
|
|
|
|
|
color: () => {
|
|
|
|
|
const intent = opts.intent.current ?? 'neutral';
|
|
|
|
|
if (intent !== 'neutral') return intent;
|
|
|
|
|
return opts.color.current ?? 'primary';
|
|
|
|
|
},
|
|
|
|
|
'aria-label': () => opts['aria-label'].current
|
|
|
|
|
},
|
|
|
|
|
events: {
|
|
|
|
|
'commit-action': () => {
|
|
|
|
|
// The consumer's onclick has already been (or will be)
|
|
|
|
|
// invoked by native button click dispatch. We only fire
|
|
|
|
|
// onPress for diagnostic / analytics consumers.
|
|
|
|
|
opts.onPress.current?.({ intent: opts.intent.current ?? 'neutral' });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
this.runtimePart = this.runtime.part('provider', {
|
|
|
|
|
id: opts.id,
|
|
|
|
|
ref: opts.ref,
|
|
|
|
|
owner: this,
|
|
|
|
|
syncAttrs: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
readonly isDisabled = $derived.by(
|
|
|
|
|
() => (this.opts.disabled.current ?? false) || (this.field?.isDisabled ?? false)
|
|
|
|
|
);
|
|
|
|
|
readonly isLoading = $derived.by(() => this.opts.loading.current ?? false);
|
|
|
|
|
|
|
|
|
|
readonly snippetProps = $derived.by(() => ({
|
|
|
|
|
loading: this.isLoading,
|
|
|
|
|
disabled: this.isDisabled
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
readonly onclick = (_e: SomaMouseEvent<HTMLButtonElement>) => {
|
|
|
|
|
if (this.isDisabled || this.isLoading) return;
|
|
|
|
|
// Sequence 'pre' — the perceptual signal fires synchronously with
|
|
|
|
|
// the click; consumer's onclick runs in parallel.
|
|
|
|
|
void this.runtime.trigger('commit-action');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render-time props. Composed of:
|
|
|
|
|
* - identity from the runtime (`id`, `data-button`, ref attachment)
|
|
|
|
|
* - the HTML `disabled` attribute
|
|
|
|
|
* - the `onclick` handler
|
|
|
|
|
*/
|
|
|
|
|
readonly props = $derived.by(() => ({
|
|
|
|
|
...this.runtimePart.props,
|
|
|
|
|
disabled: this.isDisabled || undefined,
|
|
|
|
|
onclick: this.onclick
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type { OnChangeFn, ButtonIntent };
|