@classic-homes/theme-svelte 1.13.1 → 1.15.0

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.
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/svelte';
3
+ import { axe } from 'vitest-axe';
4
+ import { createRawSnippet } from 'svelte';
5
+ import FilterToolbar from '../../lib/components/FilterToolbar.svelte';
6
+ import ActiveFilterChips from '../../lib/components/ActiveFilterChips.svelte';
7
+ const snippet = (html) => createRawSnippet(() => ({ render: () => html }));
8
+ /** Two labelled selects; labels are sr-only when inline. */
9
+ const filters = createRawSnippet((args) => ({
10
+ render: () => {
11
+ const labelClass = args().inline ? ' class="sr-only"' : '';
12
+ return `<div>
13
+ <label for="f-status"${labelClass}>Status</label><select id="f-status"><option>Any</option></select>
14
+ <label for="f-type"${labelClass}>Type</label><select id="f-type"><option>Any</option></select>
15
+ </div>`;
16
+ },
17
+ }));
18
+ const search = snippet('<input type="search" aria-label="Search signs" />');
19
+ describe('FilterToolbar', () => {
20
+ it('keeps filters inline at or under maxInline controls', () => {
21
+ render(FilterToolbar, {
22
+ props: { label: 'Sign tools', search, filters, filterCount: 2 },
23
+ });
24
+ expect(screen.queryByRole('button', { name: /Filters/ })).not.toBeInTheDocument();
25
+ expect(screen.getByLabelText('Status')).toBeInTheDocument();
26
+ });
27
+ it('folds filters behind a Filters button past maxInline', async () => {
28
+ const { container } = render(FilterToolbar, {
29
+ props: { label: 'Sign tools', search, filters, filterCount: 3, activeCount: 2 },
30
+ });
31
+ const button = screen.getByRole('button', { name: 'Filters, 2 active' });
32
+ expect(button).toHaveAttribute('aria-expanded', 'false');
33
+ const panel = screen.getByRole('region', { hidden: true });
34
+ expect(button).toHaveAttribute('aria-controls', panel.id);
35
+ // Svelte sets `inert` as a property; jsdom doesn't reflect it to an attribute.
36
+ const content = panel.firstElementChild;
37
+ expect(content.inert).toBe(true);
38
+ await fireEvent.click(button);
39
+ expect(button).toHaveAttribute('aria-expanded', 'true');
40
+ expect(content.inert).toBe(false);
41
+ expect(await axe(container)).toHaveNoViolations();
42
+ });
43
+ });
44
+ describe('ActiveFilterChips', () => {
45
+ it('removes a single filter or all of them', async () => {
46
+ const onRemove = vi.fn();
47
+ const onClearAll = vi.fn();
48
+ const { container } = render(ActiveFilterChips, {
49
+ props: { chips: [{ key: 'status', label: 'Status: Installed', onRemove }], onClearAll },
50
+ });
51
+ expect(screen.getByRole('group', { name: 'Active filters' })).toBeInTheDocument();
52
+ await fireEvent.click(screen.getByRole('button', { name: 'Remove filter Status: Installed' }));
53
+ expect(onRemove).toHaveBeenCalledOnce();
54
+ await fireEvent.click(screen.getByRole('button', { name: 'Clear all' }));
55
+ expect(onClearAll).toHaveBeenCalledOnce();
56
+ expect(await axe(container)).toHaveNoViolations();
57
+ });
58
+ it('renders nothing without chips', () => {
59
+ const { container } = render(ActiveFilterChips, { props: { chips: [] } });
60
+ expect(container.querySelector('[role="group"]')).toBeNull();
61
+ });
62
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { render, screen } from '@testing-library/svelte';
3
+ import Input from '../lib/components/Input.svelte';
4
+ import Textarea from '../lib/components/Textarea.svelte';
5
+ import Select from '../lib/components/Select.svelte';
6
+ import PasswordInput from '../lib/components/PasswordInput.svelte';
7
+ // Form controls paint their own light surface (bg-background), so they must
8
+ // also set their own text color. Otherwise a dark parent that sets white text
9
+ // (Toolbar's text-toolbar-foreground) leaks into the value: white on white.
10
+ describe('light-surface controls own their text color', () => {
11
+ it('Input', () => {
12
+ render(Input, { props: { 'aria-label': 'Search' } });
13
+ expect(screen.getByRole('textbox', { name: 'Search' })).toHaveClass('bg-background', 'text-foreground');
14
+ });
15
+ it('Textarea', () => {
16
+ render(Textarea, { props: { 'aria-label': 'Notes' } });
17
+ expect(screen.getByRole('textbox', { name: 'Notes' })).toHaveClass('bg-background', 'text-foreground');
18
+ });
19
+ it('PasswordInput', () => {
20
+ const { container } = render(PasswordInput, { props: { id: 'pw' } });
21
+ expect(container.querySelector('#pw')).toHaveClass('bg-background', 'text-foreground');
22
+ });
23
+ it('Select trigger', () => {
24
+ const { container } = render(Select, {
25
+ props: { id: 'status', options: [{ value: 'a', label: 'Active' }], value: 'a' },
26
+ });
27
+ expect(container.querySelector('#status')).toHaveClass('bg-background', 'text-foreground');
28
+ });
29
+ });
@@ -461,8 +461,8 @@
461
461
  'lead badges badges badges';
462
462
  }
463
463
 
464
- /* h1 keeps the theme's 2.5rem display size; leading is a touch looser than
465
- the global 2rem because the title clamps to two lines with overflow hidden */
464
+ /* h1 matches the theme's 2.5rem / 2.75rem heading scale; stated here too
465
+ because the title clamps to two lines with overflow hidden */
466
466
  [data-level='1'] .ah-grid {
467
467
  --ah-title-size: 2.5rem;
468
468
  --ah-title-leading: 2.75rem;
@@ -0,0 +1,61 @@
1
+ <script lang="ts" module>
2
+ export interface ActiveFilterChip {
3
+ /** Stable key, e.g. "status" or "community:12" */
4
+ key: string;
5
+ /** Chip text, e.g. "Status: Installed" */
6
+ label: string;
7
+ /** Clears just this filter */
8
+ onRemove: () => void;
9
+ }
10
+ </script>
11
+
12
+ <script lang="ts">
13
+ import type { Snippet } from 'svelte';
14
+ import { cn } from '../utils.js';
15
+ import Button from './Button.svelte';
16
+ import Icon from './Icon.svelte';
17
+
18
+ /**
19
+ * ActiveFilterChips - one removable chip per filter narrowing the results,
20
+ * shown under a FilterToolbar so collapsed filters stay visible.
21
+ *
22
+ * Each chip clears only its own filter; "Clear all" resets everything.
23
+ * Renders nothing when there are no chips and no extra content.
24
+ */
25
+ interface Props {
26
+ chips: ActiveFilterChip[];
27
+ /** Resets every filter; shows a trailing "Clear all" when set */
28
+ onClearAll?: () => void;
29
+ /** Extra notes after the chips (e.g. filters that don't apply here) */
30
+ children?: Snippet;
31
+ class?: string;
32
+ }
33
+
34
+ let { chips, onClearAll, children, class: className }: Props = $props();
35
+ </script>
36
+
37
+ {#if chips.length > 0 || children}
38
+ <div
39
+ role="group"
40
+ aria-label="Active filters"
41
+ class={cn('flex flex-wrap items-center gap-2 text-sm', className)}
42
+ >
43
+ {#each chips as chip (chip.key)}
44
+ <button
45
+ type="button"
46
+ class="inline-flex min-h-11 max-w-full items-center gap-1 rounded-full border border-border bg-muted px-3 text-xs font-medium text-foreground hover:bg-muted-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:min-h-8"
47
+ aria-label={`Remove filter ${chip.label}`}
48
+ onclick={chip.onRemove}
49
+ >
50
+ <span class="truncate">{chip.label}</span>
51
+ <Icon name="close" size={12} class="shrink-0" />
52
+ </button>
53
+ {/each}
54
+ {@render children?.()}
55
+ {#if onClearAll && chips.length > 0}
56
+ <Button variant="ghost" size="sm" class="ml-auto min-h-11 sm:min-h-8" onclick={onClearAll}>
57
+ Clear all
58
+ </Button>
59
+ {/if}
60
+ </div>
61
+ {/if}
@@ -0,0 +1,27 @@
1
+ export interface ActiveFilterChip {
2
+ /** Stable key, e.g. "status" or "community:12" */
3
+ key: string;
4
+ /** Chip text, e.g. "Status: Installed" */
5
+ label: string;
6
+ /** Clears just this filter */
7
+ onRemove: () => void;
8
+ }
9
+ import type { Snippet } from 'svelte';
10
+ /**
11
+ * ActiveFilterChips - one removable chip per filter narrowing the results,
12
+ * shown under a FilterToolbar so collapsed filters stay visible.
13
+ *
14
+ * Each chip clears only its own filter; "Clear all" resets everything.
15
+ * Renders nothing when there are no chips and no extra content.
16
+ */
17
+ interface Props {
18
+ chips: ActiveFilterChip[];
19
+ /** Resets every filter; shows a trailing "Clear all" when set */
20
+ onClearAll?: () => void;
21
+ /** Extra notes after the chips (e.g. filters that don't apply here) */
22
+ children?: Snippet;
23
+ class?: string;
24
+ }
25
+ declare const ActiveFilterChips: import("svelte").Component<Props, {}, "">;
26
+ type ActiveFilterChips = ReturnType<typeof ActiveFilterChips>;
27
+ export default ActiveFilterChips;
@@ -166,7 +166,7 @@
166
166
  onfocus={() => (open = true)}
167
167
  onclick={() => (open = true)}
168
168
  class={cn(
169
- 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-base sm:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
169
+ 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background text-foreground px-3 py-2 text-base sm:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
170
170
  error && 'border-destructive focus:ring-destructive',
171
171
  className
172
172
  )}
@@ -262,7 +262,7 @@
262
262
  onfocus={() => (open = true)}
263
263
  onclick={() => (open = true)}
264
264
  class={cn(
265
- 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-base sm:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
265
+ 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background text-foreground px-3 py-2 text-base sm:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
266
266
  error && 'border-destructive focus:ring-destructive',
267
267
  className
268
268
  )}
@@ -431,7 +431,7 @@
431
431
  onkeydown={handleInputKeyDown}
432
432
  {disabled}
433
433
  class={cn(
434
- 'flex h-10 w-full items-center rounded-md border border-input bg-background px-3 py-2 pr-10 text-base sm:text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
434
+ 'flex h-10 w-full items-center rounded-md border border-input bg-background text-foreground px-3 py-2 pr-10 text-base sm:text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
435
435
  !value && !isEditing && 'text-muted-foreground',
436
436
  displayError && 'border-destructive focus:ring-destructive',
437
437
  className
@@ -571,7 +571,7 @@
571
571
  bind:value={hour}
572
572
  onchange={updateTime}
573
573
  aria-label="Hour"
574
- class="h-8 rounded-md border border-input bg-background px-2 text-base sm:text-sm focus:outline-none focus:ring-2 focus:ring-ring"
574
+ class="h-8 rounded-md border border-input bg-background text-foreground px-2 text-base sm:text-sm focus:outline-none focus:ring-2 focus:ring-ring"
575
575
  >
576
576
  {#each hourOptions as h}
577
577
  <option value={timeFormat === '12h' ? h : h}>
@@ -584,7 +584,7 @@
584
584
  bind:value={minute}
585
585
  onchange={updateTime}
586
586
  aria-label="Minute"
587
- class="h-8 rounded-md border border-input bg-background px-2 text-base sm:text-sm focus:outline-none focus:ring-2 focus:ring-ring"
587
+ class="h-8 rounded-md border border-input bg-background text-foreground px-2 text-base sm:text-sm focus:outline-none focus:ring-2 focus:ring-ring"
588
588
  >
589
589
  {#each minuteOptions as m}
590
590
  <option value={m}>{String(m).padStart(2, '0')}</option>
@@ -595,7 +595,7 @@
595
595
  bind:value={period}
596
596
  onchange={updateTime}
597
597
  aria-label="AM or PM"
598
- class="h-8 rounded-md border border-input bg-background px-2 text-base sm:text-sm focus:outline-none focus:ring-2 focus:ring-ring"
598
+ class="h-8 rounded-md border border-input bg-background text-foreground px-2 text-base sm:text-sm focus:outline-none focus:ring-2 focus:ring-ring"
599
599
  >
600
600
  <option value="AM">AM</option>
601
601
  <option value="PM">PM</option>
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '../utils.js';
4
+ import Button from './Button.svelte';
5
+
6
+ /**
7
+ * FilterPanel - collapsible block of filter controls under a Toolbar.
8
+ *
9
+ * Opens and closes with a grid-rows height transition (no measured heights).
10
+ * While closed the content is `inert`, so the hidden controls are neither
11
+ * tabbable nor exposed to screen readers. Controls are laid out on a
12
+ * responsive grid; "Clear all" follows them when any filter is active.
13
+ *
14
+ * Usually rendered by FilterToolbar; use it directly only for a custom strip.
15
+ */
16
+ interface Props {
17
+ /** Whether the panel is expanded */
18
+ open?: boolean;
19
+ /** id for the region, so the toggle button can point `aria-controls` at it */
20
+ id?: string;
21
+ /** Accessible name for the region (default "Filters") */
22
+ label?: string;
23
+ /** Active filter count; "Clear all" shows when > 0 and `onClear` is set */
24
+ activeCount?: number;
25
+ /** Resets every filter */
26
+ onClear?: () => void;
27
+ /** The filter controls, one grid cell each */
28
+ children: Snippet;
29
+ class?: string;
30
+ }
31
+
32
+ let {
33
+ open = $bindable(false),
34
+ id,
35
+ label = 'Filters',
36
+ activeCount = 0,
37
+ onClear,
38
+ children,
39
+ class: className,
40
+ }: Props = $props();
41
+ </script>
42
+
43
+ <div
44
+ {id}
45
+ role="region"
46
+ aria-label={label}
47
+ class={cn(
48
+ 'grid transition-[grid-template-rows] duration-300 ease-in-out motion-reduce:transition-none',
49
+ open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
50
+ className
51
+ )}
52
+ >
53
+ <div class="overflow-hidden" inert={!open}>
54
+ <div class="mt-2 rounded-lg border border-border bg-muted/50 p-4">
55
+ <div class="grid items-end gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
56
+ {@render children()}
57
+ </div>
58
+ {#if onClear && activeCount > 0}
59
+ <div class="mt-3 flex justify-end">
60
+ <Button variant="ghost" size="sm" onclick={onClear}>Clear all</Button>
61
+ </div>
62
+ {/if}
63
+ </div>
64
+ </div>
65
+ </div>
@@ -0,0 +1,29 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * FilterPanel - collapsible block of filter controls under a Toolbar.
4
+ *
5
+ * Opens and closes with a grid-rows height transition (no measured heights).
6
+ * While closed the content is `inert`, so the hidden controls are neither
7
+ * tabbable nor exposed to screen readers. Controls are laid out on a
8
+ * responsive grid; "Clear all" follows them when any filter is active.
9
+ *
10
+ * Usually rendered by FilterToolbar; use it directly only for a custom strip.
11
+ */
12
+ interface Props {
13
+ /** Whether the panel is expanded */
14
+ open?: boolean;
15
+ /** id for the region, so the toggle button can point `aria-controls` at it */
16
+ id?: string;
17
+ /** Accessible name for the region (default "Filters") */
18
+ label?: string;
19
+ /** Active filter count; "Clear all" shows when > 0 and `onClear` is set */
20
+ activeCount?: number;
21
+ /** Resets every filter */
22
+ onClear?: () => void;
23
+ /** The filter controls, one grid cell each */
24
+ children: Snippet;
25
+ class?: string;
26
+ }
27
+ declare const FilterPanel: import("svelte").Component<Props, {}, "open">;
28
+ type FilterPanel = ReturnType<typeof FilterPanel>;
29
+ export default FilterPanel;
@@ -0,0 +1,133 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { MediaQuery } from 'svelte/reactivity';
4
+ import Button from './Button.svelte';
5
+ import Dialog from './Dialog.svelte';
6
+ import FilterPanel from './FilterPanel.svelte';
7
+ import Icon from './Icon.svelte';
8
+ import Toolbar from './Toolbar.svelte';
9
+
10
+ /**
11
+ * FilterToolbar - a Toolbar that folds its filter controls behind a
12
+ * "Filters" button once there are too many to sit inline.
13
+ *
14
+ * Rule: when the left side would hold more than `maxInline` controls
15
+ * (search counts, count text does not), or on a narrow viewport, the
16
+ * filters move behind the button - a collapsible FilterPanel under the
17
+ * strip on desktop, a full-screen Dialog on mobile. Otherwise they render
18
+ * inline in the strip.
19
+ *
20
+ * The page renders its controls once, in `filters`; the `inline` argument
21
+ * says whether they sit in the strip (keep labels `sr-only`) or in the
22
+ * panel/dialog (show labels). Filters apply live in every mode - the
23
+ * dialog's "Done" only closes it.
24
+ */
25
+ interface Props {
26
+ /** Accessible name for the strip, e.g. "Sign tools" */
27
+ label: string;
28
+ /** Search field; always inline */
29
+ search?: Snippet;
30
+ /** The filter controls; `inline` is false inside the panel/dialog */
31
+ filters: Snippet<[{ inline: boolean }]>;
32
+ /** How many controls `filters` renders (drives the collapse rule) */
33
+ filterCount: number;
34
+ /** Filters currently narrowing results (button badge) */
35
+ activeCount?: number;
36
+ /** Result count / summary text, after the filters or the button */
37
+ summary?: Snippet;
38
+ /** Right-aligned cluster (view toggles etc.) */
39
+ end?: Snippet;
40
+ /** Resets every filter ("Clear all" in the panel and dialog) */
41
+ onClear?: () => void;
42
+ /** Most left-side controls (search included) allowed inline (default 3) */
43
+ maxInline?: number;
44
+ /** Whether the panel/dialog is open */
45
+ open?: boolean;
46
+ /** Viewport query below which filters always collapse into a dialog */
47
+ mobileQuery?: string;
48
+ class?: string;
49
+ }
50
+
51
+ let {
52
+ label,
53
+ search,
54
+ filters,
55
+ filterCount,
56
+ activeCount = 0,
57
+ summary,
58
+ end: endSnippet,
59
+ onClear,
60
+ maxInline = 3,
61
+ open = $bindable(false),
62
+ mobileQuery = '(max-width: 1023px)',
63
+ class: className,
64
+ }: Props = $props();
65
+
66
+ const uid = $props.id();
67
+ const panelId = `filter-panel-${uid}`;
68
+
69
+ const mobile = $derived(new MediaQuery(mobileQuery, false));
70
+ const collapsed = $derived(
71
+ filterCount > 0 && (mobile.current || (search ? 1 : 0) + filterCount > maxInline)
72
+ );
73
+ </script>
74
+
75
+ <div class={className}>
76
+ <Toolbar {label} end={endSnippet}>
77
+ {@render search?.()}
78
+ {#if collapsed}
79
+ <Button
80
+ variant={open ? 'default' : 'secondary'}
81
+ size="sm"
82
+ class="min-h-11 shrink-0 sm:min-h-9"
83
+ aria-expanded={open}
84
+ aria-controls={mobile.current ? undefined : panelId}
85
+ aria-label={activeCount > 0 ? `Filters, ${activeCount} active` : 'Filters'}
86
+ onclick={() => (open = !open)}
87
+ >
88
+ <Icon name="filter" size={16} />
89
+ Filters
90
+ {#if activeCount > 0}
91
+ <span
92
+ class="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-background px-1.5 text-xs text-foreground"
93
+ aria-hidden="true"
94
+ >
95
+ {activeCount}
96
+ </span>
97
+ {/if}
98
+ </Button>
99
+ {:else}
100
+ {@render filters({ inline: true })}
101
+ {/if}
102
+ {@render summary?.()}
103
+ </Toolbar>
104
+
105
+ {#if collapsed && !mobile.current}
106
+ <FilterPanel bind:open id={panelId} {label} {activeCount} {onClear}>
107
+ {@render filters({ inline: false })}
108
+ </FilterPanel>
109
+ {/if}
110
+ </div>
111
+
112
+ {#if collapsed && mobile.current}
113
+ <Dialog
114
+ bind:open
115
+ title="Filters"
116
+ description={activeCount > 0 ? `${activeCount} active` : undefined}
117
+ size="full"
118
+ >
119
+ <div class="space-y-4">
120
+ {@render filters({ inline: false })}
121
+ </div>
122
+ {#snippet footer()}
123
+ <div class="flex w-full justify-between gap-2">
124
+ {#if onClear}
125
+ <Button variant="ghost" class="min-h-11" disabled={activeCount === 0} onclick={onClear}>
126
+ Clear all
127
+ </Button>
128
+ {/if}
129
+ <Button class="ml-auto min-h-11" onclick={() => (open = false)}>Done</Button>
130
+ </div>
131
+ {/snippet}
132
+ </Dialog>
133
+ {/if}
@@ -0,0 +1,46 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * FilterToolbar - a Toolbar that folds its filter controls behind a
4
+ * "Filters" button once there are too many to sit inline.
5
+ *
6
+ * Rule: when the left side would hold more than `maxInline` controls
7
+ * (search counts, count text does not), or on a narrow viewport, the
8
+ * filters move behind the button - a collapsible FilterPanel under the
9
+ * strip on desktop, a full-screen Dialog on mobile. Otherwise they render
10
+ * inline in the strip.
11
+ *
12
+ * The page renders its controls once, in `filters`; the `inline` argument
13
+ * says whether they sit in the strip (keep labels `sr-only`) or in the
14
+ * panel/dialog (show labels). Filters apply live in every mode - the
15
+ * dialog's "Done" only closes it.
16
+ */
17
+ interface Props {
18
+ /** Accessible name for the strip, e.g. "Sign tools" */
19
+ label: string;
20
+ /** Search field; always inline */
21
+ search?: Snippet;
22
+ /** The filter controls; `inline` is false inside the panel/dialog */
23
+ filters: Snippet<[{
24
+ inline: boolean;
25
+ }]>;
26
+ /** How many controls `filters` renders (drives the collapse rule) */
27
+ filterCount: number;
28
+ /** Filters currently narrowing results (button badge) */
29
+ activeCount?: number;
30
+ /** Result count / summary text, after the filters or the button */
31
+ summary?: Snippet;
32
+ /** Right-aligned cluster (view toggles etc.) */
33
+ end?: Snippet;
34
+ /** Resets every filter ("Clear all" in the panel and dialog) */
35
+ onClear?: () => void;
36
+ /** Most left-side controls (search included) allowed inline (default 3) */
37
+ maxInline?: number;
38
+ /** Whether the panel/dialog is open */
39
+ open?: boolean;
40
+ /** Viewport query below which filters always collapse into a dialog */
41
+ mobileQuery?: string;
42
+ class?: string;
43
+ }
44
+ declare const FilterToolbar: import("svelte").Component<Props, {}, "open">;
45
+ type FilterToolbar = ReturnType<typeof FilterToolbar>;
46
+ export default FilterToolbar;
@@ -82,7 +82,7 @@
82
82
  aria-invalid={ariaInvalid}
83
83
  aria-required={required}
84
84
  class={cn(
85
- 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base sm:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
85
+ 'flex h-10 w-full rounded-md border border-input bg-background text-foreground px-3 py-2 text-base sm:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
86
86
  ariaInvalid === true || ariaInvalid === 'true'
87
87
  ? 'border-destructive focus-visible:ring-destructive'
88
88
  : '',
@@ -227,7 +227,7 @@
227
227
  onfocus={() => (open = true)}
228
228
  onclick={() => (open = true)}
229
229
  class={cn(
230
- 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
230
+ 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
231
231
  error && 'border-destructive focus:ring-destructive'
232
232
  )}
233
233
  aria-invalid={error ? 'true' : undefined}
@@ -364,7 +364,7 @@
364
364
  onfocus={() => (open = true)}
365
365
  onclick={() => (open = true)}
366
366
  class={cn(
367
- 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
367
+ 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
368
368
  error && 'border-destructive focus:ring-destructive'
369
369
  )}
370
370
  aria-invalid={error ? 'true' : undefined}
@@ -159,7 +159,7 @@
159
159
  onclick={decrement}
160
160
  disabled={disabled || !canDecrement}
161
161
  class={cn(
162
- 'flex h-10 w-10 items-center justify-center rounded-l-md border border-r-0 border-input bg-background text-sm font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
162
+ 'flex h-10 w-10 items-center justify-center rounded-l-md border border-r-0 border-input bg-background text-foreground text-sm font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
163
163
  error && 'border-destructive'
164
164
  )}
165
165
  aria-label="Decrease value"
@@ -208,7 +208,7 @@
208
208
  onclick={increment}
209
209
  disabled={disabled || !canIncrement}
210
210
  class={cn(
211
- 'flex h-10 w-10 items-center justify-center rounded-r-md border border-l-0 border-input bg-background text-sm font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
211
+ 'flex h-10 w-10 items-center justify-center rounded-r-md border border-l-0 border-input bg-background text-foreground text-sm font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
212
212
  error && 'border-destructive'
213
213
  )}
214
214
  aria-label="Increase value"
@@ -251,7 +251,7 @@
251
251
  onclick={decrement}
252
252
  disabled={disabled || !canDecrement}
253
253
  class={cn(
254
- 'flex h-10 w-10 items-center justify-center rounded-l-md border border-r-0 border-input bg-background text-sm font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
254
+ 'flex h-10 w-10 items-center justify-center rounded-l-md border border-r-0 border-input bg-background text-foreground text-sm font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
255
255
  error && 'border-destructive'
256
256
  )}
257
257
  aria-label="Decrease value"
@@ -300,7 +300,7 @@
300
300
  onclick={increment}
301
301
  disabled={disabled || !canIncrement}
302
302
  class={cn(
303
- 'flex h-10 w-10 items-center justify-center rounded-r-md border border-l-0 border-input bg-background text-sm font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
303
+ 'flex h-10 w-10 items-center justify-center rounded-r-md border border-l-0 border-input bg-background text-foreground text-sm font-medium hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
304
304
  error && 'border-destructive'
305
305
  )}
306
306
  aria-label="Increase value"
@@ -244,7 +244,7 @@
244
244
  onpaste={handlePaste}
245
245
  onfocus={handleFocus}
246
246
  class={cn(
247
- 'h-10 w-10 text-base sm:h-12 sm:w-12 sm:text-lg text-center font-semibold rounded-md border border-input bg-background ring-offset-background transition-colors',
247
+ 'h-10 w-10 text-base sm:h-12 sm:w-12 sm:text-lg text-center font-semibold rounded-md border border-input bg-background text-foreground ring-offset-background transition-colors',
248
248
  'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
249
249
  'disabled:cursor-not-allowed disabled:opacity-50',
250
250
  error && 'border-destructive focus:ring-destructive'
@@ -295,7 +295,7 @@
295
295
  onpaste={handlePaste}
296
296
  onfocus={handleFocus}
297
297
  class={cn(
298
- 'h-10 w-10 text-base sm:h-12 sm:w-12 sm:text-lg text-center font-semibold rounded-md border border-input bg-background ring-offset-background transition-colors',
298
+ 'h-10 w-10 text-base sm:h-12 sm:w-12 sm:text-lg text-center font-semibold rounded-md border border-input bg-background text-foreground ring-offset-background transition-colors',
299
299
  'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
300
300
  'disabled:cursor-not-allowed disabled:opacity-50',
301
301
  error && 'border-destructive focus:ring-destructive'
@@ -69,7 +69,7 @@
69
69
  {id}
70
70
  {autocomplete}
71
71
  class={cn(
72
- 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 pr-10 text-base sm:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
72
+ 'flex h-10 w-full rounded-md border border-input bg-background text-foreground px-3 py-2 pr-10 text-base sm:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
73
73
  className
74
74
  )}
75
75
  oninput={handleInput}
@@ -132,7 +132,7 @@
132
132
  // Common trigger classes
133
133
  const triggerClasses = $derived(
134
134
  cn(
135
- 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
135
+ 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
136
136
  error && 'border-destructive focus:ring-destructive',
137
137
  className
138
138
  )
@@ -35,7 +35,7 @@
35
35
  canvas: 'block w-full cursor-crosshair touch-none',
36
36
  toolbar: 'flex flex-wrap items-center gap-2',
37
37
  toolButton:
38
- 'inline-flex h-9 w-9 items-center justify-center rounded-md border border-input bg-background text-sm font-medium ring-offset-background hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
38
+ 'inline-flex h-9 w-9 items-center justify-center rounded-md border border-input bg-background text-foreground text-sm font-medium ring-offset-background hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
39
39
  toolGroup: 'flex items-center gap-1.5',
40
40
  toolLabel: 'text-xs text-muted-foreground',
41
41
  colorPicker:
@@ -990,7 +990,7 @@
990
990
  oninput={handleTypedNameChange}
991
991
  placeholder="Enter your full name"
992
992
  class={cn(
993
- 'flex h-11 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
993
+ 'flex h-11 w-full rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
994
994
  error && 'border-destructive focus-visible:ring-destructive'
995
995
  )}
996
996
  {disabled}
@@ -62,7 +62,7 @@
62
62
  aria-describedby={ariaDescribedby}
63
63
  aria-invalid={ariaInvalid}
64
64
  class={cn(
65
- 'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base sm:text-sm ring-offset-background placeholder:text-gray-500 dark:placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-y',
65
+ 'flex min-h-[80px] w-full rounded-md border border-input bg-background text-foreground px-3 py-2 text-base sm:text-sm ring-offset-background placeholder:text-gray-500 dark:placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-y',
66
66
  ariaInvalid === true || ariaInvalid === 'true'
67
67
  ? 'border-destructive focus-visible:ring-destructive'
68
68
  : '',
@@ -59,6 +59,9 @@ export { default as DataTable } from './components/DataTable.svelte';
59
59
  export { default as Tabs } from './components/Tabs.svelte';
60
60
  export { default as TabPanel } from './components/TabPanel.svelte';
61
61
  export { default as Toolbar } from './components/Toolbar.svelte';
62
+ export { default as FilterToolbar } from './components/FilterToolbar.svelte';
63
+ export { default as FilterPanel } from './components/FilterPanel.svelte';
64
+ export { default as ActiveFilterChips, type ActiveFilterChip, } from './components/ActiveFilterChips.svelte';
62
65
  export { default as SegmentedControl, type SegmentedControlOption, } from './components/SegmentedControl.svelte';
63
66
  export { default as ViewToggle, type ViewToggleOption, defaultViewToggleOptions, } from './components/ViewToggle.svelte';
64
67
  export { AspectRatio, Image, Video, Audio, MediaEmbed, MediaGrid, MediaCarousel, Lightbox, } from './components/media/index.js';
package/dist/lib/index.js CHANGED
@@ -72,6 +72,9 @@ export { default as Tabs } from './components/Tabs.svelte';
72
72
  export { default as TabPanel } from './components/TabPanel.svelte';
73
73
  // Toolbar controls
74
74
  export { default as Toolbar } from './components/Toolbar.svelte';
75
+ export { default as FilterToolbar } from './components/FilterToolbar.svelte';
76
+ export { default as FilterPanel } from './components/FilterPanel.svelte';
77
+ export { default as ActiveFilterChips, } from './components/ActiveFilterChips.svelte';
75
78
  export { default as SegmentedControl, } from './components/SegmentedControl.svelte';
76
79
  export { default as ViewToggle, defaultViewToggleOptions, } from './components/ViewToggle.svelte';
77
80
  // Media Components
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/theme-svelte",
3
- "version": "1.13.1",
3
+ "version": "1.15.0",
4
4
  "description": "Svelte components for the Classic theme system",
5
5
  "type": "module",
6
6
  "svelte": "./dist/lib/index.js",