@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,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,32 @@
1
+ CollapsibleRoot(bind:open class!="{classes}")
2
+ CollapsibleTrigger.collapsible-trigger
3
+ .collapsible-chevron(class!="{open ? 'collapsible-chevron-open' : ''}")
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
+ CollapsibleContent.collapsible-content
10
+ | {@render children?.()}
11
+
12
+ <script>
13
+ import { Collapsible } from 'bits-ui'
14
+ import { cn } from '@human-synthesis/norns-ui/cn'
15
+ import Icon from './Icon.n'
16
+
17
+ {
18
+ Root: CollapsibleRoot
19
+ Trigger: CollapsibleTrigger
20
+ Content: CollapsibleContent
21
+ } := Collapsible
22
+
23
+ {
24
+ open = $bindable(false)
25
+ title
26
+ trigger
27
+ children
28
+ class: extra = ''
29
+ } .= $props()
30
+
31
+ classes := cn 'collapsible', extra
32
+ </script>
@@ -0,0 +1,62 @@
1
+ .color-picker(class!="{classes}")
2
+ label.color-picker-swatch(style!="{`background: ${value}`}")
3
+ input(
4
+ type="color"
5
+ value!="{value}"
6
+ oninput!="{(e) => value = e.currentTarget.value}"
7
+ disabled!="{disabled}"
8
+ class="sr-only"
9
+ aria-label="Pick color"
10
+ )
11
+ input.input.color-picker-text(
12
+ type="text"
13
+ value!="{value}"
14
+ oninput!="{onTextInput}"
15
+ disabled!="{disabled}"
16
+ spellcheck="false"
17
+ placeholder="#000000"
18
+ name!="{name}"
19
+ id!="{resolvedId}"
20
+ )
21
+ +if('!hideCopy')
22
+ button.btn.btn-ghost.btn-sm(type="button" aria-label="Copy" onclick!="{copy}")
23
+ Icon(name!="{copied ? 'lucide:check' : 'lucide:copy'}" size="size-3.5")
24
+
25
+ <script>
26
+ import { cn } from '@human-synthesis/norns-ui/cn'
27
+ import { getContext } from 'svelte'
28
+ import Icon from './Icon.n'
29
+
30
+ {
31
+ value = $bindable('#7c3aed')
32
+ disabled = false
33
+ hideCopy = false
34
+ name
35
+ id: providedId
36
+ class: extra = ''
37
+ } .= $props()
38
+
39
+ field := getContext 'norns-ui:field'
40
+ resolvedId := providedId ?? field?.id
41
+
42
+ classes := cn 'color-picker-wrap', extra
43
+
44
+ copied .= $state false
45
+
46
+ hexRe := /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
47
+
48
+ onTextInput := (e) =>
49
+ raw := e.currentTarget.value.trim()
50
+ next := raw.startsWith('#') ? raw : `#${raw}`
51
+ if hexRe.test(next.replace('#', ''))
52
+ value = next.toLowerCase()
53
+ else
54
+ value = next // let user keep typing; not all keystrokes are valid
55
+
56
+ copy := =>
57
+ try
58
+ await navigator.clipboard.writeText value
59
+ copied = true
60
+ setTimeout (=> copied = false), 1200
61
+ catch _
62
+ </script>
@@ -0,0 +1,48 @@
1
+ ContextMenuRoot
2
+ ContextMenuTrigger(class!="{triggerClass ?? ''}")
3
+ | {@render trigger?.()}
4
+ ContextMenuPortal
5
+ ContextMenuContent.dropdown-content
6
+ | {@render renderItems(items)}
7
+
8
+ +snippet('renderItems', list)
9
+ +each('list as item')
10
+ +if('item.separator')
11
+ ContextMenuSeparator.dropdown-separator
12
+ +if('!item.separator && !item.children')
13
+ ContextMenuItem.dropdown-item(disabled!="{item.disabled}" onSelect!="{item.onSelect}")
14
+ +if('item.icon')
15
+ Icon(name!="{item.icon}" size="size-3.5")
16
+ span {item.label}
17
+ +if('!item.separator && item.children')
18
+ ContextMenuSubRoot
19
+ ContextMenuSubTrigger.dropdown-item(disabled!="{item.disabled}")
20
+ +if('item.icon')
21
+ Icon(name!="{item.icon}" size="size-3.5")
22
+ span.flex-1 {item.label}
23
+ Icon(name="lucide:chevron-right" size="size-3.5")
24
+ ContextMenuSubContent.dropdown-content
25
+ | {@render renderItems(item.children)}
26
+
27
+ <script>
28
+ import { ContextMenu } from 'bits-ui'
29
+ import Icon from './Icon.n'
30
+
31
+ {
32
+ Root: ContextMenuRoot
33
+ Trigger: ContextMenuTrigger
34
+ Portal: ContextMenuPortal
35
+ Content: ContextMenuContent
36
+ Item: ContextMenuItem
37
+ Separator: ContextMenuSeparator
38
+ Sub: ContextMenuSubRoot
39
+ SubTrigger: ContextMenuSubTrigger
40
+ SubContent: ContextMenuSubContent
41
+ } := ContextMenu
42
+
43
+ {
44
+ items = []
45
+ trigger
46
+ triggerClass
47
+ } := $props()
48
+ </script>
@@ -0,0 +1,50 @@
1
+ button(
2
+ type="button"
3
+ class!="{classes}"
4
+ disabled!="{disabled}"
5
+ onclick!="{copy}"
6
+ aria-label!="{copied ? 'Copied' : (label ?? 'Copy to clipboard')}"
7
+ )
8
+ +if('copied')
9
+ Icon(name="lucide:check" size!="{iconSize}" class="copy-btn-check")
10
+ +if('!copied')
11
+ Icon(name!="{icon}" size!="{iconSize}")
12
+ +if('children')
13
+ | {@render children()}
14
+
15
+ <script>
16
+ import { cn } from '@human-synthesis/norns-ui/cn'
17
+ import Icon from './Icon.n'
18
+
19
+ {
20
+ value = ''
21
+ variant = 'ghost'
22
+ size = 'md'
23
+ icon = 'lucide:copy'
24
+ disabled = false
25
+ label
26
+ oncopy
27
+ children
28
+ class: extra = ''
29
+ } := $props()
30
+
31
+ copied .= $state false
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
+ 'copy-btn'
40
+ copied && 'copy-btn-copied'
41
+ extra
42
+
43
+ copy := =>
44
+ try
45
+ await navigator.clipboard.writeText String(value)
46
+ copied = true
47
+ oncopy?.(value)
48
+ setTimeout (=> copied = false), 1400
49
+ catch _
50
+ </script>