@aiaiai-pt/design-system 0.1.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.
Files changed (52) hide show
  1. package/components/Alert.svelte +100 -0
  2. package/components/Badge.svelte +108 -0
  3. package/components/BottomNav.svelte +37 -0
  4. package/components/BottomNavItem.svelte +121 -0
  5. package/components/Button.svelte +269 -0
  6. package/components/Card.svelte +108 -0
  7. package/components/Checkbox.svelte +138 -0
  8. package/components/CodeBlock.svelte +187 -0
  9. package/components/CodeEditor.svelte +221 -0
  10. package/components/CollapsibleSection.svelte +160 -0
  11. package/components/Combobox.svelte +396 -0
  12. package/components/EmptyState.svelte +148 -0
  13. package/components/FileUpload.svelte +280 -0
  14. package/components/FileUploadItem.svelte +222 -0
  15. package/components/Input.svelte +222 -0
  16. package/components/KeyValue.svelte +79 -0
  17. package/components/Label.svelte +49 -0
  18. package/components/List.svelte +70 -0
  19. package/components/ListItem.svelte +125 -0
  20. package/components/Menu.svelte +161 -0
  21. package/components/MenuItem.svelte +120 -0
  22. package/components/MenuSeparator.svelte +34 -0
  23. package/components/Modal.svelte +260 -0
  24. package/components/OptionGrid.svelte +195 -0
  25. package/components/Panel.svelte +256 -0
  26. package/components/Popover.svelte +194 -0
  27. package/components/Progress.svelte +78 -0
  28. package/components/Select.svelte +182 -0
  29. package/components/Separator.svelte +47 -0
  30. package/components/Sidebar.svelte +106 -0
  31. package/components/SidebarItem.svelte +154 -0
  32. package/components/SidebarSection.svelte +43 -0
  33. package/components/Skeleton.svelte +79 -0
  34. package/components/Status.svelte +104 -0
  35. package/components/Stepper.svelte +142 -0
  36. package/components/Tab.svelte +94 -0
  37. package/components/TabList.svelte +36 -0
  38. package/components/TabPanel.svelte +45 -0
  39. package/components/Tabs.svelte +46 -0
  40. package/components/Tag.svelte +96 -0
  41. package/components/Textarea.svelte +143 -0
  42. package/components/Toast.svelte +114 -0
  43. package/components/Toggle.svelte +132 -0
  44. package/components/index.js +70 -0
  45. package/package.json +45 -0
  46. package/tokens/base.css +175 -0
  47. package/tokens/components.css +530 -0
  48. package/tokens/semantic.css +211 -0
  49. package/tokens/themes/aiaiai.css +53 -0
  50. package/tokens/themes/bespoke-example.css +148 -0
  51. package/tokens/themes/branded-example.css +55 -0
  52. package/tokens/utilities.css +1865 -0
@@ -0,0 +1,260 @@
1
+ <!--
2
+ @component Modal
3
+
4
+ Centered overlay dialog with focus trap and backdrop.
5
+ Unlike Panel (slide-over drawer), Modal is centered and rounded.
6
+ Consumes --modal-* tokens from components.css.
7
+
8
+ @example
9
+ <Modal open={showConfirm} title="Delete Pipeline" onclose={() => showConfirm = false}>
10
+ Are you sure you want to delete this pipeline?
11
+ {#snippet footer()}
12
+ <Button variant="ghost" onclick={() => showConfirm = false}>CANCEL</Button>
13
+ <Button variant="destructive" onclick={handleDelete}>DELETE</Button>
14
+ {/snippet}
15
+ </Modal>
16
+
17
+ @example Small
18
+ <Modal open width="sm" title="Rename" onclose={close}>
19
+ <Input label="NAME" bind:value={name} />
20
+ </Modal>
21
+ -->
22
+ <script module>
23
+ let _modalUid = 0;
24
+ </script>
25
+
26
+ <script>
27
+ /**
28
+ * @typedef {'default' | 'sm' | 'lg'} Width
29
+ */
30
+
31
+ let {
32
+ /** @type {boolean} */
33
+ open = false,
34
+ /** @type {string | undefined} */
35
+ title,
36
+ /** @type {Width} */
37
+ width = 'default',
38
+ /** @type {(() => void) | undefined} */
39
+ onclose,
40
+ /** @type {string} */
41
+ class: className = '',
42
+ /** @type {import('svelte').Snippet | undefined} */
43
+ header = undefined,
44
+ /** @type {import('svelte').Snippet | undefined} */
45
+ footer = undefined,
46
+ /** @type {import('svelte').Snippet | undefined} */
47
+ children = undefined,
48
+ ...rest
49
+ } = $props();
50
+
51
+ const headerId = `modal-header-${_modalUid++}`;
52
+
53
+ /** @type {HTMLElement | undefined} */
54
+ let modalEl = $state();
55
+
56
+ const FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
57
+
58
+ // Focus trap + scroll lock
59
+ $effect(() => {
60
+ if (!open || !modalEl) return;
61
+
62
+ const previouslyFocused = /** @type {HTMLElement | null} */ (document.activeElement);
63
+ document.body.style.overflow = 'hidden';
64
+
65
+ const firstFocusable = modalEl.querySelector(FOCUSABLE);
66
+ if (firstFocusable) /** @type {HTMLElement} */ (firstFocusable).focus();
67
+
68
+ /** @param {KeyboardEvent} e */
69
+ function handleKeydown(e) {
70
+ if (e.key === 'Escape') {
71
+ onclose?.();
72
+ return;
73
+ }
74
+ if (e.key !== 'Tab') return;
75
+
76
+ const focusable = /** @type {NodeListOf<HTMLElement>} */ (modalEl?.querySelectorAll(FOCUSABLE));
77
+ if (!focusable?.length) return;
78
+
79
+ const first = focusable[0];
80
+ const last = focusable[focusable.length - 1];
81
+
82
+ if (e.shiftKey && document.activeElement === first) {
83
+ e.preventDefault();
84
+ last.focus();
85
+ } else if (!e.shiftKey && document.activeElement === last) {
86
+ e.preventDefault();
87
+ first.focus();
88
+ }
89
+ }
90
+
91
+ document.addEventListener('keydown', handleKeydown);
92
+
93
+ return () => {
94
+ document.removeEventListener('keydown', handleKeydown);
95
+ document.body.style.overflow = '';
96
+ previouslyFocused?.focus();
97
+ };
98
+ });
99
+
100
+ function handleBackdropClick() {
101
+ onclose?.();
102
+ }
103
+ </script>
104
+
105
+ {#if open}
106
+ <div class="modal-backdrop" onclick={handleBackdropClick} aria-hidden="true"></div>
107
+
108
+ <div class="modal-container">
109
+ <div
110
+ bind:this={modalEl}
111
+ class="modal modal-{width} {className}"
112
+ role="dialog"
113
+ aria-modal="true"
114
+ aria-label={!header ? title : undefined}
115
+ aria-labelledby={header ? headerId : undefined}
116
+ {...rest}
117
+ >
118
+ <div class="modal-header">
119
+ {#if header}
120
+ <div id={headerId}>{@render header()}</div>
121
+ {:else if title}
122
+ <h2 class="modal-title">{title}</h2>
123
+ {/if}
124
+ <button
125
+ class="modal-close"
126
+ onclick={onclose}
127
+ aria-label="Close"
128
+ >
129
+ <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
130
+ <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
131
+ </svg>
132
+ </button>
133
+ </div>
134
+
135
+ <div class="modal-body">
136
+ {#if children}{@render children()}{/if}
137
+ </div>
138
+
139
+ {#if footer}
140
+ <div class="modal-footer">
141
+ {@render footer()}
142
+ </div>
143
+ {/if}
144
+ </div>
145
+ </div>
146
+ {/if}
147
+
148
+ <style>
149
+ .modal-backdrop {
150
+ position: fixed;
151
+ inset: 0;
152
+ background: var(--modal-backdrop);
153
+ z-index: 50;
154
+ animation: modal-fade-in var(--duration-normal) var(--easing-enter);
155
+ }
156
+
157
+ .modal-container {
158
+ position: fixed;
159
+ inset: 0;
160
+ z-index: 51;
161
+ display: flex;
162
+ align-items: center;
163
+ justify-content: center;
164
+ padding: var(--space-lg);
165
+ pointer-events: none;
166
+ }
167
+
168
+ .modal {
169
+ pointer-events: auto;
170
+ background: var(--modal-bg);
171
+ border-radius: var(--modal-radius);
172
+ box-shadow: var(--modal-shadow);
173
+ display: flex;
174
+ flex-direction: column;
175
+ max-height: calc(100vh - calc(2 * var(--space-lg)));
176
+ animation: modal-scale-in var(--modal-transition);
177
+ }
178
+
179
+ .modal-default { width: var(--modal-width); max-width: 100%; }
180
+ .modal-sm { width: var(--modal-width-sm); max-width: 100%; }
181
+ .modal-lg { width: var(--modal-width-lg); max-width: 100%; }
182
+
183
+ .modal-header {
184
+ display: flex;
185
+ align-items: center;
186
+ justify-content: space-between;
187
+ min-height: var(--modal-header-height);
188
+ padding: 0 var(--modal-padding);
189
+ border-bottom: var(--modal-header-border);
190
+ flex-shrink: 0;
191
+ }
192
+
193
+ .modal-title {
194
+ font-family: var(--modal-header-font);
195
+ font-size: var(--modal-header-size);
196
+ font-weight: var(--modal-header-weight);
197
+ color: var(--color-text);
198
+ margin: 0;
199
+ }
200
+
201
+ .modal-close {
202
+ all: unset;
203
+ box-sizing: border-box;
204
+ cursor: pointer;
205
+ display: flex;
206
+ align-items: center;
207
+ justify-content: center;
208
+ width: 28px;
209
+ height: 28px;
210
+ border-radius: var(--radius-sm);
211
+ color: var(--color-text-secondary);
212
+ transition: all var(--duration-instant) var(--easing-default);
213
+ }
214
+
215
+ .modal-close:hover {
216
+ background: var(--color-surface-secondary);
217
+ color: var(--color-text);
218
+ }
219
+
220
+ .modal-close:focus-visible {
221
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
222
+ outline-offset: var(--focus-ring-offset);
223
+ }
224
+
225
+ .modal-close svg {
226
+ width: 16px;
227
+ height: 16px;
228
+ }
229
+
230
+ .modal-body {
231
+ flex: 1;
232
+ overflow-y: auto;
233
+ padding: var(--modal-padding);
234
+ }
235
+
236
+ .modal-footer {
237
+ display: flex;
238
+ align-items: center;
239
+ justify-content: flex-end;
240
+ gap: var(--space-sm);
241
+ padding: var(--space-md) var(--modal-padding);
242
+ border-top: var(--modal-header-border);
243
+ flex-shrink: 0;
244
+ }
245
+
246
+ @keyframes modal-fade-in {
247
+ from { opacity: 0; }
248
+ to { opacity: 1; }
249
+ }
250
+
251
+ @keyframes modal-scale-in {
252
+ from { opacity: 0; transform: scale(0.95); }
253
+ to { opacity: 1; transform: scale(1); }
254
+ }
255
+
256
+ @media (prefers-reduced-motion: reduce) {
257
+ .modal { animation: none; }
258
+ .modal-backdrop { animation: none; }
259
+ }
260
+ </style>
@@ -0,0 +1,195 @@
1
+ <!--
2
+ @component OptionGrid
3
+
4
+ Exclusive selection grid using Card (interactive + selected).
5
+ Implements radiogroup semantics with keyboard navigation.
6
+
7
+ @example
8
+ <script>
9
+ import { Database, Code } from 'phosphor-svelte';
10
+ let type = $state(null);
11
+ const options = [
12
+ { value: 'sql', label: 'SQL', description: 'SQL transform', icon: Database },
13
+ { value: 'python', label: 'Python', description: 'Python code', icon: Code },
14
+ ];
15
+ </script>
16
+
17
+ <OptionGrid {options} bind:value={type} columns={2} />
18
+
19
+ @example Compact mode (no descriptions)
20
+ <OptionGrid {options} bind:value={type} columns={3} compact />
21
+ -->
22
+ <script>
23
+ import Card from './Card.svelte';
24
+
25
+ /**
26
+ * @typedef {{ value: string, label: string, description?: string, icon?: any }} GridOption
27
+ */
28
+
29
+ let {
30
+ /** @type {GridOption[]} */
31
+ options = [],
32
+ /** @type {string | null} */
33
+ value = $bindable(null),
34
+ /** @type {number} */
35
+ columns = 2,
36
+ /** @type {boolean} */
37
+ compact = false,
38
+ /** @type {string} */
39
+ class: className = '',
40
+ ...rest
41
+ } = $props();
42
+
43
+ /** @type {HTMLElement[]} */
44
+ let buttonRefs = [];
45
+
46
+ /**
47
+ * Handle keyboard navigation within the radiogroup.
48
+ * @param {KeyboardEvent} e
49
+ */
50
+ function handleKeydown(e) {
51
+ let next = -1;
52
+
53
+ switch (e.key) {
54
+ case 'ArrowRight':
55
+ case 'ArrowDown': {
56
+ e.preventDefault();
57
+ const idx = options.findIndex((o) => o.value === value);
58
+ next = idx < options.length - 1 ? idx + 1 : 0;
59
+ break;
60
+ }
61
+ case 'ArrowLeft':
62
+ case 'ArrowUp': {
63
+ e.preventDefault();
64
+ const idx = options.findIndex((o) => o.value === value);
65
+ next = idx > 0 ? idx - 1 : options.length - 1;
66
+ break;
67
+ }
68
+ default:
69
+ return;
70
+ }
71
+
72
+ if (next >= 0) {
73
+ value = options[next].value;
74
+ buttonRefs[next]?.focus();
75
+ }
76
+ }
77
+ </script>
78
+
79
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
80
+ <!-- svelte-ignore a11y_interactive_supports_focus -->
81
+ <div
82
+ class="option-grid {className}"
83
+ class:option-grid--compact={compact}
84
+ style:--cols={columns}
85
+ role="radiogroup"
86
+ onkeydown={handleKeydown}
87
+ {...rest}
88
+ >
89
+ {#each options as option, i (option.value)}
90
+ {@const selected = value === option.value}
91
+ <Card {selected} class="option-card">
92
+ {#snippet children()}
93
+ <button
94
+ type="button"
95
+ class="option-button"
96
+ class:option-button--compact={compact}
97
+ role="radio"
98
+ aria-checked={selected}
99
+ tabindex={selected || (!value && options[0]?.value === option.value) ? 0 : -1}
100
+ onclick={() => value = option.value}
101
+ bind:this={buttonRefs[i]}
102
+ >
103
+ {#if option.icon}
104
+ <span class="option-icon" class:option-icon--compact={compact}>
105
+ <option.icon size={compact ? 20 : 24} weight="regular" />
106
+ </span>
107
+ {/if}
108
+ <span class="option-text">
109
+ <span class="option-label">{option.label}</span>
110
+ {#if !compact && option.description}
111
+ <span class="option-description">{option.description}</span>
112
+ {/if}
113
+ </span>
114
+ </button>
115
+ {/snippet}
116
+ </Card>
117
+ {/each}
118
+ </div>
119
+
120
+ <style>
121
+ .option-grid {
122
+ display: grid;
123
+ grid-template-columns: repeat(var(--cols, 2), 1fr);
124
+ gap: var(--space-sm);
125
+ }
126
+
127
+ /* Remove Card's internal padding — button handles layout */
128
+ .option-grid :global(.option-card) {
129
+ padding: 0;
130
+ }
131
+
132
+ .option-button {
133
+ display: flex;
134
+ align-items: flex-start;
135
+ gap: var(--space-sm);
136
+ width: 100%;
137
+ padding: var(--space-md);
138
+ background: none;
139
+ border: none;
140
+ cursor: pointer;
141
+ text-align: left;
142
+ color: inherit;
143
+ font: inherit;
144
+ }
145
+
146
+ .option-button--compact {
147
+ align-items: center;
148
+ padding: var(--space-sm) var(--space-md);
149
+ }
150
+
151
+ .option-button:focus-visible {
152
+ outline: none;
153
+ }
154
+
155
+ .option-icon {
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: center;
159
+ width: var(--space-xl);
160
+ height: var(--space-xl);
161
+ background: var(--color-surface-secondary);
162
+ border-radius: var(--radius-sm);
163
+ color: var(--color-text-secondary);
164
+ flex-shrink: 0;
165
+ }
166
+
167
+ .option-icon--compact {
168
+ width: auto;
169
+ height: auto;
170
+ background: none;
171
+ border-radius: 0;
172
+ }
173
+
174
+ .option-text {
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: var(--space-2xs);
178
+ min-width: 0;
179
+ }
180
+
181
+ .option-label {
182
+ font-family: var(--type-data-font);
183
+ font-size: var(--type-data-size);
184
+ font-weight: 500;
185
+ line-height: var(--type-data-leading);
186
+ color: var(--color-text);
187
+ }
188
+
189
+ .option-description {
190
+ font-family: var(--type-body-sm-font);
191
+ font-size: var(--type-body-sm-size);
192
+ line-height: var(--type-body-sm-leading);
193
+ color: var(--color-text-secondary);
194
+ }
195
+ </style>
@@ -0,0 +1,256 @@
1
+ <!--
2
+ @component Panel
3
+
4
+ Slide-over drawer surface. Opens from the right edge.
5
+ Consumes --panel-* tokens from components.css.
6
+
7
+ @example
8
+ <Panel open={showEditor} title="Edit Step" onclose={() => showEditor = false}>
9
+ Panel content here
10
+ </Panel>
11
+
12
+ @example Narrow
13
+ <Panel open width="narrow" title="Settings" onclose={close}>
14
+ Settings form
15
+ </Panel>
16
+ -->
17
+ <script module>
18
+ let _panelUid = 0;
19
+ </script>
20
+
21
+ <script>
22
+ /**
23
+ * @typedef {'default' | 'narrow' | 'wide'} Width
24
+ */
25
+
26
+ let {
27
+ /** @type {boolean} */
28
+ open = false,
29
+ /** @type {string | undefined} */
30
+ title,
31
+ /** @type {Width} */
32
+ width = 'default',
33
+ /** @type {(() => void) | undefined} */
34
+ onclose,
35
+ /** @type {string} */
36
+ class: className = '',
37
+ /** @type {import('svelte').Snippet | undefined} */
38
+ header = undefined,
39
+ /** @type {import('svelte').Snippet | undefined} */
40
+ children = undefined,
41
+ ...rest
42
+ } = $props();
43
+
44
+ const headerId = `panel-header-${_panelUid++}`;
45
+
46
+ /** @type {HTMLElement | undefined} */
47
+ let panelEl = $state();
48
+
49
+ const FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
50
+
51
+ // Focus trap: save previous focus, trap Tab, restore on close
52
+ $effect(() => {
53
+ if (!open || !panelEl) return;
54
+
55
+ const previouslyFocused = /** @type {HTMLElement | null} */ (document.activeElement);
56
+
57
+ // Focus the close button on open
58
+ const firstFocusable = panelEl.querySelector(FOCUSABLE);
59
+ if (firstFocusable) /** @type {HTMLElement} */ (firstFocusable).focus();
60
+
61
+ /** @param {KeyboardEvent} e */
62
+ function handleKeydown(e) {
63
+ if (e.key === 'Escape') {
64
+ onclose?.();
65
+ return;
66
+ }
67
+ if (e.key !== 'Tab') return;
68
+
69
+ const focusable = /** @type {NodeListOf<HTMLElement>} */ (panelEl?.querySelectorAll(FOCUSABLE));
70
+ if (!focusable?.length) return;
71
+
72
+ const first = focusable[0];
73
+ const last = focusable[focusable.length - 1];
74
+
75
+ if (e.shiftKey && document.activeElement === first) {
76
+ e.preventDefault();
77
+ last.focus();
78
+ } else if (!e.shiftKey && document.activeElement === last) {
79
+ e.preventDefault();
80
+ first.focus();
81
+ }
82
+ }
83
+
84
+ document.addEventListener('keydown', handleKeydown);
85
+
86
+ return () => {
87
+ document.removeEventListener('keydown', handleKeydown);
88
+ previouslyFocused?.focus();
89
+ };
90
+ });
91
+
92
+ function handleBackdropClick() {
93
+ onclose?.();
94
+ }
95
+ </script>
96
+
97
+ {#if open}
98
+ <!-- Backdrop -->
99
+ <div
100
+ class="panel-backdrop"
101
+ onclick={handleBackdropClick}
102
+ aria-hidden="true"
103
+ ></div>
104
+
105
+ <!-- Panel -->
106
+ <aside
107
+ bind:this={panelEl}
108
+ class="panel panel-{width} {className}"
109
+ role="dialog"
110
+ aria-modal="true"
111
+ aria-label={!header ? title : undefined}
112
+ aria-labelledby={header ? headerId : undefined}
113
+ {...rest}
114
+ >
115
+ <div class="panel-header">
116
+ {#if header}
117
+ <div id={headerId}>{@render header()}</div>
118
+ {:else if title}
119
+ <h2 class="panel-title">{title}</h2>
120
+ {/if}
121
+ <button
122
+ class="panel-close"
123
+ onclick={onclose}
124
+ aria-label="Close panel"
125
+ >
126
+ <svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
127
+ <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
128
+ </svg>
129
+ </button>
130
+ </div>
131
+
132
+ <div class="panel-body">
133
+ {#if children}{@render children()}{/if}
134
+ </div>
135
+ </aside>
136
+ {/if}
137
+
138
+ <style>
139
+ .panel-backdrop {
140
+ position: fixed;
141
+ inset: 0;
142
+ background: var(--panel-backdrop);
143
+ z-index: 40;
144
+ animation: fade-in var(--duration-normal) var(--easing-enter);
145
+ }
146
+
147
+ .panel {
148
+ position: fixed;
149
+ top: 0;
150
+ right: 0;
151
+ bottom: 0;
152
+ background: var(--panel-bg);
153
+ border-left: var(--panel-border);
154
+ box-shadow: var(--panel-shadow);
155
+ border-radius: var(--panel-radius);
156
+ z-index: 41;
157
+ display: flex;
158
+ flex-direction: column;
159
+ animation: slide-in var(--panel-transition);
160
+ }
161
+
162
+ .panel-default {
163
+ width: var(--panel-width);
164
+ max-width: 100vw;
165
+ }
166
+
167
+ .panel-narrow {
168
+ width: var(--panel-width-narrow);
169
+ max-width: 100vw;
170
+ }
171
+
172
+ .panel-wide {
173
+ width: var(--panel-width-wide);
174
+ max-width: 100vw;
175
+ }
176
+
177
+ .panel-header {
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: space-between;
181
+ height: var(--panel-header-height);
182
+ padding: 0 var(--panel-padding);
183
+ border-bottom: var(--panel-header-border);
184
+ flex-shrink: 0;
185
+ }
186
+
187
+ .panel-title {
188
+ font-family: var(--panel-header-font);
189
+ font-size: var(--panel-header-size);
190
+ font-weight: var(--panel-header-weight);
191
+ color: var(--color-text);
192
+ margin: 0;
193
+ }
194
+
195
+ .panel-close {
196
+ all: unset;
197
+ cursor: pointer;
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ width: 28px;
202
+ height: 28px;
203
+ border-radius: var(--radius-sm);
204
+ color: var(--color-text-secondary);
205
+ transition: all var(--duration-instant) var(--easing-default);
206
+ }
207
+
208
+ .panel-close:hover {
209
+ background: var(--color-surface-secondary);
210
+ color: var(--color-text);
211
+ }
212
+
213
+ .panel-close:focus-visible {
214
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
215
+ outline-offset: var(--focus-ring-offset);
216
+ }
217
+
218
+ .panel-close svg {
219
+ width: 16px;
220
+ height: 16px;
221
+ }
222
+
223
+ .panel-body {
224
+ flex: 1;
225
+ overflow-y: auto;
226
+ padding: var(--panel-padding);
227
+ }
228
+
229
+ @keyframes slide-in {
230
+ from {
231
+ transform: translateX(100%);
232
+ }
233
+ to {
234
+ transform: translateX(0);
235
+ }
236
+ }
237
+
238
+ @keyframes fade-in {
239
+ from {
240
+ opacity: 0;
241
+ }
242
+ to {
243
+ opacity: 1;
244
+ }
245
+ }
246
+
247
+ @media (prefers-reduced-motion: reduce) {
248
+ .panel {
249
+ animation: none;
250
+ }
251
+
252
+ .panel-backdrop {
253
+ animation: none;
254
+ }
255
+ }
256
+ </style>