@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.
- package/README.md +171 -25
- package/package.json +17 -15
- package/src/auto-import.js +34 -17
- package/src/components/Checkbox.n +33 -0
- package/src/components/Dialog.n +54 -0
- package/src/components/Dropdown.n +41 -0
- package/src/components/Field.n +56 -0
- package/src/components/FieldGroup.n +17 -0
- package/src/components/Form.n +45 -0
- package/src/components/Input.n +41 -0
- package/src/components/Popover.n +32 -0
- package/src/components/Radio.n +33 -0
- package/src/components/Select.n +34 -0
- package/src/components/Sheet.n +55 -0
- package/src/components/Switch.n +34 -0
- package/src/components/Tabs.n +28 -0
- package/src/components/Textarea.n +38 -0
- package/src/components/ToastProvider.n +16 -0
- package/src/components/Tooltip.n +34 -0
- package/src/index.js +24 -0
- package/src/lib/toast.svelte.js +42 -0
- package/src/styles/atoms.css +302 -0
- package/src/types/Checkbox.d.ts +11 -0
- package/src/types/Dialog.d.ts +17 -0
- package/src/types/Dropdown.d.ts +23 -0
- package/src/types/Field.d.ts +32 -0
- package/src/types/FieldGroup.d.ts +11 -0
- package/src/types/Form.d.ts +30 -0
- package/src/types/Input.d.ts +29 -0
- package/src/types/Popover.d.ts +18 -0
- package/src/types/Radio.d.ts +13 -0
- package/src/types/Select.d.ts +13 -0
- package/src/types/Sheet.d.ts +20 -0
- package/src/types/Switch.d.ts +11 -0
- package/src/types/Tabs.d.ts +17 -0
- package/src/types/Textarea.d.ts +12 -0
- package/src/types/ToastProvider.d.ts +15 -0
- package/src/types/Tooltip.d.ts +15 -0
|
@@ -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
|
+
}
|
package/src/styles/atoms.css
CHANGED
|
@@ -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
|
}
|