@aiaiai-pt/design-system 0.4.2 → 0.4.3

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,261 @@
1
+ <!--
2
+ @component FilterPanel
3
+
4
+ Typed, labeled filter bar for admin/data UIs.
5
+ Each filter renders as the appropriate widget based on its type:
6
+ select → Select dropdown, boolean → Toggle pair, search → Combobox.
7
+
8
+ Consumes --filter-chip-*, --filter-bar-clear-*, and --input-* tokens from components.css.
9
+
10
+ @example
11
+ <FilterPanel
12
+ filters={[
13
+ { key: 'status', label: 'Status', type: 'select', options: [
14
+ { value: 'open', label: 'Open' },
15
+ { value: 'closed', label: 'Closed' },
16
+ ]},
17
+ { key: 'active', label: 'Active', type: 'boolean' },
18
+ ]}
19
+ bind:value={activeFilters}
20
+ onchange={handleFilterChange}
21
+ />
22
+ -->
23
+ <script module>
24
+ /**
25
+ * @typedef {{ value: string, label: string }} FilterOption
26
+ * @typedef {{
27
+ * key: string,
28
+ * label: string,
29
+ * type: 'select' | 'boolean' | 'search',
30
+ * options?: FilterOption[],
31
+ * onsearch?: (query: string) => void,
32
+ * searchItems?: FilterOption[],
33
+ * searchLoading?: boolean,
34
+ * }} FilterFieldDef
35
+ */
36
+ </script>
37
+
38
+ <script>
39
+ import Select from './Select.svelte';
40
+ import Combobox from './Combobox.svelte';
41
+
42
+ let {
43
+ /** @type {FilterFieldDef[]} */
44
+ filters = [],
45
+ /** @type {Record<string, any>} */
46
+ value = $bindable({}),
47
+ /** @type {((value: Record<string, any>) => void) | undefined} */
48
+ onchange = undefined,
49
+ /** @type {(() => void) | undefined} */
50
+ onclear = undefined,
51
+ /** @type {string} */
52
+ class: className = '',
53
+ ...rest
54
+ } = $props();
55
+
56
+ const activeCount = $derived(
57
+ Object.values(value).filter(v => v !== undefined && v !== null && v !== '').length
58
+ );
59
+
60
+ /**
61
+ * @param {string} key
62
+ * @param {string | undefined} val
63
+ */
64
+ function setFilter(key, val) {
65
+ value = { ...value, [key]: val === '' || val === undefined ? undefined : val };
66
+ onchange?.(value);
67
+ }
68
+
69
+ /**
70
+ * @param {string} key
71
+ * @param {string} optionValue
72
+ */
73
+ function toggleBoolean(key, optionValue) {
74
+ value = {
75
+ ...value,
76
+ [key]: value[key] === optionValue ? undefined : optionValue,
77
+ };
78
+ onchange?.(value);
79
+ }
80
+
81
+ function clearAll() {
82
+ value = {};
83
+ onchange?.({});
84
+ onclear?.();
85
+ }
86
+ </script>
87
+
88
+ {#if filters.length > 0}
89
+ <div
90
+ class="filter-panel {className}"
91
+ role="group"
92
+ aria-label="Filters"
93
+ {...rest}
94
+ >
95
+ {#each filters as filter (filter.key)}
96
+ <div class="filter-field">
97
+ {#if filter.type === 'select' && filter.options}
98
+ <Select
99
+ size="sm"
100
+ label={filter.label}
101
+ value={value[filter.key] ?? ''}
102
+ options={[{ value: '', label: 'All' }, ...filter.options]}
103
+ onchange={(val) => setFilter(filter.key, val)}
104
+ />
105
+ {:else if filter.type === 'boolean'}
106
+ {@const opts = filter.options ?? [{ value: 'true', label: 'Yes' }, { value: 'false', label: 'No' }]}
107
+ <span class="filter-field-label">{filter.label}</span>
108
+ <div class="filter-bool-chips">
109
+ {#each opts as opt (opt.value)}
110
+ {@const active = value[filter.key] === opt.value}
111
+ <button
112
+ class="filter-chip"
113
+ class:filter-chip-active={active}
114
+ aria-pressed={active}
115
+ type="button"
116
+ onclick={() => toggleBoolean(filter.key, opt.value)}
117
+ >{opt.label}</button>
118
+ {/each}
119
+ </div>
120
+ {:else if filter.type === 'search'}
121
+ <Combobox
122
+ size="sm"
123
+ label={filter.label}
124
+ placeholder="All"
125
+ items={filter.searchItems ?? []}
126
+ value={value[filter.key] ?? ''}
127
+ onchange={(val) => setFilter(filter.key, val)}
128
+ onsearch={filter.onsearch}
129
+ loading={filter.searchLoading ?? false}
130
+ />
131
+ {/if}
132
+ </div>
133
+ {/each}
134
+
135
+ {#if activeCount >= 2}
136
+ <button
137
+ class="filter-panel-clear"
138
+ onclick={clearAll}
139
+ type="button"
140
+ >
141
+ Clear all
142
+ </button>
143
+ {/if}
144
+ </div>
145
+ {/if}
146
+
147
+ <style>
148
+ .filter-panel {
149
+ display: flex;
150
+ align-items: flex-end;
151
+ gap: var(--space-md);
152
+ flex-wrap: wrap;
153
+ }
154
+
155
+ .filter-field {
156
+ display: flex;
157
+ flex-direction: column;
158
+ gap: var(--space-2xs);
159
+ min-width: 140px;
160
+ max-width: 220px;
161
+ }
162
+
163
+ .filter-field-label {
164
+ font-family: var(--input-label-font);
165
+ font-size: var(--input-label-size);
166
+ letter-spacing: var(--input-label-tracking);
167
+ color: var(--input-label-color);
168
+ }
169
+
170
+ .filter-bool-chips {
171
+ display: flex;
172
+ gap: var(--filter-chip-gap);
173
+ }
174
+
175
+ /* Reuse existing filter-chip tokens */
176
+ .filter-chip {
177
+ all: unset;
178
+ box-sizing: border-box;
179
+ display: inline-flex;
180
+ align-items: center;
181
+ height: var(--filter-chip-height);
182
+ padding: var(--filter-chip-padding);
183
+ font-family: var(--filter-chip-font);
184
+ font-size: var(--filter-chip-size);
185
+ letter-spacing: var(--filter-chip-tracking);
186
+ color: var(--filter-chip-color);
187
+ background: var(--filter-chip-bg);
188
+ border: var(--filter-chip-border);
189
+ border-radius: var(--filter-chip-radius);
190
+ cursor: pointer;
191
+ white-space: nowrap;
192
+ transition: all var(--filter-chip-transition);
193
+ user-select: none;
194
+ }
195
+
196
+ .filter-chip:hover {
197
+ background: var(--filter-chip-bg-hover);
198
+ }
199
+
200
+ .filter-chip:focus-visible {
201
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
202
+ outline-offset: var(--focus-ring-offset);
203
+ }
204
+
205
+ .filter-chip-active {
206
+ background: var(--filter-chip-bg-active);
207
+ color: var(--filter-chip-color-active);
208
+ border: var(--filter-chip-border-active);
209
+ }
210
+
211
+ .filter-chip-active:hover {
212
+ background: var(--filter-chip-bg-active);
213
+ }
214
+
215
+ .filter-panel-clear {
216
+ all: unset;
217
+ box-sizing: border-box;
218
+ font-family: var(--filter-chip-font);
219
+ font-size: var(--filter-chip-size);
220
+ letter-spacing: var(--filter-chip-tracking);
221
+ color: var(--filter-bar-clear-color);
222
+ cursor: pointer;
223
+ white-space: nowrap;
224
+ padding-bottom: var(--space-xs);
225
+ transition: color var(--filter-chip-transition);
226
+ }
227
+
228
+ .filter-panel-clear:hover {
229
+ color: var(--filter-bar-clear-color-hover);
230
+ }
231
+
232
+ .filter-panel-clear:focus-visible {
233
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
234
+ outline-offset: var(--focus-ring-offset);
235
+ border-radius: var(--radius-sm);
236
+ }
237
+
238
+ @media (max-width: 640px) {
239
+ .filter-panel {
240
+ flex-wrap: nowrap;
241
+ overflow-x: auto;
242
+ -webkit-overflow-scrolling: touch;
243
+ scrollbar-width: none;
244
+ }
245
+
246
+ .filter-panel::-webkit-scrollbar {
247
+ display: none;
248
+ }
249
+
250
+ .filter-field {
251
+ flex-shrink: 0;
252
+ }
253
+ }
254
+
255
+ @media (prefers-reduced-motion: reduce) {
256
+ .filter-chip,
257
+ .filter-panel-clear {
258
+ transition: none;
259
+ }
260
+ }
261
+ </style>
@@ -0,0 +1,59 @@
1
+ export default FilterPanel;
2
+ export type FilterOption = {
3
+ value: string;
4
+ label: string;
5
+ };
6
+ export type FilterFieldDef = {
7
+ key: string;
8
+ label: string;
9
+ type: "select" | "boolean" | "search";
10
+ options?: FilterOption[];
11
+ /** Async search callback for 'search' type filters (FK relationships) */
12
+ onsearch?: (query: string) => void;
13
+ /** Items for 'search' type, managed by parent */
14
+ searchItems?: FilterOption[];
15
+ /** Loading state for 'search' type */
16
+ searchLoading?: boolean;
17
+ };
18
+ type FilterPanel = {
19
+ $on?(type: string, callback: (e: any) => void): () => void;
20
+ $set?(props: Partial<$$ComponentProps>): void;
21
+ };
22
+ /**
23
+ * FilterPanel
24
+ *
25
+ * Typed, labeled filter bar for admin/data UIs.
26
+ * Each filter renders as the appropriate widget based on its type:
27
+ * select → Select dropdown, boolean → Toggle pair, search → Combobox.
28
+ *
29
+ * @example
30
+ * <FilterPanel
31
+ * filters={[
32
+ * { key: 'status', label: 'Status', type: 'select', options: [
33
+ * { value: 'open', label: 'Open' },
34
+ * { value: 'closed', label: 'Closed' },
35
+ * ]},
36
+ * { key: 'active', label: 'Active', type: 'boolean' },
37
+ * ]}
38
+ * bind:value={activeFilters}
39
+ * onchange={handleFilterChange}
40
+ * />
41
+ */
42
+ declare const FilterPanel: import("svelte").Component<
43
+ {
44
+ filters?: FilterFieldDef[];
45
+ value?: Record<string, any>;
46
+ onchange?: (value: Record<string, any>) => void;
47
+ onclear?: () => void;
48
+ class?: string;
49
+ } & Record<string, any>,
50
+ {},
51
+ "value"
52
+ >;
53
+ type $$ComponentProps = {
54
+ filters?: FilterFieldDef[];
55
+ value?: Record<string, any>;
56
+ onchange?: (value: Record<string, any>) => void;
57
+ onclear?: () => void;
58
+ class?: string;
59
+ } & Record<string, any>;
@@ -48,6 +48,7 @@ export { default as Combobox } from "./Combobox.svelte";
48
48
  // Search
49
49
  export { default as SearchInput } from "./SearchInput.svelte";
50
50
  export { default as FilterBar } from "./FilterBar.svelte";
51
+ export { default as FilterPanel } from "./FilterPanel.svelte";
51
52
  export { default as CommandPalette } from "./CommandPalette.svelte";
52
53
 
53
54
  // Tabs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/design-system",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Design system tokens and Svelte components for aiaiai products",
5
5
  "license": "MIT",
6
6
  "type": "module",