@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,31 @@
|
|
|
1
|
+
img(
|
|
2
|
+
src!="{src}"
|
|
3
|
+
alt!="{alt}"
|
|
4
|
+
loading!="{loading}"
|
|
5
|
+
decoding="async"
|
|
6
|
+
class!="{classes}"
|
|
7
|
+
width!="{width}"
|
|
8
|
+
height!="{height}"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
<script>
|
|
12
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
src
|
|
16
|
+
alt = ''
|
|
17
|
+
loading = 'lazy'
|
|
18
|
+
fit = 'cover'
|
|
19
|
+
rounded = false
|
|
20
|
+
width
|
|
21
|
+
height
|
|
22
|
+
class: extra = ''
|
|
23
|
+
} := $props()
|
|
24
|
+
|
|
25
|
+
classes := cn
|
|
26
|
+
'block max-w-full'
|
|
27
|
+
fit && `object-${fit}`
|
|
28
|
+
rounded === true && 'rounded'
|
|
29
|
+
typeof rounded === 'string' && `rounded-${rounded}`
|
|
30
|
+
extra
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
nav.nav-menu(class!="{classes}" aria-label!="{label}")
|
|
2
|
+
ul.nav-menu-list
|
|
3
|
+
+each('sections as section')
|
|
4
|
+
li.nav-menu-item
|
|
5
|
+
button.nav-menu-trigger(type="button" aria-haspopup="true" aria-expanded="false")
|
|
6
|
+
span {section.label}
|
|
7
|
+
Icon(name="lucide:chevron-down" size="size-3.5" class="ml-1 nav-menu-chevron")
|
|
8
|
+
.nav-menu-content
|
|
9
|
+
.mega-menu-grid(class!="{`grid-cols-${section.cols ?? 3}`}")
|
|
10
|
+
+each('section.columns as column')
|
|
11
|
+
.mega-menu-column
|
|
12
|
+
+if('column.title')
|
|
13
|
+
.mega-menu-title {column.title}
|
|
14
|
+
ul.flex.flex-col.gap-0.5
|
|
15
|
+
+each('column.items as item')
|
|
16
|
+
li
|
|
17
|
+
a.nav-menu-link(href!="{item.href ?? '#'}")
|
|
18
|
+
+if('item.icon')
|
|
19
|
+
Icon(name!="{item.icon}" size="size-4" class="text-fg-muted shrink-0")
|
|
20
|
+
.flex-1
|
|
21
|
+
.text-sm.font-medium {item.label}
|
|
22
|
+
+if('item.description')
|
|
23
|
+
.text-xs(class="text-fg-muted") {item.description}
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
27
|
+
import Icon from './Icon.n'
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
sections = []
|
|
31
|
+
label = 'Site navigation'
|
|
32
|
+
class: extra = ''
|
|
33
|
+
} := $props()
|
|
34
|
+
|
|
35
|
+
classes := cn '', extra
|
|
36
|
+
</script>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
.tags-input.multi-select(
|
|
2
|
+
use:refAction
|
|
3
|
+
bind:this!="{wrapEl}"
|
|
4
|
+
class!="{hasError ? 'tags-input-err' : ''}"
|
|
5
|
+
onclick!="{focusInput}"
|
|
6
|
+
)
|
|
7
|
+
+each('value as v')
|
|
8
|
+
span.chip
|
|
9
|
+
| {labelOf(v)}
|
|
10
|
+
button.chip-remove(
|
|
11
|
+
type="button"
|
|
12
|
+
aria-label="Remove"
|
|
13
|
+
onclick!="{(e) => { e.stopPropagation(); unpick(v) }}"
|
|
14
|
+
onpointerdown!="{(e) => e.stopPropagation()}"
|
|
15
|
+
)
|
|
16
|
+
Icon(name="lucide:x" size="size-3")
|
|
17
|
+
input.tags-input-field(
|
|
18
|
+
bind:this!="{inputEl}"
|
|
19
|
+
type="text"
|
|
20
|
+
bind:value!="{term}"
|
|
21
|
+
placeholder!="{value.length === 0 ? placeholder : ''}"
|
|
22
|
+
disabled!="{disabled}"
|
|
23
|
+
role="combobox"
|
|
24
|
+
aria-expanded!="{open}"
|
|
25
|
+
aria-controls!="{listboxId}"
|
|
26
|
+
aria-autocomplete="list"
|
|
27
|
+
id!="{resolvedId}"
|
|
28
|
+
aria-invalid!="{hasError ? 'true' : undefined}"
|
|
29
|
+
oninput!="{onInput}"
|
|
30
|
+
onkeydown!="{onKeyDown}"
|
|
31
|
+
onfocus!="{() => open = true}"
|
|
32
|
+
onblur!="{onBlur}"
|
|
33
|
+
)
|
|
34
|
+
button.multi-select-toggle.shrink-0(
|
|
35
|
+
type="button"
|
|
36
|
+
tabindex="-1"
|
|
37
|
+
aria-label="Toggle options"
|
|
38
|
+
onclick!="{(e) => { e.stopPropagation(); toggle() }}"
|
|
39
|
+
)
|
|
40
|
+
Icon(name="lucide:chevrons-up-down" size="size-4")
|
|
41
|
+
|
|
42
|
+
+if('open && filtered.length > 0')
|
|
43
|
+
ul(
|
|
44
|
+
use:portal
|
|
45
|
+
use:floatAction
|
|
46
|
+
use:clickOutside!="{[onClose, open, wrapEl]}"
|
|
47
|
+
class="dropdown-content combobox-listbox"
|
|
48
|
+
data-state="open"
|
|
49
|
+
id!="{listboxId}"
|
|
50
|
+
role="listbox"
|
|
51
|
+
aria-multiselectable="true"
|
|
52
|
+
)
|
|
53
|
+
+each('filtered as item, i (item.value)')
|
|
54
|
+
li.dropdown-item(
|
|
55
|
+
role="option"
|
|
56
|
+
aria-selected!="{value.includes(item.value)}"
|
|
57
|
+
data-active!="{i === active ? 'true' : undefined}"
|
|
58
|
+
onpointerdown!="{(e) => { e.preventDefault(); pick(item) }}"
|
|
59
|
+
)
|
|
60
|
+
Icon(name="lucide:check" size="size-3.5" class!="{value.includes(item.value) ? '' : 'opacity-0'}")
|
|
61
|
+
span {item.label}
|
|
62
|
+
+if('open && filtered.length === 0')
|
|
63
|
+
div(use:portal use:floatAction use:clickOutside!="{[onClose, open, wrapEl]}" class="dropdown-content combobox-listbox" data-state="open")
|
|
64
|
+
.combobox-empty No matches
|
|
65
|
+
|
|
66
|
+
<script>
|
|
67
|
+
import { getContext } from 'svelte'
|
|
68
|
+
import Icon from './Icon.n'
|
|
69
|
+
import { portal } from '@human-synthesis/norns-ui/behaviors/portal.svelte.js'
|
|
70
|
+
import { clickOutside } from '@human-synthesis/norns-ui/behaviors/clickOutside.svelte.js'
|
|
71
|
+
import { useFloating } from '@human-synthesis/norns-ui/behaviors/floating.svelte.js'
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
items = []
|
|
75
|
+
value = $bindable([])
|
|
76
|
+
open = $bindable(false)
|
|
77
|
+
placeholder = 'Pick options…'
|
|
78
|
+
disabled = false
|
|
79
|
+
name
|
|
80
|
+
id: providedId
|
|
81
|
+
error: explicitError = false
|
|
82
|
+
} .= $props()
|
|
83
|
+
|
|
84
|
+
field := getContext 'norns-ui:field'
|
|
85
|
+
resolvedId := providedId ?? field?.id
|
|
86
|
+
hasError := explicitError || field?.hasError || false
|
|
87
|
+
|
|
88
|
+
uid := Math.random().toString(36).slice(2, 8)
|
|
89
|
+
listboxId := `listbox-${uid}`
|
|
90
|
+
|
|
91
|
+
{ reference: refAction, floating: floatAction } := useFloating
|
|
92
|
+
placement: 'bottom-start'
|
|
93
|
+
offset: 4
|
|
94
|
+
matchWidth: true
|
|
95
|
+
|
|
96
|
+
term .= $state ''
|
|
97
|
+
active .= $state 0
|
|
98
|
+
inputEl .= $state null
|
|
99
|
+
wrapEl .= $state null
|
|
100
|
+
|
|
101
|
+
filtered := $derived.by =>
|
|
102
|
+
return items unless term
|
|
103
|
+
t := term.toLowerCase()
|
|
104
|
+
items.filter (it) => it.label.toLowerCase().includes(t)
|
|
105
|
+
|
|
106
|
+
$effect =>
|
|
107
|
+
active = 0 if active >= filtered.length
|
|
108
|
+
|
|
109
|
+
labelOf := (v) => items.find((it) => it.value === v)?.label ?? v
|
|
110
|
+
|
|
111
|
+
focusInput := => inputEl?.focus()
|
|
112
|
+
|
|
113
|
+
onInput := =>
|
|
114
|
+
open = true
|
|
115
|
+
active = 0
|
|
116
|
+
|
|
117
|
+
toggle := =>
|
|
118
|
+
open = !open
|
|
119
|
+
inputEl?.focus() if open
|
|
120
|
+
|
|
121
|
+
onClose := => open = false
|
|
122
|
+
|
|
123
|
+
onBlur := =>
|
|
124
|
+
setTimeout (=> open = false), 100
|
|
125
|
+
|
|
126
|
+
pick := (item) =>
|
|
127
|
+
if value.includes(item.value)
|
|
128
|
+
value = value.filter (v) => v !== item.value
|
|
129
|
+
else
|
|
130
|
+
value = [...value, item.value]
|
|
131
|
+
term = ''
|
|
132
|
+
|
|
133
|
+
unpick := (v) =>
|
|
134
|
+
value = value.filter (x) => x !== v
|
|
135
|
+
inputEl?.focus()
|
|
136
|
+
|
|
137
|
+
onKeyDown := (e) =>
|
|
138
|
+
switch e.key
|
|
139
|
+
when 'ArrowDown'
|
|
140
|
+
e.preventDefault()
|
|
141
|
+
open = true
|
|
142
|
+
active = Math.min(filtered.length - 1, active + 1)
|
|
143
|
+
when 'ArrowUp'
|
|
144
|
+
e.preventDefault()
|
|
145
|
+
open = true
|
|
146
|
+
active = Math.max(0, active - 1)
|
|
147
|
+
when 'Enter'
|
|
148
|
+
if open && filtered[active]
|
|
149
|
+
e.preventDefault()
|
|
150
|
+
pick filtered[active]
|
|
151
|
+
when 'Backspace'
|
|
152
|
+
if !term && value.length > 0
|
|
153
|
+
value = value.slice(0, -1)
|
|
154
|
+
when 'Escape'
|
|
155
|
+
open = false
|
|
156
|
+
when 'Tab'
|
|
157
|
+
open = false
|
|
158
|
+
</script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.number-input(class!="{wrapClasses}")
|
|
2
|
+
button.number-step(type="button" aria-label="Decrement" disabled!="{disabled || atMin}" onclick!="{() => step(-1)}")
|
|
3
|
+
Icon(name="lucide:minus" size="size-3.5")
|
|
4
|
+
input.input.number-input-field(
|
|
5
|
+
type="number"
|
|
6
|
+
bind:value
|
|
7
|
+
min!="{min}"
|
|
8
|
+
max!="{max}"
|
|
9
|
+
step!="{stepSize}"
|
|
10
|
+
disabled!="{disabled}"
|
|
11
|
+
readonly!="{readonly}"
|
|
12
|
+
name!="{name}"
|
|
13
|
+
id!="{resolvedId}"
|
|
14
|
+
aria-invalid!="{hasError ? 'true' : undefined}"
|
|
15
|
+
)
|
|
16
|
+
button.number-step(type="button" aria-label="Increment" disabled!="{disabled || atMax}" onclick!="{() => step(1)}")
|
|
17
|
+
Icon(name="lucide:plus" size="size-3.5")
|
|
18
|
+
|
|
19
|
+
<script>
|
|
20
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
21
|
+
import { getContext } from 'svelte'
|
|
22
|
+
import Icon from './Icon.n'
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
value = $bindable(0)
|
|
26
|
+
min = -Infinity
|
|
27
|
+
max = Infinity
|
|
28
|
+
stepSize = 1
|
|
29
|
+
disabled = false
|
|
30
|
+
readonly = false
|
|
31
|
+
name
|
|
32
|
+
id: providedId
|
|
33
|
+
error: explicitError = false
|
|
34
|
+
class: extra = ''
|
|
35
|
+
} .= $props()
|
|
36
|
+
|
|
37
|
+
field := getContext 'norns-ui:field'
|
|
38
|
+
resolvedId := providedId ?? field?.id
|
|
39
|
+
hasError := explicitError || field?.hasError || false
|
|
40
|
+
|
|
41
|
+
atMin := $derived value <= min
|
|
42
|
+
atMax := $derived value >= max
|
|
43
|
+
|
|
44
|
+
step := (dir) =>
|
|
45
|
+
next := (value ?? 0) + dir * stepSize
|
|
46
|
+
value = Math.max min, Math.min(max, next)
|
|
47
|
+
|
|
48
|
+
wrapClasses := cn 'number-input-wrap', extra
|
|
49
|
+
</script>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
.otp-field(class!="{classes}" role="group" aria-label!="{label}")
|
|
2
|
+
+each('cells as _, i')
|
|
3
|
+
input.otp-cell(
|
|
4
|
+
type!="{mask ? 'password' : 'text'}"
|
|
5
|
+
inputmode="numeric"
|
|
6
|
+
maxlength="1"
|
|
7
|
+
autocomplete="one-time-code"
|
|
8
|
+
pattern="[0-9]*"
|
|
9
|
+
value!="{cells[i]}"
|
|
10
|
+
oninput!="{(e) => onCellInput(e, i)}"
|
|
11
|
+
onkeydown!="{(e) => onCellKey(e, i)}"
|
|
12
|
+
onpaste!="{onPaste}"
|
|
13
|
+
data-otp-index!="{i}"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
<script>
|
|
17
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
value = $bindable('')
|
|
21
|
+
length = 6
|
|
22
|
+
mask = false
|
|
23
|
+
label = 'One-time code'
|
|
24
|
+
oncomplete
|
|
25
|
+
class: extra = ''
|
|
26
|
+
} .= $props()
|
|
27
|
+
|
|
28
|
+
cells := $derived.by =>
|
|
29
|
+
out := []
|
|
30
|
+
for i of [0...length]
|
|
31
|
+
out.push value[i] ?? ''
|
|
32
|
+
out
|
|
33
|
+
|
|
34
|
+
classes := cn 'otp-field-wrap', extra
|
|
35
|
+
|
|
36
|
+
setCell := (i, ch) =>
|
|
37
|
+
next := cells.slice()
|
|
38
|
+
next[i] = ch
|
|
39
|
+
value = next.join('').replace /\s/g, ''
|
|
40
|
+
oncomplete?.(value) if value.length === length
|
|
41
|
+
|
|
42
|
+
focusCell := (i) =>
|
|
43
|
+
el := document.querySelector `[data-otp-index="${i}"]`
|
|
44
|
+
el?.focus()
|
|
45
|
+
el?.select?.()
|
|
46
|
+
|
|
47
|
+
onCellInput := (e, i) =>
|
|
48
|
+
ch := e.target.value.replace(/[^0-9]/g, '').slice(-1)
|
|
49
|
+
setCell i, ch
|
|
50
|
+
focusCell(i + 1) if ch && i < length - 1
|
|
51
|
+
|
|
52
|
+
onCellKey := (e, i) =>
|
|
53
|
+
if e.key === 'Backspace' && !cells[i] && i > 0
|
|
54
|
+
focusCell(i - 1)
|
|
55
|
+
else if e.key === 'ArrowLeft' && i > 0
|
|
56
|
+
focusCell(i - 1)
|
|
57
|
+
else if e.key === 'ArrowRight' && i < length - 1
|
|
58
|
+
focusCell(i + 1)
|
|
59
|
+
|
|
60
|
+
onPaste := (e) =>
|
|
61
|
+
text := (e.clipboardData?.getData('text') ?? '').replace(/\D/g, '').slice(0, length)
|
|
62
|
+
return unless text
|
|
63
|
+
e.preventDefault()
|
|
64
|
+
value = text.padEnd(length, ' ').slice(0, length).trimEnd()
|
|
65
|
+
oncomplete?.(value) if value.length === length
|
|
66
|
+
focusCell Math.min(text.length, length - 1)
|
|
67
|
+
</script>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
nav.pagination(aria-label="Pagination" class!="{extra}")
|
|
2
|
+
button.pagination-item(
|
|
3
|
+
type="button"
|
|
4
|
+
aria-label="First"
|
|
5
|
+
disabled!="{page <= 1}"
|
|
6
|
+
onclick!="{() => go(1)}"
|
|
7
|
+
)
|
|
8
|
+
Icon(name="lucide:chevrons-left" size="size-4")
|
|
9
|
+
button.pagination-item(
|
|
10
|
+
type="button"
|
|
11
|
+
aria-label="Previous"
|
|
12
|
+
disabled!="{page <= 1}"
|
|
13
|
+
onclick!="{() => go(page - 1)}"
|
|
14
|
+
)
|
|
15
|
+
Icon(name="lucide:chevron-left" size="size-4")
|
|
16
|
+
+each('pages as p')
|
|
17
|
+
+if('p === "ellipsis"')
|
|
18
|
+
span.pagination-ellipsis …
|
|
19
|
+
+if('p !== "ellipsis"')
|
|
20
|
+
button.pagination-item(
|
|
21
|
+
type="button"
|
|
22
|
+
class!="{p === page ? 'pagination-item-active' : ''}"
|
|
23
|
+
aria-current!="{p === page ? 'page' : undefined}"
|
|
24
|
+
onclick!="{() => go(p)}"
|
|
25
|
+
)
|
|
26
|
+
| {p}
|
|
27
|
+
button.pagination-item(
|
|
28
|
+
type="button"
|
|
29
|
+
aria-label="Next"
|
|
30
|
+
disabled!="{page >= totalPages}"
|
|
31
|
+
onclick!="{() => go(page + 1)}"
|
|
32
|
+
)
|
|
33
|
+
Icon(name="lucide:chevron-right" size="size-4")
|
|
34
|
+
button.pagination-item(
|
|
35
|
+
type="button"
|
|
36
|
+
aria-label="Last"
|
|
37
|
+
disabled!="{page >= totalPages}"
|
|
38
|
+
onclick!="{() => go(totalPages)}"
|
|
39
|
+
)
|
|
40
|
+
Icon(name="lucide:chevrons-right" size="size-4")
|
|
41
|
+
|
|
42
|
+
<script>
|
|
43
|
+
import Icon from './Icon.n'
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
page = $bindable(1)
|
|
47
|
+
total = 0
|
|
48
|
+
pageSize = 10
|
|
49
|
+
siblingCount = 1
|
|
50
|
+
class: extra = ''
|
|
51
|
+
...rest
|
|
52
|
+
} .= $props()
|
|
53
|
+
|
|
54
|
+
totalPages := $derived Math.max(1, Math.ceil(total / pageSize))
|
|
55
|
+
|
|
56
|
+
go := (p) =>
|
|
57
|
+
clamped := Math.max 1, Math.min(totalPages, p)
|
|
58
|
+
page = clamped
|
|
59
|
+
|
|
60
|
+
pages := $derived.by =>
|
|
61
|
+
out := []
|
|
62
|
+
first := 1
|
|
63
|
+
last := totalPages
|
|
64
|
+
left := Math.max(first, page - siblingCount)
|
|
65
|
+
right := Math.min(last, page + siblingCount)
|
|
66
|
+
out.push first
|
|
67
|
+
out.push 'ellipsis' if left > first + 1
|
|
68
|
+
for n of [first + 1 .. last - 1]
|
|
69
|
+
out.push n if n >= left && n <= right
|
|
70
|
+
out.push 'ellipsis' if right < last - 1
|
|
71
|
+
out.push last unless last === first
|
|
72
|
+
out
|
|
73
|
+
</script>
|
package/src/components/Popover.n
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
+if('trigger')
|
|
2
|
+
span(use:refAction bind:this!="{triggerEl}" class!="{triggerClass ?? ''}" onclick!="{toggle}")
|
|
3
|
+
| {@render trigger()}
|
|
4
|
+
|
|
5
|
+
+if('open')
|
|
6
|
+
div(
|
|
7
|
+
use:portal
|
|
8
|
+
use:floatAction
|
|
9
|
+
use:clickOutside!="{[onClose, open, triggerEl]}"
|
|
10
|
+
use:escape!="{[onClose, open]}"
|
|
11
|
+
class!="{contentClasses}"
|
|
12
|
+
data-state="open"
|
|
13
|
+
role="dialog"
|
|
14
|
+
)
|
|
15
|
+
| {@render children?.()}
|
|
8
16
|
|
|
9
17
|
<script>
|
|
10
|
-
import { Popover } from 'bits-ui'
|
|
11
18
|
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
12
|
-
|
|
13
|
-
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Portal: PopoverPortal
|
|
17
|
-
Content: PopoverContent
|
|
18
|
-
} := Popover
|
|
19
|
+
import { portal } from '@human-synthesis/norns-ui/behaviors/portal.svelte.js'
|
|
20
|
+
import { clickOutside } from '@human-synthesis/norns-ui/behaviors/clickOutside.svelte.js'
|
|
21
|
+
import { escape } from '@human-synthesis/norns-ui/behaviors/escape.svelte.js'
|
|
22
|
+
import { useFloating } from '@human-synthesis/norns-ui/behaviors/floating.svelte.js'
|
|
19
23
|
|
|
20
24
|
{
|
|
21
25
|
open = $bindable(false)
|
|
@@ -28,5 +32,16 @@ PopoverRoot(bind:open)
|
|
|
28
32
|
class: extra = ''
|
|
29
33
|
} .= $props()
|
|
30
34
|
|
|
35
|
+
placement := align === 'center' ? side : `${side}-${align}`
|
|
36
|
+
|
|
37
|
+
{ reference: refAction, floating: floatAction } := useFloating
|
|
38
|
+
placement: placement
|
|
39
|
+
offset: sideOffset
|
|
40
|
+
|
|
31
41
|
contentClasses := cn 'popover-content', extra
|
|
42
|
+
|
|
43
|
+
triggerEl .= $state null
|
|
44
|
+
|
|
45
|
+
toggle := => open = !open
|
|
46
|
+
onClose := => open = false
|
|
32
47
|
</script>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
div(
|
|
2
|
+
class!="{wrapClasses}"
|
|
3
|
+
role="progressbar"
|
|
4
|
+
aria-valuemin="0"
|
|
5
|
+
aria-valuemax!="{max}"
|
|
6
|
+
aria-valuenow!="{indeterminate ? undefined : value}"
|
|
7
|
+
)
|
|
8
|
+
.progress-bar(class!="{barClasses}" style!="{barStyle}")
|
|
9
|
+
|
|
10
|
+
<script>
|
|
11
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
value = 0
|
|
15
|
+
max = 100
|
|
16
|
+
variant = 'primary'
|
|
17
|
+
indeterminate = false
|
|
18
|
+
class: extra = ''
|
|
19
|
+
...rest
|
|
20
|
+
} := $props()
|
|
21
|
+
|
|
22
|
+
pct := $derived Math.max(0, Math.min(100, (value / max) * 100))
|
|
23
|
+
wrapClasses := cn 'progress', indeterminate && 'progress-indeterminate', extra
|
|
24
|
+
barClasses := cn variant !== 'primary' && `progress-bar-${variant}`
|
|
25
|
+
barStyle := $derived indeterminate ? '' : `width: ${pct}%`
|
|
26
|
+
</script>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
svg(
|
|
2
|
+
class!="{classes}"
|
|
3
|
+
width!="{px}"
|
|
4
|
+
height!="{px}"
|
|
5
|
+
viewBox!="0 0 {viewBox} {viewBox}"
|
|
6
|
+
role="progressbar"
|
|
7
|
+
aria-valuemin="0"
|
|
8
|
+
aria-valuemax!="{max}"
|
|
9
|
+
aria-valuenow!="{indeterminate ? undefined : value}"
|
|
10
|
+
)
|
|
11
|
+
circle(
|
|
12
|
+
cx!="{center}"
|
|
13
|
+
cy!="{center}"
|
|
14
|
+
r!="{radius}"
|
|
15
|
+
stroke="var(--color-border)"
|
|
16
|
+
stroke-width!="{stroke}"
|
|
17
|
+
fill="none"
|
|
18
|
+
)
|
|
19
|
+
circle(
|
|
20
|
+
cx!="{center}"
|
|
21
|
+
cy!="{center}"
|
|
22
|
+
r!="{radius}"
|
|
23
|
+
stroke="var(--color-primary-600)"
|
|
24
|
+
stroke-width!="{stroke}"
|
|
25
|
+
stroke-linecap="round"
|
|
26
|
+
fill="none"
|
|
27
|
+
stroke-dasharray!="{circumference}"
|
|
28
|
+
stroke-dashoffset!="{indeterminate ? circumference * 0.7 : circumference - (circumference * pct) / 100}"
|
|
29
|
+
transform!="rotate(-90 {center} {center})"
|
|
30
|
+
style="transition: stroke-dashoffset 300ms ease-out"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
<script>
|
|
34
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
value = 0
|
|
38
|
+
max = 100
|
|
39
|
+
size = 'md'
|
|
40
|
+
indeterminate = false
|
|
41
|
+
class: extra = ''
|
|
42
|
+
} := $props()
|
|
43
|
+
|
|
44
|
+
px := size === 'sm' ? 24 : size === 'lg' ? 64 : 40
|
|
45
|
+
stroke := size === 'sm' ? 3 : size === 'lg' ? 5 : 4
|
|
46
|
+
viewBox := px
|
|
47
|
+
center := px / 2
|
|
48
|
+
radius := center - stroke
|
|
49
|
+
circumference := $derived 2 * Math.PI * radius
|
|
50
|
+
pct := $derived Math.max(0, Math.min(100, (value / max) * 100))
|
|
51
|
+
classes := cn indeterminate && 'animate-spin', extra
|
|
52
|
+
</script>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
button(
|
|
2
|
+
type!="{type}"
|
|
3
|
+
class!="{classes}"
|
|
4
|
+
disabled!="{disabled}"
|
|
5
|
+
onclick!="{handleClick}"
|
|
6
|
+
)
|
|
7
|
+
+each('ripples as r (r.id)')
|
|
8
|
+
span.ripple-btn-ripple(
|
|
9
|
+
aria-hidden="true"
|
|
10
|
+
style!="{`left: ${r.x}px; top: ${r.y}px; width: ${r.size}px; height: ${r.size}px`}"
|
|
11
|
+
ontransitionend!="{() => removeRipple(r.id)}"
|
|
12
|
+
)
|
|
13
|
+
+if('icon')
|
|
14
|
+
Icon(name!="{icon}" size!="{iconSize}")
|
|
15
|
+
+if('children')
|
|
16
|
+
| {@render children()}
|
|
17
|
+
|
|
18
|
+
<script>
|
|
19
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
20
|
+
import Icon from './Icon.n'
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
variant = 'primary'
|
|
24
|
+
size = 'md'
|
|
25
|
+
type = 'button'
|
|
26
|
+
disabled = false
|
|
27
|
+
onclick
|
|
28
|
+
icon
|
|
29
|
+
children
|
|
30
|
+
class: extra = ''
|
|
31
|
+
} .= $props()
|
|
32
|
+
|
|
33
|
+
iconSize := size === 'sm' ? 'size-3.5' : size === 'lg' ? 'size-5' : 'size-4'
|
|
34
|
+
|
|
35
|
+
classes := cn
|
|
36
|
+
'btn'
|
|
37
|
+
`btn-${variant}`
|
|
38
|
+
size !== 'md' && `btn-${size}`
|
|
39
|
+
'ripple-btn'
|
|
40
|
+
extra
|
|
41
|
+
|
|
42
|
+
ripples .= $state []
|
|
43
|
+
nextId .= 0
|
|
44
|
+
|
|
45
|
+
handleClick := (e) =>
|
|
46
|
+
rect := e.currentTarget.getBoundingClientRect()
|
|
47
|
+
size := Math.max(rect.width, rect.height) * 2
|
|
48
|
+
x := e.clientX - rect.left - size / 2
|
|
49
|
+
y := e.clientY - rect.top - size / 2
|
|
50
|
+
id := ++nextId
|
|
51
|
+
ripples = [...ripples, { id, x, y, size }]
|
|
52
|
+
onclick?.(e)
|
|
53
|
+
|
|
54
|
+
removeRipple := (id) =>
|
|
55
|
+
ripples = ripples.filter (r) => r.id !== id
|
|
56
|
+
</script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
div(class!="{classes}")
|
|
2
|
+
| {@render children?.()}
|
|
3
|
+
|
|
4
|
+
<script>
|
|
5
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
horizontal = false
|
|
9
|
+
children
|
|
10
|
+
class: extra = ''
|
|
11
|
+
} := $props()
|
|
12
|
+
|
|
13
|
+
// Atoms defined in atoms.css — see `.norns-scroll-area*` rules. The
|
|
14
|
+
// utility-class approach (`overflow-y-auto overflow-x-hidden`) cannot
|
|
15
|
+
// be used here because Tailwind's content scanner doesn't see strings
|
|
16
|
+
// inside library-side `cn()` calls.
|
|
17
|
+
classes := cn
|
|
18
|
+
horizontal ? 'norns-scroll-area-horizontal' : 'norns-scroll-area'
|
|
19
|
+
extra
|
|
20
|
+
</script>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
div(role="separator" class!="{classes}" aria-orientation!="{orientation}")
|
|
2
|
+
|
|
3
|
+
<script>
|
|
4
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
5
|
+
|
|
6
|
+
{
|
|
7
|
+
orientation = 'horizontal'
|
|
8
|
+
class: extra = ''
|
|
9
|
+
} := $props()
|
|
10
|
+
|
|
11
|
+
classes := cn 'separator', `separator-${orientation}`, extra
|
|
12
|
+
</script>
|