@spaethtech/svelte-ui 0.7.1-dev.39.d6b0fa5 → 0.7.1-dev.41.d01009c

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.
@@ -154,7 +154,9 @@ examples):
154
154
 
155
155
  - **Form:** `Button` `ButtonDropdown` `Input` `Select` `List` `TextArea` `Checkbox` `Toggle` `Radio`
156
156
  `Rating` · **`FieldGroup`** (fieldset wrapper for radio/checkbox/toggle sets)
157
- - **Specialized inputs:** `PasswordInput` `EmailInput` `SearchInput` `NumberInput`
157
+ - **Specialized inputs:** `PasswordInput` `EmailInput` `SearchInput` `NumberInput` (formatting +
158
+ `percent`/`stepper`/`clamp`/`liveFormat`) `PhoneInput` (stores E.164; dep-free, inject
159
+ `parse`/`format` for per-country)
158
160
  - **Date / time:** `DatePicker` `Calendar` `TimePicker` `TimeSpinner` `TimeRangeInput` `DateTimeInput`
159
161
  - **Data:** `DataTable` `Query` — driven by the headless layer at **`@spaethtech/svelte-ui/data`**
160
162
  (query-language parser/AST, `createGrid`, `DataGrid<T>`, `DataSet`).
@@ -67,6 +67,8 @@ variant token at shared strengths, so every tinted surface is calculated identic
67
67
 
68
68
  ```
69
69
  --ui-tint-soft 12% soft resting fill (Badge fill, Alert `filled`)
70
+ --ui-tint-divider 30% soft internal divider (CardHeader/CardFooter separators; the default
71
+ secondary divider is `--ui-border-color`)
70
72
  --ui-tint-border 50% bordered surface, REST (Badge/Alert/Banner ring · Card/DataTable/Input/
71
73
  TextArea frame · Button outline)
72
74
  --ui-tint-border-hover 80% bordered field, HOVER (Input/TextArea/Card/DataTable/Button — the
@@ -78,6 +80,8 @@ e.g. `color-mix(in srgb, var(<variant-token>) var(--ui-tint-soft), transparent)`
78
80
  primary@these; a `neutral` one is text@these. Theme-independent (a theme MAY override them to tune the
79
81
  whole tint language). **Every bordered surface uses ONE calc** — `--ui-tint-border` at rest,
80
82
  `--ui-tint-border-hover` on hover/focus, from the variant token — with **no `secondary` special case**.
83
+ Internal **dividers** (Card header/footer separators) use the softer `--ui-tint-divider` (30%) tier,
84
+ likewise one calc per variant — so a divider always reads lighter than the frame at any variant.
81
85
  `--ui-border-color-strong`/`-strong-hover` have been **REMOVED** (migrate any consumer use to
82
86
  `color-mix(in srgb, var(--ui-color-secondary) var(--ui-tint-border), transparent)`, hover →
83
87
  `--ui-tint-border-hover`). (Distinct from the interactive tints above, which mix from `--ui-accent`.)
@@ -36,11 +36,10 @@
36
36
  const sepGap: Record<Size, string> = { sm: "mt-2", md: "mt-3", lg: "mt-4" };
37
37
  // The separator is a SOFT internal divider, consistent with the other section splits/footers in the
38
38
  // library (DatePicker/TimePicker/TimeRangeInput) — deliberately lighter than the Card frame + fields.
39
- // Default (secondary) the shared `--ui-border-color`; any other variant → that accent's tint.
39
+ // One calc for EVERY variant: the variant's token mixed at `--ui-tint-divider` (30%), not the 50%
40
+ // frame strength. `secondary` resolves identically to the shared `--ui-border-color`.
40
41
  const sepColor = $derived(
41
- variant === "secondary"
42
- ? "var(--ui-border-color)"
43
- : `color-mix(in srgb, var(${variantToken[variant]}) var(--ui-tint-border), transparent)`,
42
+ `color-mix(in srgb, var(${variantToken[variant]}) var(--ui-tint-divider), transparent)`,
44
43
  );
45
44
  </script>
46
45
 
@@ -36,11 +36,10 @@
36
36
  const sepGap: Record<Size, string> = { sm: "mb-2", md: "mb-3", lg: "mb-4" };
37
37
  // The separator is a SOFT internal divider, consistent with the other section splits/footers in the
38
38
  // library (DatePicker/TimePicker/TimeRangeInput) — deliberately lighter than the Card frame + fields.
39
- // Default (secondary) the shared `--ui-border-color`; any other variant → that accent's tint.
39
+ // One calc for EVERY variant: the variant's token mixed at `--ui-tint-divider` (30%), not the 50%
40
+ // frame strength. `secondary` resolves identically to the shared `--ui-border-color`.
40
41
  const sepColor = $derived(
41
- variant === "secondary"
42
- ? "var(--ui-border-color)"
43
- : `color-mix(in srgb, var(${variantToken[variant]}) var(--ui-tint-border), transparent)`,
42
+ `color-mix(in srgb, var(${variantToken[variant]}) var(--ui-tint-divider), transparent)`,
44
43
  );
45
44
  </script>
46
45
 
@@ -0,0 +1,120 @@
1
+ <script lang="ts">
2
+ import Input from "./Input.svelte";
3
+ import Phone from "~icons/mdi/phone-outline";
4
+ import type { HTMLInputAttributes } from "svelte/elements";
5
+ import type { Size } from "../types/sizes.js";
6
+ import type { Responsive } from "../types/responsive.js";
7
+ import type { Variant } from "../types/variants.js";
8
+
9
+ /**
10
+ * PhoneInput — type a phone number in ANY format; the bound `value` is stored in **E.164**
11
+ * (`+15551234567`): a leading `+`, country calling code, then the national number, digits only.
12
+ * The field shows what you type while editing and snaps to canonical E.164 on blur.
13
+ *
14
+ * **Dependency-free** by design — it normalises the E.164 *shape* only and does NOT do per-country
15
+ * formatting/validation (that needs a phone library + country data, which we don't bundle). To add
16
+ * that, inject `parse` / `format` / `validate` (e.g. backed by `libphonenumber-js`) — the component
17
+ * stays dep-free; you choose the library.
18
+ */
19
+ interface Props extends Omit<HTMLInputAttributes, "type" | "value" | "size"> {
20
+ /** Bound value in E.164 (`+15551234567`), normalised from whatever the user types. */
21
+ value: string;
22
+ class?: string;
23
+ inputClass?: string;
24
+ /** Validates the normalised E.164 value. Overrides the built-in E.164-shape check. */
25
+ validate?: (value: string) => boolean | string;
26
+ valid?: boolean;
27
+ touched?: boolean;
28
+ element?: HTMLInputElement;
29
+ size?: Responsive<Size>;
30
+ /** Shared axes — forwarded to the underlying Input. */
31
+ variant?: Variant;
32
+ borderless?: boolean;
33
+ required?: boolean;
34
+ /** Calling code (digits, no `+`) prepended when the typed number has no leading `+`. Default `"1"`
35
+ * (North America). Set to the calling code of the numbers your users enter without a `+`. */
36
+ defaultCallingCode?: string;
37
+ /** Normalise typed text → stored E.164. Override to plug a library (e.g. libphonenumber-js). */
38
+ parse?: (input: string, defaultCallingCode: string) => string;
39
+ /** Format the stored E.164 → field display (default: shown as-is). Override for a national /
40
+ * pretty format from your phone library. */
41
+ format?: (value: string) => string;
42
+ }
43
+
44
+ let {
45
+ value = $bindable(""),
46
+ valid = $bindable(true),
47
+ touched = $bindable(false),
48
+ element = $bindable(),
49
+ validate,
50
+ required = false,
51
+ defaultCallingCode = "1",
52
+ parse,
53
+ format,
54
+ placeholder = "(555) 123-4567",
55
+ ...restProps
56
+ }: Props = $props();
57
+
58
+ let displayValue = $state("");
59
+ let focused = $state(false);
60
+
61
+ // Default normalisation: an explicit international `+…` is kept (digits only after the `+`);
62
+ // otherwise the default calling code is prepended. SHAPE only — no per-country length check.
63
+ function defaultParse(raw: string, dcc: string): string {
64
+ const trimmed = (raw ?? "").trim();
65
+ if (!trimmed) return "";
66
+ const digits = trimmed.replace(/\D/g, "");
67
+ if (!digits) return "";
68
+ return trimmed.includes("+") ? `+${digits}` : `+${dcc}${digits}`;
69
+ }
70
+
71
+ const toE164 = (raw: string) => (parse ?? defaultParse)(raw, defaultCallingCode);
72
+ const toDisplay = (v: string) => (v ? (format ? format(v) : v) : "");
73
+
74
+ // Reflect external value changes into the field while the user isn't editing.
75
+ $effect(() => {
76
+ if (!focused) displayValue = toDisplay(value);
77
+ });
78
+
79
+ const defaultValidator = (v: string) => {
80
+ if (!v) return required ? "Phone number is required" : true;
81
+ // E.164 shape: `+`, a non-zero country digit, then 6–14 more digits (7–15 total).
82
+ if (!/^\+[1-9]\d{6,14}$/.test(v)) return "Enter a valid phone number";
83
+ return true;
84
+ };
85
+ const phoneValidator = $derived(validate ?? defaultValidator);
86
+
87
+ function handleInput(raw: string) {
88
+ displayValue = raw;
89
+ value = toE164(raw);
90
+ }
91
+ function handleFocus() {
92
+ focused = true;
93
+ displayValue = toDisplay(value);
94
+ }
95
+ function handleBlur() {
96
+ focused = false;
97
+ displayValue = toDisplay(value); // snap the field to the canonical (or your `format`) form
98
+ }
99
+
100
+ let stringValue = $derived(displayValue);
101
+ </script>
102
+
103
+ <Input
104
+ bind:value={stringValue}
105
+ bind:valid
106
+ bind:touched
107
+ bind:element
108
+ type="tel"
109
+ inputmode="tel"
110
+ autocomplete="tel"
111
+ {required}
112
+ validate={(val) => phoneValidator(toE164(val))}
113
+ oninput={(e) => handleInput((e.target as HTMLInputElement).value)}
114
+ onfocus={handleFocus}
115
+ onblur={handleBlur}
116
+ {placeholder}
117
+ {...restProps}
118
+ >
119
+ {#snippet icon()}<Phone />{/snippet}
120
+ </Input>
@@ -0,0 +1,41 @@
1
+ import type { HTMLInputAttributes } from "svelte/elements";
2
+ import type { Size } from "../types/sizes.js";
3
+ import type { Responsive } from "../types/responsive.js";
4
+ import type { Variant } from "../types/variants.js";
5
+ /**
6
+ * PhoneInput — type a phone number in ANY format; the bound `value` is stored in **E.164**
7
+ * (`+15551234567`): a leading `+`, country calling code, then the national number, digits only.
8
+ * The field shows what you type while editing and snaps to canonical E.164 on blur.
9
+ *
10
+ * **Dependency-free** by design — it normalises the E.164 *shape* only and does NOT do per-country
11
+ * formatting/validation (that needs a phone library + country data, which we don't bundle). To add
12
+ * that, inject `parse` / `format` / `validate` (e.g. backed by `libphonenumber-js`) — the component
13
+ * stays dep-free; you choose the library.
14
+ */
15
+ interface Props extends Omit<HTMLInputAttributes, "type" | "value" | "size"> {
16
+ /** Bound value in E.164 (`+15551234567`), normalised from whatever the user types. */
17
+ value: string;
18
+ class?: string;
19
+ inputClass?: string;
20
+ /** Validates the normalised E.164 value. Overrides the built-in E.164-shape check. */
21
+ validate?: (value: string) => boolean | string;
22
+ valid?: boolean;
23
+ touched?: boolean;
24
+ element?: HTMLInputElement;
25
+ size?: Responsive<Size>;
26
+ /** Shared axes — forwarded to the underlying Input. */
27
+ variant?: Variant;
28
+ borderless?: boolean;
29
+ required?: boolean;
30
+ /** Calling code (digits, no `+`) prepended when the typed number has no leading `+`. Default `"1"`
31
+ * (North America). Set to the calling code of the numbers your users enter without a `+`. */
32
+ defaultCallingCode?: string;
33
+ /** Normalise typed text → stored E.164. Override to plug a library (e.g. libphonenumber-js). */
34
+ parse?: (input: string, defaultCallingCode: string) => string;
35
+ /** Format the stored E.164 → field display (default: shown as-is). Override for a national /
36
+ * pretty format from your phone library. */
37
+ format?: (value: string) => string;
38
+ }
39
+ declare const PhoneInput: import("svelte").Component<Props, {}, "element" | "value" | "valid" | "touched">;
40
+ type PhoneInput = ReturnType<typeof PhoneInput>;
41
+ export default PhoneInput;
package/dist/index.d.ts CHANGED
@@ -21,6 +21,7 @@ export { default as PasswordInput } from "./components/PasswordInput.svelte";
21
21
  export { default as EmailInput } from "./components/EmailInput.svelte";
22
22
  export { default as SearchInput } from "./components/SearchInput.svelte";
23
23
  export { default as NumberInput } from "./components/NumberInput.svelte";
24
+ export { default as PhoneInput } from "./components/PhoneInput.svelte";
24
25
  export { default as DateTimeInput } from "./components/DateTimeInput.svelte";
25
26
  export { default as Calendar } from "./components/Calendar.svelte";
26
27
  export { default as DatePicker } from "./components/DatePicker.svelte";
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ export { default as PasswordInput } from "./components/PasswordInput.svelte";
24
24
  export { default as EmailInput } from "./components/EmailInput.svelte";
25
25
  export { default as SearchInput } from "./components/SearchInput.svelte";
26
26
  export { default as NumberInput } from "./components/NumberInput.svelte";
27
+ export { default as PhoneInput } from "./components/PhoneInput.svelte";
27
28
  export { default as DateTimeInput } from "./components/DateTimeInput.svelte";
28
29
  export { default as Calendar } from "./components/Calendar.svelte";
29
30
  export { default as DatePicker } from "./components/DatePicker.svelte";
package/dist/theme.css CHANGED
@@ -67,9 +67,10 @@
67
67
  * Fixed, NOT scaled by `size`, so different-size controls share a
68
68
  * corner. (Pills like Toggle/Badge use rounded-full; the tiny
69
69
  * Checkbox box keeps its own small radius — not worth a token.)
70
- * --ui-border-color soft internal divider (row separators, section splits) */
70
+ * --ui-border-color soft internal divider (row separators, section splits)
71
+ * the default (secondary) divider, mixed at `--ui-tint-divider` */
71
72
  --ui-border-radius: 0.375rem;
72
- --ui-border-color: color-mix(in srgb, var(--ui-color-secondary) 30%, transparent);
73
+ --ui-border-color: color-mix(in srgb, var(--ui-color-secondary) var(--ui-tint-divider), transparent);
73
74
  }
74
75
 
75
76
  /* One interactive-surface language, library-wide. A subtle tint of the ACCENT colour, so it lightens on
@@ -104,15 +105,17 @@
104
105
  /* Tint SCALE for a variant-tinted surface (Badge chip, Card frame, `filled` Alert…). The colour is
105
106
  * always the component's own variant token; these are the shared STRENGTHS it's mixed at, so every
106
107
  * tinted surface is calculated the same way — change one number here, every component follows:
107
- * fill: color-mix(in srgb, var(<token>) var(--ui-tint-soft), transparent) soft resting fill
108
- * border: color-mix(in srgb, var(<token>) var(--ui-tint-border), transparent) divider/frame (rest)
109
- * hover: color-mix(in srgb, var(<token>) var(--ui-tint-border-hover), transparent) bordered field, hover/focus
110
- * text: color-mix(in srgb, var(<token>) var(--ui-tint-strong), var(--ui-color-text)) foreground
108
+ * fill: color-mix(in srgb, var(<token>) var(--ui-tint-soft), transparent) soft resting fill
109
+ * divider: color-mix(in srgb, var(<token>) var(--ui-tint-divider), transparent) soft internal divider
110
+ * border: color-mix(in srgb, var(<token>) var(--ui-tint-border), transparent) frame / field border (rest)
111
+ * hover: color-mix(in srgb, var(<token>) var(--ui-tint-border-hover), transparent) bordered field, hover/focus
112
+ * text: color-mix(in srgb, var(<token>) var(--ui-tint-strong), var(--ui-color-text)) foreground
111
113
  * A primary badge is primary@soft/border/strong; a neutral one is text@… — one calculation, any
112
114
  * variant. `--ui-tint-border-hover` is the STRONGER step an interactive bordered field (Input,
113
115
  * TextArea, an outline Button) moves to on hover/focus. Theme-independent constants (no colour → no
114
116
  * per-theme scope needed) — but a theme MAY override these to tune the whole tint language. */
115
117
  --ui-tint-soft: 12%;
118
+ --ui-tint-divider: 30%; /* soft internal dividers — deliberately lighter than the 50% frame/field border */
116
119
  --ui-tint-border: 50%;
117
120
  --ui-tint-border-hover: 80%;
118
121
  --ui-tint-strong: 80%;
@@ -207,6 +207,20 @@ blur/submit).
207
207
  and **`percent`** (display ×100 `%`, store the fraction; `min`/`max`/`step` in fraction units).
208
208
  - Dep-free by design — for locale/`Intl` or currency codes, format upstream or via `validate`.
209
209
 
210
+ ### PhoneInput
211
+
212
+ Type any format; stores the bound value as **E.164** (`+15551234567`). Field shows what you type,
213
+ snaps to canonical on blur.
214
+
215
+ - **Location**: `src/lib/components/PhoneInput.svelte`
216
+ - **Value**: `string` (E.164)
217
+ - **Props**: `defaultCallingCode` (digits, default `"1"` — prepended when input has no `+`), `required`,
218
+ `validate` (validates the E.164), and the injection hooks **`parse`** (typed → E.164) / **`format`**
219
+ (E.164 → display).
220
+ - **Dep-free** — normalises the E.164 *shape* only (no per-country validation/formatting). For that,
221
+ inject `parse`/`format`/`validate` backed by a phone library (e.g. `libphonenumber-js`); the
222
+ component bundles none.
223
+
210
224
  ## Navigation
211
225
 
212
226
  ### TabStrip
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.7.1-dev.39.d6b0fa5",
3
+ "version": "0.7.1-dev.41.d01009c",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"