@human-synthesis/norns-ui 0.0.3 → 0.0.5
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 +73 -229
- package/package.json +7 -1
- package/src/auto-import.js +84 -19
- package/src/components/Accordion.n +38 -0
- package/src/components/Audio.n +24 -0
- package/src/components/Autocomplete.n +59 -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 +32 -0
- package/src/components/ColorPicker.n +62 -0
- package/src/components/ContextMenu.n +48 -0
- package/src/components/CopyButton.n +50 -0
- package/src/components/DataTable.n +61 -0
- package/src/components/DatePicker.n +68 -0
- package/src/components/DateRangePicker.n +59 -0
- package/src/components/Drawer.n +49 -0
- 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 +56 -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/PreviewCard.n +30 -0
- package/src/components/Progress.n +26 -0
- package/src/components/ProgressCircular.n +52 -0
- package/src/components/RichTooltip.n +33 -0
- package/src/components/RippleButton.n +56 -0
- package/src/components/ScrollArea.n +31 -0
- package/src/components/Separator.n +12 -0
- 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/TagsInput.n +66 -0
- package/src/components/ThemeToggler.n +43 -0
- package/src/components/TimePicker.n +166 -0
- package/src/components/Timeline.n +27 -0
- package/src/components/ToggleButton.n +29 -0
- package/src/components/ToggleButtonGroup.n +34 -0
- package/src/components/Toolbar.n +15 -0
- 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 +66 -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 +1673 -118
- package/src/styles/tokens.css +171 -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 +12 -0
- package/src/types/ColorPicker.d.ts +15 -0
- package/src/types/ContextMenu.d.ts +19 -0
- package/src/types/CopyButton.d.ts +16 -0
- package/src/types/DataTable.d.ts +27 -0
- package/src/types/DatePicker.d.ts +18 -0
- package/src/types/DateRangePicker.d.ts +17 -0
- package/src/types/Drawer.d.ts +18 -0
- 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 +13 -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/PreviewCard.d.ts +16 -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/RichTooltip.d.ts +16 -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/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/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 +15 -0
- package/src/types/ToggleButtonGroup.d.ts +20 -0
- package/src/types/Toolbar.d.ts +12 -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,61 @@
|
|
|
1
|
+
.data-table-wrap(class!="{wrapClasses}")
|
|
2
|
+
table.data-table
|
|
3
|
+
thead
|
|
4
|
+
tr
|
|
5
|
+
+each('columns as col')
|
|
6
|
+
th(
|
|
7
|
+
class!="{col.class ?? ''}"
|
|
8
|
+
style!="{col.width ? `width: ${col.width}` : undefined}"
|
|
9
|
+
)
|
|
10
|
+
+if('col.sortable')
|
|
11
|
+
button.data-table-sort(type="button" onclick!="{() => onSort(col.key)}")
|
|
12
|
+
span {col.label ?? col.key}
|
|
13
|
+
Icon(name!="{sortIcon(col.key)}" size="size-3.5" class="text-fg-muted")
|
|
14
|
+
+if('!col.sortable')
|
|
15
|
+
| {col.label ?? col.key}
|
|
16
|
+
tbody
|
|
17
|
+
+if('rows.length === 0')
|
|
18
|
+
tr.data-table-empty
|
|
19
|
+
td(colspan!="{columns.length}")
|
|
20
|
+
| {emptyMessage}
|
|
21
|
+
+each('rows as row, i')
|
|
22
|
+
tr(onclick!="{onrowclick ? () => onrowclick(row, i) : undefined}" class!="{onrowclick ? 'data-table-row-clickable' : ''}")
|
|
23
|
+
+each('columns as col')
|
|
24
|
+
td(class!="{col.cellClass ?? ''}")
|
|
25
|
+
| {row[col.key]}
|
|
26
|
+
|
|
27
|
+
<script>
|
|
28
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
29
|
+
import Icon from './Icon.n'
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
columns = []
|
|
33
|
+
rows = []
|
|
34
|
+
striped = false
|
|
35
|
+
dense = false
|
|
36
|
+
stickyHeader = false
|
|
37
|
+
emptyMessage = 'No data'
|
|
38
|
+
sortKey = $bindable(undefined)
|
|
39
|
+
sortDir = $bindable('asc')
|
|
40
|
+
onrowclick
|
|
41
|
+
class: extra = ''
|
|
42
|
+
} .= $props()
|
|
43
|
+
|
|
44
|
+
wrapClasses := cn
|
|
45
|
+
'data-table-root'
|
|
46
|
+
striped && 'data-table-striped'
|
|
47
|
+
dense && 'data-table-dense'
|
|
48
|
+
stickyHeader && 'data-table-sticky'
|
|
49
|
+
extra
|
|
50
|
+
|
|
51
|
+
onSort := (key) =>
|
|
52
|
+
if sortKey === key
|
|
53
|
+
sortDir = sortDir === 'asc' ? 'desc' : 'asc'
|
|
54
|
+
else
|
|
55
|
+
sortKey = key
|
|
56
|
+
sortDir = 'asc'
|
|
57
|
+
|
|
58
|
+
sortIcon := (key) =>
|
|
59
|
+
return 'lucide:chevrons-up-down' unless sortKey === key
|
|
60
|
+
sortDir === 'asc' ? 'lucide:chevron-up' : 'lucide:chevron-down'
|
|
61
|
+
</script>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
PopoverRoot(bind:open)
|
|
2
|
+
PopoverTrigger.input.date-picker-trigger(
|
|
3
|
+
type="button"
|
|
4
|
+
disabled!="{disabled}"
|
|
5
|
+
id!="{resolvedId}"
|
|
6
|
+
aria-label!="{label ?? 'Pick a date'}"
|
|
7
|
+
class!="{hasError ? 'input-err' : ''}"
|
|
8
|
+
)
|
|
9
|
+
Icon(name="lucide:calendar" size="size-4" class="text-fg-muted shrink-0")
|
|
10
|
+
span.flex-1.text-left.truncate
|
|
11
|
+
| {value ? formatted : placeholder}
|
|
12
|
+
Icon(name="lucide:chevron-down" size="size-3.5" class="text-fg-muted shrink-0")
|
|
13
|
+
PopoverPortal
|
|
14
|
+
PopoverContent.popover-content(
|
|
15
|
+
class="p-3 w-[20rem]"
|
|
16
|
+
onOpenAutoFocus!="{(e) => e.preventDefault()}"
|
|
17
|
+
onCloseAutoFocus!="{(e) => e.preventDefault()}"
|
|
18
|
+
)
|
|
19
|
+
Calendar(
|
|
20
|
+
bind:value
|
|
21
|
+
min!="{min}"
|
|
22
|
+
max!="{max}"
|
|
23
|
+
onchange!="{handleChange}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
<script>
|
|
27
|
+
import { Popover } from 'bits-ui'
|
|
28
|
+
import { getContext } from 'svelte'
|
|
29
|
+
import Icon from './Icon.n'
|
|
30
|
+
import Calendar from './Calendar.n'
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
Root: PopoverRoot
|
|
34
|
+
Trigger: PopoverTrigger
|
|
35
|
+
Portal: PopoverPortal
|
|
36
|
+
Content: PopoverContent
|
|
37
|
+
} := Popover
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
value = $bindable('')
|
|
41
|
+
open = $bindable(false)
|
|
42
|
+
min = ''
|
|
43
|
+
max = ''
|
|
44
|
+
placeholder = 'Pick a date'
|
|
45
|
+
disabled = false
|
|
46
|
+
closeOnSelect = true
|
|
47
|
+
label
|
|
48
|
+
name
|
|
49
|
+
id: providedId
|
|
50
|
+
error: explicitError = false
|
|
51
|
+
} .= $props()
|
|
52
|
+
|
|
53
|
+
field := getContext 'norns-ui:field'
|
|
54
|
+
resolvedId := providedId ?? field?.id
|
|
55
|
+
hasError := explicitError || field?.hasError || false
|
|
56
|
+
|
|
57
|
+
formatter := new Intl.DateTimeFormat undefined, { dateStyle: 'medium' }
|
|
58
|
+
|
|
59
|
+
formatted := $derived.by =>
|
|
60
|
+
return '' unless value
|
|
61
|
+
try
|
|
62
|
+
formatter.format new Date(`${value}T00:00:00`)
|
|
63
|
+
catch _
|
|
64
|
+
value
|
|
65
|
+
|
|
66
|
+
handleChange := =>
|
|
67
|
+
open = false if closeOnSelect
|
|
68
|
+
</script>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
PopoverRoot(bind:open)
|
|
2
|
+
PopoverTrigger.input.date-picker-trigger(type="button" disabled!="{disabled}" aria-label="Pick a date range")
|
|
3
|
+
Icon(name="lucide:calendar-range" size="size-4" class="text-fg-muted shrink-0")
|
|
4
|
+
span.flex-1.text-left.truncate
|
|
5
|
+
| {label}
|
|
6
|
+
Icon(name="lucide:chevron-down" size="size-3.5" class="text-fg-muted shrink-0")
|
|
7
|
+
PopoverPortal
|
|
8
|
+
PopoverContent.popover-content(
|
|
9
|
+
class="p-3 w-[20rem]"
|
|
10
|
+
onOpenAutoFocus!="{(e) => e.preventDefault()}"
|
|
11
|
+
onCloseAutoFocus!="{(e) => e.preventDefault()}"
|
|
12
|
+
)
|
|
13
|
+
Calendar(
|
|
14
|
+
bind:rangeValue!="{value}"
|
|
15
|
+
range!="{true}"
|
|
16
|
+
min!="{min}"
|
|
17
|
+
max!="{max}"
|
|
18
|
+
)
|
|
19
|
+
.flex.justify-end.gap-2.mt-3
|
|
20
|
+
button.btn.btn-ghost.btn-sm(type="button" onclick!="{clear}") Clear
|
|
21
|
+
button.btn.btn-primary.btn-sm(type="button" onclick!="{() => open = false}") Done
|
|
22
|
+
|
|
23
|
+
<script>
|
|
24
|
+
import { Popover } from 'bits-ui'
|
|
25
|
+
import Icon from './Icon.n'
|
|
26
|
+
import Calendar from './Calendar.n'
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
Root: PopoverRoot
|
|
30
|
+
Trigger: PopoverTrigger
|
|
31
|
+
Portal: PopoverPortal
|
|
32
|
+
Content: PopoverContent
|
|
33
|
+
} := Popover
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
value = $bindable({ start: '', end: '' })
|
|
37
|
+
open = $bindable(false)
|
|
38
|
+
min = ''
|
|
39
|
+
max = ''
|
|
40
|
+
placeholder = 'Pick a range'
|
|
41
|
+
disabled = false
|
|
42
|
+
} .= $props()
|
|
43
|
+
|
|
44
|
+
formatter := new Intl.DateTimeFormat undefined, { dateStyle: 'medium' }
|
|
45
|
+
|
|
46
|
+
fmt := (s) =>
|
|
47
|
+
return '' unless s
|
|
48
|
+
try
|
|
49
|
+
formatter.format new Date(`${s}T00:00:00`)
|
|
50
|
+
catch _
|
|
51
|
+
s
|
|
52
|
+
|
|
53
|
+
label := $derived.by =>
|
|
54
|
+
return placeholder unless value?.start
|
|
55
|
+
return `${fmt(value.start)} – …` unless value?.end
|
|
56
|
+
`${fmt(value.start)} – ${fmt(value.end)}`
|
|
57
|
+
|
|
58
|
+
clear := => value = { start: '', end: '' }
|
|
59
|
+
</script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
DialogRoot(bind:open)
|
|
2
|
+
+if('trigger')
|
|
3
|
+
DialogTrigger(class!="{triggerClass ?? undefined}")
|
|
4
|
+
| {@render trigger()}
|
|
5
|
+
DialogPortal
|
|
6
|
+
DialogOverlay.dialog-overlay
|
|
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('!hideClose')
|
|
18
|
+
DialogClose.dialog-close(aria-label="Close")
|
|
19
|
+
| ✕
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
import { Dialog } from 'bits-ui'
|
|
23
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
Root: DialogRoot
|
|
27
|
+
Trigger: DialogTrigger
|
|
28
|
+
Portal: DialogPortal
|
|
29
|
+
Overlay: DialogOverlay
|
|
30
|
+
Content: DialogContent
|
|
31
|
+
Title: DialogTitle
|
|
32
|
+
Description: DialogDescription
|
|
33
|
+
Close: DialogClose
|
|
34
|
+
} := Dialog
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
open = $bindable(false)
|
|
38
|
+
side = 'left'
|
|
39
|
+
title
|
|
40
|
+
description
|
|
41
|
+
hideClose = false
|
|
42
|
+
trigger
|
|
43
|
+
children
|
|
44
|
+
triggerClass
|
|
45
|
+
class: extra = ''
|
|
46
|
+
} .= $props()
|
|
47
|
+
|
|
48
|
+
contentClasses := cn 'sheet-content', `sheet-${side}`, extra
|
|
49
|
+
</script>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
span(class!="{classes}" style!="{styleStr}")
|
|
2
|
+
| {@render children?.()}
|
|
3
|
+
|
|
4
|
+
<script>
|
|
5
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
from = 'var(--color-primary-500)'
|
|
9
|
+
to = 'var(--color-info-500)'
|
|
10
|
+
via
|
|
11
|
+
animate = true
|
|
12
|
+
angle = 90
|
|
13
|
+
children
|
|
14
|
+
class: extra = ''
|
|
15
|
+
} := $props()
|
|
16
|
+
|
|
17
|
+
classes := cn 'gradient-text', animate && 'gradient-text-anim', extra
|
|
18
|
+
|
|
19
|
+
stops := $derived via ? `${from}, ${via}, ${to}` : `${from}, ${to}`
|
|
20
|
+
styleStr := $derived `background-image: linear-gradient(${angle}deg, ${stops})`
|
|
21
|
+
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
header(class!="{classes}")
|
|
2
|
+
.norns-header-brand
|
|
3
|
+
| {@render brand?.()}
|
|
4
|
+
+if('nav')
|
|
5
|
+
nav.norns-header-nav
|
|
6
|
+
| {@render nav()}
|
|
7
|
+
.norns-header-actions
|
|
8
|
+
| {@render actions?.()}
|
|
9
|
+
|
|
10
|
+
<script>
|
|
11
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
sticky = false
|
|
15
|
+
brand
|
|
16
|
+
nav
|
|
17
|
+
actions
|
|
18
|
+
class: extra = ''
|
|
19
|
+
...rest
|
|
20
|
+
} := $props()
|
|
21
|
+
|
|
22
|
+
classes := cn 'norns-header', sticky && 'norns-header-sticky', extra
|
|
23
|
+
</script>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
section(class!="{classes}")
|
|
2
|
+
+if('image')
|
|
3
|
+
img.hero-image(src!="{image}" alt="" loading="lazy")
|
|
4
|
+
.relative
|
|
5
|
+
h1.hero-title
|
|
6
|
+
+if('gradient')
|
|
7
|
+
GradientText
|
|
8
|
+
| {title}
|
|
9
|
+
+if('!gradient')
|
|
10
|
+
| {title}
|
|
11
|
+
+if('description')
|
|
12
|
+
p.hero-description
|
|
13
|
+
| {description}
|
|
14
|
+
+if('children')
|
|
15
|
+
.mt-4
|
|
16
|
+
| {@render children()}
|
|
17
|
+
+if('actions')
|
|
18
|
+
.hero-actions
|
|
19
|
+
| {@render actions()}
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
23
|
+
import GradientText from './GradientText.n'
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
title = ''
|
|
27
|
+
description
|
|
28
|
+
image
|
|
29
|
+
align = 'left'
|
|
30
|
+
gradient = false
|
|
31
|
+
actions
|
|
32
|
+
children
|
|
33
|
+
class: extra = ''
|
|
34
|
+
} := $props()
|
|
35
|
+
|
|
36
|
+
classes := cn 'hero', align === 'center' && 'hero-center', extra
|
|
37
|
+
</script>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
nav.nav-menu(class!="{classes}" aria-label!="{label}")
|
|
2
|
+
ul.nav-menu-list
|
|
3
|
+
+each('items as item')
|
|
4
|
+
li.nav-menu-item
|
|
5
|
+
+if('item.children')
|
|
6
|
+
button.nav-menu-trigger(type="button" aria-haspopup="true" aria-expanded="false")
|
|
7
|
+
span {item.label}
|
|
8
|
+
Icon(name="lucide:chevron-down" size="size-3.5" class="ml-1 nav-menu-chevron")
|
|
9
|
+
.nav-menu-content
|
|
10
|
+
ul.flex.flex-col.gap-0.5.p-1(class="min-w-[12rem]")
|
|
11
|
+
+each('item.children as child')
|
|
12
|
+
li
|
|
13
|
+
a.nav-menu-link(href!="{child.href ?? '#'}" onclick!="{(e) => onClickItem(e, child)}")
|
|
14
|
+
+if('child.icon')
|
|
15
|
+
Icon(name!="{child.icon}" size="size-4" class="text-fg-muted shrink-0")
|
|
16
|
+
span.flex-1 {child.label}
|
|
17
|
+
+if('!item.children')
|
|
18
|
+
a.nav-menu-trigger(href!="{item.href ?? '#'}" onclick!="{(e) => onClickItem(e, item)}")
|
|
19
|
+
span {item.label}
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
23
|
+
import Icon from './Icon.n'
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
items = []
|
|
27
|
+
label = 'Main navigation'
|
|
28
|
+
onSelect
|
|
29
|
+
class: extra = ''
|
|
30
|
+
} := $props()
|
|
31
|
+
|
|
32
|
+
classes := cn '', extra
|
|
33
|
+
|
|
34
|
+
onClickItem := (e, item) =>
|
|
35
|
+
if onSelect
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
onSelect item
|
|
38
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
IconifyIcon(icon!="{name}" class!="{classes}" flip!="{flip}" rotate!="{rotate}")
|
|
2
|
+
|
|
3
|
+
<script>
|
|
4
|
+
import IconifyIcon from '@iconify/svelte'
|
|
5
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
name
|
|
9
|
+
size = 'size-4'
|
|
10
|
+
flip
|
|
11
|
+
rotate
|
|
12
|
+
class: extra = ''
|
|
13
|
+
} := $props()
|
|
14
|
+
|
|
15
|
+
classes := cn 'inline-block shrink-0 align-[-0.125em]', size, extra
|
|
16
|
+
</script>
|
|
@@ -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,56 @@
|
|
|
1
|
+
ComboboxRoot(bind:value bind:open type="multiple")
|
|
2
|
+
.combobox-input-wrap
|
|
3
|
+
ComboboxInput.input.combobox-input(
|
|
4
|
+
placeholder!="{placeholder}"
|
|
5
|
+
disabled!="{disabled}"
|
|
6
|
+
oninput!="{(e) => term = e.currentTarget.value}"
|
|
7
|
+
)
|
|
8
|
+
ComboboxTrigger.combobox-trigger(aria-label="Toggle options")
|
|
9
|
+
Icon(name="lucide:chevrons-up-down" size="size-4")
|
|
10
|
+
+if('value.length > 0')
|
|
11
|
+
.flex.flex-wrap.gap-1.mt-2
|
|
12
|
+
+each('value as v')
|
|
13
|
+
span.chip
|
|
14
|
+
| {labelOf(v)}
|
|
15
|
+
ComboboxPortal
|
|
16
|
+
ComboboxContent.dropdown-content
|
|
17
|
+
ComboboxViewport
|
|
18
|
+
+each('filtered as item')
|
|
19
|
+
ComboboxItem.dropdown-item(value!="{item.value}" label!="{item.label}")
|
|
20
|
+
Icon(name="lucide:check" size="size-3.5" class="opacity-0 data-[selected]:opacity-100")
|
|
21
|
+
span {item.label}
|
|
22
|
+
+if('filtered.length === 0')
|
|
23
|
+
.dropdown-item(class="text-fg-muted")
|
|
24
|
+
| No matches
|
|
25
|
+
|
|
26
|
+
<script>
|
|
27
|
+
import { Combobox } from 'bits-ui'
|
|
28
|
+
import Icon from './Icon.n'
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
Root: ComboboxRoot
|
|
32
|
+
Input: ComboboxInput
|
|
33
|
+
Trigger: ComboboxTrigger
|
|
34
|
+
Portal: ComboboxPortal
|
|
35
|
+
Content: ComboboxContent
|
|
36
|
+
Viewport: ComboboxViewport
|
|
37
|
+
Item: ComboboxItem
|
|
38
|
+
} := Combobox
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
items = []
|
|
42
|
+
value = $bindable([])
|
|
43
|
+
open = $bindable(false)
|
|
44
|
+
placeholder = 'Search…'
|
|
45
|
+
disabled = false
|
|
46
|
+
} .= $props()
|
|
47
|
+
|
|
48
|
+
term .= $state ''
|
|
49
|
+
|
|
50
|
+
filtered := $derived.by =>
|
|
51
|
+
return items unless term
|
|
52
|
+
t := term.toLowerCase()
|
|
53
|
+
items.filter (it) => it.label.toLowerCase().includes(t)
|
|
54
|
+
|
|
55
|
+
labelOf := (v) => items.find((it) => it.value === v)?.label ?? v
|
|
56
|
+
</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>
|