@spaethtech/svelte-ui 0.21.5-dev.149.8ed1480 → 0.21.5-dev.150.7bf55da
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/dist/components/Input.svelte +48 -20
- package/dist/components/Input.svelte.d.ts +1 -1
- package/dist/theme.css +3 -0
- package/docs/themes.md +21 -0
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { HTMLInputAttributes } from "svelte/elements";
|
|
4
4
|
import type { Size } from "../types/sizes.js";
|
|
5
5
|
import { responsiveClasses, type Responsive } from "../types/responsive.js";
|
|
6
|
-
import type
|
|
6
|
+
import { variantToken, type Variant } from "../types/variants.js";
|
|
7
7
|
import FieldChrome, { nextFieldId } from "./FieldChrome.svelte";
|
|
8
8
|
|
|
9
9
|
// Omit the native `size` attribute (character width, a number) — we repurpose `size` as the
|
|
@@ -191,22 +191,50 @@
|
|
|
191
191
|
paddingClasses = "px-2";
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
194
|
+
// Frame + fill follow the field's "liveness", ordered by the shared border-tint ladder — all from
|
|
195
|
+
// the variant's OWN token via `--field-accent` (set on the wrapper below; neutral/ghost → the
|
|
196
|
+
// neutral token), so there's NO `secondary` special-case: editable 50% > read-only 35% > disabled
|
|
197
|
+
// 15% (quiet → live). Read-only and disabled take the muted neutral fill; editable fills with the
|
|
198
|
+
// page background. `borderless` (editable only) drops the visible frame while keeping the 1px box.
|
|
199
|
+
const editableFrame = borderless ? "[border-color:transparent]" : borderVariantClass[variant];
|
|
199
200
|
// Field background follows the variant axis: `ghost` is transparent (chromeless); every other
|
|
200
201
|
// variant fills with --ui-color-background so the field reads as a field on a non-white page.
|
|
201
|
-
const
|
|
202
|
+
const editableBg =
|
|
202
203
|
variant === "ghost" ? "bg-transparent" : "[background-color:var(--ui-color-background)]";
|
|
204
|
+
const mutedFill =
|
|
205
|
+
"[background-color:color-mix(in_srgb,var(--ui-color-neutral)_var(--ui-tint-fill-muted),transparent)]";
|
|
206
|
+
|
|
207
|
+
let fillStyles: string;
|
|
208
|
+
let frameStyles: string;
|
|
209
|
+
let stateStyles = ""; // cursor + (for read-only) the inset focus ring
|
|
210
|
+
if (disabled) {
|
|
211
|
+
// Quietest frame; muted fill; not focusable (no ring). No more secondary-blue border.
|
|
212
|
+
fillStyles = mutedFill;
|
|
213
|
+
frameStyles =
|
|
214
|
+
"[border-color:color-mix(in_srgb,var(--field-accent)_var(--ui-tint-border-disabled),transparent)]";
|
|
215
|
+
stateStyles = "cursor-not-allowed";
|
|
216
|
+
} else if (readonly) {
|
|
217
|
+
// A distinct read-only look: quieter-than-editable frame (35% → 80% on hover/focus), muted fill,
|
|
218
|
+
// full-strength text, and the focus ring kept (keyboard users land here). The ring is drawn INSIDE
|
|
219
|
+
// the frame (outline-offset -2px) so a scrolling Card/Dialog can't clip it.
|
|
220
|
+
fillStyles = mutedFill;
|
|
221
|
+
frameStyles =
|
|
222
|
+
"[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)]";
|
|
223
|
+
stateStyles =
|
|
224
|
+
"focus-within:[outline:2px_solid_color-mix(in_srgb,currentColor_70%,transparent)] focus-within:[outline-offset:-2px]";
|
|
225
|
+
} else {
|
|
226
|
+
fillStyles = editableBg;
|
|
227
|
+
frameStyles = editableFrame;
|
|
228
|
+
stateStyles =
|
|
229
|
+
"focus-within:[outline:2px_solid_color-mix(in_srgb,currentColor_70%,transparent)] focus-within:[outline-offset:-2px]";
|
|
230
|
+
}
|
|
231
|
+
|
|
203
232
|
const themeStyles = [
|
|
204
|
-
|
|
233
|
+
fillStyles,
|
|
205
234
|
"border", // 1px — matches --ui-border (List/TextArea); uniform across the library
|
|
206
235
|
paddingClasses,
|
|
207
236
|
frameStyles,
|
|
208
|
-
|
|
209
|
-
"focus-within:[outline-offset:0px]",
|
|
237
|
+
stateStyles,
|
|
210
238
|
].join(" ");
|
|
211
239
|
|
|
212
240
|
// Error state (shared): chrome `error` set OR a failing `validate` (once touched).
|
|
@@ -216,15 +244,7 @@
|
|
|
216
244
|
"[border-color:var(--ui-color-error)] focus-within:[ring-color:var(--ui-color-error)]";
|
|
217
245
|
}
|
|
218
246
|
|
|
219
|
-
|
|
220
|
-
// (readonly renders like a normal field: same 1px frame + hover/focus glow, e.g. Select's trigger.)
|
|
221
|
-
let disabledStyles = "";
|
|
222
|
-
if (disabled) {
|
|
223
|
-
disabledStyles =
|
|
224
|
-
"![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";
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return `${base} ${themeStyles} ${validationStyles} ${disabledStyles}`.trim();
|
|
247
|
+
return `${base} ${themeStyles} ${validationStyles}`.trim();
|
|
228
248
|
});
|
|
229
249
|
|
|
230
250
|
let inputClasses = $derived.by(() => {
|
|
@@ -240,6 +260,10 @@
|
|
|
240
260
|
"disabled:bg-transparent",
|
|
241
261
|
"disabled:[color:color-mix(in_srgb,var(--ui-color-text)_40%,transparent)]",
|
|
242
262
|
"disabled:cursor-not-allowed",
|
|
263
|
+
// Read-only keeps full-strength text but reads as non-editable: a text cursor (it's selectable)
|
|
264
|
+
// and no blinking caret (nothing to type).
|
|
265
|
+
"read-only:cursor-text",
|
|
266
|
+
"read-only:[caret-color:transparent]",
|
|
243
267
|
].join(" ");
|
|
244
268
|
|
|
245
269
|
return `${base} ${themeStyles} ${inputClass}`.trim();
|
|
@@ -323,7 +347,11 @@
|
|
|
323
347
|
class={containerClass}
|
|
324
348
|
>
|
|
325
349
|
{#snippet control({ describedBy })}
|
|
326
|
-
<div
|
|
350
|
+
<div
|
|
351
|
+
bind:this={fieldElement}
|
|
352
|
+
class="relative {wrapperClasses}"
|
|
353
|
+
style="user-select: none; --field-accent: var({variantToken[variant]});"
|
|
354
|
+
>
|
|
327
355
|
{#if icon && iconPosition === "left"}
|
|
328
356
|
{#if iconClickable}
|
|
329
357
|
<div
|
|
@@ -2,7 +2,7 @@ import type { Snippet } from "svelte";
|
|
|
2
2
|
import type { HTMLInputAttributes } from "svelte/elements";
|
|
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
|
interface Props extends Omit<HTMLInputAttributes, "size"> {
|
|
7
7
|
/** Accepts number too so remote-form field spreads (`{...field.as(...)}`) type-check. */
|
|
8
8
|
value?: string | number;
|
package/dist/theme.css
CHANGED
|
@@ -124,7 +124,10 @@
|
|
|
124
124
|
* TextArea, an outline Button) moves to on hover/focus. Theme-independent constants (no colour → no
|
|
125
125
|
* per-theme scope needed) — but a theme MAY override these to tune the whole tint language. */
|
|
126
126
|
--ui-tint-soft: 12%;
|
|
127
|
+
--ui-tint-fill-muted: 5%; /* disabled / read-only field fill — a faint neutral wash (quieter than soft) */
|
|
128
|
+
--ui-tint-border-disabled: 15%; /* disabled field frame — the QUIETEST frame (a disabled control recedes) */
|
|
127
129
|
--ui-tint-divider: 30%; /* soft internal dividers — deliberately lighter than the 50% frame/field border */
|
|
130
|
+
--ui-tint-border-readonly: 35%; /* read-only field frame — quieter than editable (50%), louder than a divider */
|
|
128
131
|
--ui-tint-border: 50%;
|
|
129
132
|
--ui-tint-border-hover: 80%;
|
|
130
133
|
--ui-tint-strong: 80%;
|
package/docs/themes.md
CHANGED
|
@@ -65,6 +65,27 @@ theme-independent** (identical in light and dark) — only the accent color flip
|
|
|
65
65
|
`color-mix(in srgb, var(--ui-color-neutral) var(--ui-tint-border), transparent)` for a frame edge,
|
|
66
66
|
`--ui-tint-border-hover` for its hover.)
|
|
67
67
|
|
|
68
|
+
### Field states — the border-tint ladder
|
|
69
|
+
|
|
70
|
+
A field's frame reflects how "live" it is, all from the **variant's own token** (so a `neutral` field is
|
|
71
|
+
monochrome, a `primary` field tints blue — there is **no `secondary` special-case** for disabled):
|
|
72
|
+
|
|
73
|
+
| state | border tint | fill |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| editable | `--ui-tint-border` (50%) → `--ui-tint-border-hover` (80%) on hover/focus | page background |
|
|
76
|
+
| **read-only** | `--ui-tint-border-readonly` (**35%**) → 80% on hover/focus | `--ui-tint-fill-muted` |
|
|
77
|
+
| **disabled** | `--ui-tint-border-disabled` (**15%**) | `--ui-tint-fill-muted` |
|
|
78
|
+
|
|
79
|
+
- `--ui-tint-border-readonly` — `35%` — read-only frame (quieter than editable, louder than a divider).
|
|
80
|
+
- `--ui-tint-border-disabled` — `15%` — disabled frame, the **quietest** (a disabled control recedes).
|
|
81
|
+
- `--ui-tint-fill-muted` — `5%` — the faint neutral wash behind a read-only or disabled field
|
|
82
|
+
(`color-mix(in srgb, var(--ui-color-neutral) var(--ui-tint-fill-muted), transparent)`).
|
|
83
|
+
|
|
84
|
+
Read-only keeps **full-strength text**, a text cursor, and the focus ring (it's selectable/focusable),
|
|
85
|
+
but hides the caret; disabled dims its text to 40%. The focus ring is drawn **inside** the frame
|
|
86
|
+
(`outline-offset: -2px`) so a scrolling `Card`/`Dialog` can't clip it. Overriding these percentages
|
|
87
|
+
per theme retunes read-only/disabled everywhere at once.
|
|
88
|
+
|
|
68
89
|
## Sizing — the `size` axis (`sm` / `md` / `lg`)
|
|
69
90
|
|
|
70
91
|
### Control heights
|