@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.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -229
  3. package/package.json +7 -1
  4. package/src/auto-import.js +84 -19
  5. package/src/components/Accordion.n +38 -0
  6. package/src/components/Audio.n +24 -0
  7. package/src/components/Autocomplete.n +59 -0
  8. package/src/components/Avatar.n +27 -0
  9. package/src/components/AvatarGroup.n +25 -0
  10. package/src/components/Badge.n +20 -0
  11. package/src/components/Banner.n +25 -0
  12. package/src/components/Breadcrumbs.n +22 -0
  13. package/src/components/Btn.n +29 -8
  14. package/src/components/ButtonGroup.n +14 -0
  15. package/src/components/Calendar.n +153 -0
  16. package/src/components/Card.n +34 -0
  17. package/src/components/Carousel.n +66 -0
  18. package/src/components/Chip.n +27 -0
  19. package/src/components/Collapsible.n +32 -0
  20. package/src/components/ColorPicker.n +62 -0
  21. package/src/components/ContextMenu.n +48 -0
  22. package/src/components/CopyButton.n +50 -0
  23. package/src/components/DataTable.n +61 -0
  24. package/src/components/DatePicker.n +68 -0
  25. package/src/components/DateRangePicker.n +59 -0
  26. package/src/components/Drawer.n +49 -0
  27. package/src/components/GradientText.n +21 -0
  28. package/src/components/Header.n +23 -0
  29. package/src/components/HeroBanner.n +37 -0
  30. package/src/components/HierarchicalMenu.n +38 -0
  31. package/src/components/Icon.n +16 -0
  32. package/src/components/Image.n +31 -0
  33. package/src/components/MegaMenu.n +36 -0
  34. package/src/components/MultiSelect.n +56 -0
  35. package/src/components/NumberInput.n +49 -0
  36. package/src/components/OtpField.n +67 -0
  37. package/src/components/Pagination.n +73 -0
  38. package/src/components/PreviewCard.n +30 -0
  39. package/src/components/Progress.n +26 -0
  40. package/src/components/ProgressCircular.n +52 -0
  41. package/src/components/RichTooltip.n +33 -0
  42. package/src/components/RippleButton.n +56 -0
  43. package/src/components/ScrollArea.n +31 -0
  44. package/src/components/Separator.n +12 -0
  45. package/src/components/ShinyButton.n +36 -0
  46. package/src/components/Skeleton.n +27 -0
  47. package/src/components/Stepper.n +35 -0
  48. package/src/components/Surface.n +10 -0
  49. package/src/components/TagsInput.n +66 -0
  50. package/src/components/ThemeToggler.n +43 -0
  51. package/src/components/TimePicker.n +166 -0
  52. package/src/components/Timeline.n +27 -0
  53. package/src/components/ToggleButton.n +29 -0
  54. package/src/components/ToggleButtonGroup.n +34 -0
  55. package/src/components/Toolbar.n +15 -0
  56. package/src/components/Tree.n +57 -0
  57. package/src/components/Uploader.n +88 -0
  58. package/src/components/Video.n +26 -0
  59. package/src/index.js +66 -0
  60. package/src/motion/AnimatedNumber.n +55 -0
  61. package/src/motion/GradientBackground.n +25 -0
  62. package/src/motion/LiquidButton.n +48 -0
  63. package/src/motion/Reveal.n +37 -0
  64. package/src/motion/Sparkles.n +53 -0
  65. package/src/motion/index.js +18 -0
  66. package/src/styles/atoms.css +1673 -118
  67. package/src/styles/tokens.css +171 -40
  68. package/src/types/Accordion.d.ts +17 -0
  69. package/src/types/AnimatedNumber.d.ts +15 -0
  70. package/src/types/Audio.d.ts +18 -0
  71. package/src/types/Autocomplete.d.ts +17 -0
  72. package/src/types/Avatar.d.ts +14 -0
  73. package/src/types/AvatarGroup.d.ts +15 -0
  74. package/src/types/Badge.d.ts +14 -0
  75. package/src/types/Banner.d.ts +15 -0
  76. package/src/types/Breadcrumbs.d.ts +15 -0
  77. package/src/types/Btn.d.ts +10 -0
  78. package/src/types/ButtonGroup.d.ts +10 -0
  79. package/src/types/Calendar.d.ts +20 -0
  80. package/src/types/Card.d.ts +19 -0
  81. package/src/types/Carousel.d.ts +23 -0
  82. package/src/types/Chip.d.ts +12 -0
  83. package/src/types/Collapsible.d.ts +12 -0
  84. package/src/types/ColorPicker.d.ts +15 -0
  85. package/src/types/ContextMenu.d.ts +19 -0
  86. package/src/types/CopyButton.d.ts +16 -0
  87. package/src/types/DataTable.d.ts +27 -0
  88. package/src/types/DatePicker.d.ts +18 -0
  89. package/src/types/DateRangePicker.d.ts +17 -0
  90. package/src/types/Drawer.d.ts +18 -0
  91. package/src/types/GradientBackground.d.ts +14 -0
  92. package/src/types/GradientText.d.ts +19 -0
  93. package/src/types/Header.d.ts +12 -0
  94. package/src/types/HeroBanner.d.ts +17 -0
  95. package/src/types/HierarchicalMenu.d.ts +20 -0
  96. package/src/types/Icon.d.ts +21 -0
  97. package/src/types/Image.d.ts +15 -0
  98. package/src/types/LiquidButton.d.ts +15 -0
  99. package/src/types/MegaMenu.d.ts +29 -0
  100. package/src/types/MultiSelect.d.ts +13 -0
  101. package/src/types/NumberInput.d.ts +17 -0
  102. package/src/types/OtpField.d.ts +13 -0
  103. package/src/types/Pagination.d.ts +13 -0
  104. package/src/types/PreviewCard.d.ts +16 -0
  105. package/src/types/Progress.d.ts +14 -0
  106. package/src/types/ProgressCircular.d.ts +14 -0
  107. package/src/types/Reveal.d.ts +20 -0
  108. package/src/types/RichTooltip.d.ts +16 -0
  109. package/src/types/RippleButton.d.ts +15 -0
  110. package/src/types/ScrollArea.d.ts +11 -0
  111. package/src/types/Separator.d.ts +9 -0
  112. package/src/types/ShinyButton.d.ts +15 -0
  113. package/src/types/Skeleton.d.ts +13 -0
  114. package/src/types/Sparkles.d.ts +13 -0
  115. package/src/types/Stepper.d.ts +19 -0
  116. package/src/types/Surface.d.ts +10 -0
  117. package/src/types/TagsInput.d.ts +17 -0
  118. package/src/types/ThemeToggler.d.ts +17 -0
  119. package/src/types/TimePicker.d.ts +20 -0
  120. package/src/types/Timeline.d.ts +17 -0
  121. package/src/types/ToggleButton.d.ts +15 -0
  122. package/src/types/ToggleButtonGroup.d.ts +20 -0
  123. package/src/types/Toolbar.d.ts +12 -0
  124. package/src/types/Tree.d.ts +20 -0
  125. package/src/types/Uploader.d.ts +15 -0
  126. 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>