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