@human-synthesis/norns-ui 0.0.3 → 0.0.6
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/LICENSE +21 -0
- package/README.md +81 -225
- package/package.json +13 -3
- package/src/auto-import.js +81 -19
- package/src/components/Accordion.n +39 -0
- package/src/components/Audio.n +24 -0
- package/src/components/Autocomplete.n +134 -0
- package/src/components/Avatar.n +27 -0
- package/src/components/AvatarGroup.n +25 -0
- package/src/components/Badge.n +20 -0
- package/src/components/Banner.n +25 -0
- package/src/components/Breadcrumbs.n +22 -0
- package/src/components/Btn.n +29 -8
- package/src/components/ButtonGroup.n +14 -0
- package/src/components/Calendar.n +153 -0
- package/src/components/Card.n +34 -0
- package/src/components/Carousel.n +66 -0
- package/src/components/Chip.n +27 -0
- package/src/components/Collapsible.n +29 -0
- package/src/components/ColorPicker.n +62 -0
- package/src/components/ContextMenu.n +67 -0
- package/src/components/CopyButton.n +50 -0
- package/src/components/DataTable.n +61 -0
- package/src/components/DatePicker.n +56 -0
- package/src/components/DateRangePicker.n +47 -0
- package/src/components/Dialog.n +35 -22
- package/src/components/Dropdown.n +54 -23
- package/src/components/GradientText.n +21 -0
- package/src/components/Header.n +23 -0
- package/src/components/HeroBanner.n +37 -0
- package/src/components/HierarchicalMenu.n +38 -0
- package/src/components/Icon.n +16 -0
- package/src/components/Image.n +31 -0
- package/src/components/MegaMenu.n +36 -0
- package/src/components/MultiSelect.n +158 -0
- package/src/components/NumberInput.n +49 -0
- package/src/components/OtpField.n +67 -0
- package/src/components/Pagination.n +73 -0
- package/src/components/Popover.n +30 -15
- package/src/components/Progress.n +26 -0
- package/src/components/ProgressCircular.n +52 -0
- package/src/components/RippleButton.n +56 -0
- package/src/components/ScrollArea.n +20 -0
- package/src/components/Separator.n +12 -0
- package/src/components/Sheet.n +36 -22
- package/src/components/ShinyButton.n +36 -0
- package/src/components/Skeleton.n +27 -0
- package/src/components/Stepper.n +35 -0
- package/src/components/Surface.n +10 -0
- package/src/components/Tabs.n +39 -13
- package/src/components/TagsInput.n +66 -0
- package/src/components/ThemeToggler.n +43 -0
- package/src/components/TimePicker.n +153 -0
- package/src/components/Timeline.n +27 -0
- package/src/components/ToggleButton.n +35 -0
- package/src/components/ToggleButtonGroup.n +54 -0
- package/src/components/Toolbar.n +15 -0
- package/src/components/Tooltip.n +52 -19
- package/src/components/Tree.n +57 -0
- package/src/components/Uploader.n +88 -0
- package/src/components/Video.n +26 -0
- package/src/index.js +64 -1
- package/src/lib/behaviors/clickOutside.svelte.js +54 -0
- package/src/lib/behaviors/escape.svelte.js +36 -0
- package/src/lib/behaviors/floating.svelte.js +137 -0
- package/src/lib/behaviors/focusTrap.svelte.js +80 -0
- package/src/lib/behaviors/index.js +14 -0
- package/src/lib/behaviors/portal.svelte.js +32 -0
- package/src/lib/behaviors/rovingTabindex.svelte.js +76 -0
- package/src/lib/behaviors/scrollLock.svelte.js +56 -0
- package/src/motion/AnimatedNumber.n +55 -0
- package/src/motion/GradientBackground.n +25 -0
- package/src/motion/LiquidButton.n +48 -0
- package/src/motion/Reveal.n +37 -0
- package/src/motion/Sparkles.n +53 -0
- package/src/motion/index.js +18 -0
- package/src/styles/atoms.css +1481 -223
- package/src/styles/tokens.css +204 -40
- package/src/types/Accordion.d.ts +17 -0
- package/src/types/AnimatedNumber.d.ts +15 -0
- package/src/types/Audio.d.ts +18 -0
- package/src/types/Autocomplete.d.ts +17 -0
- package/src/types/Avatar.d.ts +14 -0
- package/src/types/AvatarGroup.d.ts +15 -0
- package/src/types/Badge.d.ts +14 -0
- package/src/types/Banner.d.ts +15 -0
- package/src/types/Breadcrumbs.d.ts +15 -0
- package/src/types/Btn.d.ts +10 -0
- package/src/types/ButtonGroup.d.ts +10 -0
- package/src/types/Calendar.d.ts +20 -0
- package/src/types/Card.d.ts +19 -0
- package/src/types/Carousel.d.ts +23 -0
- package/src/types/Chip.d.ts +12 -0
- package/src/types/Collapsible.d.ts +13 -0
- package/src/types/ColorPicker.d.ts +15 -0
- package/src/types/ContextMenu.d.ts +24 -0
- package/src/types/CopyButton.d.ts +16 -0
- package/src/types/DataTable.d.ts +27 -0
- package/src/types/DatePicker.d.ts +23 -0
- package/src/types/DateRangePicker.d.ts +17 -0
- package/src/types/Dialog.d.ts +4 -0
- package/src/types/Dropdown.d.ts +2 -1
- package/src/types/GradientBackground.d.ts +14 -0
- package/src/types/GradientText.d.ts +19 -0
- package/src/types/Header.d.ts +12 -0
- package/src/types/HeroBanner.d.ts +17 -0
- package/src/types/HierarchicalMenu.d.ts +20 -0
- package/src/types/Icon.d.ts +21 -0
- package/src/types/Image.d.ts +15 -0
- package/src/types/LiquidButton.d.ts +15 -0
- package/src/types/MegaMenu.d.ts +29 -0
- package/src/types/MultiSelect.d.ts +16 -0
- package/src/types/NumberInput.d.ts +17 -0
- package/src/types/OtpField.d.ts +13 -0
- package/src/types/Pagination.d.ts +13 -0
- package/src/types/Progress.d.ts +14 -0
- package/src/types/ProgressCircular.d.ts +14 -0
- package/src/types/Reveal.d.ts +20 -0
- package/src/types/RippleButton.d.ts +15 -0
- package/src/types/ScrollArea.d.ts +11 -0
- package/src/types/Separator.d.ts +9 -0
- package/src/types/Sheet.d.ts +3 -0
- package/src/types/ShinyButton.d.ts +15 -0
- package/src/types/Skeleton.d.ts +13 -0
- package/src/types/Sparkles.d.ts +13 -0
- package/src/types/Stepper.d.ts +19 -0
- package/src/types/Surface.d.ts +10 -0
- package/src/types/Tabs.d.ts +2 -0
- package/src/types/TagsInput.d.ts +17 -0
- package/src/types/ThemeToggler.d.ts +17 -0
- package/src/types/TimePicker.d.ts +20 -0
- package/src/types/Timeline.d.ts +17 -0
- package/src/types/ToggleButton.d.ts +16 -0
- package/src/types/ToggleButtonGroup.d.ts +21 -0
- package/src/types/Toolbar.d.ts +12 -0
- package/src/types/Tooltip.d.ts +3 -0
- package/src/types/Tree.d.ts +20 -0
- package/src/types/Uploader.d.ts +15 -0
- package/src/types/Video.d.ts +20 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
.combobox-input-wrap(use:refAction bind:this!="{wrapEl}")
|
|
2
|
+
input.input.combobox-input(
|
|
3
|
+
bind:this!="{inputEl}"
|
|
4
|
+
type="text"
|
|
5
|
+
bind:value!="{term}"
|
|
6
|
+
placeholder!="{placeholder}"
|
|
7
|
+
disabled!="{disabled}"
|
|
8
|
+
role="combobox"
|
|
9
|
+
aria-expanded!="{open}"
|
|
10
|
+
aria-controls!="{listboxId}"
|
|
11
|
+
aria-autocomplete="list"
|
|
12
|
+
aria-activedescendant!="{open && filtered[active] ? optionId(active) : undefined}"
|
|
13
|
+
id!="{resolvedId}"
|
|
14
|
+
aria-invalid!="{hasError ? 'true' : undefined}"
|
|
15
|
+
oninput!="{onInput}"
|
|
16
|
+
onkeydown!="{onKeyDown}"
|
|
17
|
+
onfocus!="{() => open = true}"
|
|
18
|
+
onblur!="{onBlur}"
|
|
19
|
+
)
|
|
20
|
+
button.combobox-trigger(type="button" tabindex="-1" aria-label="Toggle options" onclick!="{toggle}")
|
|
21
|
+
Icon(name="lucide:chevrons-up-down" size="size-4")
|
|
22
|
+
|
|
23
|
+
+if('open && filtered.length > 0')
|
|
24
|
+
ul(
|
|
25
|
+
use:portal
|
|
26
|
+
use:floatAction
|
|
27
|
+
use:clickOutside!="{[onClose, open, wrapEl]}"
|
|
28
|
+
class="dropdown-content combobox-listbox"
|
|
29
|
+
data-state="open"
|
|
30
|
+
id!="{listboxId}"
|
|
31
|
+
role="listbox"
|
|
32
|
+
)
|
|
33
|
+
+each('filtered as item, i (item.value)')
|
|
34
|
+
li.dropdown-item(
|
|
35
|
+
role="option"
|
|
36
|
+
id!="{optionId(i)}"
|
|
37
|
+
aria-selected!="{value === item.value}"
|
|
38
|
+
data-active!="{i === active ? 'true' : undefined}"
|
|
39
|
+
onpointerdown!="{(e) => { e.preventDefault(); pick(item) }}"
|
|
40
|
+
)
|
|
41
|
+
Icon(name="lucide:check" size="size-3.5" class!="{value === item.value ? '' : 'opacity-0'}")
|
|
42
|
+
span {item.label}
|
|
43
|
+
+if('open && filtered.length === 0')
|
|
44
|
+
div(use:portal use:floatAction use:clickOutside!="{[onClose, open, wrapEl]}" class="dropdown-content combobox-listbox" data-state="open")
|
|
45
|
+
.combobox-empty No matches
|
|
46
|
+
|
|
47
|
+
<script>
|
|
48
|
+
import { getContext } from 'svelte'
|
|
49
|
+
import Icon from './Icon.n'
|
|
50
|
+
import { portal } from '@human-synthesis/norns-ui/behaviors/portal.svelte.js'
|
|
51
|
+
import { clickOutside } from '@human-synthesis/norns-ui/behaviors/clickOutside.svelte.js'
|
|
52
|
+
import { useFloating } from '@human-synthesis/norns-ui/behaviors/floating.svelte.js'
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
items = []
|
|
56
|
+
value = $bindable(undefined)
|
|
57
|
+
open = $bindable(false)
|
|
58
|
+
placeholder = 'Search…'
|
|
59
|
+
disabled = false
|
|
60
|
+
name
|
|
61
|
+
id: providedId
|
|
62
|
+
error: explicitError = false
|
|
63
|
+
} .= $props()
|
|
64
|
+
|
|
65
|
+
field := getContext 'norns-ui:field'
|
|
66
|
+
resolvedId := providedId ?? field?.id
|
|
67
|
+
hasError := explicitError || field?.hasError || false
|
|
68
|
+
|
|
69
|
+
uid := Math.random().toString(36).slice(2, 8)
|
|
70
|
+
listboxId := `listbox-${uid}`
|
|
71
|
+
optionId := (i) => `option-${uid}-${i}`
|
|
72
|
+
|
|
73
|
+
{ reference: refAction, floating: floatAction } := useFloating
|
|
74
|
+
placement: 'bottom-start'
|
|
75
|
+
offset: 4
|
|
76
|
+
matchWidth: true
|
|
77
|
+
|
|
78
|
+
term .= $state ''
|
|
79
|
+
active .= $state 0
|
|
80
|
+
inputEl .= $state null
|
|
81
|
+
wrapEl .= $state null
|
|
82
|
+
|
|
83
|
+
// Reset term to selected label when value changes externally.
|
|
84
|
+
$effect =>
|
|
85
|
+
if value !== undefined
|
|
86
|
+
match := items.find (it) => it.value === value
|
|
87
|
+
term = match?.label ?? '' if match && term !== match.label
|
|
88
|
+
|
|
89
|
+
filtered := $derived.by =>
|
|
90
|
+
return items unless term
|
|
91
|
+
t := term.toLowerCase()
|
|
92
|
+
items.filter (it) => it.label.toLowerCase().includes(t)
|
|
93
|
+
|
|
94
|
+
$effect =>
|
|
95
|
+
active = 0 if active >= filtered.length
|
|
96
|
+
|
|
97
|
+
onInput := =>
|
|
98
|
+
open = true
|
|
99
|
+
active = 0
|
|
100
|
+
|
|
101
|
+
toggle := =>
|
|
102
|
+
open = !open
|
|
103
|
+
inputEl?.focus() if open
|
|
104
|
+
|
|
105
|
+
onClose := => open = false
|
|
106
|
+
|
|
107
|
+
onBlur := =>
|
|
108
|
+
// Close after click handlers complete; pointerdown picks before this fires.
|
|
109
|
+
setTimeout (=> open = false), 100
|
|
110
|
+
|
|
111
|
+
pick := (item) =>
|
|
112
|
+
value = item.value
|
|
113
|
+
term = item.label
|
|
114
|
+
open = false
|
|
115
|
+
|
|
116
|
+
onKeyDown := (e) =>
|
|
117
|
+
switch e.key
|
|
118
|
+
when 'ArrowDown'
|
|
119
|
+
e.preventDefault()
|
|
120
|
+
open = true
|
|
121
|
+
active = Math.min(filtered.length - 1, active + 1)
|
|
122
|
+
when 'ArrowUp'
|
|
123
|
+
e.preventDefault()
|
|
124
|
+
open = true
|
|
125
|
+
active = Math.max(0, active - 1)
|
|
126
|
+
when 'Enter'
|
|
127
|
+
if open && filtered[active]
|
|
128
|
+
e.preventDefault()
|
|
129
|
+
pick filtered[active]
|
|
130
|
+
when 'Escape'
|
|
131
|
+
open = false
|
|
132
|
+
when 'Tab'
|
|
133
|
+
open = false
|
|
134
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
span(class!="{classes}")
|
|
2
|
+
+if('src')
|
|
3
|
+
img(src!="{src}" alt!="{name ?? ''}" loading="lazy")
|
|
4
|
+
+if('!src && name')
|
|
5
|
+
| {initials}
|
|
6
|
+
+if('!src && !name')
|
|
7
|
+
Icon(name="lucide:user" size="size-1/2" class="opacity-50")
|
|
8
|
+
|
|
9
|
+
<script>
|
|
10
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
11
|
+
import Icon from './Icon.n'
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
src
|
|
15
|
+
name
|
|
16
|
+
size = 'md'
|
|
17
|
+
class: extra = ''
|
|
18
|
+
...rest
|
|
19
|
+
} := $props()
|
|
20
|
+
|
|
21
|
+
initials := $derived.by =>
|
|
22
|
+
return '' unless name
|
|
23
|
+
parts := name.trim().split /\s+/
|
|
24
|
+
`${parts[0][0] ?? ''}${parts[1]?.[0] ?? ''}`.toUpperCase()
|
|
25
|
+
|
|
26
|
+
classes := cn 'avatar', `avatar-${size}`, extra
|
|
27
|
+
</script>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.avatar-group(class!="{classes}" role="group" aria-label!="{label}")
|
|
2
|
+
+each('visible as item, i')
|
|
3
|
+
span.avatar-group-item(style!="{`--avatar-group-i: ${i}`}")
|
|
4
|
+
Avatar(src!="{item.src}" name!="{item.name}" size!="{size}")
|
|
5
|
+
+if('overflow > 0')
|
|
6
|
+
span.avatar-group-overflow(class!="{`avatar avatar-${size}`}" aria-hidden="true")
|
|
7
|
+
| +{overflow}
|
|
8
|
+
|
|
9
|
+
<script>
|
|
10
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
11
|
+
import Avatar from './Avatar.n'
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
items = []
|
|
15
|
+
max = 5
|
|
16
|
+
size = 'md'
|
|
17
|
+
label = 'People'
|
|
18
|
+
class: extra = ''
|
|
19
|
+
} := $props()
|
|
20
|
+
|
|
21
|
+
visible := $derived items.slice(0, max)
|
|
22
|
+
overflow := $derived Math.max(0, items.length - max)
|
|
23
|
+
|
|
24
|
+
classes := cn 'avatar-group', extra
|
|
25
|
+
</script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
span(class!="{classes}")
|
|
2
|
+
| {@render children?.()}
|
|
3
|
+
|
|
4
|
+
<script>
|
|
5
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
variant = 'default'
|
|
9
|
+
size = 'md'
|
|
10
|
+
children
|
|
11
|
+
class: extra = ''
|
|
12
|
+
...rest
|
|
13
|
+
} := $props()
|
|
14
|
+
|
|
15
|
+
classes := cn
|
|
16
|
+
'badge'
|
|
17
|
+
variant !== 'default' && `badge-${variant}`
|
|
18
|
+
size !== 'md' && `badge-${size}`
|
|
19
|
+
extra
|
|
20
|
+
</script>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
div(class!="{classes}" role="status")
|
|
2
|
+
+if('icon')
|
|
3
|
+
.banner-icon
|
|
4
|
+
Icon(name!="{icon}" size="size-5")
|
|
5
|
+
.banner-body
|
|
6
|
+
| {@render children?.()}
|
|
7
|
+
+if('actions')
|
|
8
|
+
.banner-actions
|
|
9
|
+
| {@render actions()}
|
|
10
|
+
|
|
11
|
+
<script>
|
|
12
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
13
|
+
import Icon from './Icon.n'
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
variant = 'info'
|
|
17
|
+
icon
|
|
18
|
+
actions
|
|
19
|
+
children
|
|
20
|
+
class: extra = ''
|
|
21
|
+
...rest
|
|
22
|
+
} := $props()
|
|
23
|
+
|
|
24
|
+
classes := cn 'banner', `banner-${variant}`, extra
|
|
25
|
+
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
nav.breadcrumbs(aria-label="Breadcrumb" class!="{extra}")
|
|
2
|
+
ol.flex.items-center.gap-1
|
|
3
|
+
+each('items as item, i')
|
|
4
|
+
li.breadcrumb-item
|
|
5
|
+
+if('item.href && i < items.length - 1')
|
|
6
|
+
a.breadcrumb-link(href!="{item.href}")
|
|
7
|
+
| {item.label}
|
|
8
|
+
+if('!item.href || i === items.length - 1')
|
|
9
|
+
span(class!="{i === items.length - 1 ? 'breadcrumb-current' : ''}")
|
|
10
|
+
| {item.label}
|
|
11
|
+
+if('i < items.length - 1')
|
|
12
|
+
span.breadcrumb-separator(aria-hidden="true")
|
|
13
|
+
| {separator}
|
|
14
|
+
|
|
15
|
+
<script>
|
|
16
|
+
{
|
|
17
|
+
items = []
|
|
18
|
+
separator = '/'
|
|
19
|
+
class: extra = ''
|
|
20
|
+
...rest
|
|
21
|
+
} := $props()
|
|
22
|
+
</script>
|
package/src/components/Btn.n
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
type!="{type}"
|
|
3
|
-
class!="{classes}"
|
|
4
|
-
disabled!="{disabled || loading}"
|
|
5
|
-
onclick!="{onclick}"
|
|
6
|
-
aria-busy!="{loading ? 'true' : undefined}"
|
|
7
|
-
)
|
|
1
|
+
+snippet('body')
|
|
8
2
|
+if('loading')
|
|
9
3
|
span.ui-spinner(aria-hidden="true")
|
|
4
|
+
+if('!loading && icon')
|
|
5
|
+
Icon(name!="{icon}" size!="{iconSize}")
|
|
10
6
|
+if('!loading && leading')
|
|
11
7
|
| {@render leading()}
|
|
12
8
|
+if('children')
|
|
@@ -14,8 +10,28 @@ button(
|
|
|
14
10
|
+if('!loading && trailing')
|
|
15
11
|
| {@render trailing()}
|
|
16
12
|
|
|
13
|
+
+if('href')
|
|
14
|
+
a(
|
|
15
|
+
href!="{href}"
|
|
16
|
+
target!="{target}"
|
|
17
|
+
rel!="{rel}"
|
|
18
|
+
class!="{classes}"
|
|
19
|
+
aria-disabled!="{disabled || loading ? 'true' : undefined}"
|
|
20
|
+
)
|
|
21
|
+
| {@render body()}
|
|
22
|
+
+if('!href')
|
|
23
|
+
button(
|
|
24
|
+
type!="{type}"
|
|
25
|
+
class!="{classes}"
|
|
26
|
+
disabled!="{disabled || loading}"
|
|
27
|
+
onclick!="{onclick}"
|
|
28
|
+
aria-busy!="{loading ? 'true' : undefined}"
|
|
29
|
+
)
|
|
30
|
+
| {@render body()}
|
|
31
|
+
|
|
17
32
|
<script>
|
|
18
33
|
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
34
|
+
import Icon from './Icon.n'
|
|
19
35
|
|
|
20
36
|
{
|
|
21
37
|
variant = 'primary'
|
|
@@ -24,11 +40,16 @@ button(
|
|
|
24
40
|
disabled = false
|
|
25
41
|
type = 'button'
|
|
26
42
|
onclick = undefined
|
|
43
|
+
icon
|
|
44
|
+
href
|
|
45
|
+
target
|
|
46
|
+
rel
|
|
27
47
|
children
|
|
28
48
|
leading
|
|
29
49
|
trailing
|
|
30
50
|
class: extra = ''
|
|
31
51
|
} := $props()
|
|
32
52
|
|
|
33
|
-
classes := cn 'btn', `btn-${variant}`, size !== 'md' && `btn-${size}`, extra
|
|
53
|
+
classes := cn 'btn', `btn-${variant}`, size !== 'md' && `btn-${size}`, 'no-underline', extra
|
|
54
|
+
iconSize := size === 'sm' ? 'size-3.5' : size === 'lg' ? 'size-5' : 'size-4'
|
|
34
55
|
</script>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
div(role="group" class!="{classes}")
|
|
2
|
+
| {@render children?.()}
|
|
3
|
+
|
|
4
|
+
<script>
|
|
5
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
orientation = 'horizontal'
|
|
9
|
+
children
|
|
10
|
+
class: extra = ''
|
|
11
|
+
} := $props()
|
|
12
|
+
|
|
13
|
+
classes := cn 'btn-group', orientation === 'vertical' && 'btn-group-vertical', extra
|
|
14
|
+
</script>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
.calendar(class!="{classes}" role="application" aria-label!="{label}")
|
|
2
|
+
.calendar-header
|
|
3
|
+
button.calendar-nav(type="button" aria-label="Previous month" onclick!="{prevMonth}")
|
|
4
|
+
Icon(name="lucide:chevron-left" size="size-4")
|
|
5
|
+
button.calendar-heading-btn(type="button" aria-label="Jump to today" onclick!="{goToday}")
|
|
6
|
+
| {monthLabel}
|
|
7
|
+
button.calendar-nav(type="button" aria-label="Next month" onclick!="{nextMonth}")
|
|
8
|
+
Icon(name="lucide:chevron-right" size="size-4")
|
|
9
|
+
|
|
10
|
+
.calendar-row.calendar-weekdays
|
|
11
|
+
+each('weekdays as wd')
|
|
12
|
+
.calendar-weekday {wd}
|
|
13
|
+
|
|
14
|
+
+each('weeks as week')
|
|
15
|
+
.calendar-row
|
|
16
|
+
+each('week as cell')
|
|
17
|
+
button.calendar-day(
|
|
18
|
+
type="button"
|
|
19
|
+
class!="{cellClass(cell)}"
|
|
20
|
+
disabled!="{cell.disabled}"
|
|
21
|
+
aria-pressed!="{cell.selected}"
|
|
22
|
+
tabindex!="{cell.outside ? -1 : 0}"
|
|
23
|
+
onclick!="{() => onSelect(cell)}"
|
|
24
|
+
onmouseenter!="{() => range && (hovered = cell.iso)}"
|
|
25
|
+
)
|
|
26
|
+
| {cell.day}
|
|
27
|
+
|
|
28
|
+
<script>
|
|
29
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
30
|
+
import Icon from './Icon.n'
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
value = $bindable('') // ISO YYYY-MM-DD (single-date mode)
|
|
34
|
+
rangeValue = $bindable({ start: '', end: '' }) // (range mode)
|
|
35
|
+
range = false
|
|
36
|
+
min = ''
|
|
37
|
+
max = ''
|
|
38
|
+
label = 'Calendar'
|
|
39
|
+
onchange
|
|
40
|
+
class: extra = ''
|
|
41
|
+
} .= $props()
|
|
42
|
+
|
|
43
|
+
classes := cn 'calendar', extra
|
|
44
|
+
|
|
45
|
+
weekdays := ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
46
|
+
monthFmt := new Intl.DateTimeFormat undefined, { month: 'long', year: 'numeric' }
|
|
47
|
+
|
|
48
|
+
// View anchor — initialised from value (or today) once.
|
|
49
|
+
initial := =>
|
|
50
|
+
s := range ? rangeValue?.start : value
|
|
51
|
+
s ? new Date(`${s}T00:00:00`) : new Date()
|
|
52
|
+
anchor .= $state initial()
|
|
53
|
+
|
|
54
|
+
hovered .= $state ''
|
|
55
|
+
|
|
56
|
+
monthLabel := $derived monthFmt.format(anchor)
|
|
57
|
+
|
|
58
|
+
prevMonth := =>
|
|
59
|
+
d := new Date anchor
|
|
60
|
+
d.setMonth d.getMonth() - 1
|
|
61
|
+
anchor = d
|
|
62
|
+
|
|
63
|
+
nextMonth := =>
|
|
64
|
+
d := new Date anchor
|
|
65
|
+
d.setMonth d.getMonth() + 1
|
|
66
|
+
anchor = d
|
|
67
|
+
|
|
68
|
+
goToday := => anchor = new Date()
|
|
69
|
+
|
|
70
|
+
pad := (n) => String(n).padStart 2, '0'
|
|
71
|
+
iso := (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
72
|
+
todayIso := iso new Date()
|
|
73
|
+
|
|
74
|
+
weeks := $derived.by =>
|
|
75
|
+
year := anchor.getFullYear()
|
|
76
|
+
month := anchor.getMonth()
|
|
77
|
+
firstDow := new Date(year, month, 1).getDay()
|
|
78
|
+
daysInMonth := new Date(year, month + 1, 0).getDate()
|
|
79
|
+
|
|
80
|
+
// Build a flat 42-cell grid (6 rows × 7 cols).
|
|
81
|
+
cells := []
|
|
82
|
+
// leading days from previous month
|
|
83
|
+
i .= 0
|
|
84
|
+
while i < firstDow
|
|
85
|
+
d := new Date(year, month, -(firstDow - 1 - i))
|
|
86
|
+
cells.push buildCell(d, true)
|
|
87
|
+
i++
|
|
88
|
+
// current month
|
|
89
|
+
j .= 1
|
|
90
|
+
while j <= daysInMonth
|
|
91
|
+
d := new Date(year, month, j)
|
|
92
|
+
cells.push buildCell(d, false)
|
|
93
|
+
j++
|
|
94
|
+
// trailing
|
|
95
|
+
k .= 1
|
|
96
|
+
while cells.length < 42
|
|
97
|
+
d := new Date(year, month + 1, k)
|
|
98
|
+
cells.push buildCell(d, true)
|
|
99
|
+
k++
|
|
100
|
+
|
|
101
|
+
// Group into 6 rows of 7
|
|
102
|
+
out := []
|
|
103
|
+
w .= 0
|
|
104
|
+
while w < 6
|
|
105
|
+
out.push cells.slice(w * 7, w * 7 + 7)
|
|
106
|
+
w++
|
|
107
|
+
out
|
|
108
|
+
|
|
109
|
+
buildCell := (d, outside) =>
|
|
110
|
+
isoStr := iso(d)
|
|
111
|
+
isToday := isoStr === todayIso
|
|
112
|
+
disabled .= false
|
|
113
|
+
disabled = true if min && isoStr < min
|
|
114
|
+
disabled = true if max && isoStr > max
|
|
115
|
+
selected .= false
|
|
116
|
+
inRange .= false
|
|
117
|
+
if range
|
|
118
|
+
s := rangeValue?.start ?? ''
|
|
119
|
+
e := rangeValue?.end ?? ''
|
|
120
|
+
selected = !!((s && s === isoStr) || (e && e === isoStr))
|
|
121
|
+
if s && !e && hovered
|
|
122
|
+
lo := s < hovered ? s : hovered
|
|
123
|
+
hi := s < hovered ? hovered : s
|
|
124
|
+
inRange = isoStr > lo && isoStr < hi
|
|
125
|
+
else if s && e
|
|
126
|
+
inRange = isoStr > s && isoStr < e
|
|
127
|
+
else
|
|
128
|
+
selected = value === isoStr
|
|
129
|
+
{ day: d.getDate(), iso: isoStr, outside, disabled, selected, today: isToday, inRange }
|
|
130
|
+
|
|
131
|
+
cellClass := (cell) =>
|
|
132
|
+
cn
|
|
133
|
+
cell.outside && 'calendar-day-outside'
|
|
134
|
+
cell.today && 'calendar-day-today'
|
|
135
|
+
cell.selected && 'calendar-day-selected'
|
|
136
|
+
cell.inRange && 'calendar-day-in-range'
|
|
137
|
+
|
|
138
|
+
onSelect := (cell) =>
|
|
139
|
+
return if cell.disabled
|
|
140
|
+
if range
|
|
141
|
+
s := rangeValue?.start ?? ''
|
|
142
|
+
e := rangeValue?.end ?? ''
|
|
143
|
+
if !s || (s && e)
|
|
144
|
+
rangeValue = { start: cell.iso, end: '' }
|
|
145
|
+
else if cell.iso < s
|
|
146
|
+
rangeValue = { start: cell.iso, end: s }
|
|
147
|
+
else
|
|
148
|
+
rangeValue = { start: s, end: cell.iso }
|
|
149
|
+
onchange?.(rangeValue)
|
|
150
|
+
else
|
|
151
|
+
value = cell.iso
|
|
152
|
+
onchange?.(value)
|
|
153
|
+
</script>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
+snippet('body')
|
|
2
|
+
+if('header')
|
|
3
|
+
.card-header
|
|
4
|
+
| {@render header()}
|
|
5
|
+
.card-body(class!="{padded ? '' : 'p-0'}")
|
|
6
|
+
| {@render children?.()}
|
|
7
|
+
+if('footer')
|
|
8
|
+
.card-footer
|
|
9
|
+
| {@render footer()}
|
|
10
|
+
|
|
11
|
+
+if('href')
|
|
12
|
+
a(href!="{href}" target!="{target}" rel!="{rel}" class!="{classes}")
|
|
13
|
+
| {@render body()}
|
|
14
|
+
+if('!href')
|
|
15
|
+
div(class!="{classes}")
|
|
16
|
+
| {@render body()}
|
|
17
|
+
|
|
18
|
+
<script>
|
|
19
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
padded = true
|
|
23
|
+
interactive = false
|
|
24
|
+
href
|
|
25
|
+
target
|
|
26
|
+
rel
|
|
27
|
+
header
|
|
28
|
+
footer
|
|
29
|
+
children
|
|
30
|
+
class: extra = ''
|
|
31
|
+
} := $props()
|
|
32
|
+
|
|
33
|
+
classes := cn 'card', (interactive || href) && 'card-interactive', extra
|
|
34
|
+
</script>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
.carousel(class!="{classes}" onmouseenter!="{pause}" onmouseleave!="{resume}")
|
|
2
|
+
.carousel-track(style!="{trackStyle}")
|
|
3
|
+
+each('slides as item, i')
|
|
4
|
+
.carousel-slide
|
|
5
|
+
+if('item.src')
|
|
6
|
+
img(src!="{item.src}" alt!="{item.alt ?? ''}" loading="lazy" class="block w-full h-full object-cover")
|
|
7
|
+
+if('!item.src && children')
|
|
8
|
+
| {@render children(item, i)}
|
|
9
|
+
+if('slides.length > 1')
|
|
10
|
+
button.carousel-arrow.carousel-arrow-prev(
|
|
11
|
+
type="button"
|
|
12
|
+
aria-label="Previous"
|
|
13
|
+
onclick!="{prev}"
|
|
14
|
+
)
|
|
15
|
+
Icon(name="lucide:chevron-left" size="size-5")
|
|
16
|
+
button.carousel-arrow.carousel-arrow-next(
|
|
17
|
+
type="button"
|
|
18
|
+
aria-label="Next"
|
|
19
|
+
onclick!="{next}"
|
|
20
|
+
)
|
|
21
|
+
Icon(name="lucide:chevron-right" size="size-5")
|
|
22
|
+
.carousel-dots
|
|
23
|
+
+each('slides as _, i')
|
|
24
|
+
button.carousel-dot(
|
|
25
|
+
type="button"
|
|
26
|
+
aria-label!="{`Go to slide ${i + 1}`}"
|
|
27
|
+
class!="{i === index ? 'carousel-dot-active' : ''}"
|
|
28
|
+
onclick!="{() => index = i}"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
<script>
|
|
32
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
33
|
+
import Icon from './Icon.n'
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
slides = []
|
|
37
|
+
index = $bindable(0)
|
|
38
|
+
autoplay = false
|
|
39
|
+
interval = 5000
|
|
40
|
+
aspect = 'video'
|
|
41
|
+
children
|
|
42
|
+
class: extra = ''
|
|
43
|
+
} .= $props()
|
|
44
|
+
|
|
45
|
+
// Default aspect ratios; user can pass `class="aspect-square"` etc. to override.
|
|
46
|
+
aspectClass := aspect === 'square' ? 'aspect-square'
|
|
47
|
+
: aspect === 'wide' ? 'aspect-[21/9]'
|
|
48
|
+
: aspect === 'video' ? 'aspect-video'
|
|
49
|
+
: ''
|
|
50
|
+
|
|
51
|
+
classes := cn 'carousel', aspectClass, extra
|
|
52
|
+
|
|
53
|
+
trackStyle := $derived `transform: translateX(-${index * 100}%)`
|
|
54
|
+
|
|
55
|
+
prev := => index = (index - 1 + slides.length) % slides.length
|
|
56
|
+
next := => index = (index + 1) % slides.length
|
|
57
|
+
|
|
58
|
+
paused .= $state false
|
|
59
|
+
pause := => paused = true
|
|
60
|
+
resume := => paused = false
|
|
61
|
+
|
|
62
|
+
$effect =>
|
|
63
|
+
return unless autoplay && slides.length > 1
|
|
64
|
+
id := setInterval (=> next() unless paused), interval
|
|
65
|
+
=> clearInterval id
|
|
66
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
span(class!="{classes}")
|
|
2
|
+
+if('icon')
|
|
3
|
+
Icon(name!="{icon}" size="size-3.5")
|
|
4
|
+
| {@render children?.()}
|
|
5
|
+
+if('removable')
|
|
6
|
+
button.chip-remove(
|
|
7
|
+
type="button"
|
|
8
|
+
aria-label="Remove"
|
|
9
|
+
onclick!="{onremove}"
|
|
10
|
+
)
|
|
11
|
+
Icon(name="lucide:x" size="size-3")
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
15
|
+
import Icon from './Icon.n'
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
removable = false
|
|
19
|
+
onremove = undefined
|
|
20
|
+
icon
|
|
21
|
+
children
|
|
22
|
+
class: extra = ''
|
|
23
|
+
...rest
|
|
24
|
+
} := $props()
|
|
25
|
+
|
|
26
|
+
classes := cn 'chip', extra
|
|
27
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
details.collapsible(class!="{classes}" bind:open ontoggle!="{onToggle}")
|
|
2
|
+
summary.collapsible-trigger
|
|
3
|
+
.collapsible-chevron
|
|
4
|
+
Icon(name="lucide:chevron-right" size="size-4")
|
|
5
|
+
+if('title')
|
|
6
|
+
span.collapsible-title {title}
|
|
7
|
+
+if('!title && trigger')
|
|
8
|
+
| {@render trigger()}
|
|
9
|
+
.collapsible-content
|
|
10
|
+
| {@render children?.()}
|
|
11
|
+
|
|
12
|
+
<script>
|
|
13
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
14
|
+
import Icon from './Icon.n'
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
open = $bindable(false)
|
|
18
|
+
title
|
|
19
|
+
trigger
|
|
20
|
+
children
|
|
21
|
+
onopenchange
|
|
22
|
+
class: extra = ''
|
|
23
|
+
} .= $props()
|
|
24
|
+
|
|
25
|
+
classes := cn 'collapsible', extra
|
|
26
|
+
|
|
27
|
+
onToggle := (e) =>
|
|
28
|
+
onopenchange?.(e.target.open)
|
|
29
|
+
</script>
|