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