@spaethtech/svelte-ui 0.21.1-dev.125.4911439 → 0.21.1-dev.127.89441ce
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 +12 -2
- package/dist/components/Calendar.svelte +2 -2
- package/dist/components/DatePicker.svelte +2 -2
- package/dist/components/DatePicker2.svelte +5 -0
- package/dist/components/Select2.svelte +23 -10
- package/dist/components/TimePicker.svelte +2 -1
- package/dist/components/TimeRangeInput.svelte +2 -2
- package/dist/components/TimeSpinner.svelte +2 -1
- package/dist/i18n.d.ts +10 -0
- package/dist/i18n.js +30 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/components.md +10 -1
- package/package.json +1 -1
package/MIGRATION.md
CHANGED
|
@@ -8,6 +8,16 @@ upgrade are documented here, each with a dated heading and a concrete before/aft
|
|
|
8
8
|
|
|
9
9
|
## The `*2` native-first variants (transitional)
|
|
10
10
|
|
|
11
|
+
> ### ⚠️ EXPERIMENTAL — proof-of-concept, NOT ready for use
|
|
12
|
+
>
|
|
13
|
+
> The `*2` components (`Select2`, `Disclosure2`, `Accordion2`, `DatePicker2`) are a **proof-of-concept
|
|
14
|
+
> under active evaluation in a single test bed (php-svelte) only**. They ship on the **`@dev`** channel
|
|
15
|
+
> so that test bed can exercise them — **do not adopt them in any other project yet.** The API, the
|
|
16
|
+
> behaviour, and even whether each one survives are still in flux (e.g. reset semantics are still being
|
|
17
|
+
> settled). Treat them as unstable: no compatibility promise, and they may change or be removed without
|
|
18
|
+
> a migration note while in this phase. When one is actually ready, this banner comes off and the swap
|
|
19
|
+
> is announced here. Until then: **look, test in php-svelte, don't build on them.**
|
|
20
|
+
|
|
11
21
|
Several components are getting a **native-first, no-JS / SSR-correct** rebuild so SSR apps (e.g. a
|
|
12
22
|
PHP-rendered front end) render and submit them with **no client JavaScript**. Rather than change the
|
|
13
23
|
existing components in place and risk the apps that already depend on them, each rebuild ships
|
|
@@ -37,7 +47,7 @@ FieldChrome + bordered field box + chevron). Differences:
|
|
|
37
47
|
| --- | --- |
|
|
38
48
|
| `options`, `value`, `placeholder`, `disabled`, `size`, `variant`, `borderless`, `name`, `class`, `label`, `aside`, `error`, `description`, `required`, `onSelection` | ✅ Honoured (identical semantics). `name` now sits on the `<select>` itself — it submits natively, so no hidden input. |
|
|
39
49
|
| `inputClass` | ✅ Applied to the `<select>` (alias of the new `selectClass`). |
|
|
40
|
-
| `multiSelect` | ⚠️ Maps to native `<select multiple>` — a **listbox**, not the dropdown trigger.
|
|
50
|
+
| `multiSelect` | ⚠️ Maps to native `<select multiple>` — a **listbox**, not the dropdown trigger. Renders as a usable multi-row listbox (bounded height, no chevron); single-select is the pixel-perfect path, the multi appearance intentionally diverges. **Submit shape differs:** `Select` multi submits ONE comma-joined field (`tags=red,blue`); `Select2` multi submits the native repeated-field encoding, so a server collects it as an array — in PHP name the field `tags[]` (a bare `tags` keeps only the last). A PHP action therefore changes when you swap a multi `Select` → `Select2`. |
|
|
41
51
|
| `itemSnippet`, `itemClass`, `dropdownClass` | ❌ **Accepted but ignored** — the open list is the OS dropdown and can't be styled/custom-rendered. |
|
|
42
52
|
| `maxVisibleItems`, `itemHeight`, `autoAdvance` | ❌ Accepted but ignored — the OS controls the dropdown. |
|
|
43
53
|
| `url`, `searchParam`, `transform`, `debounceMs`, `maxResults`, `headers`, `searchTerm`, `onError`, `onLoadStart`, `onLoadEnd` | ❌ Accepted but ignored — **no remote search** (a native `<select>` has no type-to-search box). Load options yourself and pass `options`. |
|
|
@@ -98,7 +108,7 @@ drop-in for single/datetime.
|
|
|
98
108
|
|
|
99
109
|
| `DatePicker` prop | `DatePicker2` behaviour |
|
|
100
110
|
| --- | --- |
|
|
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). |
|
|
111
|
+
| `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). Survives `form.reset()` / SvelteKit `use:enhance` (`reset: true`): the input's `defaultValue` tracks the bound value, so a reset restores the date instead of blanking it. |
|
|
102
112
|
| `range` | ❌ **No native equivalent.** Falls back to a single date input (dev warning). Use two `DatePicker2`s (start/end) or stay on `DatePicker`. |
|
|
103
113
|
| `step`, `seconds`, `meridiem`, `weekStart` | ❌ Accepted but ignored — the OS picker owns time granularity + week layout. |
|
|
104
114
|
| `locale` | ❌ Ignored — the native input formats per the user's browser/OS locale. |
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
-->
|
|
12
12
|
<script lang="ts">
|
|
13
|
-
import { uiI18n } from "../i18n.js";
|
|
13
|
+
import { uiI18n, safeLocale } from "../i18n.js";
|
|
14
14
|
const i18n = uiI18n();
|
|
15
15
|
import type { Size } from "../types/sizes.js";
|
|
16
16
|
import { responsiveClasses, type Responsive } from "../types/responsive.js";
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
min,
|
|
46
46
|
max,
|
|
47
47
|
weekStart,
|
|
48
|
-
locale = i18n.locale
|
|
48
|
+
locale = safeLocale(i18n.locale),
|
|
49
49
|
size = "md",
|
|
50
50
|
variant = "primary",
|
|
51
51
|
class: cls = "",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
-->
|
|
15
15
|
<script lang="ts">
|
|
16
|
-
import { uiI18n } from "../i18n.js";
|
|
16
|
+
import { uiI18n, safeLocale } from "../i18n.js";
|
|
17
17
|
const i18n = uiI18n();
|
|
18
18
|
import type { HTMLAttributes } from "svelte/elements";
|
|
19
19
|
import type { Size } from "../types/sizes.js";
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
min,
|
|
61
61
|
max,
|
|
62
62
|
weekStart,
|
|
63
|
-
locale = i18n.locale
|
|
63
|
+
locale = safeLocale(i18n.locale),
|
|
64
64
|
placeholder = range ? "Select dates" : time ? "Select date & time" : "Select date",
|
|
65
65
|
disabled = false,
|
|
66
66
|
size = "md",
|
|
@@ -88,9 +88,14 @@
|
|
|
88
88
|
}
|
|
89
89
|
</script>
|
|
90
90
|
|
|
91
|
+
<!-- `defaultValue` tracks the bound value so `form.reset()` / SvelteKit `use:enhance` (reset:true)
|
|
92
|
+
restores it instead of blanking the native <input>: Svelte binds `value` as a property only, so
|
|
93
|
+
without a matching reset-default a reset wipes the date (and the next native submit posts ""). See
|
|
94
|
+
MIGRATION.md / CHANGELOG. -->
|
|
91
95
|
<Input
|
|
92
96
|
type={time ? "datetime-local" : "date"}
|
|
93
97
|
value={strValue}
|
|
98
|
+
defaultValue={strValue}
|
|
94
99
|
oninput={(e) => onInput((e.currentTarget as HTMLInputElement).value)}
|
|
95
100
|
{min}
|
|
96
101
|
{max}
|
|
@@ -183,15 +183,20 @@
|
|
|
183
183
|
const padRightClass = $derived(responsiveClasses(size, padRightMap));
|
|
184
184
|
|
|
185
185
|
// Field box — mirrors Input.wrapperClasses (icon on the right → pl-2 pr-0 on the frame).
|
|
186
|
+
// `multiSelect` is a native <select multiple> LISTBOX, not a dropdown trigger: it can't live in the
|
|
187
|
+
// fixed single-line height without clipping, so the box grows to the listbox (items-stretch, auto
|
|
188
|
+
// height, symmetric padding) and the chevron is dropped. Single-select stays pixel-identical.
|
|
186
189
|
const wrapperClasses = $derived.by(() => {
|
|
187
|
-
const base =
|
|
190
|
+
const base = multiSelect
|
|
191
|
+
? `relative w-full flex items-stretch [border-radius:var(--ui-border-radius)]`
|
|
192
|
+
: `relative w-full ${heightClass} flex items-center [border-radius:var(--ui-border-radius)]`;
|
|
188
193
|
const frameStyles = borderless ? "[border-color:transparent]" : borderVariantClass[variant];
|
|
189
194
|
const fieldBg =
|
|
190
195
|
variant === "ghost" ? "bg-transparent" : "[background-color:var(--ui-color-background)]";
|
|
191
196
|
const themeStyles = [
|
|
192
197
|
fieldBg,
|
|
193
198
|
"border",
|
|
194
|
-
"pl-2 pr-0",
|
|
199
|
+
multiSelect ? "p-1" : "pl-2 pr-0",
|
|
195
200
|
frameStyles,
|
|
196
201
|
"focus-within:[outline:2px_solid_color-mix(in_srgb,currentColor_70%,transparent)]",
|
|
197
202
|
"focus-within:[outline-offset:0px]",
|
|
@@ -206,8 +211,12 @@
|
|
|
206
211
|
});
|
|
207
212
|
|
|
208
213
|
// The <select> itself — mirrors Input's inner <input> (flex-1, transparent, no border/outline).
|
|
214
|
+
// Multi mode keeps the native listbox chrome: full width, native row layout (no appearance-none,
|
|
215
|
+
// no single-line height, no chevron right-padding).
|
|
209
216
|
const selectClasses = $derived.by(() => {
|
|
210
|
-
const base =
|
|
217
|
+
const base = multiSelect
|
|
218
|
+
? `w-full bg-transparent border-none outline-none ${textClass} min-w-0 cursor-pointer`
|
|
219
|
+
: `flex-1 h-full bg-transparent border-none outline-none ${textClass} ${padRightClass} leading-tight p-0 min-w-0 appearance-none cursor-pointer`;
|
|
211
220
|
const color = hasValue ? "[color:var(--ui-color-text)]" : placeholderVariantColor[variant];
|
|
212
221
|
const disabledColor =
|
|
213
222
|
"disabled:[color:color-mix(in_srgb,var(--ui-color-text)_40%,transparent)] disabled:cursor-not-allowed disabled:bg-transparent";
|
|
@@ -234,6 +243,7 @@
|
|
|
234
243
|
before hydration / with JS off — and `onchange` keeps client state in sync. -->
|
|
235
244
|
<select
|
|
236
245
|
multiple={multiSelect}
|
|
246
|
+
size={multiSelect ? Math.max(2, Math.min(options.length, 6)) : undefined}
|
|
237
247
|
{name}
|
|
238
248
|
{disabled}
|
|
239
249
|
{required}
|
|
@@ -260,16 +270,19 @@
|
|
|
260
270
|
</select>
|
|
261
271
|
|
|
262
272
|
<!-- Decorative chevron — same box/colour/size as Input's right icon. pointer-events-none so a
|
|
263
|
-
click anywhere (incl. here) falls through to the <select> beneath.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
>
|
|
273
|
+
click anywhere (incl. here) falls through to the <select> beneath. Dropdown affordance only:
|
|
274
|
+
a multi listbox has no dropdown, so it's hidden there. -->
|
|
275
|
+
{#if !multiSelect}
|
|
267
276
|
<span
|
|
268
|
-
class="
|
|
277
|
+
class="pointer-events-none absolute inset-y-0 right-0 {iconBoxClass} flex items-center justify-center select-none"
|
|
269
278
|
>
|
|
270
|
-
<
|
|
279
|
+
<span
|
|
280
|
+
class="inline-flex {svgClass} items-center justify-center [color:var(--ui-color-secondary)] [&_svg]:w-full [&_svg]:h-full"
|
|
281
|
+
>
|
|
282
|
+
<ChevronDown />
|
|
283
|
+
</span>
|
|
271
284
|
</span>
|
|
272
|
-
|
|
285
|
+
{/if}
|
|
273
286
|
</div>
|
|
274
287
|
{/snippet}
|
|
275
288
|
</FieldChrome>
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import Input from "./Input.svelte";
|
|
18
18
|
import Popup from "./Popup.svelte";
|
|
19
19
|
import TimeSpinner from "./TimeSpinner.svelte";
|
|
20
|
+
import { safeLocale } from "../i18n.js";
|
|
20
21
|
import IconClock from "~icons/mdi/clock-outline";
|
|
21
22
|
|
|
22
23
|
interface Props extends Omit<HTMLInputAttributes, "value" | "size" | "min" | "max" | "step"> {
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
min = "00:00",
|
|
48
49
|
max = "23:59",
|
|
49
50
|
meridiem = true,
|
|
50
|
-
locale =
|
|
51
|
+
locale = safeLocale(),
|
|
51
52
|
size = "md",
|
|
52
53
|
variant = "primary",
|
|
53
54
|
disabled = false,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
-->
|
|
11
11
|
<script lang="ts">
|
|
12
|
-
import { uiI18n } from "../i18n.js";
|
|
12
|
+
import { uiI18n, safeLocale } from "../i18n.js";
|
|
13
13
|
const i18n = uiI18n();
|
|
14
14
|
import type { HTMLInputAttributes } from "svelte/elements";
|
|
15
15
|
import type { FieldChromeProps } from "./field-chrome.js";
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
min = "00:00",
|
|
52
52
|
max = "23:59",
|
|
53
53
|
meridiem = true,
|
|
54
|
-
locale =
|
|
54
|
+
locale = safeLocale(),
|
|
55
55
|
size = "md",
|
|
56
56
|
variant = "primary",
|
|
57
57
|
disabled = false,
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { responsiveClasses, type Responsive } from "../types/responsive.js";
|
|
15
15
|
import { variantToken, type Variant } from "../types/variants.js";
|
|
16
16
|
import { resolveStep, type StepSpec } from "../types/time.js";
|
|
17
|
+
import { safeLocale } from "../i18n.js";
|
|
17
18
|
import IconUp from "~icons/mdi/chevron-up";
|
|
18
19
|
import IconDown from "~icons/mdi/chevron-down";
|
|
19
20
|
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
min,
|
|
45
46
|
max,
|
|
46
47
|
meridiem = true,
|
|
47
|
-
locale =
|
|
48
|
+
locale = safeLocale(),
|
|
48
49
|
size = "md",
|
|
49
50
|
variant = "primary",
|
|
50
51
|
class: cls = "",
|
package/dist/i18n.d.ts
CHANGED
|
@@ -71,6 +71,16 @@ export type UiMessages = {
|
|
|
71
71
|
};
|
|
72
72
|
/** English defaults — the exact literals the components used to hard-code (no behaviour change). */
|
|
73
73
|
export declare const defaultUiMessages: UiMessages;
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a valid BCP-47 locale tag for `Intl.*`. A raw `navigator.language` is NOT guaranteed to be a
|
|
76
|
+
* valid BCP-47 tag — on a POSIX/`LANG=C` host the browser can report `en-US@posix`, and handing that
|
|
77
|
+
* straight to `Intl.Locale`/`Intl.DateTimeFormat` throws `RangeError: Invalid language tag`, which (in a
|
|
78
|
+
* component's reactive scope) kills hydration. This validates the preferred tag, then
|
|
79
|
+
* `navigator.language`, falling back to each candidate's primary subtag (e.g. `en-US@posix` → `en`) and
|
|
80
|
+
* finally `"en-US"`. A tag that is already valid is returned unchanged, so formatted output is
|
|
81
|
+
* byte-identical for every well-formed locale.
|
|
82
|
+
*/
|
|
83
|
+
export declare function safeLocale(preferred?: string): string;
|
|
74
84
|
/** What a component reads. `locale` is `undefined` unless a provider sets one — Intl components then
|
|
75
85
|
* fall back to `navigator.language`, preserving current behaviour for no-op consumers. */
|
|
76
86
|
export type UiI18n = {
|
package/dist/i18n.js
CHANGED
|
@@ -61,6 +61,36 @@ export const defaultUiMessages = {
|
|
|
61
61
|
selectTheme: "Select theme...",
|
|
62
62
|
menu: "Menu",
|
|
63
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a valid BCP-47 locale tag for `Intl.*`. A raw `navigator.language` is NOT guaranteed to be a
|
|
66
|
+
* valid BCP-47 tag — on a POSIX/`LANG=C` host the browser can report `en-US@posix`, and handing that
|
|
67
|
+
* straight to `Intl.Locale`/`Intl.DateTimeFormat` throws `RangeError: Invalid language tag`, which (in a
|
|
68
|
+
* component's reactive scope) kills hydration. This validates the preferred tag, then
|
|
69
|
+
* `navigator.language`, falling back to each candidate's primary subtag (e.g. `en-US@posix` → `en`) and
|
|
70
|
+
* finally `"en-US"`. A tag that is already valid is returned unchanged, so formatted output is
|
|
71
|
+
* byte-identical for every well-formed locale.
|
|
72
|
+
*/
|
|
73
|
+
export function safeLocale(preferred) {
|
|
74
|
+
const isValid = (tag) => {
|
|
75
|
+
try {
|
|
76
|
+
return Intl.getCanonicalLocales(tag).length > 0;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const candidates = [preferred, typeof navigator !== "undefined" ? navigator.language : undefined];
|
|
83
|
+
for (const c of candidates) {
|
|
84
|
+
if (!c)
|
|
85
|
+
continue;
|
|
86
|
+
if (isValid(c))
|
|
87
|
+
return c;
|
|
88
|
+
const primary = c.split(/[-_@.]/)[0]; // strip region/extension/POSIX modifier/charset
|
|
89
|
+
if (primary && isValid(primary))
|
|
90
|
+
return primary;
|
|
91
|
+
}
|
|
92
|
+
return "en-US";
|
|
93
|
+
}
|
|
64
94
|
const KEY = Symbol("svelte-ui-i18n");
|
|
65
95
|
const resolve = (v) => typeof v === "function" ? v() : v;
|
|
66
96
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -86,7 +86,7 @@ export { anchored, computePlacement, resolveBoundary } from "./positioning/ancho
|
|
|
86
86
|
export type { Side, Align, Boundary, PlaceOptions, Placement, AnchoredParams, } from "./positioning/anchored.js";
|
|
87
87
|
export { tooltip } from "./positioning/tooltip.js";
|
|
88
88
|
export type { TooltipOptions } from "./positioning/tooltip.js";
|
|
89
|
-
export { setUiI18n, uiI18n, defaultUiMessages } from "./i18n.js";
|
|
89
|
+
export { setUiI18n, uiI18n, defaultUiMessages, safeLocale } from "./i18n.js";
|
|
90
90
|
export type { UiMessages, UiI18n } from "./i18n.js";
|
|
91
91
|
export type { Variant } from "./types/variants.js";
|
|
92
92
|
export type { Size } from "./types/sizes.js";
|
package/dist/index.js
CHANGED
|
@@ -85,6 +85,6 @@ export { anchored, computePlacement, resolveBoundary } from "./positioning/ancho
|
|
|
85
85
|
// Tooltip action (use:tooltip) — themed tooltips on the `anchored` engine, no global handler.
|
|
86
86
|
export { tooltip } from "./positioning/tooltip.js";
|
|
87
87
|
// Internal localization (opt-in) — override svelte-ui's own microcopy + Intl locale at the app root.
|
|
88
|
-
export { setUiI18n, uiI18n, defaultUiMessages } from "./i18n.js";
|
|
88
|
+
export { setUiI18n, uiI18n, defaultUiMessages, safeLocale } from "./i18n.js";
|
|
89
89
|
export { BREAKPOINT_PX } from "./types/breakpoints.js";
|
|
90
90
|
export { responsiveClasses, resolveScalar } from "./types/responsive.js";
|
package/docs/components.md
CHANGED
|
@@ -93,6 +93,8 @@ Custom dropdown built on `Input` + `List`.
|
|
|
93
93
|
|
|
94
94
|
### Select2 (native-first)
|
|
95
95
|
|
|
96
|
+
> ⚠️ **Experimental — proof-of-concept, NOT ready for use.** Evaluated in the php-svelte test bed only, on the `@dev` channel; the API/behaviour are unstable and it may change or be removed without notice. Don't adopt it in a project yet. See the `*2` banner in `MIGRATION.md`.
|
|
97
|
+
|
|
96
98
|
A **no-JS / SSR-correct** sibling of `Select` that renders a real `<select>` — it works with
|
|
97
99
|
JavaScript disabled, submits in a plain `<form>` (its `name` carries the value, no hidden input), and
|
|
98
100
|
shows the right option in SSR / before hydration. The **closed control is pixel-identical** to
|
|
@@ -106,7 +108,10 @@ shows the right option in SSR / before hydration. The **closed control is pixel-
|
|
|
106
108
|
- **Intentional divergences** (native `<select>` limits): the open list is the OS dropdown (no
|
|
107
109
|
`itemSnippet`/`itemClass`/`dropdownClass`), **no remote search** (`url`/`searchParam`/`transform`/…
|
|
108
110
|
are accepted but ignored — load `options` yourself), no `methods.openDropdown()`, and `multiSelect`
|
|
109
|
-
maps to a native `<select multiple>` listbox
|
|
111
|
+
maps to a native `<select multiple>` listbox (a usable multi-row box, not the dropdown trigger). Note
|
|
112
|
+
the multi **submit shape** differs from `Select`: `Select` multi posts one comma-joined field, while
|
|
113
|
+
`Select2` multi posts the native repeated-field encoding — collect it as an array (in PHP, name the
|
|
114
|
+
field `tags[]`). Full matrix + codemod: `MIGRATION.md`.
|
|
110
115
|
- **Use when**: a static choice must render/submit server-side without JS. Stay on `Select` for
|
|
111
116
|
remote search, custom option rendering, or the multi-select trigger look.
|
|
112
117
|
- **Transitional**: ships alongside `Select` under the `2` suffix; see `MIGRATION.md` for the
|
|
@@ -310,6 +315,8 @@ independent. Propagates `size`/`variant`/`disabled` to children.
|
|
|
310
315
|
|
|
311
316
|
### Disclosure2 / Accordion2 (native-first)
|
|
312
317
|
|
|
318
|
+
> ⚠️ **Experimental — proof-of-concept, NOT ready for use.** Evaluated in the php-svelte test bed only, on the `@dev` channel; the API/behaviour are unstable and it may change or be removed without notice. Don't adopt it in a project yet. See the `*2` banner in `MIGRATION.md`.
|
|
319
|
+
|
|
313
320
|
**No-JS / SSR-correct** siblings built on real `<details>/<summary>`: they expand, rotate the chevron,
|
|
314
321
|
and (via `Accordion2`'s shared `<details name>`) enforce single-open **with no JavaScript**, correct
|
|
315
322
|
in SSR. The `<summary>` wears `Button`'s ghost-header classes, so the header is **pixel-identical** to
|
|
@@ -417,6 +424,8 @@ snaps to canonical on blur.
|
|
|
417
424
|
|
|
418
425
|
### DatePicker2 (native-first)
|
|
419
426
|
|
|
427
|
+
> ⚠️ **Experimental — proof-of-concept, NOT ready for use.** Evaluated in the php-svelte test bed only, on the `@dev` channel; the API/behaviour are unstable and it may change or be removed without notice. Don't adopt it in a project yet. See the `*2` banner in `MIGRATION.md`.
|
|
428
|
+
|
|
420
429
|
**No-JS / SSR-correct** date entry: reuses `Input` with a real `<input type="date">` (or
|
|
421
430
|
`datetime-local` in `time` mode), so it works without JavaScript, pops the browser's native picker,
|
|
422
431
|
and submits via `name`. The field **frame** is pixel-identical to `DatePicker` (same `Input`).
|