@marianmeres/stuic 3.180.0 → 3.181.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/dist/components/ColorPicker/ColorPicker.svelte +457 -0
- package/dist/components/ColorPicker/ColorPicker.svelte.d.ts +75 -0
- package/dist/components/ColorPicker/README.md +220 -0
- package/dist/components/ColorPicker/color-value.d.ts +32 -0
- package/dist/components/ColorPicker/color-value.js +55 -0
- package/dist/components/ColorPicker/i18n-sk.d.ts +17 -0
- package/dist/components/ColorPicker/i18n-sk.js +48 -0
- package/dist/components/ColorPicker/i18n.d.ts +64 -0
- package/dist/components/ColorPicker/i18n.js +75 -0
- package/dist/components/ColorPicker/index.css +275 -0
- package/dist/components/ColorPicker/index.d.ts +4 -0
- package/dist/components/ColorPicker/index.js +4 -0
- package/dist/components/ColorPicker/palettes.d.ts +41 -0
- package/dist/components/ColorPicker/palettes.js +49 -0
- package/dist/components/FieldsBuilder/README.md +3 -1
- package/dist/components/FieldsBuilder/types.d.ts +7 -2
- package/dist/components/FieldsBuilder/utils.d.ts +2 -1
- package/dist/components/FieldsBuilder/utils.js +4 -15
- package/dist/components/Nav/Nav.svelte +7 -2
- package/dist/components/Nav/Nav.svelte.d.ts +7 -2
- package/dist/components/Nav/README.md +2 -2
- package/dist/components/TabbedMenu/README.md +14 -13
- package/dist/components/TabbedMenu/TabbedMenu.svelte +7 -2
- package/dist/components/TabbedMenu/TabbedMenu.svelte.d.ts +7 -2
- package/dist/index.css +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/tr.d.ts +26 -14
- package/dist/utils/tr.js +43 -19
- package/docs/_archive/maybe-todo.md +9 -2
- package/docs/domains/components.md +58 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
```
|
|
25
25
|
src/lib/
|
|
26
|
-
├── components/ #
|
|
26
|
+
├── components/ # 80 component directories
|
|
27
27
|
├── actions/ # 16 Svelte actions (use: directives)
|
|
28
28
|
├── attachments/ # Svelte attachments ({@attach} — preferred for new DOM helpers)
|
|
29
29
|
├── utils/ # 55 utility modules (48 on the barrel)
|
|
@@ -146,7 +146,7 @@ so it is the only confusable pair — do not "fix" one into the other.
|
|
|
146
146
|
|
|
147
147
|
### Domain Docs
|
|
148
148
|
|
|
149
|
-
- [Components](./docs/domains/components.md) —
|
|
149
|
+
- [Components](./docs/domains/components.md) — 80 component directories, Props pattern, snippets
|
|
150
150
|
- [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
|
|
151
151
|
- [CSS presets](./docs/domains/css-presets.md) — ratio-locked frame (letterbox), safe-area, scrollbar
|
|
152
152
|
- [Actions](./docs/domains/actions.md) — 16 Svelte directives
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import type { TranslateFn } from "../../types.js";
|
|
4
|
+
import type {
|
|
5
|
+
ValidateOptions,
|
|
6
|
+
ValidationResult,
|
|
7
|
+
} from "../../actions/validate.svelte.js";
|
|
8
|
+
import type { ColorPickerSwatch } from "./palettes.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Which custom-color controls are rendered under the palette:
|
|
12
|
+
* - `"both"` (default) — the native picker button and the hex text field
|
|
13
|
+
* - `"native"` — only the native `<input type="color">` button
|
|
14
|
+
* - `"text"` — only the hex text field
|
|
15
|
+
* - `false` — none; the palette is the only source of values
|
|
16
|
+
*/
|
|
17
|
+
export type ColorPickerCustom = "both" | "native" | "text" | false;
|
|
18
|
+
|
|
19
|
+
export interface Props extends Omit<
|
|
20
|
+
HTMLAttributes<HTMLDivElement>,
|
|
21
|
+
"children" | "onchange"
|
|
22
|
+
> {
|
|
23
|
+
/**
|
|
24
|
+
* Current color (bindable). Any CSS color string; `""` = no color. Stored
|
|
25
|
+
* verbatim — the component never converts between color spaces, so what a
|
|
26
|
+
* swatch holds is what you get back.
|
|
27
|
+
*/
|
|
28
|
+
value?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The swatches. Either bare CSS color strings or `{ value, label }` objects.
|
|
31
|
+
* Defaults to `COLOR_PICKER_PALETTE` (12 hues + white/grey/black); pass
|
|
32
|
+
* `COLOR_PICKER_PALETTE_THEME` for design-token colors, or `[]` for none.
|
|
33
|
+
*/
|
|
34
|
+
palette?: ColorPickerSwatch[];
|
|
35
|
+
/**
|
|
36
|
+
* Cap the palette at this many swatches per row (a narrower container
|
|
37
|
+
* wraps to fewer rather than overflowing). Unset = as many as fit.
|
|
38
|
+
* ArrowUp / ArrowDown always step by the *rendered* row, whatever this is.
|
|
39
|
+
*/
|
|
40
|
+
columns?: number;
|
|
41
|
+
/** Which custom-color controls to show (default `"both"`) */
|
|
42
|
+
custom?: ColorPickerCustom;
|
|
43
|
+
/**
|
|
44
|
+
* Offer clearing: a crossed-out swatch after the palette, plus Delete /
|
|
45
|
+
* Backspace, plus emptying the hex field — all setting `value` to `""`.
|
|
46
|
+
*/
|
|
47
|
+
allowClear?: boolean;
|
|
48
|
+
/** Disable interaction (the hidden input is disabled too, so nothing submits) */
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
/** Accessible name of the swatch group (default `t("color")`, "Color") */
|
|
51
|
+
label?: string;
|
|
52
|
+
/** Form field name (hidden input) */
|
|
53
|
+
name?: string;
|
|
54
|
+
/** Require a non-empty value (enforced by the built-in validator) */
|
|
55
|
+
required?: boolean;
|
|
56
|
+
/** i18n translate function (see `createColorPickerT`) */
|
|
57
|
+
t?: TranslateFn;
|
|
58
|
+
/**
|
|
59
|
+
* Fires when the user commits a color: a swatch click / arrow key, the
|
|
60
|
+
* native picker's `change`, or the hex field on Enter or blur. NOT while
|
|
61
|
+
* the native picker is being dragged — `value` does update live there, so
|
|
62
|
+
* `bind:value` previews, but only the commit is an `onchange`.
|
|
63
|
+
*/
|
|
64
|
+
onchange?: (value: string) => void;
|
|
65
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
66
|
+
setValidationResult?: (res: ValidationResult) => void;
|
|
67
|
+
/** Skip all default styling */
|
|
68
|
+
unstyled?: boolean;
|
|
69
|
+
/** Additional CSS classes */
|
|
70
|
+
class?: string;
|
|
71
|
+
/** Class for every swatch button */
|
|
72
|
+
classSwatch?: string;
|
|
73
|
+
/** Bindable root element reference */
|
|
74
|
+
el?: HTMLDivElement;
|
|
75
|
+
/** Bindable hidden input reference */
|
|
76
|
+
inputEl?: HTMLInputElement;
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<script lang="ts">
|
|
81
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
82
|
+
import { validate as validateAction } from "../../actions/validate.svelte.js";
|
|
83
|
+
import { isCssColor, isSameColor, normalizeHex } from "./color-value.js";
|
|
84
|
+
import { COLOR_PICKER_PALETTE } from "./palettes.js";
|
|
85
|
+
import { t_default } from "./i18n.js";
|
|
86
|
+
|
|
87
|
+
let {
|
|
88
|
+
value = $bindable(""),
|
|
89
|
+
palette = COLOR_PICKER_PALETTE,
|
|
90
|
+
columns,
|
|
91
|
+
custom = "both",
|
|
92
|
+
allowClear = true,
|
|
93
|
+
disabled = false,
|
|
94
|
+
label,
|
|
95
|
+
name,
|
|
96
|
+
required = false,
|
|
97
|
+
t = t_default,
|
|
98
|
+
onchange,
|
|
99
|
+
validate: validateProp,
|
|
100
|
+
setValidationResult,
|
|
101
|
+
unstyled = false,
|
|
102
|
+
class: classProp,
|
|
103
|
+
classSwatch: classSwatchProp,
|
|
104
|
+
el = $bindable(),
|
|
105
|
+
inputEl = $bindable(),
|
|
106
|
+
...rest
|
|
107
|
+
}: Props = $props();
|
|
108
|
+
|
|
109
|
+
let _value = $derived(typeof value === "string" ? value : "");
|
|
110
|
+
|
|
111
|
+
// Normalized entries; empty/invalid ones are dropped so a swatch always has
|
|
112
|
+
// something to paint.
|
|
113
|
+
let _entries = $derived(
|
|
114
|
+
(Array.isArray(palette) ? palette : [])
|
|
115
|
+
.map((s) => (typeof s === "string" ? { value: s } : s))
|
|
116
|
+
.filter((s) => !!s && typeof s.value === "string" && !!s.value.trim())
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
let _showNative = $derived(custom === "both" || custom === "native");
|
|
120
|
+
let _showText = $derived(custom === "both" || custom === "text");
|
|
121
|
+
|
|
122
|
+
let _cols = $derived(
|
|
123
|
+
Number.isFinite(columns) && (columns as number) >= 1 ? Math.floor(columns!) : 0
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// The clear swatch is the last member of the radio group (when enabled).
|
|
127
|
+
let _navCount = $derived(_entries.length + (allowClear ? 1 : 0));
|
|
128
|
+
let _selected = $derived.by(() => {
|
|
129
|
+
if (allowClear && _value === "") return _entries.length;
|
|
130
|
+
return _entries.findIndex((e) => isSameColor(e.value, _value));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Roving tabindex: the checked swatch is the single tab stop; with a custom
|
|
135
|
+
* (off-palette) color checked, nothing is — so fall back to the first one.
|
|
136
|
+
*/
|
|
137
|
+
function tabindexOf(i: number): 0 | -1 {
|
|
138
|
+
if (_selected >= 0) return _selected === i ? 0 : -1;
|
|
139
|
+
return i === 0 ? 0 : -1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function labelOf(e: { value: string; label?: string }): string {
|
|
143
|
+
return e.label ? t(e.label, null, e.label) : e.value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// `<input type="color">` only accepts `#rrggbb`; anything else (a token
|
|
147
|
+
// reference, `transparent`, an empty value) shows as black until picked.
|
|
148
|
+
let _nativeValue = $derived(normalizeHex(_value) ?? "#000000");
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The hex field owns its own DOM value while it is focused — a half-typed
|
|
152
|
+
* "#3b" must survive the keystroke, and the value round-trip must not fight
|
|
153
|
+
* the caret. So it is written explicitly (`syncText`) rather than driven by a
|
|
154
|
+
* reactive `value=`: Svelte skips a write whenever the expression matches
|
|
155
|
+
* what it last wrote, which is exactly the snap-back-after-typing case.
|
|
156
|
+
*/
|
|
157
|
+
let textEl: HTMLInputElement | undefined = $state();
|
|
158
|
+
|
|
159
|
+
/** Push the current value into the hex field, dropping whatever was typed. */
|
|
160
|
+
function syncText(input?: HTMLInputElement | null) {
|
|
161
|
+
const target = input ?? textEl;
|
|
162
|
+
if (target && target.value !== _value) target.value = _value;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ...which leaves the field to follow value changes made anywhere else (a
|
|
166
|
+
// swatch, the native picker, the consumer) — but never under the caret.
|
|
167
|
+
$effect(() => {
|
|
168
|
+
void _value;
|
|
169
|
+
if (textEl && document.activeElement !== textEl) syncText();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
/** Accepts hex in any spelling, then any color the browser understands. */
|
|
173
|
+
function parseColor(raw: string): string | null {
|
|
174
|
+
const v = (raw ?? "").trim();
|
|
175
|
+
if (!v) return allowClear ? "" : null;
|
|
176
|
+
return normalizeHex(v) ?? (isCssColor(v) ? v : null);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Enter / blur in the hex field: commit it, or snap back if it is garbage. */
|
|
180
|
+
function commitText(input: HTMLInputElement) {
|
|
181
|
+
const parsed = parseColor(input.value);
|
|
182
|
+
if (parsed === null) return syncText(input);
|
|
183
|
+
commit(parsed);
|
|
184
|
+
// show the normalized spelling ("#0f0" -> "#00ff00")
|
|
185
|
+
input.value = parsed;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Live update, no event — the native picker's drag stream, and a valid color
|
|
190
|
+
* being typed into the hex field.
|
|
191
|
+
*/
|
|
192
|
+
function preview(v: string) {
|
|
193
|
+
if (disabled) return;
|
|
194
|
+
value = v;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** A user commit: syncs the hidden input (revalidation) and fires `onchange`. */
|
|
198
|
+
function commit(v: string) {
|
|
199
|
+
if (disabled) return;
|
|
200
|
+
value = v;
|
|
201
|
+
// The hidden input's DOM value is only synced on the next flush, but the
|
|
202
|
+
// validate action reads it synchronously in its "change" listener — so
|
|
203
|
+
// write it by hand before dispatching. Svelte re-applies the same string
|
|
204
|
+
// later (a no-op).
|
|
205
|
+
if (inputEl) {
|
|
206
|
+
inputEl.value = v;
|
|
207
|
+
inputEl.dispatchEvent(new Event("change", { bubbles: true }));
|
|
208
|
+
}
|
|
209
|
+
onchange?.(v);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function valueAt(i: number): string {
|
|
213
|
+
return i === _entries.length ? "" : (_entries[i]?.value ?? "");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Every group member, in DOM order (the clear swatch last). */
|
|
217
|
+
function members(): HTMLElement[] {
|
|
218
|
+
return [
|
|
219
|
+
...(el?.querySelectorAll<HTMLElement>(`[role="radiogroup"] [data-index]`) ?? []),
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* How many swatches actually share the top row right now. The palette wraps,
|
|
225
|
+
* so this — not the `columns` prop — is what ArrowUp / ArrowDown must step by:
|
|
226
|
+
* it stays true when a narrow screen wraps to fewer per row, and it gives the
|
|
227
|
+
* default (uncapped) palette working vertical arrows for free.
|
|
228
|
+
*/
|
|
229
|
+
function rowLength(): number {
|
|
230
|
+
const nodes = members();
|
|
231
|
+
if (nodes.length < 2) return 1;
|
|
232
|
+
const top = nodes[0].offsetTop;
|
|
233
|
+
let n = 0;
|
|
234
|
+
while (n < nodes.length && nodes[n].offsetTop === top) n++;
|
|
235
|
+
return n || 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function focusAt(i: number) {
|
|
239
|
+
// keyed off the role, not the class — `unstyled` removes the classes
|
|
240
|
+
el?.querySelector<HTMLButtonElement>(
|
|
241
|
+
`[role="radiogroup"] [data-index="${i}"]`
|
|
242
|
+
)?.focus();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Select the i-th group member. Re-picking the checked one is a no-op event-wise
|
|
247
|
+
* (a radio does not fire `change` when it is already the checked one), but it
|
|
248
|
+
* still moves focus — which is what arrow navigation needs.
|
|
249
|
+
*/
|
|
250
|
+
function pick(i: number) {
|
|
251
|
+
if (i < 0 || i >= _navCount) return;
|
|
252
|
+
if (i !== _selected) commit(valueAt(i));
|
|
253
|
+
focusAt(i);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function onkeydown(e: KeyboardEvent) {
|
|
257
|
+
if (disabled || !_navCount) return;
|
|
258
|
+
const from = Number((e.target as HTMLElement)?.dataset?.index);
|
|
259
|
+
const cur = Number.isFinite(from) ? from : Math.max(_selected, 0);
|
|
260
|
+
const step = rowLength();
|
|
261
|
+
let next: number;
|
|
262
|
+
switch (e.key) {
|
|
263
|
+
// Horizontal wraps (radiogroup convention), vertical does not — a
|
|
264
|
+
// wrapped row has no meaningful cell above the first one.
|
|
265
|
+
case "ArrowRight":
|
|
266
|
+
next = (cur + 1) % _navCount;
|
|
267
|
+
break;
|
|
268
|
+
case "ArrowLeft":
|
|
269
|
+
next = (cur - 1 + _navCount) % _navCount;
|
|
270
|
+
break;
|
|
271
|
+
case "ArrowDown":
|
|
272
|
+
next = cur + step;
|
|
273
|
+
if (next >= _navCount) return;
|
|
274
|
+
break;
|
|
275
|
+
case "ArrowUp":
|
|
276
|
+
next = cur - step;
|
|
277
|
+
if (next < 0) return;
|
|
278
|
+
break;
|
|
279
|
+
case "Home":
|
|
280
|
+
next = 0;
|
|
281
|
+
break;
|
|
282
|
+
case "End":
|
|
283
|
+
next = _navCount - 1;
|
|
284
|
+
break;
|
|
285
|
+
case "Delete":
|
|
286
|
+
case "Backspace":
|
|
287
|
+
if (!allowClear) return;
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
// already cleared -> handled, but nothing changed (no event)
|
|
290
|
+
if (_value !== "") commit("");
|
|
291
|
+
return;
|
|
292
|
+
default:
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
pick(next);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let _class = $derived(unstyled ? classProp : twMerge("stuic-color-picker", classProp));
|
|
300
|
+
let _classSwatch = $derived(
|
|
301
|
+
unstyled ? classSwatchProp : twMerge("stuic-color-picker-swatch", classSwatchProp)
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
//
|
|
305
|
+
let _doValidate: (() => void) | undefined = $state();
|
|
306
|
+
// Local copy of the last validation result so getValidation() works even
|
|
307
|
+
// when no external setValidationResult was provided.
|
|
308
|
+
let _validation: ValidationResult | undefined = $state();
|
|
309
|
+
|
|
310
|
+
/** Trigger validation now. Reaches the parent via `setValidationResult`. */
|
|
311
|
+
export function validate(): ValidationResult | undefined {
|
|
312
|
+
_doValidate?.();
|
|
313
|
+
return _validation;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Clear the inline validation message and reset `setCustomValidity`. */
|
|
317
|
+
export function clearValidation(): void {
|
|
318
|
+
_validation = undefined;
|
|
319
|
+
inputEl?.setCustomValidity?.("");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Current validation state. */
|
|
323
|
+
export function getValidation(): ValidationResult | undefined {
|
|
324
|
+
return _validation;
|
|
325
|
+
}
|
|
326
|
+
</script>
|
|
327
|
+
|
|
328
|
+
<div
|
|
329
|
+
bind:this={el}
|
|
330
|
+
class={_class}
|
|
331
|
+
data-disabled={!unstyled && disabled ? "" : undefined}
|
|
332
|
+
{...rest}
|
|
333
|
+
>
|
|
334
|
+
{#if _navCount}
|
|
335
|
+
<!-- The group is a composite widget with a roving tabindex: the checked
|
|
336
|
+
swatch is the tab stop, never the container. Making the container
|
|
337
|
+
focusable would add a second stop and let a click on the gap between
|
|
338
|
+
swatches pull focus off them. -->
|
|
339
|
+
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
340
|
+
<div
|
|
341
|
+
class={unstyled ? undefined : "stuic-color-picker-swatches"}
|
|
342
|
+
role="radiogroup"
|
|
343
|
+
aria-label={label || t("color", null, "Color")}
|
|
344
|
+
aria-required={required ? "true" : undefined}
|
|
345
|
+
aria-disabled={disabled ? "true" : undefined}
|
|
346
|
+
aria-invalid={_validation && !_validation.valid ? "true" : undefined}
|
|
347
|
+
data-columns={!unstyled && _cols ? _cols : undefined}
|
|
348
|
+
style={_cols ? `--stuic-color-picker-columns: ${_cols};` : undefined}
|
|
349
|
+
{onkeydown}
|
|
350
|
+
>
|
|
351
|
+
{#each _entries as entry, i (entry.value + "-" + i)}
|
|
352
|
+
<button
|
|
353
|
+
type="button"
|
|
354
|
+
role="radio"
|
|
355
|
+
class={_classSwatch}
|
|
356
|
+
style="--stuic-color-picker-swatch-color: {entry.value};"
|
|
357
|
+
data-index={i}
|
|
358
|
+
data-selected={!unstyled && _selected === i ? "" : undefined}
|
|
359
|
+
aria-checked={_selected === i}
|
|
360
|
+
aria-label={labelOf(entry)}
|
|
361
|
+
title={labelOf(entry)}
|
|
362
|
+
tabindex={tabindexOf(i)}
|
|
363
|
+
{disabled}
|
|
364
|
+
onclick={() => pick(i)}
|
|
365
|
+
></button>
|
|
366
|
+
{/each}
|
|
367
|
+
{#if allowClear}
|
|
368
|
+
{@const i = _entries.length}
|
|
369
|
+
<button
|
|
370
|
+
type="button"
|
|
371
|
+
role="radio"
|
|
372
|
+
class={_classSwatch}
|
|
373
|
+
data-index={i}
|
|
374
|
+
data-clear={!unstyled ? "" : undefined}
|
|
375
|
+
data-selected={!unstyled && _selected === i ? "" : undefined}
|
|
376
|
+
aria-checked={_selected === i}
|
|
377
|
+
aria-label={t("no_color", null, "No color")}
|
|
378
|
+
title={t("no_color", null, "No color")}
|
|
379
|
+
tabindex={tabindexOf(i)}
|
|
380
|
+
{disabled}
|
|
381
|
+
onclick={() => pick(i)}
|
|
382
|
+
></button>
|
|
383
|
+
{/if}
|
|
384
|
+
</div>
|
|
385
|
+
{/if}
|
|
386
|
+
|
|
387
|
+
{#if _showNative || _showText}
|
|
388
|
+
<div class={unstyled ? undefined : "stuic-color-picker-custom"}>
|
|
389
|
+
{#if _showNative}
|
|
390
|
+
<input
|
|
391
|
+
type="color"
|
|
392
|
+
class={unstyled ? undefined : "stuic-color-picker-native"}
|
|
393
|
+
value={_nativeValue}
|
|
394
|
+
{disabled}
|
|
395
|
+
aria-label={t("custom_color", null, "Custom color")}
|
|
396
|
+
title={t("custom_color", null, "Custom color")}
|
|
397
|
+
oninput={(e) => preview(e.currentTarget.value)}
|
|
398
|
+
onchange={(e) => commit(e.currentTarget.value)}
|
|
399
|
+
/>
|
|
400
|
+
{/if}
|
|
401
|
+
{#if _showText}
|
|
402
|
+
<input
|
|
403
|
+
bind:this={textEl}
|
|
404
|
+
type="text"
|
|
405
|
+
class={unstyled ? undefined : "stuic-color-picker-text"}
|
|
406
|
+
value={_value}
|
|
407
|
+
{disabled}
|
|
408
|
+
autocomplete="off"
|
|
409
|
+
autocapitalize="none"
|
|
410
|
+
autocorrect="off"
|
|
411
|
+
spellcheck="false"
|
|
412
|
+
inputmode="text"
|
|
413
|
+
placeholder={t("hex_placeholder", null, "#rrggbb")}
|
|
414
|
+
aria-label={t("hex_value", null, "Hex value")}
|
|
415
|
+
oninput={(e) => {
|
|
416
|
+
const parsed = parseColor(e.currentTarget.value);
|
|
417
|
+
// preview only — the commit (and `onchange`) waits for
|
|
418
|
+
// Enter / blur, so a partial value never fires an event
|
|
419
|
+
if (parsed !== null) preview(parsed);
|
|
420
|
+
}}
|
|
421
|
+
onchange={(e) => commitText(e.currentTarget)}
|
|
422
|
+
onblur={(e) => syncText(e.currentTarget)}
|
|
423
|
+
/>
|
|
424
|
+
{/if}
|
|
425
|
+
</div>
|
|
426
|
+
{/if}
|
|
427
|
+
|
|
428
|
+
<input
|
|
429
|
+
bind:this={inputEl}
|
|
430
|
+
type="hidden"
|
|
431
|
+
{name}
|
|
432
|
+
value={_value}
|
|
433
|
+
{disabled}
|
|
434
|
+
use:validateAction={() => {
|
|
435
|
+
const customOpts =
|
|
436
|
+
typeof validateProp === "object" && validateProp ? validateProp : {};
|
|
437
|
+
const userValidator = customOpts.customValidator;
|
|
438
|
+
return {
|
|
439
|
+
enabled: validateProp !== false,
|
|
440
|
+
...customOpts,
|
|
441
|
+
// Hidden inputs are barred from native constraint validation, so
|
|
442
|
+
// `required` is enforced here, then the consumer's validator runs.
|
|
443
|
+
customValidator(val, ctx, input) {
|
|
444
|
+
if (required && !String(val ?? "").trim()) {
|
|
445
|
+
return t("required", null, "Please select a color");
|
|
446
|
+
}
|
|
447
|
+
return userValidator?.(val, ctx, input) || "";
|
|
448
|
+
},
|
|
449
|
+
setValidationResult: (res) => {
|
|
450
|
+
_validation = res;
|
|
451
|
+
setValidationResult?.(res);
|
|
452
|
+
},
|
|
453
|
+
setDoValidate: (fn) => (_doValidate = fn),
|
|
454
|
+
};
|
|
455
|
+
}}
|
|
456
|
+
/>
|
|
457
|
+
</div>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
2
|
+
import type { TranslateFn } from "../../types.js";
|
|
3
|
+
import type { ValidateOptions, ValidationResult } from "../../actions/validate.svelte.js";
|
|
4
|
+
import type { ColorPickerSwatch } from "./palettes.js";
|
|
5
|
+
/**
|
|
6
|
+
* Which custom-color controls are rendered under the palette:
|
|
7
|
+
* - `"both"` (default) — the native picker button and the hex text field
|
|
8
|
+
* - `"native"` — only the native `<input type="color">` button
|
|
9
|
+
* - `"text"` — only the hex text field
|
|
10
|
+
* - `false` — none; the palette is the only source of values
|
|
11
|
+
*/
|
|
12
|
+
export type ColorPickerCustom = "both" | "native" | "text" | false;
|
|
13
|
+
export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children" | "onchange"> {
|
|
14
|
+
/**
|
|
15
|
+
* Current color (bindable). Any CSS color string; `""` = no color. Stored
|
|
16
|
+
* verbatim — the component never converts between color spaces, so what a
|
|
17
|
+
* swatch holds is what you get back.
|
|
18
|
+
*/
|
|
19
|
+
value?: string;
|
|
20
|
+
/**
|
|
21
|
+
* The swatches. Either bare CSS color strings or `{ value, label }` objects.
|
|
22
|
+
* Defaults to `COLOR_PICKER_PALETTE` (12 hues + white/grey/black); pass
|
|
23
|
+
* `COLOR_PICKER_PALETTE_THEME` for design-token colors, or `[]` for none.
|
|
24
|
+
*/
|
|
25
|
+
palette?: ColorPickerSwatch[];
|
|
26
|
+
/**
|
|
27
|
+
* Cap the palette at this many swatches per row (a narrower container
|
|
28
|
+
* wraps to fewer rather than overflowing). Unset = as many as fit.
|
|
29
|
+
* ArrowUp / ArrowDown always step by the *rendered* row, whatever this is.
|
|
30
|
+
*/
|
|
31
|
+
columns?: number;
|
|
32
|
+
/** Which custom-color controls to show (default `"both"`) */
|
|
33
|
+
custom?: ColorPickerCustom;
|
|
34
|
+
/**
|
|
35
|
+
* Offer clearing: a crossed-out swatch after the palette, plus Delete /
|
|
36
|
+
* Backspace, plus emptying the hex field — all setting `value` to `""`.
|
|
37
|
+
*/
|
|
38
|
+
allowClear?: boolean;
|
|
39
|
+
/** Disable interaction (the hidden input is disabled too, so nothing submits) */
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
/** Accessible name of the swatch group (default `t("color")`, "Color") */
|
|
42
|
+
label?: string;
|
|
43
|
+
/** Form field name (hidden input) */
|
|
44
|
+
name?: string;
|
|
45
|
+
/** Require a non-empty value (enforced by the built-in validator) */
|
|
46
|
+
required?: boolean;
|
|
47
|
+
/** i18n translate function (see `createColorPickerT`) */
|
|
48
|
+
t?: TranslateFn;
|
|
49
|
+
/**
|
|
50
|
+
* Fires when the user commits a color: a swatch click / arrow key, the
|
|
51
|
+
* native picker's `change`, or the hex field on Enter or blur. NOT while
|
|
52
|
+
* the native picker is being dragged — `value` does update live there, so
|
|
53
|
+
* `bind:value` previews, but only the commit is an `onchange`.
|
|
54
|
+
*/
|
|
55
|
+
onchange?: (value: string) => void;
|
|
56
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
57
|
+
setValidationResult?: (res: ValidationResult) => void;
|
|
58
|
+
/** Skip all default styling */
|
|
59
|
+
unstyled?: boolean;
|
|
60
|
+
/** Additional CSS classes */
|
|
61
|
+
class?: string;
|
|
62
|
+
/** Class for every swatch button */
|
|
63
|
+
classSwatch?: string;
|
|
64
|
+
/** Bindable root element reference */
|
|
65
|
+
el?: HTMLDivElement;
|
|
66
|
+
/** Bindable hidden input reference */
|
|
67
|
+
inputEl?: HTMLInputElement;
|
|
68
|
+
}
|
|
69
|
+
declare const ColorPicker: import("svelte").Component<Props, {
|
|
70
|
+
validate: () => ValidationResult | undefined;
|
|
71
|
+
clearValidation: () => void;
|
|
72
|
+
getValidation: () => ValidationResult | undefined;
|
|
73
|
+
}, "el" | "value" | "inputEl">;
|
|
74
|
+
type ColorPicker = ReturnType<typeof ColorPicker>;
|
|
75
|
+
export default ColorPicker;
|