@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 { Variant } from "../types/variants.js";
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 frameStyles = borderless ? "[border-color:transparent]" : borderVariantClass[variant];
194
- const fieldBg =
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
- fieldBg,
216
+ fillStyles,
198
217
  "border",
199
218
  multiSelect ? "p-1" : "pl-2 pr-0",
200
219
  frameStyles,
201
- "focus-within:[outline:2px_solid_color-mix(in_srgb,currentColor_70%,transparent)]",
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
- const disabledStyles = disabled
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 { Variant } from "../types/variants.js";
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 { Variant } from "../types/variants.js";
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
- // Border colour from the variant map (same calc as Input); `borderless` → transparent (its own
323
- // background), keeping the 1px box + focus outline.
324
- const frameStyles = borderless ? "[border-color:transparent]" : borderVariantClass[variant];
325
- // Field background follows the variant axis: `ghost` transparent; every other variant fills with
326
- // --ui-color-background so the field reads as a field on a non-white surface.
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 themeStyles = [
330
- fieldBg,
331
- frameStyles,
332
- "focus-within:[outline:2px_solid_color-mix(in_srgb,currentColor_70%,transparent)]",
333
- "focus-within:[outline-offset:0px]",
334
- ].join(" ");
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
- // Disabled state — borders are uniformly 1px, so no padding compensation is needed.
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 { Variant } from "../types/variants.js";
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.21.5-dev.150.7bf55da",
3
+ "version": "0.21.5-dev.151.b182d28",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"