@spaethtech/svelte-ui 0.5.1-dev.27.5011e8e → 0.5.1-dev.29.9ae5ffe

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.
@@ -166,6 +166,9 @@ examples):
166
166
  - **Feedback:** `Alert` `Banner` `Badge` `Toaster` + `toast`
167
167
  - **Layout:** `Card` `CardHeader` `CardBody` `CardFooter` · **`Grid`** (auto-placed equal-cell grid;
168
168
  `columns` + `gap`)
169
+ - **Disclosure:** **`Disclosure`** (one expand/collapse section; `title`/`header`, `bind:open`, panel
170
+ stays mounted so state is preserved) · **`Accordion`** (coordinated group — `multiple` single/multi,
171
+ `bind:value`; children are `<Disclosure value="…">`, no per-item `bind:open`)
169
172
  - **Display / theme:** `NotesEditor` · `ThemeSelector` (Auto/Light/Dark Select) · `ThemeToggle`
170
173
  (compact ☀/☾ icon button, same persisted state)
171
174
  - **Types / utils:** `Variant` `Size` `TabDefinition` (types), `responsiveClasses` / `resolveScalar`,
@@ -0,0 +1,69 @@
1
+ <!--
2
+ /**
3
+ * Accordion — groups `Disclosure`s under coordinated open/close rules. `multiple={false}` (default) is
4
+ * single-open (opening one closes the rest); `multiple` lets each toggle independently. Bind `value`
5
+ * (a string for single, string[] for multiple); child `<Disclosure value="…">`s report through
6
+ * context — no per-item `bind:open`. `size`/`variant`/`disabled` propagate to children.
7
+ */
8
+ -->
9
+ <script lang="ts">
10
+ import type { Snippet } from "svelte";
11
+ import type { Size } from "../types/sizes.js";
12
+ import type { Variant } from "../types/variants.js";
13
+ import type { Responsive } from "../types/responsive.js";
14
+ import { setDisclosureGroup } from "./disclosure.js";
15
+
16
+ let {
17
+ multiple = false,
18
+ value = $bindable(),
19
+ collapsible = true,
20
+ size = "md",
21
+ variant,
22
+ disabled = false,
23
+ class: cls = "",
24
+ children,
25
+ }: {
26
+ /** false (default) = single-open; true = independent multi-open. */
27
+ multiple?: boolean;
28
+ /** Open key (single) or keys (multiple). Bindable. */
29
+ value?: string | string[];
30
+ /** Single mode: may the open panel be closed to none? Default true. */
31
+ collapsible?: boolean;
32
+ size?: Responsive<Size>;
33
+ variant?: Variant;
34
+ disabled?: boolean;
35
+ class?: string;
36
+ children: Snippet;
37
+ } = $props();
38
+
39
+ setDisclosureGroup({
40
+ inGroup: true,
41
+ isOpen(v) {
42
+ return multiple ? Array.isArray(value) && value.includes(v) : value === v;
43
+ },
44
+ toggle(v) {
45
+ if (multiple) {
46
+ const arr = Array.isArray(value) ? value : [];
47
+ value = arr.includes(v) ? arr.filter((x) => x !== v) : [...arr, v];
48
+ } else {
49
+ // open a new panel, or (if re-clicking the open one) close it when collapsible
50
+ value = value === v ? (collapsible ? "" : v) : v;
51
+ }
52
+ },
53
+ get size() {
54
+ return size;
55
+ },
56
+ get variant() {
57
+ return variant;
58
+ },
59
+ get disabled() {
60
+ return disabled;
61
+ },
62
+ });
63
+ </script>
64
+
65
+ <div
66
+ class="flex flex-col [&>*:not(:last-child)]:border-b [&>*]:[border-color:var(--ui-border-color)] {cls}"
67
+ >
68
+ {@render children()}
69
+ </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; true = independent multi-open. */
7
+ multiple?: boolean;
8
+ /** Open key (single) or keys (multiple). Bindable. */
9
+ value?: string | string[];
10
+ /** Single mode: may the open panel be closed to none? Default true. */
11
+ collapsible?: boolean;
12
+ size?: Responsive<Size>;
13
+ variant?: Variant;
14
+ disabled?: boolean;
15
+ class?: string;
16
+ children: Snippet;
17
+ };
18
+ declare const Accordion: import("svelte").Component<$$ComponentProps, {}, "value">;
19
+ type Accordion = ReturnType<typeof Accordion>;
20
+ export default Accordion;
@@ -0,0 +1,115 @@
1
+ <!--
2
+ /**
3
+ * Disclosure — a single expand/collapse section: a `<Button>` header (title + rotating chevron) over
4
+ * a content panel. Standalone it owns `bind:open`; inside an `Accordion` it reports to the group via
5
+ * context (keyed by `value`) and the group owns which panel(s) are open.
6
+ *
7
+ * The panel animates with `grid-template-rows: 0fr → 1fr` (smooth for unknown heights, pure CSS), and
8
+ * stays MOUNTED when closed — panel state is preserved. Collapsed, it's `inert` + `aria-hidden` (out
9
+ * of the tab order + a11y tree). The transition is `motion-safe:` (reduced-motion → instant).
10
+ */
11
+ -->
12
+ <script lang="ts">
13
+ import type { Snippet } from "svelte";
14
+ import ChevronDown from "~icons/mdi/chevron-down";
15
+ import type { Size } from "../types/sizes.js";
16
+ import { responsiveClasses, type Responsive } from "../types/responsive.js";
17
+ import type { Variant } from "../types/variants.js";
18
+ import Button from "./Button.svelte";
19
+ import { nextFieldId } from "./FieldChrome.svelte";
20
+ import { getDisclosureGroup } from "./disclosure.js";
21
+
22
+ let {
23
+ title = null,
24
+ header,
25
+ open = $bindable(false),
26
+ value,
27
+ disabled = false,
28
+ size,
29
+ variant,
30
+ icon,
31
+ id,
32
+ class: cls = "",
33
+ children,
34
+ }: {
35
+ /** Header text. Use the `header` snippet instead for full custom header content. */
36
+ title?: string | null;
37
+ header?: Snippet;
38
+ /** Bindable open state — standalone only (ignored inside an Accordion, which owns it). */
39
+ open?: boolean;
40
+ /** This panel's key. Required inside an `Accordion`; ignored standalone. */
41
+ value?: string;
42
+ disabled?: boolean;
43
+ size?: Responsive<Size>;
44
+ /** Header hover/accent (default `ghost`; inherits the Accordion's variant). */
45
+ variant?: Variant;
46
+ /** Optional leading icon in the header. */
47
+ icon?: Snippet;
48
+ id?: string;
49
+ class?: string;
50
+ children: Snippet;
51
+ } = $props();
52
+
53
+ const grp = getDisclosureGroup();
54
+ const inGroup = $derived(!!grp?.inGroup && value != null);
55
+ const effSize = $derived(size ?? grp?.size ?? "md");
56
+ const effVariant = $derived(variant ?? grp?.variant ?? "ghost");
57
+ const effDisabled = $derived(disabled || !!grp?.disabled);
58
+
59
+ const isOpen = $derived(inGroup ? grp!.isOpen(value!) : open);
60
+ function handleToggle() {
61
+ if (inGroup) grp!.toggle(value!);
62
+ else open = !open;
63
+ }
64
+
65
+ const uid = nextFieldId();
66
+ const headerId = $derived(id ?? uid);
67
+ const panelId = $derived(`${headerId}-panel`);
68
+
69
+ const textMap: Record<Size, string> = { sm: "text-xs", md: "text-sm", lg: "text-base" };
70
+ const labelText = $derived(responsiveClasses(effSize, textMap));
71
+ </script>
72
+
73
+ <section class={cls}>
74
+ <Button
75
+ variant={effVariant}
76
+ size={effSize}
77
+ disabled={effDisabled}
78
+ onclick={handleToggle}
79
+ class="w-full [&>div]:w-full"
80
+ id={headerId}
81
+ aria-expanded={isOpen}
82
+ aria-controls={panelId}
83
+ >
84
+ {#if icon}<span class="inline-flex shrink-0">{@render icon()}</span>{/if}
85
+ {#if header}
86
+ <span class="flex-1 text-left">{@render header()}</span>
87
+ {:else}
88
+ <span class="flex-1 text-left font-medium {labelText}">{title}</span>
89
+ {/if}
90
+ <ChevronDown
91
+ class="shrink-0 motion-safe:transition-transform motion-safe:duration-200 {isOpen
92
+ ? 'rotate-180'
93
+ : ''}"
94
+ />
95
+ </Button>
96
+
97
+ <div
98
+ class="grid motion-safe:transition-[grid-template-rows] motion-safe:duration-200 {isOpen
99
+ ? '[grid-template-rows:1fr]'
100
+ : '[grid-template-rows:0fr]'}"
101
+ >
102
+ <div
103
+ class="overflow-hidden [min-height:0]"
104
+ id={panelId}
105
+ role="region"
106
+ aria-labelledby={headerId}
107
+ inert={!isOpen || undefined}
108
+ aria-hidden={!isOpen || undefined}
109
+ >
110
+ <div class="px-1 pt-1 pb-3 {labelText} [color:var(--ui-color-text)]">
111
+ {@render children()}
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </section>
@@ -0,0 +1,25 @@
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
+ /** Bindable open state — standalone only (ignored inside an Accordion, which owns it). */
10
+ open?: boolean;
11
+ /** This panel's key. Required inside an `Accordion`; ignored standalone. */
12
+ value?: string;
13
+ disabled?: boolean;
14
+ size?: Responsive<Size>;
15
+ /** Header hover/accent (default `ghost`; inherits the Accordion's variant). */
16
+ variant?: Variant;
17
+ /** Optional leading icon in the header. */
18
+ icon?: Snippet;
19
+ id?: string;
20
+ class?: string;
21
+ children: Snippet;
22
+ };
23
+ declare const Disclosure: import("svelte").Component<$$ComponentProps, {}, "open">;
24
+ type Disclosure = ReturnType<typeof Disclosure>;
25
+ export default Disclosure;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
3
  import type { Size } from "../types/sizes.js";
4
- import { responsiveClasses, type Responsive } from "../types/responsive.js";
4
+ import { responsiveClasses, resolveScalar, type Responsive } from "../types/responsive.js";
5
5
  import { gridColsClass, gapClass, type Columns, type Gap } from "./field-group.js";
6
6
 
7
7
  /**
@@ -47,8 +47,16 @@
47
47
  const hasDesc = $derived(isSnippet(description) ? true : !!(description && String(description).trim()));
48
48
 
49
49
  // Auto-placed CSS grid: each direct child (a Radio/Checkbox/Toggle label) is one cell — no
50
- // per-child prop. `columns` sets the equal-width tracks; `gap` is the single density knob.
51
- const itemsClass = $derived(`grid ${gridColsClass(columns)} ${gapClass(gap, { x: 6, y: 1 })}`);
50
+ // per-child prop. `columns` sets the equal-width tracks; `gap` is the single density knob and, when
51
+ // unset, scales with `size` (controls grow h-6/h-8/h-10, so the gap tracks them).
52
+ const sizeGap: Record<Size, Gap> = {
53
+ sm: { x: 4, y: 0.5 },
54
+ md: { x: 6, y: 1 },
55
+ lg: { x: 8, y: 2 },
56
+ };
57
+ const itemsClass = $derived(
58
+ `grid ${gridColsClass(columns)} ${gapClass(gap, sizeGap[resolveScalar(size, "md")])}`,
59
+ );
52
60
  </script>
53
61
 
54
62
  <fieldset class="m-0 flex min-w-0 flex-col gap-1 border-0 p-0 {cls}">
@@ -0,0 +1,13 @@
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 DisclosureGroupContext {
5
+ inGroup: true;
6
+ isOpen(value: string): boolean;
7
+ toggle(value: string): void;
8
+ size?: Responsive<Size>;
9
+ variant?: Variant;
10
+ disabled?: boolean;
11
+ }
12
+ export declare function setDisclosureGroup(ctx: DisclosureGroupContext): void;
13
+ export declare function getDisclosureGroup(): DisclosureGroupContext | undefined;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shared context for `Accordion` → `Disclosure` (mirrors `field-group.ts`). A `Disclosure` inside an
3
+ * `Accordion` reads its open-state from the group (keyed by `value`) and toggles through it, so the
4
+ * group owns single-open / multi-open rules; it also inherits the group's `size`/`variant`/`disabled`.
5
+ */
6
+ import { getContext, setContext } from "svelte";
7
+ const KEY = Symbol("svelte-ui-disclosure-group");
8
+ export function setDisclosureGroup(ctx) {
9
+ setContext(KEY, ctx);
10
+ }
11
+ export function getDisclosureGroup() {
12
+ return getContext(KEY);
13
+ }
@@ -27,15 +27,15 @@ export function gridColsClass(columns) {
27
27
  }
28
28
  // Static literal maps (Tailwind-scannable via the shipped dist). Non-responsive — one gap per grid.
29
29
  const GAP = {
30
- "0": "gap-0", "1": "gap-1", "1.5": "gap-1.5", "2": "gap-2", "3": "gap-3",
30
+ "0": "gap-0", "0.5": "gap-0.5", "1": "gap-1", "1.5": "gap-1.5", "2": "gap-2", "3": "gap-3",
31
31
  "4": "gap-4", "5": "gap-5", "6": "gap-6", "8": "gap-8",
32
32
  };
33
33
  const GAP_X = {
34
- "0": "gap-x-0", "1": "gap-x-1", "1.5": "gap-x-1.5", "2": "gap-x-2", "3": "gap-x-3",
34
+ "0": "gap-x-0", "0.5": "gap-x-0.5", "1": "gap-x-1", "1.5": "gap-x-1.5", "2": "gap-x-2", "3": "gap-x-3",
35
35
  "4": "gap-x-4", "5": "gap-x-5", "6": "gap-x-6", "8": "gap-x-8",
36
36
  };
37
37
  const GAP_Y = {
38
- "0": "gap-y-0", "1": "gap-y-1", "1.5": "gap-y-1.5", "2": "gap-y-2", "3": "gap-y-3",
38
+ "0": "gap-y-0", "0.5": "gap-y-0.5", "1": "gap-y-1", "1.5": "gap-y-1.5", "2": "gap-y-2", "3": "gap-y-3",
39
39
  "4": "gap-y-4", "5": "gap-y-5", "6": "gap-y-6", "8": "gap-y-8",
40
40
  };
41
41
  /** Resolve a `Gap` (or the fallback when unset) to `gap-*` / `gap-x-* gap-y-*` classes. */
package/dist/index.d.ts CHANGED
@@ -37,6 +37,8 @@ export { default as Radio } from "./components/Radio.svelte";
37
37
  export { default as FieldGroup } from "./components/FieldGroup.svelte";
38
38
  export { default as Grid } from "./components/Grid.svelte";
39
39
  export type { Columns, Gap } from "./components/field-group.js";
40
+ export { default as Disclosure } from "./components/Disclosure.svelte";
41
+ export { default as Accordion } from "./components/Accordion.svelte";
40
42
  export { default as SideBarMenu } from "./components/SideBarMenu/SideBarMenu.svelte";
41
43
  export type { SideBarMenuItem, SideBarMenuBadge, SideBarMenuBadgeVariant, } from "./components/SideBarMenu/SideBarMenu.svelte";
42
44
  export { default as TabStrip } from "./components/TabStrip/TabStrip.svelte";
package/dist/index.js CHANGED
@@ -41,6 +41,8 @@ export { default as Toggle } from "./components/Toggle.svelte";
41
41
  export { default as Radio } from "./components/Radio.svelte";
42
42
  export { default as FieldGroup } from "./components/FieldGroup.svelte";
43
43
  export { default as Grid } from "./components/Grid.svelte";
44
+ export { default as Disclosure } from "./components/Disclosure.svelte";
45
+ export { default as Accordion } from "./components/Accordion.svelte";
44
46
  // Navigation Components
45
47
  export { default as SideBarMenu } from "./components/SideBarMenu/SideBarMenu.svelte";
46
48
  export { default as TabStrip } from "./components/TabStrip/TabStrip.svelte";
@@ -130,7 +130,8 @@ One wrapper for a set of Radios, Checkboxes, or Toggles under a shared legend (a
130
130
  - **Location**: `src/lib/components/FieldGroup.svelte`
131
131
  - **Props**: `label` (legend), `aside`, `description`, `error`, `required` (the shared field-chrome
132
132
  vocabulary), `columns` (`number | { base, md, … }`, default `1` = stacked; higher = that many
133
- equal auto-placed columns), `gap` (`number | { x, y }`, default `{ x: 6, y: 1 }`), `size`,
133
+ equal auto-placed columns), `gap` (`number | { x, y }`; default scales with `size`
134
+ `sm {x:4,y:0.5}` · `md {x:6,y:1}` · `lg {x:8,y:2}`), `size`,
134
135
  `variant`, `disabled` (propagate to children), and for radio groups `value` (bindable) + `name`.
135
136
  Children render their label inline; radios auto-share the group value/name via context.
136
137
 
@@ -144,6 +145,27 @@ per-child prop). Works with any content. Uniform cells only (for variable widths
144
145
  - **Props**: `columns` (`number | { base, md, … }`, 1–6, default `1`), `gap` (`number | { x, y }`,
145
146
  default `4`), `class`, `children`. Shares `gridColsClass`/`gapClass` with `FieldGroup`.
146
147
 
148
+ ### Disclosure
149
+
150
+ A single expand/collapse section — a `<Button>` header (title + rotating chevron) over a content
151
+ panel. The panel animates via `grid-template-rows` and **stays mounted when collapsed** (panel state
152
+ preserved), `inert` + `aria-hidden` when closed. Standalone `bind:open`; inside an `Accordion` it
153
+ reports via context (keyed by `value`).
154
+
155
+ - **Location**: `src/lib/components/Disclosure.svelte`
156
+ - **Axes**: `size`, `variant` (header; default `ghost`)
157
+ - **Props**: `title` (or `header` Snippet), `open` (bindable, standalone), `value` (key — required in
158
+ an Accordion), `disabled`, `icon` (Snippet), `id`, `class`, `children` (panel).
159
+
160
+ ### Accordion
161
+
162
+ Groups Disclosures under coordinated rules. `multiple={false}` (default) = single-open; `multiple` =
163
+ independent. Propagates `size`/`variant`/`disabled` to children.
164
+
165
+ - **Location**: `src/lib/components/Accordion.svelte`
166
+ - **Props**: `multiple` (default `false`), `value` (bindable — `string` single / `string[]` multiple),
167
+ `collapsible` (single mode, default `true`), `size`, `variant`, `disabled`, `class`, `children`.
168
+
147
169
  ### Rating
148
170
 
149
171
  Read-only star-rating display (5-star, from a 10-point average).
package/docs/usage.md CHANGED
@@ -160,6 +160,34 @@ responsive; `gap` is symmetric (a number) or per-axis (`{ x, y }`).
160
160
 
161
161
  For variable-width cells, use your own grid + `col-span-*`; `Grid` is uniform cells by design.
162
162
 
163
+ ### Disclosure & Accordion
164
+
165
+ A `Disclosure` is one expand/collapse section (`bind:open`); an `Accordion` coordinates a set
166
+ (single-open by default). The panel stays mounted when collapsed, so its state is preserved.
167
+
168
+ ```svelte
169
+ <script>
170
+ import { Disclosure, Accordion } from "@spaethtech/svelte-ui";
171
+ let open = $state(false);
172
+ let section = $state("shipping"); // the open panel's key (single-open)
173
+ </script>
174
+
175
+ <!-- Standalone -->
176
+ <Disclosure title="Advanced options" bind:open>
177
+ …panel content…
178
+ </Disclosure>
179
+
180
+ <!-- Coordinated group (single-open) -->
181
+ <Accordion bind:value={section}>
182
+ <Disclosure value="shipping" title="Shipping">…</Disclosure>
183
+ <Disclosure value="returns" title="Returns">…</Disclosure>
184
+ <Disclosure value="warranty" title="Warranty">…</Disclosure>
185
+ </Accordion>
186
+
187
+ <!-- Multi-open: bind an array -->
188
+ <!-- <Accordion multiple bind:value={openKeys}> … </Accordion> -->
189
+ ```
190
+
163
191
  ### Button
164
192
 
165
193
  ```svelte
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.5.1-dev.27.5011e8e",
3
+ "version": "0.5.1-dev.29.9ae5ffe",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"