@human-synthesis/norns-ui 0.0.1 → 0.0.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,32 @@
1
+ PopoverRoot(bind:open)
2
+ +if('trigger')
3
+ PopoverTrigger(class!="{triggerClass ?? undefined}")
4
+ | {@render trigger()}
5
+ PopoverPortal
6
+ PopoverContent(class!="{contentClasses}" side!="{side}" align!="{align}" sideOffset!="{sideOffset}")
7
+ | {@render children?.()}
8
+
9
+ <script>
10
+ import { Popover } from 'bits-ui'
11
+ import { cn } from '@human-synthesis/norns-ui/cn'
12
+
13
+ {
14
+ Root: PopoverRoot
15
+ Trigger: PopoverTrigger
16
+ Portal: PopoverPortal
17
+ Content: PopoverContent
18
+ } := Popover
19
+
20
+ {
21
+ open = $bindable(false)
22
+ side = 'bottom'
23
+ align = 'center'
24
+ sideOffset = 8
25
+ trigger
26
+ children
27
+ triggerClass
28
+ class: extra = ''
29
+ } .= $props()
30
+
31
+ contentClasses := cn 'popover-content', extra
32
+ </script>
@@ -0,0 +1,33 @@
1
+ input(
2
+ type="radio"
3
+ id!="{resolvedId}"
4
+ name!="{name}"
5
+ value!="{value}"
6
+ bind:group
7
+ required!="{required}"
8
+ disabled!="{disabled}"
9
+ aria-invalid!="{hasError ? 'true' : undefined}"
10
+ class!="{classes}"
11
+ )
12
+
13
+ <script>
14
+ import { cn } from '@human-synthesis/norns-ui/cn'
15
+ import { getContext } from 'svelte'
16
+
17
+ {
18
+ group = $bindable()
19
+ value
20
+ name
21
+ id: providedId
22
+ required = false
23
+ disabled = false
24
+ error: explicitError = false
25
+ class: extra = ''
26
+ } .= $props()
27
+
28
+ field := getContext 'norns-ui:field'
29
+ resolvedId := providedId ?? field?.id
30
+ hasError := explicitError || field?.hasError || false
31
+
32
+ classes := cn 'radio', hasError && 'radio-err', extra
33
+ </script>
@@ -0,0 +1,34 @@
1
+ select(
2
+ id!="{resolvedId}"
3
+ name!="{name}"
4
+ bind:value
5
+ required!="{required}"
6
+ disabled!="{disabled}"
7
+ aria-invalid!="{hasError ? 'true' : undefined}"
8
+ aria-describedby!="{describedBy}"
9
+ class!="{classes}"
10
+ )
11
+ | {@render children?.()}
12
+
13
+ <script>
14
+ import { cn } from '@human-synthesis/norns-ui/cn'
15
+ import { getContext } from 'svelte'
16
+
17
+ {
18
+ value = $bindable()
19
+ name
20
+ id: providedId
21
+ required = false
22
+ disabled = false
23
+ error: explicitError = false
24
+ children
25
+ class: extra = ''
26
+ } .= $props()
27
+
28
+ field := getContext 'norns-ui:field'
29
+ resolvedId := providedId ?? field?.id
30
+ hasError := explicitError || field?.hasError || false
31
+ describedBy := resolvedId && hasError ? `${resolvedId}-error` : undefined
32
+
33
+ classes := cn 'select', hasError && 'select-err', extra
34
+ </script>
@@ -0,0 +1,55 @@
1
+ DialogRoot(bind:open)
2
+ +if('trigger')
3
+ DialogTrigger(class!="{triggerClass ?? undefined}")
4
+ | {@render trigger()}
5
+ DialogPortal
6
+ DialogOverlay(class!="{overlayClasses}")
7
+ DialogContent(class!="{contentClasses}" data-side!="{side}")
8
+ +if('title')
9
+ DialogTitle.dialog-title
10
+ | {title}
11
+ +if('description')
12
+ DialogDescription.dialog-description
13
+ | {description}
14
+ +if('children')
15
+ .sheet-body
16
+ | {@render children()}
17
+ +if('actions')
18
+ .dialog-actions
19
+ | {@render actions()}
20
+ +if('!hideClose')
21
+ DialogClose.dialog-close(aria-label="Close")
22
+ | ✕
23
+
24
+ <script>
25
+ import { Dialog } from 'bits-ui'
26
+ import { cn } from '@human-synthesis/norns-ui/cn'
27
+
28
+ {
29
+ Root: DialogRoot
30
+ Trigger: DialogTrigger
31
+ Portal: DialogPortal
32
+ Overlay: DialogOverlay
33
+ Content: DialogContent
34
+ Title: DialogTitle
35
+ Description: DialogDescription
36
+ Close: DialogClose
37
+ } := Dialog
38
+
39
+ {
40
+ open = $bindable(false)
41
+ side = 'right'
42
+ title
43
+ description
44
+ hideClose = false
45
+ trigger
46
+ actions
47
+ children
48
+ triggerClass
49
+ overlayClass = ''
50
+ class: extra = ''
51
+ } .= $props()
52
+
53
+ overlayClasses := cn 'dialog-overlay', overlayClass
54
+ contentClasses := cn 'sheet-content', `sheet-${side}`, extra
55
+ </script>
@@ -0,0 +1,34 @@
1
+ input(
2
+ type="checkbox"
3
+ role="switch"
4
+ id!="{resolvedId}"
5
+ name!="{name}"
6
+ value!="{value}"
7
+ bind:checked
8
+ required!="{required}"
9
+ disabled!="{disabled}"
10
+ aria-invalid!="{hasError ? 'true' : undefined}"
11
+ class!="{classes}"
12
+ )
13
+
14
+ <script>
15
+ import { cn } from '@human-synthesis/norns-ui/cn'
16
+ import { getContext } from 'svelte'
17
+
18
+ {
19
+ checked = $bindable(false)
20
+ value
21
+ name
22
+ id: providedId
23
+ required = false
24
+ disabled = false
25
+ error: explicitError = false
26
+ class: extra = ''
27
+ } .= $props()
28
+
29
+ field := getContext 'norns-ui:field'
30
+ resolvedId := providedId ?? field?.id
31
+ hasError := explicitError || field?.hasError || false
32
+
33
+ classes := cn 'switch', extra
34
+ </script>
@@ -0,0 +1,28 @@
1
+ TabsRoot(bind:value class!="{rootClasses}")
2
+ TabsList.tabs-list
3
+ +each('items as item (item.value)')
4
+ TabsTrigger.tabs-trigger(value!="{item.value}" disabled!="{item.disabled}")
5
+ | {item.label}
6
+ +each('items as item (item.value)')
7
+ TabsContent.tabs-content(value!="{item.value}")
8
+ | {@render item.panel?.()}
9
+
10
+ <script>
11
+ import { Tabs } from 'bits-ui'
12
+ import { cn } from '@human-synthesis/norns-ui/cn'
13
+
14
+ {
15
+ Root: TabsRoot
16
+ List: TabsList
17
+ Trigger: TabsTrigger
18
+ Content: TabsContent
19
+ } := Tabs
20
+
21
+ {
22
+ value = $bindable()
23
+ items = []
24
+ class: extra = ''
25
+ } .= $props()
26
+
27
+ rootClasses := cn 'tabs-root', extra
28
+ </script>
@@ -0,0 +1,38 @@
1
+ textarea(
2
+ id!="{resolvedId}"
3
+ name!="{name}"
4
+ bind:value
5
+ placeholder!="{placeholder}"
6
+ rows!="{rows}"
7
+ required!="{required}"
8
+ disabled!="{disabled}"
9
+ readonly!="{readonly}"
10
+ aria-invalid!="{hasError ? 'true' : undefined}"
11
+ aria-describedby!="{describedBy}"
12
+ class!="{classes}"
13
+ )
14
+
15
+ <script>
16
+ import { cn } from '@human-synthesis/norns-ui/cn'
17
+ import { getContext } from 'svelte'
18
+
19
+ {
20
+ value = $bindable('')
21
+ name
22
+ id: providedId
23
+ placeholder
24
+ rows = 4
25
+ required = false
26
+ disabled = false
27
+ readonly = false
28
+ error: explicitError = false
29
+ class: extra = ''
30
+ } .= $props()
31
+
32
+ field := getContext 'norns-ui:field'
33
+ resolvedId := providedId ?? field?.id
34
+ hasError := explicitError || field?.hasError || false
35
+ describedBy := resolvedId && hasError ? `${resolvedId}-error` : undefined
36
+
37
+ classes := cn 'textarea', hasError && 'textarea-err', extra
38
+ </script>
@@ -0,0 +1,16 @@
1
+ div(role="region" aria-label="Notifications" class!="{regionClasses}")
2
+ +each('toastStore.items as item (item.id)')
3
+ div(class!="{`toast toast-${item.variant}`}" role="status" data-id!="{item.id}")
4
+ .toast-message
5
+ | {item.message}
6
+ button.toast-close(type="button" aria-label="Dismiss" onclick!="{() => dismiss(item.id)}")
7
+ | ✕
8
+
9
+ <script>
10
+ import { cn } from '@human-synthesis/norns-ui/cn'
11
+ import { toastStore, dismiss } from '@human-synthesis/norns-ui/toast'
12
+
13
+ { class: extra = '' } := $props()
14
+
15
+ regionClasses := cn 'toast-region', extra
16
+ </script>
@@ -0,0 +1,34 @@
1
+ TooltipProvider(delayDuration!="{delay}")
2
+ TooltipRoot
3
+ TooltipTrigger(class!="{triggerClass ?? undefined}")
4
+ | {@render trigger?.()}
5
+ TooltipPortal
6
+ TooltipContent(class!="{contentClasses}" side!="{side}" sideOffset!="{sideOffset}")
7
+ | {content ?? ''}
8
+ | {@render children?.()}
9
+
10
+ <script>
11
+ import { Tooltip } from 'bits-ui'
12
+ import { cn } from '@human-synthesis/norns-ui/cn'
13
+
14
+ {
15
+ Provider: TooltipProvider
16
+ Root: TooltipRoot
17
+ Trigger: TooltipTrigger
18
+ Portal: TooltipPortal
19
+ Content: TooltipContent
20
+ } := Tooltip
21
+
22
+ {
23
+ content
24
+ side = 'top'
25
+ sideOffset = 6
26
+ delay = 300
27
+ trigger
28
+ children
29
+ triggerClass
30
+ class: extra = ''
31
+ } .= $props()
32
+
33
+ contentClasses := cn 'tooltip-content', extra
34
+ </script>
package/src/index.js CHANGED
@@ -7,7 +7,31 @@
7
7
  * code that auto-import doesn't process (e.g. dynamic mount).
8
8
  */
9
9
 
10
+ // Phase 1
10
11
  export { default as Btn } from './components/Btn.n';
12
+
13
+ // Phase 2 — forms tier
14
+ export { default as Checkbox } from './components/Checkbox.n';
15
+ export { default as Field } from './components/Field.n';
16
+ export { default as FieldGroup } from './components/FieldGroup.n';
17
+ export { default as Form } from './components/Form.n';
18
+ export { default as Input } from './components/Input.n';
19
+ export { default as Radio } from './components/Radio.n';
20
+ export { default as Select } from './components/Select.n';
21
+ export { default as Switch } from './components/Switch.n';
22
+ export { default as Textarea } from './components/Textarea.n';
23
+
24
+ // Phase 3 — behavior tier (Bits UI)
25
+ export { default as Dialog } from './components/Dialog.n';
26
+ export { default as Sheet } from './components/Sheet.n';
27
+ export { default as Popover } from './components/Popover.n';
28
+ export { default as Dropdown } from './components/Dropdown.n';
29
+ export { default as Tooltip } from './components/Tooltip.n';
30
+ export { default as Tabs } from './components/Tabs.n';
31
+ export { default as ToastProvider } from './components/ToastProvider.n';
32
+
33
+ // Helpers
11
34
  export { cn } from './lib/cn.js';
12
35
  export { variantClasses } from './lib/variants.js';
13
36
  export { presetUI } from './auto-import.js';
37
+ export { toast, notify, dismiss, clear } from './lib/toast.svelte.js';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Toast store + helpers. Single in-process queue, mounted once via
3
+ * <ToastProvider>. `toast(msg)` pushes from anywhere — server actions,
4
+ * client effects, button handlers.
5
+ */
6
+
7
+ let nextId = 0;
8
+
9
+ export const toastStore = $state({ items: [] });
10
+
11
+ /**
12
+ * Push a toast. Returns the toast id so the caller can dismiss early.
13
+ *
14
+ * @param {string} message
15
+ * @param {{ variant?: 'info' | 'success' | 'warning' | 'error', duration?: number }} [opts]
16
+ * `duration` in ms, default 4000. Set to 0 for sticky.
17
+ */
18
+ export function toast(message, opts = {}) {
19
+ const id = ++nextId;
20
+ const item = {
21
+ id,
22
+ message,
23
+ variant: opts.variant ?? 'info',
24
+ duration: opts.duration ?? 4000
25
+ };
26
+ toastStore.items.push(item);
27
+ if (item.duration > 0) {
28
+ setTimeout(() => dismiss(id), item.duration);
29
+ }
30
+ return id;
31
+ }
32
+
33
+ export const notify = toast;
34
+
35
+ export function dismiss(id) {
36
+ const i = toastStore.items.findIndex((t) => t.id === id);
37
+ if (i >= 0) toastStore.items.splice(i, 1);
38
+ }
39
+
40
+ export function clear() {
41
+ toastStore.items.length = 0;
42
+ }
@@ -59,4 +59,306 @@
59
59
  .ui-spinner {
60
60
  @apply inline-block size-4 animate-spin rounded-full border-2 border-current border-t-transparent;
61
61
  }
62
+
63
+ /* --- Text inputs --- */
64
+ .input {
65
+ @apply block w-full rounded-md border border-neutral-300 bg-white text-sm text-neutral-900 shadow-sm;
66
+ @apply placeholder:text-neutral-400;
67
+ @apply transition-[border-color,box-shadow] duration-150 ease-out;
68
+ @apply focus:outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/30;
69
+ @apply disabled:cursor-not-allowed disabled:bg-neutral-50 disabled:opacity-70;
70
+ height: var(--ui-h-md);
71
+ padding-inline: 0.75rem;
72
+ }
73
+
74
+ .input-sm {
75
+ @apply text-xs;
76
+ height: var(--ui-h-sm);
77
+ padding-inline: 0.625rem;
78
+ }
79
+
80
+ .input-lg {
81
+ @apply text-base;
82
+ height: var(--ui-h-lg);
83
+ padding-inline: 0.875rem;
84
+ }
85
+
86
+ .input-err {
87
+ @apply border-red-500 focus:border-red-500 focus:ring-red-500/30;
88
+ }
89
+
90
+ /* Textarea overrides .input's height */
91
+ .textarea {
92
+ @apply block w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 shadow-sm;
93
+ @apply placeholder:text-neutral-400;
94
+ @apply transition-[border-color,box-shadow] duration-150 ease-out;
95
+ @apply focus:outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/30;
96
+ @apply disabled:cursor-not-allowed disabled:bg-neutral-50 disabled:opacity-70;
97
+ min-height: calc(var(--ui-h-md) * 2);
98
+ }
99
+
100
+ .textarea-err {
101
+ @apply border-red-500 focus:border-red-500 focus:ring-red-500/30;
102
+ }
103
+
104
+ .select {
105
+ @apply block w-full rounded-md border border-neutral-300 bg-white text-sm text-neutral-900 shadow-sm;
106
+ @apply transition-[border-color,box-shadow] duration-150 ease-out;
107
+ @apply focus:outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/30;
108
+ @apply disabled:cursor-not-allowed disabled:bg-neutral-50 disabled:opacity-70;
109
+ height: var(--ui-h-md);
110
+ padding-inline: 0.75rem;
111
+ }
112
+
113
+ .select-err {
114
+ @apply border-red-500 focus:border-red-500 focus:ring-red-500/30;
115
+ }
116
+
117
+ /* --- Checkbox / Radio --- */
118
+ .checkbox {
119
+ @apply size-4 cursor-pointer rounded border-neutral-300 text-primary-600 shadow-sm;
120
+ @apply focus:outline-none focus:ring-2 focus:ring-primary-500/30 focus:ring-offset-1;
121
+ @apply disabled:cursor-not-allowed disabled:opacity-60;
122
+ }
123
+
124
+ .checkbox-err {
125
+ @apply border-red-500 focus:ring-red-500/30;
126
+ }
127
+
128
+ .radio {
129
+ @apply size-4 cursor-pointer border-neutral-300 text-primary-600 shadow-sm;
130
+ @apply focus:outline-none focus:ring-2 focus:ring-primary-500/30 focus:ring-offset-1;
131
+ @apply disabled:cursor-not-allowed disabled:opacity-60;
132
+ }
133
+
134
+ .radio-err {
135
+ @apply border-red-500 focus:ring-red-500/30;
136
+ }
137
+
138
+ /* --- Switch (styled checkbox / toggle) ---
139
+ * Implemented as a checkbox the user wraps in a label; we provide the
140
+ * pill track via .switch and the moving knob via ::before.
141
+ */
142
+ .switch {
143
+ @apply relative inline-flex h-5 w-9 shrink-0 cursor-pointer appearance-none rounded-full bg-neutral-200 shadow-inner;
144
+ @apply transition-colors duration-150 ease-out;
145
+ @apply checked:bg-primary-600;
146
+ @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/30 focus-visible:ring-offset-1;
147
+ @apply disabled:cursor-not-allowed disabled:opacity-60;
148
+ }
149
+ .switch::before {
150
+ content: '';
151
+ @apply absolute left-0.5 top-0.5 size-4 rounded-full bg-white shadow;
152
+ @apply transition-transform duration-150 ease-out;
153
+ }
154
+ .switch:checked::before {
155
+ transform: translateX(1rem);
156
+ }
157
+
158
+ /* --- Field (label + control + help/error) --- */
159
+ .field {
160
+ @apply flex flex-col;
161
+ }
162
+
163
+ .field-label {
164
+ @apply mb-1 block text-sm font-medium text-neutral-700;
165
+ }
166
+
167
+ .field-required {
168
+ @apply ml-0.5 text-red-600;
169
+ }
170
+
171
+ .field-help {
172
+ @apply mt-1 text-xs text-neutral-500;
173
+ }
174
+
175
+ .field-error {
176
+ @apply mt-1 text-xs text-red-600;
177
+ }
178
+
179
+ .field-row {
180
+ @apply flex items-center gap-2;
181
+ }
182
+
183
+ /* Field-group: border-grouped collection of fields */
184
+ .field-group {
185
+ @apply space-y-3 rounded-md border border-neutral-200 p-4;
186
+ }
187
+
188
+ .field-group-legend {
189
+ @apply -mx-1 -mt-1 px-1 text-sm font-medium text-neutral-700;
190
+ }
191
+
192
+ /* --- Form (vertical stack of fields) --- */
193
+ .form {
194
+ @apply space-y-4;
195
+ }
196
+
197
+ /* --- Dialog (modal) ---
198
+ * Bits UI toggles `data-state="open|closed"`. We use plain CSS transitions
199
+ * targeting that attribute for fade + scale; no `tailwindcss-animate`
200
+ * dependency, no v4 plugin.
201
+ */
202
+ .dialog-overlay {
203
+ @apply fixed inset-0 z-50 bg-black/50 backdrop-blur-sm opacity-0 transition-opacity duration-150;
204
+ }
205
+ .dialog-overlay[data-state='open'] {
206
+ @apply opacity-100;
207
+ }
208
+
209
+ .dialog-content {
210
+ @apply fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2;
211
+ @apply rounded-lg border border-neutral-200 bg-white p-6 shadow-xl;
212
+ @apply opacity-0 scale-95 transition duration-150;
213
+ }
214
+ .dialog-content[data-state='open'] {
215
+ @apply opacity-100 scale-100;
216
+ }
217
+
218
+ .dialog-title {
219
+ @apply text-lg font-semibold leading-tight tracking-tight text-neutral-900;
220
+ }
221
+
222
+ .dialog-description {
223
+ @apply mt-1 text-sm text-neutral-600;
224
+ }
225
+
226
+ .dialog-body {
227
+ @apply mt-4;
228
+ }
229
+
230
+ .dialog-actions {
231
+ @apply mt-6 flex items-center justify-end gap-2;
232
+ }
233
+
234
+ .dialog-close {
235
+ @apply absolute right-3 top-3 inline-flex size-7 items-center justify-center rounded-md text-neutral-500;
236
+ @apply hover:bg-neutral-100 hover:text-neutral-900;
237
+ @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500;
238
+ }
239
+
240
+ /* --- Sheet (side panel) --- */
241
+ .sheet-content {
242
+ @apply fixed z-50 border border-neutral-200 bg-white shadow-xl transition-transform duration-200 ease-out;
243
+ }
244
+ .sheet-right {
245
+ @apply right-0 top-0 h-full w-full max-w-md translate-x-full p-6;
246
+ }
247
+ .sheet-right[data-state='open'] {
248
+ @apply translate-x-0;
249
+ }
250
+ .sheet-left {
251
+ @apply left-0 top-0 h-full w-full max-w-md -translate-x-full p-6;
252
+ }
253
+ .sheet-left[data-state='open'] {
254
+ @apply translate-x-0;
255
+ }
256
+ .sheet-top {
257
+ @apply left-0 top-0 w-full -translate-y-full p-6;
258
+ }
259
+ .sheet-top[data-state='open'] {
260
+ @apply translate-y-0;
261
+ }
262
+ .sheet-bottom {
263
+ @apply bottom-0 left-0 w-full translate-y-full p-6;
264
+ }
265
+ .sheet-bottom[data-state='open'] {
266
+ @apply translate-y-0;
267
+ }
268
+
269
+ .sheet-body {
270
+ @apply mt-4;
271
+ }
272
+
273
+ /* --- Popover --- */
274
+ .popover-content {
275
+ @apply z-50 rounded-md border border-neutral-200 bg-white p-3 text-sm shadow-md;
276
+ @apply opacity-0 scale-95 transition duration-150;
277
+ }
278
+ .popover-content[data-state='open'] {
279
+ @apply opacity-100 scale-100;
280
+ }
281
+
282
+ /* --- Dropdown menu --- */
283
+ .dropdown-content {
284
+ @apply z-50 min-w-[8rem] rounded-md border border-neutral-200 bg-white p-1 text-sm shadow-md;
285
+ @apply opacity-0 scale-95 transition duration-150;
286
+ }
287
+ .dropdown-content[data-state='open'] {
288
+ @apply opacity-100 scale-100;
289
+ }
290
+ .dropdown-item {
291
+ @apply flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm text-neutral-800 outline-none;
292
+ @apply data-[highlighted]:bg-neutral-100 data-[highlighted]:text-neutral-900;
293
+ @apply data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50;
294
+ }
295
+ .dropdown-separator {
296
+ @apply -mx-1 my-1 h-px bg-neutral-200;
297
+ }
298
+
299
+ /* --- Tooltip --- */
300
+ .tooltip-content {
301
+ @apply z-50 overflow-hidden rounded-md bg-neutral-900 px-2 py-1 text-xs text-white shadow-md;
302
+ @apply opacity-0 scale-95 transition duration-150;
303
+ }
304
+ .tooltip-content[data-state='delayed-open'],
305
+ .tooltip-content[data-state='instant-open'] {
306
+ @apply opacity-100 scale-100;
307
+ }
308
+
309
+ /* --- Tabs --- */
310
+ .tabs-root {
311
+ @apply space-y-3;
312
+ }
313
+ .tabs-list {
314
+ @apply inline-flex h-9 items-center justify-center rounded-md bg-neutral-100 p-1 text-neutral-600;
315
+ }
316
+ .tabs-trigger {
317
+ @apply inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1 text-sm font-medium;
318
+ @apply transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500;
319
+ @apply data-[state=active]:bg-white data-[state=active]:text-neutral-900 data-[state=active]:shadow-sm;
320
+ @apply disabled:cursor-not-allowed disabled:opacity-50;
321
+ }
322
+ .tabs-content {
323
+ @apply focus-visible:outline-none;
324
+ }
325
+
326
+ /* --- Toast --- */
327
+ .toast-region {
328
+ @apply pointer-events-none fixed bottom-4 right-4 z-[100] flex w-full max-w-sm flex-col gap-2;
329
+ }
330
+ .toast {
331
+ @apply pointer-events-auto flex items-start gap-3 rounded-md border border-neutral-200 bg-white p-3 shadow-md;
332
+ animation: norns-toast-in 200ms ease-out;
333
+ }
334
+ @keyframes norns-toast-in {
335
+ from {
336
+ opacity: 0;
337
+ transform: translateX(1rem);
338
+ }
339
+ to {
340
+ opacity: 1;
341
+ transform: translateX(0);
342
+ }
343
+ }
344
+ .toast-info {
345
+ @apply border-neutral-200;
346
+ }
347
+ .toast-success {
348
+ @apply border-green-200 bg-green-50 text-green-900;
349
+ }
350
+ .toast-warning {
351
+ @apply border-amber-200 bg-amber-50 text-amber-900;
352
+ }
353
+ .toast-error {
354
+ @apply border-red-200 bg-red-50 text-red-900;
355
+ }
356
+ .toast-message {
357
+ @apply flex-1 text-sm leading-snug;
358
+ }
359
+ .toast-close {
360
+ @apply -mr-1 -mt-0.5 inline-flex size-6 shrink-0 items-center justify-center rounded text-neutral-500;
361
+ @apply hover:bg-neutral-100 hover:text-neutral-900;
362
+ @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500;
363
+ }
62
364
  }