@spaethtech/svelte-ui 0.21.5-dev.150.7bf55da → 0.21.5-dev.151.b182d28
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.
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import FieldChrome, { nextFieldId } from "./FieldChrome.svelte";
|
|
25
25
|
import type { Size } from "../types/sizes.js";
|
|
26
26
|
import { responsiveClasses, type Responsive } from "../types/responsive.js";
|
|
27
|
-
import type
|
|
27
|
+
import { variantToken, type Variant } from "../types/variants.js";
|
|
28
28
|
|
|
29
29
|
type Option = Record<string, any> & {
|
|
30
30
|
value: string;
|
|
@@ -190,24 +190,39 @@
|
|
|
190
190
|
const base = multiSelect
|
|
191
191
|
? `relative w-full flex items-stretch [border-radius:var(--ui-border-radius)]`
|
|
192
192
|
: `relative w-full ${heightClass} flex items-center [border-radius:var(--ui-border-radius)]`;
|
|
193
|
-
const
|
|
194
|
-
const
|
|
193
|
+
const editableFrame = borderless ? "[border-color:transparent]" : borderVariantClass[variant];
|
|
194
|
+
const editableBg =
|
|
195
195
|
variant === "ghost" ? "bg-transparent" : "[background-color:var(--ui-color-background)]";
|
|
196
|
+
// Disabled picks ONE frame (not appended) so it isn't overridden by the editable border at equal
|
|
197
|
+
// specificity: the quietest frame from the field's OWN variant token via --field-accent (NOT the old
|
|
198
|
+
// secondary-blue) + the shared muted fill. A native <select> has no read-only state. The focus ring
|
|
199
|
+
// is drawn INSIDE the frame (offset -2px) so a scrolling container can't clip it (matches Input).
|
|
200
|
+
let fillStyles: string;
|
|
201
|
+
let frameStyles: string;
|
|
202
|
+
let stateStyles = "";
|
|
203
|
+
if (disabled) {
|
|
204
|
+
fillStyles =
|
|
205
|
+
"[background-color:color-mix(in_srgb,var(--ui-color-neutral)_var(--ui-tint-fill-muted),transparent)]";
|
|
206
|
+
frameStyles =
|
|
207
|
+
"[border-color:color-mix(in_srgb,var(--field-accent)_var(--ui-tint-border-disabled),transparent)]";
|
|
208
|
+
stateStyles = "cursor-not-allowed";
|
|
209
|
+
} else {
|
|
210
|
+
fillStyles = editableBg;
|
|
211
|
+
frameStyles = editableFrame;
|
|
212
|
+
stateStyles =
|
|
213
|
+
"focus-within:[outline:2px_solid_color-mix(in_srgb,currentColor_70%,transparent)] focus-within:[outline-offset:-2px]";
|
|
214
|
+
}
|
|
196
215
|
const themeStyles = [
|
|
197
|
-
|
|
216
|
+
fillStyles,
|
|
198
217
|
"border",
|
|
199
218
|
multiSelect ? "p-1" : "pl-2 pr-0",
|
|
200
219
|
frameStyles,
|
|
201
|
-
|
|
202
|
-
"focus-within:[outline-offset:0px]",
|
|
220
|
+
stateStyles,
|
|
203
221
|
].join(" ");
|
|
204
222
|
const validationStyles = invalid
|
|
205
223
|
? "[border-color:var(--ui-color-error)] focus-within:[ring-color:var(--ui-color-error)]"
|
|
206
224
|
: "";
|
|
207
|
-
|
|
208
|
-
? "![background-color:color-mix(in_srgb,var(--ui-color-text)_5%,transparent)] [border-color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border),transparent)] cursor-not-allowed"
|
|
209
|
-
: "";
|
|
210
|
-
return `${base} ${themeStyles} ${validationStyles} ${disabledStyles}`.trim();
|
|
225
|
+
return `${base} ${themeStyles} ${validationStyles}`.trim();
|
|
211
226
|
});
|
|
212
227
|
|
|
213
228
|
// The <select> itself — mirrors Input's inner <input> (flex-1, transparent, no border/outline).
|
|
@@ -237,7 +252,7 @@
|
|
|
237
252
|
|
|
238
253
|
<FieldChrome {label} {aside} {error} {description} required={!!required} {controlId} {size} class={className}>
|
|
239
254
|
{#snippet control({ describedBy })}
|
|
240
|
-
<div class={wrapperClasses}>
|
|
255
|
+
<div class={wrapperClasses} style="--field-accent: var({variantToken[variant]});">
|
|
241
256
|
<!-- The real control: native <select>. We set `selected` per-option (rather than `bind:value`,
|
|
242
257
|
which forbids a dynamic `multiple`) so SSR emits the correct selected option — right text
|
|
243
258
|
before hydration / with JS off — and `onchange` keeps client state in sync. -->
|
|
@@ -21,7 +21,7 @@ import type { HTMLSelectAttributes } from "svelte/elements";
|
|
|
21
21
|
import type { FieldChromeProps } from "./field-chrome.js";
|
|
22
22
|
import type { Size } from "../types/sizes.js";
|
|
23
23
|
import { type Responsive } from "../types/responsive.js";
|
|
24
|
-
import type
|
|
24
|
+
import { type Variant } from "../types/variants.js";
|
|
25
25
|
type Option = Record<string, any> & {
|
|
26
26
|
value: string;
|
|
27
27
|
label: string;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { Snippet } from "svelte";
|
|
7
7
|
import type { Size } from "../types/sizes.js";
|
|
8
8
|
import { responsiveClasses, resolveScalar, type Responsive } from "../types/responsive.js";
|
|
9
|
-
import type
|
|
9
|
+
import { variantToken, type Variant } from "../types/variants.js";
|
|
10
10
|
import { DEV } from "esm-env";
|
|
11
11
|
import FieldChrome, { nextFieldId } from "./FieldChrome.svelte";
|
|
12
12
|
|
|
@@ -319,19 +319,37 @@
|
|
|
319
319
|
let wrapperClasses = $derived.by(() => {
|
|
320
320
|
const base = `relative w-full [border-radius:var(--ui-border-radius)] border ${padClass}`;
|
|
321
321
|
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const fieldBg =
|
|
322
|
+
// Frame + fill follow the field's liveness, all from the variant's OWN token via `--field-accent`
|
|
323
|
+
// (set on the wrapper below; neutral/ghost → the neutral token) — same ladder + inset focus ring as
|
|
324
|
+
// Input: editable 50% > read-only 35% > disabled 15%, with a muted neutral fill for the inert states.
|
|
325
|
+
const editableFrame = borderless ? "[border-color:transparent]" : borderVariantClass[variant];
|
|
326
|
+
const editableBg =
|
|
328
327
|
variant === "ghost" ? "bg-transparent" : "[background-color:var(--ui-color-background)]";
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
"focus-within:[outline:2px_solid_color-mix(in_srgb,currentColor_70%,transparent)]"
|
|
333
|
-
|
|
334
|
-
|
|
328
|
+
const mutedFill =
|
|
329
|
+
"![background-color:color-mix(in_srgb,var(--ui-color-neutral)_var(--ui-tint-fill-muted),transparent)]";
|
|
330
|
+
const insetRing =
|
|
331
|
+
"focus-within:[outline:2px_solid_color-mix(in_srgb,currentColor_70%,transparent)] focus-within:[outline-offset:-2px]";
|
|
332
|
+
|
|
333
|
+
let fillStyles: string;
|
|
334
|
+
let frameStyles: string;
|
|
335
|
+
let stateStyles = "";
|
|
336
|
+
if (disabled) {
|
|
337
|
+
fillStyles = mutedFill;
|
|
338
|
+
frameStyles =
|
|
339
|
+
"[border-color:color-mix(in_srgb,var(--field-accent)_var(--ui-tint-border-disabled),transparent)]";
|
|
340
|
+
stateStyles = "cursor-not-allowed";
|
|
341
|
+
} else if (readonly) {
|
|
342
|
+
fillStyles = mutedFill;
|
|
343
|
+
frameStyles =
|
|
344
|
+
"[border-color:color-mix(in_srgb,var(--field-accent)_var(--ui-tint-border-readonly),transparent)] hover:[border-color:color-mix(in_srgb,var(--field-accent)_var(--ui-tint-border-hover),transparent)] focus-within:[border-color:color-mix(in_srgb,var(--field-accent)_var(--ui-tint-border-hover),transparent)]";
|
|
345
|
+
stateStyles = insetRing;
|
|
346
|
+
} else {
|
|
347
|
+
fillStyles = editableBg;
|
|
348
|
+
frameStyles = editableFrame;
|
|
349
|
+
stateStyles = insetRing;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const themeStyles = [fillStyles, frameStyles, stateStyles].join(" ");
|
|
335
353
|
|
|
336
354
|
// Error state (shared): chrome `error` set OR a failing `validate` (once touched).
|
|
337
355
|
let validationStyles = "";
|
|
@@ -340,12 +358,7 @@
|
|
|
340
358
|
"[border-color:var(--ui-color-error)] focus-within:[outline:2px_solid_color-mix(in_srgb,var(--ui-color-error)_70%,transparent)]";
|
|
341
359
|
}
|
|
342
360
|
|
|
343
|
-
|
|
344
|
-
const disabledStyles = disabled
|
|
345
|
-
? "![background-color:color-mix(in_srgb,var(--ui-color-text)_5%,transparent)] [border-color:color-mix(in_srgb,var(--ui-color-secondary)_50%,transparent)] cursor-not-allowed"
|
|
346
|
-
: "";
|
|
347
|
-
|
|
348
|
-
return `${base} ${themeStyles} ${validationStyles} ${disabledStyles}`.trim();
|
|
361
|
+
return `${base} ${themeStyles} ${validationStyles}`.trim();
|
|
349
362
|
});
|
|
350
363
|
|
|
351
364
|
let textareaClasses = $derived.by(() => {
|
|
@@ -358,6 +371,9 @@
|
|
|
358
371
|
"disabled:bg-transparent",
|
|
359
372
|
"disabled:[color:color-mix(in_srgb,var(--ui-color-text)_40%,transparent)]",
|
|
360
373
|
"disabled:cursor-not-allowed",
|
|
374
|
+
// Read-only (but not disabled): text cursor, no caret — matches Input. (`effectiveReadonly` sets
|
|
375
|
+
// the readonly attr on disabled too, so gate on the raw props to avoid fighting disabled's cursor.)
|
|
376
|
+
readonly && !disabled ? "cursor-text [caret-color:transparent]" : "",
|
|
361
377
|
].join(" ");
|
|
362
378
|
|
|
363
379
|
// Padding adjustment for the copy icon (only when the button is shown)
|
|
@@ -708,6 +724,7 @@
|
|
|
708
724
|
}}
|
|
709
725
|
role="group"
|
|
710
726
|
aria-label={i18n.messages.textAreaControls}
|
|
727
|
+
style="--field-accent: var({variantToken[variant]});"
|
|
711
728
|
>
|
|
712
729
|
<textarea
|
|
713
730
|
bind:this={textareaElement}
|
|
@@ -2,7 +2,7 @@ import type { HTMLTextareaAttributes } from "svelte/elements";
|
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import type { Size } from "../types/sizes.js";
|
|
4
4
|
import { type Responsive } from "../types/responsive.js";
|
|
5
|
-
import type
|
|
5
|
+
import { type Variant } from "../types/variants.js";
|
|
6
6
|
type TextAreaCopy = "none" | "selection" | "button";
|
|
7
7
|
interface Props extends Omit<HTMLTextareaAttributes, "oncopy"> {
|
|
8
8
|
value?: string;
|