@spaethtech/svelte-ui 0.21.1-dev.120.d0d8cfa → 0.21.1-dev.122.60c38bb

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/MIGRATION.md CHANGED
@@ -58,3 +58,52 @@ grep -rl 'Select' src | xargs sed -i \
58
58
 
59
59
  Verify: the field lines up with its siblings, submits in your form with JS disabled, and shows the
60
60
  right option on first paint (view source — the `selected` attribute is server-rendered).
61
+
62
+ ### `Disclosure2` / `Accordion2` vs `Disclosure` / `Accordion`
63
+
64
+ `Disclosure2` is a real `<details>/<summary>`; `Accordion2` groups them with a shared native
65
+ `<details name>`. The **header is pixel-identical** to `Disclosure`'s `Button` header. Differences:
66
+
67
+ | `Disclosure`/`Accordion` prop | `*2` behaviour |
68
+ | --- | --- |
69
+ | `title`, `header`, `icon`, `size`, `variant`, `disabled`, `id`, `class`, `children` | ✅ Honoured (identical look). `disabled` makes the summary non-interactive + `aria-disabled` (a native `<details>` has no real disabled state). |
70
+ | `open` (Disclosure) | ✅ Sets the initial `open` attribute (SSR-correct) and is kept in sync on `toggle`. With JS off, the browser owns subsequent toggles. |
71
+ | `value` (Disclosure, Accordion) | ✅ Used to compute which panel starts open inside `Accordion2`. In `Accordion2` the group `value` is **initial state only** — with JS off the binding doesn't track user toggles (the browser does, via `name`). |
72
+ | `multiple` (Accordion) | ✅ `false` → shared `<details name>` (browser single-open); `true` → no name (independent). |
73
+ | panel height **animation** (`grid-template-rows: 0fr→1fr`) | ⚠️ Open/close is **instant** — a native `<details>` doesn't height-animate (pre-`::details-content`). The chevron still rotates. Matches reduced-motion behaviour. |
74
+ | `collapsible={false}` (Accordion single mode) | ❌ Best-effort — "never zero open" can't be enforced without JS. |
75
+ | panel stays **mounted when closed** (`inert`) | ⚠️ A closed `<details>` hides its content natively; it is not kept in the a11y tree the same way. Don't rely on closed-panel DOM state. |
76
+
77
+ `Disclosure2` adds one prop over `Disclosure`: **`name`** (the native `<details name>` — set it
78
+ yourself for a no-JS group without `Accordion2`, or let `Accordion2` provide it).
79
+
80
+ **When NOT to swap:** if you depend on the height animation or on reading open state back through the
81
+ binding without JS, stay on `Disclosure`/`Accordion`.
82
+
83
+ **Codemod (per app, when adopting):**
84
+
85
+ ```bash
86
+ grep -rl 'Disclosure\|Accordion' src | xargs sed -i \
87
+ -e 's/\b\(Disclosure\|Accordion\)\b/\12/g'
88
+ # Review: this also rewrites import specifiers and any prose; check each hit.
89
+ ```
90
+
91
+ ### `DatePicker2` vs `DatePicker`
92
+
93
+ `DatePicker2` reuses `Input` with a real `<input type="date">` / `datetime-local`. **Unlike the other
94
+ `*2` components, the trigger content is NOT a pixel match** — the native control shows editable date
95
+ segments + the browser's picker indicator, not `DatePicker`'s formatted label + MDI calendar. The
96
+ field *frame* is identical (same `Input`), and the value format is identical, so the binding is
97
+ drop-in for single/datetime.
98
+
99
+ | `DatePicker` prop | `DatePicker2` behaviour |
100
+ | --- | --- |
101
+ | `value` (single `YYYY-MM-DD`, datetime `YYYY-MM-DDTHH:MM`), `time`, `min`, `max`, `size`, `variant`, `disabled`, `name`, `class` | ✅ Honoured. `name` sits on the `<input>` (submits natively — no hidden input). |
102
+ | `range` | ❌ **No native equivalent.** Falls back to a single date input (dev warning). Use two `DatePicker2`s (start/end) or stay on `DatePicker`. |
103
+ | `step`, `seconds`, `meridiem`, `weekStart` | ❌ Accepted but ignored — the OS picker owns time granularity + week layout. |
104
+ | `locale` | ❌ Ignored — the native input formats per the user's browser/OS locale. |
105
+ | `placeholder` | ❌ Ignored — native date inputs don't show placeholder text. |
106
+ | formatted label (e.g. "Mar 5, 2026") | ⚠️ Shown as native editable segments (e.g. `03/05/2026`) instead. |
107
+
108
+ **When NOT to swap:** range mode, a custom formatted trigger, or a specific locale/time-granularity
109
+ UI — stay on `DatePicker`. Swap for plain single-date / datetime fields that must work without JS.
@@ -0,0 +1,76 @@
1
+ <!--
2
+ /**
3
+ * Accordion2 — a native-first, no-JS sibling of `Accordion` (transitional `*2`, see MIGRATION.md).
4
+ * It groups `Disclosure2` (`<details>`) children. In single-open mode (`multiple={false}`, default)
5
+ * it hands every child the SAME `<details name>`, so the BROWSER enforces "opening one closes the
6
+ * rest" — with no JavaScript, no context callbacks, correct in SSR. `multiple` drops the shared name
7
+ * so each panel toggles independently. `size`/`variant`/`disabled` propagate to children; the group's
8
+ * `value` sets which panel(s) start open.
9
+ *
10
+ * Differences from `Accordion` (see MIGRATION.md): `value` is INITIAL state only — with JS off the
11
+ * browser owns subsequent open/close, so the binding doesn't track user toggles. `collapsible={false}`
12
+ * (never allow zero open) can't be enforced without JS and is treated as best-effort.
13
+ */
14
+ -->
15
+ <script lang="ts">
16
+ import type { Snippet } from "svelte";
17
+ import type { Size } from "../types/sizes.js";
18
+ import type { Variant } from "../types/variants.js";
19
+ import type { Responsive } from "../types/responsive.js";
20
+ import { nextFieldId } from "./FieldChrome.svelte";
21
+ import { setDisclosure2Group } from "./disclosure2.js";
22
+
23
+ let {
24
+ multiple = false,
25
+ value = $bindable(),
26
+ collapsible: _collapsible = true,
27
+ size = "md",
28
+ variant,
29
+ disabled = false,
30
+ class: cls = "",
31
+ children,
32
+ }: {
33
+ /** false (default) = single-open (native `<details name>`); true = independent multi-open. */
34
+ multiple?: boolean;
35
+ /** Open key (single) or keys (multiple). INITIAL state only in the native variant. Bindable. */
36
+ value?: string | string[];
37
+ /** Accepted for Accordion compatibility; not enforceable without JS (best-effort). */
38
+ collapsible?: boolean;
39
+ size?: Responsive<Size>;
40
+ variant?: Variant;
41
+ disabled?: boolean;
42
+ class?: string;
43
+ children: Snippet;
44
+ } = $props();
45
+
46
+ // One shared <details name> per Accordion2 instance → browser-enforced single-open.
47
+ const groupName = nextFieldId();
48
+
49
+ setDisclosure2Group({
50
+ inGroup: true,
51
+ get name() {
52
+ return multiple ? undefined : groupName;
53
+ },
54
+ get value() {
55
+ return value;
56
+ },
57
+ get multiple() {
58
+ return multiple;
59
+ },
60
+ get size() {
61
+ return size;
62
+ },
63
+ get variant() {
64
+ return variant;
65
+ },
66
+ get disabled() {
67
+ return disabled;
68
+ },
69
+ });
70
+ </script>
71
+
72
+ <div
73
+ class="flex flex-col [&>*:not(:last-child)]:border-b [&>*]:[border-color:var(--ui-border-color)] {cls}"
74
+ >
75
+ {@render children()}
76
+ </div>
@@ -0,0 +1,20 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { Size } from "../types/sizes.js";
3
+ import type { Variant } from "../types/variants.js";
4
+ import type { Responsive } from "../types/responsive.js";
5
+ type $$ComponentProps = {
6
+ /** false (default) = single-open (native `<details name>`); true = independent multi-open. */
7
+ multiple?: boolean;
8
+ /** Open key (single) or keys (multiple). INITIAL state only in the native variant. Bindable. */
9
+ value?: string | string[];
10
+ /** Accepted for Accordion compatibility; not enforceable without JS (best-effort). */
11
+ collapsible?: boolean;
12
+ size?: Responsive<Size>;
13
+ variant?: Variant;
14
+ disabled?: boolean;
15
+ class?: string;
16
+ children: Snippet;
17
+ };
18
+ declare const Accordion2: import("svelte").Component<$$ComponentProps, {}, "value">;
19
+ type Accordion2 = ReturnType<typeof Accordion2>;
20
+ export default Accordion2;
@@ -0,0 +1,103 @@
1
+ <!--
2
+ /**
3
+ * DatePicker2 — a native-first, no-JS/SSR-correct sibling of `DatePicker` (transitional `*2`, see
4
+ * MIGRATION.md). It reuses `Input` with a real `<input type="date">` (or `type="datetime-local">` in
5
+ * `time` mode), so date entry works with NO JavaScript, pops the browser's native date picker, and
6
+ * submits in a plain `<form>` via `name` — and the field FRAME (border/height/radius/focus) is
7
+ * pixel-identical to `DatePicker` because it is the same `Input`.
8
+ *
9
+ * HONEST caveat (unlike the other `*2` components): the native control renders its value as editable
10
+ * segments (e.g. `03/05/2025`) with the browser's own picker indicator, so the trigger *content* is
11
+ * NOT pixel-identical to `DatePicker`'s formatted label ("Mar 5, 2025") + MDI calendar. Only the
12
+ * frame matches. Value format for single/datetime is identical, so the binding is drop-in.
13
+ *
14
+ * Scope: single date + `time` (datetime). `range` has no native equivalent — if set, it falls back
15
+ * to a single date input (dev warning). `step`/`seconds`/`meridiem`/`weekStart`/`locale`/`placeholder`
16
+ * are accepted for drop-in compatibility but not honoured (the OS owns the native picker). See
17
+ * MIGRATION.md.
18
+ */
19
+ -->
20
+ <script lang="ts">
21
+ import type { HTMLInputAttributes } from "svelte/elements";
22
+ import type { Size } from "../types/sizes.js";
23
+ import type { Responsive } from "../types/responsive.js";
24
+ import type { Variant } from "../types/variants.js";
25
+ import type { StepSpec } from "../types/time.js";
26
+ import Input from "./Input.svelte";
27
+ import { DEV } from "esm-env";
28
+
29
+ type RangeValue = { start: string | null; end: string | null };
30
+
31
+ let {
32
+ value = $bindable(),
33
+ range = false,
34
+ time = false,
35
+ min,
36
+ max,
37
+ disabled = false,
38
+ size = "md",
39
+ variant = "primary",
40
+ name,
41
+ class: cls = "",
42
+ // ── Accepted for DatePicker compatibility, but the native picker owns these (ignored). ──
43
+ step: _step,
44
+ seconds: _seconds,
45
+ meridiem: _meridiem,
46
+ weekStart: _weekStart,
47
+ locale: _locale,
48
+ placeholder: _placeholder,
49
+ ...rest
50
+ }: {
51
+ /** `YYYY-MM-DD` (single) or `YYYY-MM-DDTHH:MM` (`time`). Drop-in with DatePicker's single/datetime. */
52
+ value?: string | RangeValue | null;
53
+ range?: boolean;
54
+ time?: boolean;
55
+ min?: string;
56
+ max?: string;
57
+ disabled?: boolean;
58
+ size?: Responsive<Size>;
59
+ variant?: Variant;
60
+ /** Native form field name — the `<input>` carries it; submits with no JS. */
61
+ name?: string;
62
+ class?: string;
63
+ // accepted-but-ignored
64
+ step?: StepSpec;
65
+ seconds?: boolean;
66
+ meridiem?: boolean;
67
+ weekStart?: 0 | 1;
68
+ locale?: string;
69
+ placeholder?: string;
70
+ } & Omit<
71
+ HTMLInputAttributes,
72
+ "type" | "value" | "size" | "min" | "max" | "name" | "class" | "disabled"
73
+ > = $props();
74
+
75
+ $effect(() => {
76
+ if (DEV && range) {
77
+ console.warn(
78
+ "DatePicker2: `range` has no native equivalent — falling back to a single date input. " +
79
+ "Use two DatePicker2s (start/end) or stay on DatePicker for range. See MIGRATION.md.",
80
+ );
81
+ }
82
+ });
83
+
84
+ // The native control only takes a string. Coerce away a RangeValue (range isn't supported here).
85
+ const strValue = $derived(typeof value === "string" ? value : "");
86
+ function onInput(v: string) {
87
+ value = v;
88
+ }
89
+ </script>
90
+
91
+ <Input
92
+ type={time ? "datetime-local" : "date"}
93
+ value={strValue}
94
+ oninput={(e) => onInput((e.currentTarget as HTMLInputElement).value)}
95
+ {min}
96
+ {max}
97
+ {name}
98
+ {disabled}
99
+ {size}
100
+ {variant}
101
+ class={cls}
102
+ {...rest}
103
+ />
@@ -0,0 +1,32 @@
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
+ import type { StepSpec } from "../types/time.js";
6
+ type RangeValue = {
7
+ start: string | null;
8
+ end: string | null;
9
+ };
10
+ type $$ComponentProps = {
11
+ /** `YYYY-MM-DD` (single) or `YYYY-MM-DDTHH:MM` (`time`). Drop-in with DatePicker's single/datetime. */
12
+ value?: string | RangeValue | null;
13
+ range?: boolean;
14
+ time?: boolean;
15
+ min?: string;
16
+ max?: string;
17
+ disabled?: boolean;
18
+ size?: Responsive<Size>;
19
+ variant?: Variant;
20
+ /** Native form field name — the `<input>` carries it; submits with no JS. */
21
+ name?: string;
22
+ class?: string;
23
+ step?: StepSpec;
24
+ seconds?: boolean;
25
+ meridiem?: boolean;
26
+ weekStart?: 0 | 1;
27
+ locale?: string;
28
+ placeholder?: string;
29
+ } & Omit<HTMLInputAttributes, "type" | "value" | "size" | "min" | "max" | "name" | "class" | "disabled">;
30
+ declare const DatePicker2: import("svelte").Component<$$ComponentProps, {}, "value">;
31
+ type DatePicker2 = ReturnType<typeof DatePicker2>;
32
+ export default DatePicker2;
@@ -0,0 +1,198 @@
1
+ <!--
2
+ /**
3
+ * Disclosure2 — a native-first, no-JS/SSR-correct sibling of `Disclosure` (transitional `*2`, see
4
+ * MIGRATION.md). It is a real `<details>/<summary>`: it expands/collapses with NO JavaScript, is
5
+ * open on first paint when `open` is set (SSR-correct), and gets correct button/expanded semantics
6
+ * from the browser for free. The `<summary>` wears `Button`'s exact ghost-header classes (copied
7
+ * verbatim), so the header is pixel-identical to `Disclosure`'s `<Button>` header — including the
8
+ * resting look, hover tint (native `:hover`), focus ring, and the chevron that rotates on open (pure
9
+ * CSS via `details[open]`).
10
+ *
11
+ * Differences from `Disclosure` (inherent to `<details>`, see MIGRATION.md):
12
+ * - The open/close is INSTANT (no `grid-template-rows` height animation) except on browsers with
13
+ * `::details-content` + `interpolate-size` support, where it eases. Reduced-motion → instant,
14
+ * same as `Disclosure`.
15
+ * - Grouping is native: pass the same `name` to several `Disclosure2`s (what `Accordion2` does) for
16
+ * a browser-enforced single-open accordion — no context/JS. `value` is accepted but unused.
17
+ * - `disabled` makes the summary non-interactive (pointer-events-none + aria-disabled); a native
18
+ * `<details>` has no real disabled state.
19
+ */
20
+ -->
21
+ <script lang="ts">
22
+ import type { Snippet } from "svelte";
23
+ import ChevronDown from "~icons/mdi/chevron-down";
24
+ import type { Size } from "../types/sizes.js";
25
+ import { responsiveClasses, type Responsive } from "../types/responsive.js";
26
+ import type { Variant } from "../types/variants.js";
27
+ import { nextFieldId } from "./FieldChrome.svelte";
28
+ import { getDisclosure2Group } from "./disclosure2.js";
29
+
30
+ let {
31
+ title = null,
32
+ header,
33
+ open = $bindable(false),
34
+ value,
35
+ disabled = false,
36
+ size,
37
+ variant,
38
+ name,
39
+ icon,
40
+ id,
41
+ class: cls = "",
42
+ children,
43
+ }: {
44
+ /** Header text. Use the `header` snippet instead for full custom header content. */
45
+ title?: string | null;
46
+ header?: Snippet;
47
+ /** Initial + bindable open state. Sets the `open` attribute (SSR-correct); kept in sync on toggle.
48
+ Inside an `Accordion2`, the group's `value` matching this panel's `value` also opens it. */
49
+ open?: boolean;
50
+ /** This panel's key — matched against an `Accordion2` group's `value` to set the initial open. */
51
+ value?: string;
52
+ disabled?: boolean;
53
+ size?: Responsive<Size>;
54
+ /** Header hover/accent (default `ghost`; inherits the Accordion2's variant). */
55
+ variant?: Variant;
56
+ /** Native `<details name>` — same `name` across siblings = browser-enforced single-open group. */
57
+ name?: string;
58
+ icon?: Snippet;
59
+ id?: string;
60
+ class?: string;
61
+ children: Snippet;
62
+ } = $props();
63
+
64
+ const grp = getDisclosure2Group();
65
+ // Resolve axes: explicit prop → group → default (mirrors Disclosure's precedence).
66
+ const effSize = $derived(size ?? grp?.size ?? "md");
67
+ const effVariant = $derived(variant ?? grp?.variant ?? "ghost");
68
+ const effDisabled = $derived(disabled || !!grp?.disabled);
69
+ // Single-open group → share the browser-enforced `<details name>`; multiple/standalone → own name.
70
+ const effName = $derived(name ?? (grp && !grp.multiple ? grp.name : undefined));
71
+ // Initial open: explicit `open`, or (in a group) this panel's `value` matching the group value.
72
+ const groupOpen = $derived(
73
+ grp == null || value == null
74
+ ? false
75
+ : grp.multiple
76
+ ? Array.isArray(grp.value) && grp.value.includes(value)
77
+ : grp.value === value,
78
+ );
79
+ const effOpen = $derived(open || groupOpen);
80
+
81
+ const uid = nextFieldId();
82
+ const headerId = $derived(id ?? uid);
83
+ const panelId = $derived(`${headerId}-panel`);
84
+
85
+ // ── Header chrome — mirrors a <Button variant size> (borderless=true, the Disclosure default). ──
86
+ const baseClasses =
87
+ "flex w-full items-center [border-radius:var(--ui-border-radius)] font-medium transition-colors cursor-pointer border focus-visible:[outline:2px_solid_color-mix(in_srgb,var(--ui-color-text)_70%,transparent)] focus-visible:[outline-offset:0px]";
88
+ const padMap: Record<Size, string> = { sm: "h-6 px-2.5", md: "h-8 px-4", lg: "h-10 px-6" };
89
+ const textMap: Record<Size, string> = { sm: "text-xs", md: "text-sm", lg: "text-base" };
90
+ const svgMap: Record<Size, string> = {
91
+ sm: "[&_svg]:w-3.5 [&_svg]:h-3.5",
92
+ md: "[&_svg]:w-4 [&_svg]:h-4",
93
+ lg: "[&_svg]:w-5 [&_svg]:h-5",
94
+ };
95
+ // FILL — copied VERBATIM from Button.fillClasses.
96
+ const fillClasses: Record<Variant, string> = {
97
+ primary:
98
+ "[background-color:var(--ui-color-primary)] text-white hover:[background-color:color-mix(in_srgb,var(--ui-color-primary)_90%,black)] [&_svg]:fill-current",
99
+ secondary:
100
+ "[background-color:var(--ui-color-secondary)] text-white hover:[background-color:color-mix(in_srgb,var(--ui-color-secondary)_90%,black)] [&_svg]:fill-current",
101
+ danger:
102
+ "[background-color:var(--ui-color-error)] text-white hover:[background-color:color-mix(in_srgb,var(--ui-color-error)_90%,black)] [&_svg]:fill-current",
103
+ info: "[background-color:var(--ui-color-info)] text-white hover:[background-color:color-mix(in_srgb,var(--ui-color-info)_90%,black)] [&_svg]:fill-current",
104
+ success:
105
+ "[background-color:var(--ui-color-success)] text-white hover:[background-color:color-mix(in_srgb,var(--ui-color-success)_90%,black)] [&_svg]:fill-current",
106
+ warning:
107
+ "[background-color:var(--ui-color-warning)] text-white hover:[background-color:color-mix(in_srgb,var(--ui-color-warning)_90%,black)] [&_svg]:fill-current",
108
+ ghost:
109
+ "bg-transparent [color:var(--ui-color-text)] hover:[background-color:var(--ui-color-hover)] [&_svg]:fill-current",
110
+ neutral:
111
+ "[background-color:var(--ui-color-text)] [color:var(--ui-color-background)] hover:[background-color:color-mix(in_srgb,var(--ui-color-text)_85%,var(--ui-color-background))] [&_svg]:fill-current",
112
+ };
113
+ // BORDER (borderless=true: tracks the fill) — copied VERBATIM from Button.borderMatchClasses.
114
+ const borderMatchClasses: Record<Variant, string> = {
115
+ primary:
116
+ "[border-color:var(--ui-color-primary)] hover:[border-color:color-mix(in_srgb,var(--ui-color-primary)_90%,black)]",
117
+ secondary:
118
+ "[border-color:var(--ui-color-secondary)] hover:[border-color:color-mix(in_srgb,var(--ui-color-secondary)_90%,black)]",
119
+ danger:
120
+ "[border-color:var(--ui-color-error)] hover:[border-color:color-mix(in_srgb,var(--ui-color-error)_90%,black)]",
121
+ info: "[border-color:var(--ui-color-info)] hover:[border-color:color-mix(in_srgb,var(--ui-color-info)_90%,black)]",
122
+ success:
123
+ "[border-color:var(--ui-color-success)] hover:[border-color:color-mix(in_srgb,var(--ui-color-success)_90%,black)]",
124
+ warning:
125
+ "[border-color:var(--ui-color-warning)] hover:[border-color:color-mix(in_srgb,var(--ui-color-warning)_90%,black)]",
126
+ ghost: "border-transparent hover:[border-color:var(--ui-color-hover)]",
127
+ neutral:
128
+ "[border-color:var(--ui-color-text)] hover:[border-color:color-mix(in_srgb,var(--ui-color-text)_85%,var(--ui-color-background))]",
129
+ };
130
+
131
+ const labelText = $derived(responsiveClasses(effSize, textMap));
132
+ const svgClass = $derived(responsiveClasses(effSize, svgMap));
133
+ const summaryClasses = $derived(
134
+ [
135
+ baseClasses,
136
+ responsiveClasses(effSize, padMap),
137
+ labelText,
138
+ responsiveClasses(effVariant, fillClasses),
139
+ responsiveClasses(effVariant, borderMatchClasses),
140
+ effDisabled ? "pointer-events-none opacity-50 cursor-not-allowed" : "",
141
+ "ui-disc2-summary",
142
+ ]
143
+ .filter(Boolean)
144
+ .join(" "),
145
+ );
146
+
147
+ // Keep `open` in sync for JS consumers (harmless with JS off — the browser toggles natively).
148
+ function onToggle(event: Event & { currentTarget: HTMLDetailsElement }) {
149
+ open = event.currentTarget.open;
150
+ }
151
+ </script>
152
+
153
+ <details class="ui-disc2 {cls}" name={effName} open={effOpen} ontoggle={onToggle}>
154
+ <summary
155
+ class={summaryClasses}
156
+ id={headerId}
157
+ aria-controls={panelId}
158
+ aria-disabled={effDisabled || undefined}
159
+ >
160
+ {#if icon}<span class="inline-flex shrink-0 {svgClass}">{@render icon()}</span>{/if}
161
+ {#if header}
162
+ <span class="flex-1 text-left">{@render header()}</span>
163
+ {:else}
164
+ <span class="flex-1 text-left">{title}</span>
165
+ {/if}
166
+ <ChevronDown class="ui-disc2-chevron shrink-0 {svgClass}" />
167
+ </summary>
168
+
169
+ <div
170
+ id={panelId}
171
+ role="region"
172
+ aria-labelledby={headerId}
173
+ class="ui-disc2-panel px-1 pt-1 pb-3 {labelText} [color:var(--ui-color-text)]"
174
+ >
175
+ {@render children()}
176
+ </div>
177
+ </details>
178
+
179
+ <style>
180
+ /* Hide the native disclosure triangle — we draw our own chevron. */
181
+ .ui-disc2 > summary {
182
+ list-style: none;
183
+ }
184
+ .ui-disc2 > summary::-webkit-details-marker {
185
+ display: none;
186
+ }
187
+ /* Chevron rotates on open — pure CSS, so it's correct with JS off and in SSR. Open/close itself is
188
+ instant (a native <details> doesn't height-animate pre-`::details-content`); documented in
189
+ MIGRATION.md. Reduced-motion skips the chevron transition, matching Disclosure. */
190
+ .ui-disc2[open] > summary :global(.ui-disc2-chevron) {
191
+ transform: rotate(180deg);
192
+ }
193
+ @media (prefers-reduced-motion: no-preference) {
194
+ .ui-disc2 :global(.ui-disc2-chevron) {
195
+ transition: transform 200ms;
196
+ }
197
+ }
198
+ </style>
@@ -0,0 +1,27 @@
1
+ import type { Snippet } from "svelte";
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
+ type $$ComponentProps = {
6
+ /** Header text. Use the `header` snippet instead for full custom header content. */
7
+ title?: string | null;
8
+ header?: Snippet;
9
+ /** Initial + bindable open state. Sets the `open` attribute (SSR-correct); kept in sync on toggle.
10
+ Inside an `Accordion2`, the group's `value` matching this panel's `value` also opens it. */
11
+ open?: boolean;
12
+ /** This panel's key — matched against an `Accordion2` group's `value` to set the initial open. */
13
+ value?: string;
14
+ disabled?: boolean;
15
+ size?: Responsive<Size>;
16
+ /** Header hover/accent (default `ghost`; inherits the Accordion2's variant). */
17
+ variant?: Variant;
18
+ /** Native `<details name>` — same `name` across siblings = browser-enforced single-open group. */
19
+ name?: string;
20
+ icon?: Snippet;
21
+ id?: string;
22
+ class?: string;
23
+ children: Snippet;
24
+ };
25
+ declare const Disclosure2: import("svelte").Component<$$ComponentProps, {}, "open">;
26
+ type Disclosure2 = ReturnType<typeof Disclosure2>;
27
+ export default Disclosure2;
@@ -0,0 +1,16 @@
1
+ import type { Size } from "../types/sizes.js";
2
+ import type { Variant } from "../types/variants.js";
3
+ import type { Responsive } from "../types/responsive.js";
4
+ export interface Disclosure2GroupContext {
5
+ inGroup: true;
6
+ /** Shared `<details name>` for single-open mode; `undefined` in multiple mode (independent). */
7
+ name?: string;
8
+ /** The group's open key (single) or keys (multiple) — used to compute each child's initial `open`. */
9
+ value?: string | string[];
10
+ multiple: boolean;
11
+ size?: Responsive<Size>;
12
+ variant?: Variant;
13
+ disabled?: boolean;
14
+ }
15
+ export declare function setDisclosure2Group(ctx: Disclosure2GroupContext): void;
16
+ export declare function getDisclosure2Group(): Disclosure2GroupContext | undefined;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Shared context for `Accordion2` → `Disclosure2` — the native-first sibling of `disclosure.ts`.
3
+ * Unlike the JS group (which owns `isOpen`/`toggle`), this one carries only STATIC data the child
4
+ * reads at render: a shared `<details name>` (single-open mode → the browser enforces exclusivity
5
+ * with no JS) and the group's initial `value`/axes. There is no runtime toggle callback — the native
6
+ * `<details>` elements coordinate themselves.
7
+ */
8
+ import { getContext, setContext } from "svelte";
9
+ const KEY = Symbol("svelte-ui-disclosure2-group");
10
+ export function setDisclosure2Group(ctx) {
11
+ setContext(KEY, ctx);
12
+ }
13
+ export function getDisclosure2Group() {
14
+ return getContext(KEY);
15
+ }
package/dist/index.d.ts CHANGED
@@ -70,6 +70,9 @@ export { Stepper } from "./components/Stepper/index.js";
70
70
  export type { StepItem } from "./components/Stepper/index.js";
71
71
  export { default as Disclosure } from "./components/Disclosure.svelte";
72
72
  export { default as Accordion } from "./components/Accordion.svelte";
73
+ export { default as Disclosure2 } from "./components/Disclosure2.svelte";
74
+ export { default as Accordion2 } from "./components/Accordion2.svelte";
75
+ export { default as DatePicker2 } from "./components/DatePicker2.svelte";
73
76
  export { default as SideBarMenu } from "./components/SideBarMenu/SideBarMenu.svelte";
74
77
  export type { SideBarMenuItem, SideBarMenuBadge, SideBarMenuBadgeVariant, SideBarMenuApi, SideBarMenuMode, SideBarMenuActiveRule, } from "./components/SideBarMenu/SideBarMenu.svelte";
75
78
  export { Breadcrumbs } from "./components/Breadcrumbs/index.js";
package/dist/index.js CHANGED
@@ -68,6 +68,10 @@ export { Carousel } from "./components/Carousel/index.js";
68
68
  export { Stepper } from "./components/Stepper/index.js";
69
69
  export { default as Disclosure } from "./components/Disclosure.svelte";
70
70
  export { default as Accordion } from "./components/Accordion.svelte";
71
+ // Native-first, no-JS/SSR-correct variants (transitional `*2` — see MIGRATION.md).
72
+ export { default as Disclosure2 } from "./components/Disclosure2.svelte";
73
+ export { default as Accordion2 } from "./components/Accordion2.svelte";
74
+ export { default as DatePicker2 } from "./components/DatePicker2.svelte";
71
75
  // Navigation Components
72
76
  export { default as SideBarMenu } from "./components/SideBarMenu/SideBarMenu.svelte";
73
77
  export { Breadcrumbs } from "./components/Breadcrumbs/index.js";
@@ -308,6 +308,24 @@ independent. Propagates `size`/`variant`/`disabled` to children.
308
308
  - **Props**: `multiple` (default `false`), `value` (bindable — `string` single / `string[]` multiple),
309
309
  `collapsible` (single mode, default `true`), `size`, `variant`, `disabled`, `class`, `children`.
310
310
 
311
+ ### Disclosure2 / Accordion2 (native-first)
312
+
313
+ **No-JS / SSR-correct** siblings built on real `<details>/<summary>`: they expand, rotate the chevron,
314
+ and (via `Accordion2`'s shared `<details name>`) enforce single-open **with no JavaScript**, correct
315
+ in SSR. The `<summary>` wears `Button`'s ghost-header classes, so the header is **pixel-identical** to
316
+ `Disclosure` at every `size`/`variant`.
317
+
318
+ - **Location**: `src/lib/components/Disclosure2.svelte`, `src/lib/components/Accordion2.svelte`
319
+ - **Props**: a drop-in **superset** of `Disclosure`/`Accordion` — same `title`/`header`/`open`/`value`/
320
+ `size`/`variant`/`disabled`/`icon`/`class`/`children` and `multiple`/`value`. `Disclosure2` also
321
+ takes a native `name` (shared → browser single-open group).
322
+ - **Intentional divergences** (native `<details>`): open/close is **instant** (no height animation —
323
+ reduced-motion parity), the group `value` is **initial state only** (with JS off the browser owns
324
+ toggles, so the binding doesn't track), and `collapsible={false}` is best-effort. Full matrix:
325
+ `MIGRATION.md`.
326
+ - **Use when**: expandable content must work server-side / without JS (FAQs, nav sections).
327
+ - **Transitional**: ships alongside the originals under the `2` suffix; see `MIGRATION.md`.
328
+
311
329
  ### Rating
312
330
 
313
331
  Star rating in two modes: a read-only display, or an interactive picker.
@@ -397,6 +415,23 @@ snaps to canonical on blur.
397
415
  inject `parse`/`format`/`validate` backed by a phone library (e.g. `libphonenumber-js`); the
398
416
  component bundles none.
399
417
 
418
+ ### DatePicker2 (native-first)
419
+
420
+ **No-JS / SSR-correct** date entry: reuses `Input` with a real `<input type="date">` (or
421
+ `datetime-local` in `time` mode), so it works without JavaScript, pops the browser's native picker,
422
+ and submits via `name`. The field **frame** is pixel-identical to `DatePicker` (same `Input`).
423
+
424
+ - **Location**: `src/lib/components/DatePicker2.svelte`
425
+ - **Props**: drop-in superset of `DatePicker`'s single/datetime use — `value` (`YYYY-MM-DD` or
426
+ `YYYY-MM-DDTHH:MM`, identical format), `time`, `min`, `max`, `size`, `variant`, `disabled`, `name`,
427
+ `class`.
428
+ - **Caveat (unlike the other `*2`)**: the native control renders its value as **editable segments**
429
+ with the browser's picker indicator, so the trigger *content* is not a pixel match for
430
+ `DatePicker`'s formatted label — only the frame is. `range` has **no native equivalent** (falls
431
+ back to a single input with a dev warning); `step`/`seconds`/`meridiem`/`weekStart`/`locale`/
432
+ `placeholder` are accepted but ignored (the OS owns the picker). See `MIGRATION.md`.
433
+ - **Transitional**: ships alongside `DatePicker` under the `2` suffix.
434
+
400
435
  ## Navigation
401
436
 
402
437
  ### Breadcrumbs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.21.1-dev.120.d0d8cfa",
3
+ "version": "0.21.1-dev.122.60c38bb",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"
@@ -205,6 +205,10 @@
205
205
  "types": "./dist/components/DatePicker.svelte.d.ts",
206
206
  "svelte": "./dist/components/DatePicker.svelte"
207
207
  },
208
+ "./date-picker2": {
209
+ "types": "./dist/components/DatePicker2.svelte.d.ts",
210
+ "svelte": "./dist/components/DatePicker2.svelte"
211
+ },
208
212
  "./time-spinner": {
209
213
  "types": "./dist/components/TimeSpinner.svelte.d.ts",
210
214
  "svelte": "./dist/components/TimeSpinner.svelte"
@@ -301,6 +305,14 @@
301
305
  "types": "./dist/components/Accordion.svelte.d.ts",
302
306
  "svelte": "./dist/components/Accordion.svelte"
303
307
  },
308
+ "./disclosure2": {
309
+ "types": "./dist/components/Disclosure2.svelte.d.ts",
310
+ "svelte": "./dist/components/Disclosure2.svelte"
311
+ },
312
+ "./accordion2": {
313
+ "types": "./dist/components/Accordion2.svelte.d.ts",
314
+ "svelte": "./dist/components/Accordion2.svelte"
315
+ },
304
316
  "./side-bar-menu": {
305
317
  "types": "./dist/components/SideBarMenu/SideBarMenu.svelte.d.ts",
306
318
  "svelte": "./dist/components/SideBarMenu/SideBarMenu.svelte"