@human-synthesis/norns-ui 0.0.2 → 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 (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -129
  3. package/package.json +23 -15
  4. package/src/auto-import.js +96 -22
  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/Dialog.n +54 -0
  27. package/src/components/Drawer.n +49 -0
  28. package/src/components/Dropdown.n +41 -0
  29. package/src/components/Field.n +29 -20
  30. package/src/components/Form.n +22 -0
  31. package/src/components/GradientText.n +21 -0
  32. package/src/components/Header.n +23 -0
  33. package/src/components/HeroBanner.n +37 -0
  34. package/src/components/HierarchicalMenu.n +38 -0
  35. package/src/components/Icon.n +16 -0
  36. package/src/components/Image.n +31 -0
  37. package/src/components/MegaMenu.n +36 -0
  38. package/src/components/MultiSelect.n +56 -0
  39. package/src/components/NumberInput.n +49 -0
  40. package/src/components/OtpField.n +67 -0
  41. package/src/components/Pagination.n +73 -0
  42. package/src/components/Popover.n +32 -0
  43. package/src/components/PreviewCard.n +30 -0
  44. package/src/components/Progress.n +26 -0
  45. package/src/components/ProgressCircular.n +52 -0
  46. package/src/components/RichTooltip.n +33 -0
  47. package/src/components/RippleButton.n +56 -0
  48. package/src/components/ScrollArea.n +31 -0
  49. package/src/components/Separator.n +12 -0
  50. package/src/components/Sheet.n +55 -0
  51. package/src/components/ShinyButton.n +36 -0
  52. package/src/components/Skeleton.n +27 -0
  53. package/src/components/Stepper.n +35 -0
  54. package/src/components/Surface.n +10 -0
  55. package/src/components/Tabs.n +28 -0
  56. package/src/components/TagsInput.n +66 -0
  57. package/src/components/ThemeToggler.n +43 -0
  58. package/src/components/TimePicker.n +166 -0
  59. package/src/components/Timeline.n +27 -0
  60. package/src/components/ToastProvider.n +16 -0
  61. package/src/components/ToggleButton.n +29 -0
  62. package/src/components/ToggleButtonGroup.n +34 -0
  63. package/src/components/Toolbar.n +15 -0
  64. package/src/components/Tooltip.n +34 -0
  65. package/src/components/Tree.n +57 -0
  66. package/src/components/Uploader.n +88 -0
  67. package/src/components/Video.n +26 -0
  68. package/src/index.js +76 -0
  69. package/src/lib/toast.svelte.js +42 -0
  70. package/src/motion/AnimatedNumber.n +55 -0
  71. package/src/motion/GradientBackground.n +25 -0
  72. package/src/motion/LiquidButton.n +48 -0
  73. package/src/motion/Reveal.n +37 -0
  74. package/src/motion/Sparkles.n +53 -0
  75. package/src/motion/index.js +18 -0
  76. package/src/styles/atoms.css +1794 -71
  77. package/src/styles/tokens.css +171 -40
  78. package/src/types/Accordion.d.ts +17 -0
  79. package/src/types/AnimatedNumber.d.ts +15 -0
  80. package/src/types/Audio.d.ts +18 -0
  81. package/src/types/Autocomplete.d.ts +17 -0
  82. package/src/types/Avatar.d.ts +14 -0
  83. package/src/types/AvatarGroup.d.ts +15 -0
  84. package/src/types/Badge.d.ts +14 -0
  85. package/src/types/Banner.d.ts +15 -0
  86. package/src/types/Breadcrumbs.d.ts +15 -0
  87. package/src/types/Btn.d.ts +10 -0
  88. package/src/types/ButtonGroup.d.ts +10 -0
  89. package/src/types/Calendar.d.ts +20 -0
  90. package/src/types/Card.d.ts +19 -0
  91. package/src/types/Carousel.d.ts +23 -0
  92. package/src/types/Chip.d.ts +12 -0
  93. package/src/types/Collapsible.d.ts +12 -0
  94. package/src/types/ColorPicker.d.ts +15 -0
  95. package/src/types/ContextMenu.d.ts +19 -0
  96. package/src/types/CopyButton.d.ts +16 -0
  97. package/src/types/DataTable.d.ts +27 -0
  98. package/src/types/DatePicker.d.ts +18 -0
  99. package/src/types/DateRangePicker.d.ts +17 -0
  100. package/src/types/Dialog.d.ts +17 -0
  101. package/src/types/Drawer.d.ts +18 -0
  102. package/src/types/Dropdown.d.ts +23 -0
  103. package/src/types/Field.d.ts +14 -3
  104. package/src/types/Form.d.ts +17 -0
  105. package/src/types/GradientBackground.d.ts +14 -0
  106. package/src/types/GradientText.d.ts +19 -0
  107. package/src/types/Header.d.ts +12 -0
  108. package/src/types/HeroBanner.d.ts +17 -0
  109. package/src/types/HierarchicalMenu.d.ts +20 -0
  110. package/src/types/Icon.d.ts +21 -0
  111. package/src/types/Image.d.ts +15 -0
  112. package/src/types/LiquidButton.d.ts +15 -0
  113. package/src/types/MegaMenu.d.ts +29 -0
  114. package/src/types/MultiSelect.d.ts +13 -0
  115. package/src/types/NumberInput.d.ts +17 -0
  116. package/src/types/OtpField.d.ts +13 -0
  117. package/src/types/Pagination.d.ts +13 -0
  118. package/src/types/Popover.d.ts +18 -0
  119. package/src/types/PreviewCard.d.ts +16 -0
  120. package/src/types/Progress.d.ts +14 -0
  121. package/src/types/ProgressCircular.d.ts +14 -0
  122. package/src/types/Reveal.d.ts +20 -0
  123. package/src/types/RichTooltip.d.ts +16 -0
  124. package/src/types/RippleButton.d.ts +15 -0
  125. package/src/types/ScrollArea.d.ts +11 -0
  126. package/src/types/Separator.d.ts +9 -0
  127. package/src/types/Sheet.d.ts +20 -0
  128. package/src/types/ShinyButton.d.ts +15 -0
  129. package/src/types/Skeleton.d.ts +13 -0
  130. package/src/types/Sparkles.d.ts +13 -0
  131. package/src/types/Stepper.d.ts +19 -0
  132. package/src/types/Surface.d.ts +10 -0
  133. package/src/types/Tabs.d.ts +17 -0
  134. package/src/types/TagsInput.d.ts +17 -0
  135. package/src/types/ThemeToggler.d.ts +17 -0
  136. package/src/types/TimePicker.d.ts +20 -0
  137. package/src/types/Timeline.d.ts +17 -0
  138. package/src/types/ToastProvider.d.ts +15 -0
  139. package/src/types/ToggleButton.d.ts +15 -0
  140. package/src/types/ToggleButtonGroup.d.ts +20 -0
  141. package/src/types/Toolbar.d.ts +12 -0
  142. package/src/types/Tooltip.d.ts +15 -0
  143. package/src/types/Tree.d.ts +20 -0
  144. package/src/types/Uploader.d.ts +15 -0
  145. package/src/types/Video.d.ts +20 -0
@@ -0,0 +1,28 @@
1
+ TabsRoot(bind:value class!="{rootClasses}")
2
+ TabsList.tabs-list
3
+ +each('items as item (item.value)')
4
+ TabsTrigger.tabs-trigger(value!="{item.value}" disabled!="{item.disabled}")
5
+ | {item.label}
6
+ +each('items as item (item.value)')
7
+ TabsContent.tabs-content(value!="{item.value}")
8
+ | {@render item.panel?.()}
9
+
10
+ <script>
11
+ import { Tabs } from 'bits-ui'
12
+ import { cn } from '@human-synthesis/norns-ui/cn'
13
+
14
+ {
15
+ Root: TabsRoot
16
+ List: TabsList
17
+ Trigger: TabsTrigger
18
+ Content: TabsContent
19
+ } := Tabs
20
+
21
+ {
22
+ value = $bindable()
23
+ items = []
24
+ class: extra = ''
25
+ } .= $props()
26
+
27
+ rootClasses := cn 'tabs-root', extra
28
+ </script>
@@ -0,0 +1,66 @@
1
+ .tags-input(class!="{wrapClasses}" onclick!="{focusInput}")
2
+ +each('value as tag, i')
3
+ span.chip
4
+ | {tag}
5
+ button.chip-remove(type="button" aria-label!="{`Remove ${tag}`}" onclick!="{(e) => { e.stopPropagation(); remove(i) }}")
6
+ Icon(name="lucide:x" size="size-3")
7
+ input.tags-input-field(
8
+ bind:this!="{inputEl}"
9
+ bind:value!="{draft}"
10
+ type="text"
11
+ placeholder!="{placeholder}"
12
+ disabled!="{disabled}"
13
+ onkeydown!="{onKey}"
14
+ onblur!="{commitOnBlur ? commit : undefined}"
15
+ name!="{name}"
16
+ id!="{resolvedId}"
17
+ aria-invalid!="{hasError ? 'true' : undefined}"
18
+ )
19
+
20
+ <script>
21
+ import { cn } from '@human-synthesis/norns-ui/cn'
22
+ import { getContext } from 'svelte'
23
+ import Icon from './Icon.n'
24
+
25
+ {
26
+ value = $bindable([])
27
+ max
28
+ placeholder = 'Add a tag…'
29
+ commitOnBlur = true
30
+ separators = [',', 'Enter']
31
+ disabled = false
32
+ name
33
+ id: providedId
34
+ error: explicitError = false
35
+ class: extra = ''
36
+ } .= $props()
37
+
38
+ field := getContext 'norns-ui:field'
39
+ resolvedId := providedId ?? field?.id
40
+ hasError := explicitError || field?.hasError || false
41
+
42
+ draft .= $state ''
43
+ inputEl .= $state null
44
+
45
+ wrapClasses := cn 'tags-input-wrap', hasError && 'tags-input-err', extra
46
+
47
+ focusInput := => inputEl?.focus()
48
+
49
+ commit := =>
50
+ t := draft.trim()
51
+ return unless t
52
+ return if max && value.length >= max
53
+ return if value.includes(t)
54
+ value = [...value, t]
55
+ draft = ''
56
+
57
+ remove := (i) =>
58
+ value = value.filter (_, idx) => idx !== i
59
+
60
+ onKey := (e) =>
61
+ if separators.includes(e.key)
62
+ e.preventDefault()
63
+ commit()
64
+ else if e.key === 'Backspace' && !draft && value.length > 0
65
+ remove(value.length - 1)
66
+ </script>
@@ -0,0 +1,43 @@
1
+ button(
2
+ type="button"
3
+ class!="{classes}"
4
+ onclick!="{toggle}"
5
+ aria-label="Toggle theme"
6
+ title="Toggle theme"
7
+ )
8
+ Icon(name!="{lightIcon}" size!="{iconSize}" class="theme-toggle-light")
9
+ Icon(name!="{darkIcon}" size!="{iconSize}" class="theme-toggle-dark")
10
+ +if('showLabel')
11
+ span.theme-toggle-light
12
+ | {labelLight}
13
+ span.theme-toggle-dark
14
+ | {labelDark}
15
+
16
+ <script>
17
+ import { cn } from '@human-synthesis/norns-ui/cn'
18
+ import Icon from './Icon.n'
19
+
20
+ {
21
+ variant = 'ghost'
22
+ size = 'md'
23
+ showLabel = false
24
+ labelLight = 'Dark'
25
+ labelDark = 'Light'
26
+ lightIcon = 'lucide:moon'
27
+ darkIcon = 'lucide:sun'
28
+ storageKey = 'norns-theme'
29
+ class: extra = ''
30
+ } := $props()
31
+
32
+ iconSize := size === 'sm' ? 'size-3.5' : size === 'lg' ? 'size-5' : 'size-4'
33
+
34
+ classes := cn 'btn' , `btn-${variant}`, size !== 'md' && `btn-${size}`, extra
35
+
36
+ toggle := =>
37
+ root := document.documentElement
38
+ next := root.dataset.theme === 'dark' ? 'light' : 'dark'
39
+ root.dataset.theme = next === 'dark' ? 'dark' : ''
40
+ try
41
+ localStorage.setItem storageKey, next
42
+ catch _
43
+ </script>
@@ -0,0 +1,166 @@
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 time'}"
7
+ class!="{hasError ? 'input-err' : ''}"
8
+ )
9
+ Icon(name="lucide:clock" size="size-4" class="text-fg-muted shrink-0")
10
+ span.flex-1.text-left.font-mono.tabular-nums
11
+ | {value || 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-2 time-picker-popover"
16
+ onOpenAutoFocus!="{(e) => e.preventDefault()}"
17
+ onCloseAutoFocus!="{(e) => e.preventDefault()}"
18
+ )
19
+ .time-picker-grid
20
+ .time-picker-col(role="listbox" aria-label="Hour" bind:this!="{hourCol}")
21
+ +each('hours as h')
22
+ button.time-picker-cell(
23
+ type="button"
24
+ class!="{h === currentHour ? 'time-picker-cell-selected' : ''}"
25
+ onclick!="{() => setHour(h)}"
26
+ data-time-h!="{h}"
27
+ )
28
+ | {pad(h)}
29
+ .time-picker-sep :
30
+ .time-picker-col(role="listbox" aria-label="Minute" bind:this!="{minuteCol}")
31
+ +each('minutes as m')
32
+ button.time-picker-cell(
33
+ type="button"
34
+ class!="{m === currentMinute ? 'time-picker-cell-selected' : ''}"
35
+ onclick!="{() => setMinute(m)}"
36
+ data-time-m!="{m}"
37
+ )
38
+ | {pad(m)}
39
+ +if('hourCycle === 12')
40
+ .time-picker-col(role="listbox" aria-label="AM/PM")
41
+ button.time-picker-cell(
42
+ type="button"
43
+ class!="{period === 'AM' ? 'time-picker-cell-selected' : ''}"
44
+ onclick!="{() => setPeriod('AM')}"
45
+ )
46
+ | AM
47
+ button.time-picker-cell(
48
+ type="button"
49
+ class!="{period === 'PM' ? 'time-picker-cell-selected' : ''}"
50
+ onclick!="{() => setPeriod('PM')}"
51
+ )
52
+ | PM
53
+ .flex.justify-between.gap-2.mt-2.pt-2(class="border-t border-border")
54
+ button.btn.btn-ghost.btn-sm(type="button" onclick!="{clearValue}") Clear
55
+ button.btn.btn-primary.btn-sm(type="button" onclick!="{() => open = false}") Done
56
+
57
+ <script>
58
+ import { Popover } from 'bits-ui'
59
+ import { getContext } from 'svelte'
60
+ import Icon from './Icon.n'
61
+
62
+ {
63
+ Root: PopoverRoot
64
+ Trigger: PopoverTrigger
65
+ Portal: PopoverPortal
66
+ Content: PopoverContent
67
+ } := Popover
68
+
69
+ {
70
+ value = $bindable('')
71
+ open = $bindable(false)
72
+ minuteStep = 5
73
+ hourCycle = 24
74
+ placeholder = 'Pick a time'
75
+ disabled = false
76
+ label
77
+ name
78
+ id: providedId
79
+ error: explicitError = false
80
+ } .= $props()
81
+
82
+ field := getContext 'norns-ui:field'
83
+ resolvedId := providedId ?? field?.id
84
+ hasError := explicitError || field?.hasError || false
85
+
86
+ pad := (n) => String(n).padStart 2, '0'
87
+
88
+ parts := $derived.by =>
89
+ if !value
90
+ return { h: 0, m: 0 }
91
+ [hStr, mStr] := value.split ':'
92
+ { h: parseInt(hStr ?? '0', 10), m: parseInt(mStr ?? '0', 10) }
93
+
94
+ hours := $derived.by =>
95
+ out := []
96
+ if hourCycle === 12
97
+ out.push 12
98
+ i .= 1
99
+ while i <= 11
100
+ out.push i
101
+ i++
102
+ else
103
+ i .= 0
104
+ while i < 24
105
+ out.push i
106
+ i++
107
+ out
108
+
109
+ minutes := $derived.by =>
110
+ out := []
111
+ i .= 0
112
+ while i < 60
113
+ out.push i
114
+ i += minuteStep
115
+ out
116
+
117
+ currentHour := $derived.by =>
118
+ h := parts.h
119
+ if hourCycle === 12
120
+ h === 0 ? 12 : (h > 12 ? h - 12 : h)
121
+ else
122
+ h
123
+
124
+ currentMinute := $derived parts.m
125
+ period := $derived parts.h >= 12 ? 'PM' : 'AM'
126
+
127
+ emit := (h24, m) =>
128
+ value = `${pad(h24)}:${pad(m)}`
129
+
130
+ setHour := (displayed) =>
131
+ nextH .= displayed
132
+ if hourCycle === 12
133
+ if period === 'PM'
134
+ nextH = displayed === 12 ? 12 : displayed + 12
135
+ else
136
+ nextH = displayed === 12 ? 0 : displayed
137
+ emit nextH, parts.m
138
+
139
+ setMinute := (m) => emit parts.h, m
140
+
141
+ setPeriod := (p) =>
142
+ nextH .= parts.h
143
+ if p === 'PM' && parts.h < 12
144
+ nextH = parts.h + 12
145
+ else if p === 'AM' && parts.h >= 12
146
+ nextH = parts.h - 12
147
+ emit nextH, parts.m
148
+
149
+ clearValue := => value = ''
150
+
151
+ hourCol .= $state null
152
+ minuteCol .= $state null
153
+
154
+ // When the popover opens, scroll each column to its currently-selected cell.
155
+ $effect =>
156
+ return unless open && (hourCol || minuteCol)
157
+ queueMicrotask =>
158
+ scrollSelected hourCol, currentHour, 'data-time-h'
159
+ scrollSelected minuteCol, currentMinute, 'data-time-m'
160
+
161
+ scrollSelected := (col, value, attr) =>
162
+ return unless col
163
+ target := col.querySelector `[${attr}="${value}"]`
164
+ return unless target
165
+ col.scrollTop = target.offsetTop - col.clientHeight / 2 + target.clientHeight / 2
166
+ </script>
@@ -0,0 +1,27 @@
1
+ ol(class!="{classes}")
2
+ +each('items as item, i')
3
+ li.timeline-item
4
+ .timeline-dot(class!="{item.variant ? `timeline-dot-${item.variant}` : ''}")
5
+ +if('item.icon')
6
+ Icon(name!="{item.icon}" size="size-3")
7
+ +if('i < items.length - 1')
8
+ .timeline-line
9
+ .timeline-body
10
+ +if('item.time')
11
+ .timeline-time {item.time}
12
+ +if('item.title')
13
+ .timeline-title {item.title}
14
+ +if('item.description')
15
+ .timeline-description {item.description}
16
+
17
+ <script>
18
+ import { cn } from '@human-synthesis/norns-ui/cn'
19
+ import Icon from './Icon.n'
20
+
21
+ {
22
+ items = []
23
+ class: extra = ''
24
+ } := $props()
25
+
26
+ classes := cn 'timeline', extra
27
+ </script>
@@ -0,0 +1,16 @@
1
+ div(role="region" aria-label="Notifications" class!="{regionClasses}")
2
+ +each('toastStore.items as item (item.id)')
3
+ div(class!="{`toast toast-${item.variant}`}" role="status" data-id!="{item.id}")
4
+ .toast-message
5
+ | {item.message}
6
+ button.toast-close(type="button" aria-label="Dismiss" onclick!="{() => dismiss(item.id)}")
7
+ | ✕
8
+
9
+ <script>
10
+ import { cn } from '@human-synthesis/norns-ui/cn'
11
+ import { toastStore, dismiss } from '@human-synthesis/norns-ui/toast'
12
+
13
+ { class: extra = '' } := $props()
14
+
15
+ regionClasses := cn 'toast-region', extra
16
+ </script>
@@ -0,0 +1,29 @@
1
+ ToggleRoot(bind:pressed class!="{classes}" disabled!="{disabled}" onPressedChange!="{onpressedchange}")
2
+ +if('icon')
3
+ Icon(name!="{icon}" size="size-4")
4
+ | {@render children?.()}
5
+
6
+ <script>
7
+ import { Toggle } from 'bits-ui'
8
+ import { cn } from '@human-synthesis/norns-ui/cn'
9
+ import Icon from './Icon.n'
10
+
11
+ { Root: ToggleRoot } := Toggle
12
+
13
+ {
14
+ pressed = $bindable(false)
15
+ disabled = false
16
+ icon
17
+ variant = 'default'
18
+ size = 'md'
19
+ onpressedchange
20
+ children
21
+ class: extra = ''
22
+ } .= $props()
23
+
24
+ classes := cn
25
+ 'toggle-btn'
26
+ variant !== 'default' && `toggle-btn-${variant}`
27
+ size !== 'md' && `toggle-btn-${size}`
28
+ extra
29
+ </script>
@@ -0,0 +1,34 @@
1
+ ToggleGroupRoot(
2
+ type!="{multiple ? 'multiple' : 'single'}"
3
+ bind:value
4
+ class!="{classes}"
5
+ disabled!="{disabled}"
6
+ )
7
+ +each('items as item')
8
+ ToggleGroupItem.toggle-btn(
9
+ value!="{item.value}"
10
+ disabled!="{item.disabled}"
11
+ aria-label!="{item.label}"
12
+ )
13
+ +if('item.icon')
14
+ Icon(name!="{item.icon}" size="size-4")
15
+ +if('item.label && !item.iconOnly')
16
+ span {item.label}
17
+
18
+ <script>
19
+ import { ToggleGroup } from 'bits-ui'
20
+ import { cn } from '@human-synthesis/norns-ui/cn'
21
+ import Icon from './Icon.n'
22
+
23
+ { Root: ToggleGroupRoot, Item: ToggleGroupItem } := ToggleGroup
24
+
25
+ {
26
+ items = []
27
+ value = $bindable(undefined)
28
+ multiple = false
29
+ disabled = false
30
+ class: extra = ''
31
+ } .= $props()
32
+
33
+ classes := cn 'btn-group toggle-group', extra
34
+ </script>
@@ -0,0 +1,15 @@
1
+ div(role="toolbar" aria-label!="{label}" aria-orientation!="{orientation}" class!="{classes}")
2
+ | {@render children?.()}
3
+
4
+ <script>
5
+ import { cn } from '@human-synthesis/norns-ui/cn'
6
+
7
+ {
8
+ orientation = 'horizontal'
9
+ label
10
+ children
11
+ class: extra = ''
12
+ } := $props()
13
+
14
+ classes := cn 'toolbar', `toolbar-${orientation}`, extra
15
+ </script>
@@ -0,0 +1,34 @@
1
+ TooltipProvider(delayDuration!="{delay}")
2
+ TooltipRoot
3
+ TooltipTrigger(class!="{triggerClass ?? undefined}")
4
+ | {@render trigger?.()}
5
+ TooltipPortal
6
+ TooltipContent(class!="{contentClasses}" side!="{side}" sideOffset!="{sideOffset}")
7
+ | {content ?? ''}
8
+ | {@render children?.()}
9
+
10
+ <script>
11
+ import { Tooltip } from 'bits-ui'
12
+ import { cn } from '@human-synthesis/norns-ui/cn'
13
+
14
+ {
15
+ Provider: TooltipProvider
16
+ Root: TooltipRoot
17
+ Trigger: TooltipTrigger
18
+ Portal: TooltipPortal
19
+ Content: TooltipContent
20
+ } := Tooltip
21
+
22
+ {
23
+ content
24
+ side = 'top'
25
+ sideOffset = 6
26
+ delay = 300
27
+ trigger
28
+ children
29
+ triggerClass
30
+ class: extra = ''
31
+ } .= $props()
32
+
33
+ contentClasses := cn 'tooltip-content', extra
34
+ </script>
@@ -0,0 +1,57 @@
1
+ ul.tree(class!="{classes}" role="tree")
2
+ +each('items as node')
3
+ | {@render renderNode(node, 0)}
4
+
5
+ +snippet('renderNode', node, depth)
6
+ li.tree-node(
7
+ role="treeitem"
8
+ aria-expanded!="{hasChildren(node) ? isExpanded(node.id) : undefined}"
9
+ aria-selected!="{node.id === selected}"
10
+ )
11
+ button.tree-row(
12
+ type="button"
13
+ class!="{node.id === selected ? 'tree-row-selected' : ''}"
14
+ style!="{`padding-left: ${0.5 + depth * 1.25}rem`}"
15
+ onclick!="{() => onRowClick(node)}"
16
+ )
17
+ +if('hasChildren(node)')
18
+ span.tree-chevron(class!="{isExpanded(node.id) ? 'tree-chevron-open' : ''}")
19
+ Icon(name="lucide:chevron-right" size="size-3.5")
20
+ +if('!hasChildren(node)')
21
+ span.tree-chevron-spacer
22
+ +if('node.icon')
23
+ Icon(name!="{node.icon}" size="size-4" class="text-fg-muted shrink-0")
24
+ span.tree-label
25
+ | {node.label}
26
+ +if('hasChildren(node) && isExpanded(node.id)')
27
+ ul(role="group")
28
+ +each('node.children as child')
29
+ | {@render renderNode(child, depth + 1)}
30
+
31
+ <script>
32
+ import { cn } from '@human-synthesis/norns-ui/cn'
33
+ import Icon from './Icon.n'
34
+
35
+ {
36
+ items = []
37
+ expanded = $bindable([])
38
+ selected = $bindable(undefined)
39
+ class: extra = ''
40
+ } .= $props()
41
+
42
+ classes := cn 'tree', extra
43
+
44
+ hasChildren := (node) => Array.isArray(node.children) && node.children.length > 0
45
+ isExpanded := (id) => expanded.includes(id)
46
+
47
+ toggle := (id) =>
48
+ if isExpanded(id)
49
+ expanded = expanded.filter (x) => x !== id
50
+ else
51
+ expanded = [...expanded, id]
52
+
53
+ onRowClick := (node) =>
54
+ if hasChildren(node)
55
+ toggle node.id
56
+ selected = node.id
57
+ </script>
@@ -0,0 +1,88 @@
1
+ .uploader(
2
+ class!="{classes}"
3
+ ondragover!="{onDragOver}"
4
+ ondragleave!="{onDragLeave}"
5
+ ondrop!="{onDrop}"
6
+ role="button"
7
+ tabindex="0"
8
+ onclick!="{() => inputEl?.click()}"
9
+ onkeydown!="{(e) => { if (e.key === 'Enter' || e.key === ' ') inputEl?.click() }}"
10
+ )
11
+ input(
12
+ type="file"
13
+ bind:this!="{inputEl}"
14
+ multiple!="{multiple}"
15
+ accept!="{accept}"
16
+ disabled!="{disabled}"
17
+ class="sr-only"
18
+ onchange!="{(e) => onPick(e.currentTarget.files)}"
19
+ )
20
+ Icon(name="lucide:upload-cloud" size="size-8" class="text-fg-muted")
21
+ .uploader-text
22
+ span.uploader-primary
23
+ | {placeholder}
24
+ +if('helper')
25
+ span.uploader-helper
26
+ | {helper}
27
+
28
+ +if('files.length > 0')
29
+ ul.uploader-list
30
+ +each('files as file, i')
31
+ li.uploader-item
32
+ Icon(name="lucide:file" size="size-4" class="text-fg-muted shrink-0")
33
+ .flex-1.min-w-0
34
+ .text-sm.truncate {file.name}
35
+ .text-xs(class="text-fg-muted") {humanSize(file.size)}
36
+ button.btn.btn-ghost.btn-sm(type="button" aria-label="Remove" onclick!="{() => remove(i)}")
37
+ Icon(name="lucide:x" size="size-3.5")
38
+
39
+ <script>
40
+ import { cn } from '@human-synthesis/norns-ui/cn'
41
+ import Icon from './Icon.n'
42
+
43
+ {
44
+ files = $bindable([])
45
+ multiple = true
46
+ accept
47
+ disabled = false
48
+ placeholder = 'Drop files here or click to browse'
49
+ helper
50
+ onchange
51
+ class: extra = ''
52
+ } .= $props()
53
+
54
+ inputEl .= $state null
55
+ dragging .= $state false
56
+
57
+ classes := cn 'uploader-root', dragging && 'uploader-dragging', extra
58
+
59
+ humanSize := (n) =>
60
+ return '' unless n
61
+ if n < 1024
62
+ `${n} B`
63
+ else if n < 1024 * 1024
64
+ `${(n / 1024).toFixed(1)} KB`
65
+ else
66
+ `${(n / 1024 / 1024).toFixed(1)} MB`
67
+
68
+ onPick := (list) =>
69
+ return unless list
70
+ next := Array.from list
71
+ files = multiple ? [...files, ...next] : next.slice(0, 1)
72
+ onchange?.(files)
73
+
74
+ remove := (i) =>
75
+ files = files.filter (_, idx) => idx !== i
76
+ onchange?.(files)
77
+
78
+ onDragOver := (e) =>
79
+ e.preventDefault()
80
+ dragging = true
81
+
82
+ onDragLeave := => dragging = false
83
+
84
+ onDrop := (e) =>
85
+ e.preventDefault()
86
+ dragging = false
87
+ onPick e.dataTransfer?.files
88
+ </script>
@@ -0,0 +1,26 @@
1
+ video(src!="{src}" controls!="{controls}" autoplay!="{autoplay}" loop!="{looping}" muted!="{muted}" playsinline!="{playsinline}" preload!="{preload}" poster!="{poster}" class!="{classes}")
2
+ +each('sources as source')
3
+ source(src!="{source.src}" type!="{source.type}")
4
+ | {fallback}
5
+
6
+ <script>
7
+ import { cn } from '@human-synthesis/norns-ui/cn'
8
+
9
+ // Note: HTML attribute `loop` becomes `looping` here because `loop` is a
10
+ // Civet reserved word.
11
+ {
12
+ src
13
+ sources = []
14
+ controls = true
15
+ autoplay = false
16
+ loop: looping = false
17
+ muted = false
18
+ playsinline = true
19
+ preload = 'metadata'
20
+ poster
21
+ fallback = 'Your browser does not support the video element.'
22
+ class: extra = ''
23
+ } := $props()
24
+
25
+ classes := cn 'video-player block w-full rounded-md', extra
26
+ </script>