diff --git a/src/uix/eidos/components/gradient-builder/gradient-builder-presets.svelte b/src/uix/eidos/components/gradient-builder/gradient-builder-presets.svelte index 6d8a9b28e..ec3bda69f 100644 --- a/src/uix/eidos/components/gradient-builder/gradient-builder-presets.svelte +++ b/src/uix/eidos/components/gradient-builder/gradient-builder-presets.svelte @@ -12,11 +12,24 @@ import { getGradientBuilderVisualContext } from './context.svelte'; import type { GradientBuilderPresetsProps } from './types'; - let { provided, savedPresets, canSave, onSave, onDelete }: GradientBuilderPresetsProps = $props(); + let { + provided, + savedPresets, + canSave, + onSave, + onDelete, + value, + size, + onApply + }: GradientBuilderPresetsProps = $props(); - const provider = GradientBuilderProvider.require(); + // Provider-optional: inside a GradientBuilder it drives apply/value; the + // GradientPicker's Presets tab is OUTSIDE that provider and passes `onApply` + // + `value` + `size` instead. + const provider = GradientBuilderProvider.get(); const ctx = getGradientBuilderVisualContext(); - const size = $derived(ctx?.getSize() ?? 'md'); + const resolvedSize = $derived(size ?? ctx?.getSize() ?? 'md'); + const currentValue = $derived(value ?? provider?.value); const eidos = ActiveEidos.require(); const label = $derived(eidos.langs.ts('#?components.gradient-builder.presets|Gradient presets')); const saveLabel = $derived( @@ -32,12 +45,16 @@ const showDelete = $derived( selectedSaved !== null && savedPresets[selectedSaved] !== undefined && - gradientToCss(savedPresets[selectedSaved]) === gradientToCss(provider.value) + currentValue !== undefined && + gradientToCss(savedPresets[selectedSaved]) === gradientToCss(currentValue) ); function apply(g: Gradient, savedIndex: number | null): void { - provider.reset(g); - provider.commit(); + if (onApply) onApply(g); + else if (provider) { + provider.reset(g); + provider.commit(); + } selectedSaved = savedIndex; } function buttonAction(): void { @@ -75,7 +92,7 @@ + {#if !isSelectedEndpoint} + + {/if} {acceptLabel} diff --git a/src/uix/eidos/components/gradient-builder/types.ts b/src/uix/eidos/components/gradient-builder/types.ts index bcae96911..e966c23bf 100644 --- a/src/uix/eidos/components/gradient-builder/types.ts +++ b/src/uix/eidos/components/gradient-builder/types.ts @@ -28,7 +28,12 @@ export type GradientBuilderProps = SomaProviderProps & { maxPresets?: number; }; -/** The presets panel (shown when `presets` is defined). */ +/** + * The presets panel (shown when `presets` is defined). Reused by the + * GradientPicker's Presets tab: pass `value` / `onApply` / `size` to drive it + * from the picker's provider; omitted, it falls back to the GradientBuilder + * provider + visual context. + */ export type GradientBuilderPresetsProps = { /** Provided presets (select-only). */ provided: Gradient[]; @@ -40,6 +45,12 @@ export type GradientBuilderPresetsProps = { onSave?: () => void; /** Delete the saved preset at index `i`. */ onDelete?: (i: number) => void; + /** Current gradient for the save↔delete compare. @default the builder provider's value */ + value?: Gradient; + /** Visual size. @default the builder visual context (→ 'md') */ + size?: GradientBuilderSize; + /** Apply a selected preset. @default the builder provider (`reset` + `commit`) */ + onApply?: (g: Gradient) => void; }; export type GradientBuilderTrackProps = SomaTrackProps; diff --git a/src/uix/eidos/components/gradient-picker/gradient-picker-content.svelte b/src/uix/eidos/components/gradient-picker/gradient-picker-content.svelte index fc05822bc..60d5f8cc4 100644 --- a/src/uix/eidos/components/gradient-picker/gradient-picker-content.svelte +++ b/src/uix/eidos/components/gradient-picker/gradient-picker-content.svelte @@ -1,8 +1,15 @@ provider.setValue(g)} > - -
- + + + +
+
- + + + + + + + + {#if hasPresets} + +
+ + + + {#snippet child({ props }: { props: Record })} + + {/snippet} + + + + + + + +
+ {/if} diff --git a/src/uix/eidos/components/gradient-picker/gradient-picker-presets.svelte b/src/uix/eidos/components/gradient-picker/gradient-picker-presets.svelte new file mode 100644 index 000000000..6bf663517 --- /dev/null +++ b/src/uix/eidos/components/gradient-picker/gradient-picker-presets.svelte @@ -0,0 +1,36 @@ + + +
+ {#each provided as preset, i (i)} + + {/each} + {#each provider.savedPresets as preset, i (i)} + + {/each} +
diff --git a/src/uix/eidos/components/gradient-picker/gradient-picker-save-action.svelte b/src/uix/eidos/components/gradient-picker/gradient-picker-save-action.svelte new file mode 100644 index 000000000..2245d368e --- /dev/null +++ b/src/uix/eidos/components/gradient-picker/gradient-picker-save-action.svelte @@ -0,0 +1,35 @@ + + + diff --git a/src/uix/eidos/components/gradient-picker/gradient-picker.css b/src/uix/eidos/components/gradient-picker/gradient-picker.css index da49cb63d..696ab2226 100644 --- a/src/uix/eidos/components/gradient-picker/gradient-picker.css +++ b/src/uix/eidos/components/gradient-picker/gradient-picker.css @@ -87,7 +87,8 @@ background: var(--gp-current-gradient, transparent); } -/* ── Content — sized to fit the embedded editor (Area + sliders) ─────────── */ +/* ── Content — same width as the ColorPicker (the editor + a three-item footer + * fit; the add-to-presets action lives in the body, not the footer) ────────── */ [data-gradient-picker-content] { inline-size: 20rem; } @@ -98,9 +99,73 @@ inline-size: 22rem; } -.gradient-picker-controls { +/* Let the shell + body shrink to the popover width instead of forcing the + min-content of a wide row (tabs / kind switch / footer) to overflow the panel + (mirrors the ColorPicker). Without this the shell sizes to min-content and the + viewport scrolls / clips on the inline-end. */ +[data-popover-content][data-gradient-picker-content] [data-picker-shell], +[data-popover-content][data-gradient-picker-content] [data-picker-body] { + min-inline-size: 0; +} + +/* No tabs — the editor is the popover body; presets open in a panel from the + * Layers button (saves the tab-header height). */ + +/* ── Stops list — content-sizes up to a max, then scrolls (ScrollArea autosize), + * so a long list doesn't grow the popover ──────────────────────────────────── */ +[data-gradient-picker-stoplist-scroll] { + --scroll-area-auto-max-block: calc(132px * var(--scaling, 1)); + /* A slim scrollbar track (thinner than the sm default), no inner padding. */ + --scroll-area-scrollbar-size: var(--space-1-5); + --scroll-area-scrollbar-padding: 0; +} +/* Reserve the scrollbar's width PLUS a gap so the stop rows keep clear of it — + the list content stops short of the overlay track with breathing room. */ +[data-gradient-picker-stoplist-scroll] [data-scroll-area-viewport] { + padding-inline-end: var(--space-3); +} + +/* ── Presets panel (opens from the Layers button) + its swatch grid ─────────── */ +[data-gradient-picker-presets-panel] { + inline-size: 15rem; + max-inline-size: min(15rem, calc(100vw - var(--space-4))); +} +[data-gradient-picker-presets] { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(48px, 1fr)); + gap: var(--space-2); + align-content: start; +} +[data-gradient-picker-preset] { + inline-size: 100%; + block-size: calc(32px * var(--scaling, 1)); + padding: 0; + border: var(--border-width) solid var(--color-border-subtle); + border-radius: var(--radius-md); + cursor: pointer; +} +[data-gradient-picker-preset]:hover { + border-color: var(--color-border-strong); +} +[data-gradient-picker-preset]:focus-visible { + outline: var(--focus-ring-width) solid var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); +} +[data-gradient-picker-preset][data-selected] { + border-color: var(--color-primary-border); + outline: var(--focus-ring-width) solid var(--color-primary-border); + outline-offset: 2px; +} + +/* ── Actions row below the stops — [ Añadir degradado (fills) · Layers ] ────── */ +[data-gradient-picker-actions-row] { display: flex; align-items: center; - justify-content: space-between; - gap: var(--space-3); + gap: var(--space-2); +} +[data-gradient-picker-save-action] { + flex: 1 1 auto; +} +[data-gradient-picker-presets-toggle] { + flex: none; } diff --git a/src/uix/eidos/components/gradient-picker/gradient-picker.svelte b/src/uix/eidos/components/gradient-picker/gradient-picker.svelte index bd30c2d92..d496dfe76 100644 --- a/src/uix/eidos/components/gradient-picker/gradient-picker.svelte +++ b/src/uix/eidos/components/gradient-picker/gradient-picker.svelte @@ -18,6 +18,8 @@ open = $bindable(false), mode = 'inline', disabled = false, + presets, + maxPresets = 10, children, ...rest }: GradientPickerProps = $props(); @@ -39,6 +41,8 @@ bind:open {mode} {disabled} + {presets} + {maxPresets} data-size={resolvedSize} data-variant={variant} > diff --git a/src/uix/eidos/components/gradient-picker/index.ts b/src/uix/eidos/components/gradient-picker/index.ts index 3731c5bb8..d2a445ac4 100644 --- a/src/uix/eidos/components/gradient-picker/index.ts +++ b/src/uix/eidos/components/gradient-picker/index.ts @@ -13,6 +13,8 @@ import GradientPickerComponent from './gradient-picker.svelte'; import Trigger from './gradient-picker-trigger.svelte'; import ValueSwatch from './gradient-picker-value-swatch.svelte'; import Content from './gradient-picker-content.svelte'; +import Presets from './gradient-picker-presets.svelte'; +import SaveAction from './gradient-picker-save-action.svelte'; import Anchor from '../popover/popover-anchor.svelte'; import Overlay from '../popover/popover-overlay.svelte'; import Arrow from '../popover/popover-arrow.svelte'; @@ -31,6 +33,8 @@ type GradientPickerNamespace = typeof GradientPickerComponent & { Anchor: typeof Anchor; Overlay: typeof Overlay; Content: typeof Content; + Presets: typeof Presets; + SaveAction: typeof SaveAction; Arrow: typeof Arrow; PopoverClose: typeof PopoverClose; Footer: typeof Footer; @@ -46,6 +50,8 @@ GradientPicker.Portal = Portal; GradientPicker.Anchor = Anchor; GradientPicker.Overlay = Overlay; GradientPicker.Content = Content; +GradientPicker.Presets = Presets; +GradientPicker.SaveAction = SaveAction; GradientPicker.Arrow = Arrow; GradientPicker.PopoverClose = PopoverClose; GradientPicker.Footer = Footer; diff --git a/src/uix/eidos/components/gradient-picker/types.ts b/src/uix/eidos/components/gradient-picker/types.ts index edd88a8f9..53acfb6c6 100644 --- a/src/uix/eidos/components/gradient-picker/types.ts +++ b/src/uix/eidos/components/gradient-picker/types.ts @@ -10,6 +10,8 @@ import type { GradientBuilderSize } from '../gradient-builder/types'; export type GradientPickerSize = Extract; export type GradientPickerVariant = ControlVariant; +// `value` / `open` / `deferValue` / `mode` / `presets` / `maxPresets` come from +// SomaProviderProps; eidos only adds the visual axes. export type GradientPickerProps = SomaProviderProps & { /** Visual size for the trigger, popover and editor. @default 'md' */ size?: ResponsiveProp; diff --git a/src/uix/langs/components/gradient-picker.ts b/src/uix/langs/components/gradient-picker.ts index a3f3545c8..8b656fe02 100644 --- a/src/uix/langs/components/gradient-picker.ts +++ b/src/uix/langs/components/gradient-picker.ts @@ -19,5 +19,25 @@ export const gradientPickerLangs = { content: { es: 'Editor de degradado', en: 'Gradient editor' + }, + 'tab-editor': { + es: 'Editor', + en: 'Editor' + }, + 'tab-presets': { + es: 'Predefinidos', + en: 'Presets' + }, + presets: { + es: 'Degradados predefinidos', + en: 'Gradient presets' + }, + 'add-preset': { + es: 'Añadir degradado', + en: 'Add gradient' + }, + 'remove-preset': { + es: 'Quitar degradado', + en: 'Remove gradient' } } satisfies LangNode; diff --git a/src/uix/morfo/components/gradient-picker.ts b/src/uix/morfo/components/gradient-picker.ts index ee1927a6c..441a7146f 100644 --- a/src/uix/morfo/components/gradient-picker.ts +++ b/src/uix/morfo/components/gradient-picker.ts @@ -30,10 +30,34 @@ export const gradientPickerMorfo = { semantic: { family: 'commit', verb: 'reset', - target: v.partRef('content'), + target: v.partRef('provider'), sequence: 'post', intent: 'neutral' } + }, + { + // User saved the current gradient to the presets (the Add action). + // Per the Button doctrine, the consumer emits the RESULT commit — + // the press's `contact.activate` only marks the gesture. + name: 'commit-save', + semantic: { + family: 'commit', + verb: 'save', + target: v.partRef('provider'), + sequence: 'post', + intent: 'fulfill' + } + }, + { + // User removed a saved preset (the Remove action). + name: 'commit-remove', + semantic: { + family: 'commit', + verb: 'remove', + target: v.partRef('provider'), + sequence: 'post', + intent: 'loss' + } } ], diff --git a/src/uix/sema/components/gradient-picker.ts b/src/uix/sema/components/gradient-picker.ts new file mode 100644 index 000000000..febbfb237 --- /dev/null +++ b/src/uix/sema/components/gradient-picker.ts @@ -0,0 +1,47 @@ +import { semaSelector } from '$uix/morfo'; +import { gradientPickerMorfo } from '$uix/morfo/components/gradient-picker'; +import { sound } from '../sounds'; +import type { Sema } from '../sema-map'; + +/** + * GradientPicker perceptual defaults. + * + * The editing firma (handle / commit while dragging stops, recoloring, …) + * lives on the embedded GradientBuilder. The picker owns only its own value + * commits — the RESULT commits the Button doctrine leaves to the consumer + * (the press itself is a neutral `contact.activate`): + * + * • commit-save — a gradient saved to the presets: a fulfilling chime. + * • commit-remove — a saved preset removed: a soft descending "gone" cue. + * • commit-reset — cleared back to the initial value: a neutral reset. + * + * The `fulfill` / `loss` / `neutral` intents further shape pitch / gain / + * contour via the cascade's intent deltas — the base sound just names the body. + */ + +const onProvider = (matchers?: Parameters>[2]) => + semaSelector(gradientPickerMorfo, 'provider', matchers); + +export const gradientPickerSema: Sema = { + name: 'gradient-picker', + cascade: [ + { + selector: onProvider({ eventName: 'commit-save' }), + channels: ['sound', 'haptic'], + sound: sound('handle.snap.chime', { gain: 0.1 }), + haptic: { kind: 'tick', intensity: 0.2, duration: 8 } + }, + { + selector: onProvider({ eventName: 'commit-remove' }), + channels: ['sound', 'haptic'], + sound: sound('handle.release.soft', { gain: 0.1 }), + haptic: { kind: 'tick', intensity: 0.18, duration: 8 } + }, + { + selector: onProvider({ eventName: 'commit-reset' }), + channels: ['sound', 'haptic'], + sound: sound('handle.release.soft', { gain: 0.1 }), + haptic: { kind: 'tick', intensity: 0.18, duration: 8 } + } + ] +}; diff --git a/src/uix/soma/components/gradient-picker/components/gradient-picker.svelte b/src/uix/soma/components/gradient-picker/components/gradient-picker.svelte index 6284e0fb5..e35f925e0 100644 --- a/src/uix/soma/components/gradient-picker/components/gradient-picker.svelte +++ b/src/uix/soma/components/gradient-picker/components/gradient-picker.svelte @@ -1,7 +1,18 @@ {#if child} diff --git a/src/uix/soma/components/gradient-picker/gradient-picker-provider.svelte.ts b/src/uix/soma/components/gradient-picker/gradient-picker-provider.svelte.ts index e73adb71a..c850a8054 100644 --- a/src/uix/soma/components/gradient-picker/gradient-picker-provider.svelte.ts +++ b/src/uix/soma/components/gradient-picker/gradient-picker-provider.svelte.ts @@ -1,11 +1,7 @@ -import { untrack } from 'svelte'; -import { watch } from '$libs/reactive'; import { context, type ProviderOpts, type WithRefOpts } from '../../provider'; import { boolToEmptyStrOrUndef } from '$adom'; import { type ActiveProps, type StateProps } from '$libs/reactive'; -import type { OnChangeFn } from '../../types'; import { gradientToCss, type Gradient } from '$libs/gradient'; -import { pickerShellContext, type PickerShellHandle, type PickerShellMode } from '../picker-shell'; import { gradientPickerMorfo } from '../../../morfo/components/gradient-picker'; import { Soma } from '../../core/soma.svelte'; @@ -25,20 +21,28 @@ export const DEFAULT_PICKER_GRADIENT: Gradient = { interface GradientPickerOpts extends ProviderOpts, - StateProps<{ value: Gradient; open: boolean }>, + StateProps<{ + /** The WORKING gradient (`picker.workingValue`) — what the editor edits. */ + value: Gradient; + open: boolean; + /** The COMMITTED (bound) gradient — what the trigger chip reflects. */ + committedValue: Gradient; + }>, ActiveProps<{ disabled: boolean; - mode: PickerShellMode; - onValueCommit: OnChangeFn; + presets: Gradient[] | undefined; + maxPresets: number; }> {} /** - * Headless GradientPicker — the popover-picker shell around the interactive - * GradientBuilder. Owns the `open` state and the commit / cancel / clear - * lifecycle (exposed to `` via `pickerShellContext`). The - * gradient editing itself flows through the composed GradientBuilder, which - * binds the SAME `value` — so `cancel()` reverts to the value the popover - * opened with. + * Domain coordinator for GradientPicker. The transaction — open / commit / + * cancel / clear + `deferValue` — lives in the composed generic + * `PickerProvider` (see `components/gradient-picker.svelte`); this class only + * owns the morfo runtime (provider / trigger / value-swatch parts) and the + * working gradient it is handed (bound to `picker.workingValue`), exposing its + * serialized CSS for the trigger swatch + value chip. The embedded + * GradientBuilder edits that working value; the footer `` parts + * drive the generic picker via `pickerShellContext`. */ export class GradientPickerProvider { readonly opts: GradientPickerOpts; @@ -62,10 +66,12 @@ export class GradientPickerProvider { return this.opts.disabled.current; } - /** The committed gradient, serialized for the trigger swatch / value chip. */ - readonly css = $derived.by(() => gradientToCss(this.opts.value.current)); + /** The COMMITTED gradient, serialized for the trigger swatch / value chip — so + * a deferred picker's chip stays put until Accept (the editor edits the + * working `value`; in live mode committed === working). */ + readonly css = $derived.by(() => gradientToCss(this.opts.committedValue.current)); - /** The live gradient — read/written by the composed GradientBuilder body. */ + /** The working gradient — read/written by the composed GradientBuilder body. */ get value(): Gradient { return this.opts.value.current; } @@ -73,6 +79,67 @@ export class GradientPickerProvider { this.opts.value.current = next; } + // ── Presets + saved gradients ───────────────────────────────────────────── + // Mirrors ColorPickerProvider: `presets` is a read-only palette; `savedPresets` + // is the editable list the footer Save/Delete action mutates. A saved preset is + // "selected" (→ Delete) only when the user CLICKS it, not merely when it matches + // the value. State lives here (soma) so it reads the WORKING value — correct in + // both live and deferred modes. + + /** The provided preset palette (undefined → no Presets tab). */ + readonly presets = $derived.by(() => this.opts.presets.current); + /** User-saved gradients (footer Save appends; Delete removes). */ + savedPresets = $state([]); + /** The saved preset the user explicitly selected (drives Save↔Delete). */ + selectedSaved = $state(null); + + private get maxPresets(): number { + return this.opts.maxPresets.current; + } + + /** Current gradient can be saved: under cap, not already present. */ + readonly canSave = $derived.by(() => { + if (this.savedPresets.length >= this.maxPresets) return false; + const k = gradientToCss(this.value); + return ![...(this.presets ?? []), ...this.savedPresets].some((g) => gradientToCss(g) === k); + }); + + /** Show "delete" instead of "save": a saved preset is selected AND is the value. */ + readonly showDelete = $derived.by(() => { + const i = this.selectedSaved; + return ( + i !== null && + this.savedPresets[i] !== undefined && + gradientToCss(this.savedPresets[i]) === gradientToCss(this.value) + ); + }); + + /** Apply a preset to the working value; mark it selected when it's a saved one. */ + applyPreset(g: Gradient, savedIndex: number | null): void { + this.setValue(g); + this.selectedSaved = savedIndex; + } + /** Mark a saved preset selected (called by the presets grid). */ + selectSaved(i: number): void { + this.selectedSaved = i; + } + /** Save the current working gradient to the saved list. */ + savePreset(): void { + if (!this.canSave) return; + this.savedPresets = [...this.savedPresets, this.value]; + // The result-commit firma (a "saved" cue) — the Button press only emits + // the neutral `contact.activate`; the evaluative commit is ours to fire. + void this.runtime.trigger('commit-save'); + } + /** Delete a saved preset (defaults to the selected one). */ + deletePreset(i?: number): void { + const target = i ?? this.selectedSaved; + if (target === null || target === undefined) return; + this.savedPresets = this.savedPresets.filter((_, idx) => idx !== target); + this.selectedSaved = null; + void this.runtime.trigger('commit-remove'); + } + private constructor(opts: GradientPickerOpts) { this.opts = opts; this.soma = Soma.require(); @@ -87,52 +154,12 @@ export class GradientPickerProvider { context: GradientPickerProvider.ctx, syncAttrs: true }); - - // Snapshot the value on the opening edge so `cancel()` can revert. - watch( - () => this.opts.open.current, - (now, prev) => { - if (now && !prev) this.valueOnOpen = untrack(() => this.opts.value.current); - } - ); - - // Expose commit / cancel / clear to descendant `` parts. - pickerShellContext.set(this.pickerShellHandle); } setTriggerNode(el: HTMLElement | null): void { this.triggerNode = el; } - // ── PickerShell lifecycle ──────────────────────────────────────────────── - private valueOnOpen: Gradient | undefined = undefined; - - /** Save the current gradient and close (drives ``). */ - commit(): void { - this.opts.open.current = false; - this.opts.onValueCommit.current(this.opts.value.current); - } - - /** Revert to the value the popover opened with and close (``). */ - cancel(): void { - if (this.valueOnOpen !== undefined) this.opts.value.current = this.valueOnOpen; - this.opts.open.current = false; - } - - /** Reset the gradient to the default without closing (``). */ - clear(): void { - if (this.isDisabled) return; - this.opts.value.current = DEFAULT_PICKER_GRADIENT; - void this.runtime.trigger('commit-reset'); - } - - readonly pickerShellHandle: PickerShellHandle = { - getMode: () => this.opts.mode.current, - commit: () => this.commit(), - cancel: () => this.cancel(), - clear: () => this.clear() - }; - get props() { return this.runtimePart.renderProps(); } diff --git a/src/uix/soma/components/gradient-picker/types.ts b/src/uix/soma/components/gradient-picker/types.ts index 8e5519b21..e329b82c5 100644 --- a/src/uix/soma/components/gradient-picker/types.ts +++ b/src/uix/soma/components/gradient-picker/types.ts @@ -13,16 +13,29 @@ export type GradientPickerProviderProps = WithChild<{ id?: string; /** The gradient being edited. Bindable. */ value?: Gradient; - /** Called when the user commits (Save / Done). */ - onValueCommit?: OnChangeFn; + /** Called when the value changes (live — `deferValue` is off). */ + onValueChange?: OnChangeFn; /** Popover open state. Bindable. @default false */ open?: boolean; /** Called when the open state changes. */ onOpenChange?: OnChangeFn; + /** Called after the open/close animation completes. */ + onOpenChangeComplete?: OnChangeFn; + /** + * Defer edits to an internal draft until the user accepts (Listo). When off + * (default) edits are live and Cancel reverts to the value the popover opened + * with; when on, the bound value only updates on accept and any other close + * discards the draft. @default false + */ + deferValue?: boolean; /** Footer layout mode — `inline` (clear/cancel) vs `modal` (cancel/save). @default 'inline' */ mode?: PickerShellMode; /** @default false */ disabled?: boolean; + /** Preset gradients. Defined (even `[]`) → the picker exposes a Presets tab + footer Save action. */ + presets?: Gradient[]; + /** Max number of user-saved presets. @default 10 */ + maxPresets?: number; }> & Without; diff --git a/web/routes/uix/+layout@.svelte b/web/routes/uix/+layout@.svelte index d8cc8c05d..8974de550 100644 --- a/web/routes/uix/+layout@.svelte +++ b/web/routes/uix/+layout@.svelte @@ -13,6 +13,8 @@ import { fileUploadSema } from '$uix/sema/components/file-upload'; import { floatPanelSema } from '$uix/sema/components/float-panel'; import { formSema } from '$uix/sema/components/form'; + import { gradientBuilderSema } from '$uix/sema/components/gradient-builder'; + import { gradientPickerSema } from '$uix/sema/components/gradient-picker'; import { imageAdjustmentsSema } from '$uix/sema/components/image-adjustments'; import { imagePickerSema } from '$uix/sema/components/image-picker'; import { knobSema } from '$uix/sema/components/knob'; @@ -85,6 +87,8 @@ fileUploadSema, floatPanelSema, formSema, + gradientBuilderSema, + gradientPickerSema, imageAdjustmentsSema, imagePickerSema, knobSema, diff --git a/web/routes/uix/components/gradient-picker/+page.svelte b/web/routes/uix/components/gradient-picker/+page.svelte index 97ee2212c..eb42a8d9a 100644 --- a/web/routes/uix/components/gradient-picker/+page.svelte +++ b/web/routes/uix/components/gradient-picker/+page.svelte @@ -31,18 +31,77 @@ let size = $state('md'); let variant = $state('surface'); let mode = $state<'inline' | 'modal'>('inline'); + let deferValue = $state(false); + let maxPresets = $state(10); let disabled = $state(false); let open = $state(false); + // A distinct 3-stop gradient (not one of the presets) so the "Añadir degradado" + // action starts enabled — you can save it, then it disables (already a preset). let value = $state({ kind: 'linear', - angle: 45, + angle: 120, stops: [ - { color: { kind: 'css', value: '#ec4899' }, position: 0 }, - { color: { kind: 'css', value: '#8b5cf6' }, position: 1 } + { color: { kind: 'css', value: '#f97316' }, position: 0 }, + { color: { kind: 'css', value: '#db2777' }, position: 0.5 }, + { color: { kind: 'css', value: '#7c3aed' }, position: 1 } ] }); const css = $derived(gradientToCss(value)); + // Demo presets — passing them splits the popover into two tabs (Editor · + // Predefinidos), with the save-to-presets action (save↔delete, capped at + // maxPresets), mirroring the ColorPicker's saved swatches. + const presets: Gradient[] = [ + { + kind: 'linear', + angle: 45, + stops: [ + { color: { kind: 'css', value: '#ec4899' }, position: 0 }, + { color: { kind: 'css', value: '#8b5cf6' }, position: 1 } + ] + }, + { + kind: 'linear', + angle: 90, + stops: [ + { color: { kind: 'css', value: '#f59e0b' }, position: 0 }, + { color: { kind: 'css', value: '#ef4444' }, position: 1 } + ] + }, + { + kind: 'linear', + angle: 135, + stops: [ + { color: { kind: 'css', value: '#06b6d4' }, position: 0 }, + { color: { kind: 'css', value: '#3b82f6' }, position: 1 } + ] + }, + { + kind: 'linear', + angle: 90, + stops: [ + { color: { kind: 'css', value: '#10b981' }, position: 0 }, + { color: { kind: 'css', value: '#84cc16' }, position: 1 } + ] + }, + { + kind: 'linear', + angle: 180, + stops: [ + { color: { kind: 'css', value: '#6366f1' }, position: 0 }, + { color: { kind: 'css', value: '#ec4899' }, position: 1 } + ] + }, + { + kind: 'linear', + angle: 90, + stops: [ + { color: { kind: 'css', value: '#14b8a6' }, position: 0 }, + { color: { kind: 'css', value: '#8b5cf6' }, position: 1 } + ] + } + ]; + // ── System (foundation) axes — applied to the stage ───────────────────── let density = $state('comfortable'); let scaling = $state(1); @@ -118,7 +177,7 @@ dir={dir} style={stageStyle} > - + @@ -203,6 +262,18 @@ disabled {disabled ? 'on' : 'off'} + +