@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,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,67 @@
1
+ span(class!="{triggerClass ?? ''}" oncontextmenu!="{onContext}")
2
+ | {@render trigger?.()}
3
+
4
+ +if('open')
5
+ div(
6
+ use:portal
7
+ use:clickOutside!="{[onClose, open]}"
8
+ use:escape!="{[onClose, open]}"
9
+ use:rovingTabindex!="{{ orientation: 'vertical', selector: '[data-roving]' }}"
10
+ bind:this!="{menuEl}"
11
+ class="dropdown-content"
12
+ data-state="open"
13
+ role="menu"
14
+ style="position: absolute"
15
+ )
16
+ +each('items as item')
17
+ +if('item.separator')
18
+ .dropdown-separator(role="separator")
19
+ +if('!item.separator')
20
+ button.dropdown-item(
21
+ type="button"
22
+ role="menuitem"
23
+ data-roving
24
+ data-disabled!="{item.disabled ? '' : undefined}"
25
+ disabled!="{item.disabled}"
26
+ onclick!="{() => onItem(item)}"
27
+ )
28
+ +if('item.icon')
29
+ Icon(name!="{item.icon}" size="size-3.5" class="text-fg-muted shrink-0")
30
+ span.flex-1 {item.label}
31
+
32
+ <script>
33
+ import { positionAt } from '@human-synthesis/norns-ui/behaviors/floating.svelte.js'
34
+ import { portal } from '@human-synthesis/norns-ui/behaviors/portal.svelte.js'
35
+ import { clickOutside } from '@human-synthesis/norns-ui/behaviors/clickOutside.svelte.js'
36
+ import { escape } from '@human-synthesis/norns-ui/behaviors/escape.svelte.js'
37
+ import { rovingTabindex } from '@human-synthesis/norns-ui/behaviors/rovingTabindex.svelte.js'
38
+ import Icon from './Icon.n'
39
+
40
+ {
41
+ items = []
42
+ trigger
43
+ triggerClass
44
+ } := $props()
45
+
46
+ open .= $state false
47
+ menuEl .= $state null
48
+ clickX .= 0
49
+ clickY .= 0
50
+
51
+ onContext := (e) =>
52
+ e.preventDefault()
53
+ clickX = e.clientX
54
+ clickY = e.clientY
55
+ open = true
56
+
57
+ $effect =>
58
+ return unless open && menuEl
59
+ positionAt menuEl, clickX, clickY, { placement: 'right-start' }
60
+
61
+ onClose := => open = false
62
+
63
+ onItem := (item) =>
64
+ return if item.disabled
65
+ item.onSelect?.()
66
+ open = false
67
+ </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>
@@ -0,0 +1,61 @@
1
+ .data-table-wrap(class!="{wrapClasses}")
2
+ table.data-table
3
+ thead
4
+ tr
5
+ +each('columns as col')
6
+ th(
7
+ class!="{col.class ?? ''}"
8
+ style!="{col.width ? `width: ${col.width}` : undefined}"
9
+ )
10
+ +if('col.sortable')
11
+ button.data-table-sort(type="button" onclick!="{() => onSort(col.key)}")
12
+ span {col.label ?? col.key}
13
+ Icon(name!="{sortIcon(col.key)}" size="size-3.5" class="text-fg-muted")
14
+ +if('!col.sortable')
15
+ | {col.label ?? col.key}
16
+ tbody
17
+ +if('rows.length === 0')
18
+ tr.data-table-empty
19
+ td(colspan!="{columns.length}")
20
+ | {emptyMessage}
21
+ +each('rows as row, i')
22
+ tr(onclick!="{onrowclick ? () => onrowclick(row, i) : undefined}" class!="{onrowclick ? 'data-table-row-clickable' : ''}")
23
+ +each('columns as col')
24
+ td(class!="{col.cellClass ?? ''}")
25
+ | {row[col.key]}
26
+
27
+ <script>
28
+ import { cn } from '@human-synthesis/norns-ui/cn'
29
+ import Icon from './Icon.n'
30
+
31
+ {
32
+ columns = []
33
+ rows = []
34
+ striped = false
35
+ dense = false
36
+ stickyHeader = false
37
+ emptyMessage = 'No data'
38
+ sortKey = $bindable(undefined)
39
+ sortDir = $bindable('asc')
40
+ onrowclick
41
+ class: extra = ''
42
+ } .= $props()
43
+
44
+ wrapClasses := cn
45
+ 'data-table-root'
46
+ striped && 'data-table-striped'
47
+ dense && 'data-table-dense'
48
+ stickyHeader && 'data-table-sticky'
49
+ extra
50
+
51
+ onSort := (key) =>
52
+ if sortKey === key
53
+ sortDir = sortDir === 'asc' ? 'desc' : 'asc'
54
+ else
55
+ sortKey = key
56
+ sortDir = 'asc'
57
+
58
+ sortIcon := (key) =>
59
+ return 'lucide:chevrons-up-down' unless sortKey === key
60
+ sortDir === 'asc' ? 'lucide:chevron-up' : 'lucide:chevron-down'
61
+ </script>
@@ -0,0 +1,56 @@
1
+ Popover(bind:open class="p-3 w-[20rem]")
2
+ +snippet('trigger')
3
+ button.input.date-picker-trigger(
4
+ type="button"
5
+ disabled!="{disabled}"
6
+ id!="{resolvedId}"
7
+ aria-label!="{label ?? 'Pick a date'}"
8
+ class!="{hasError ? 'input-err' : ''}"
9
+ )
10
+ Icon(name="lucide:calendar" size="size-4" class="text-fg-muted shrink-0")
11
+ span.flex-1.text-left.truncate
12
+ | {value ? formatted : placeholder}
13
+ Icon(name="lucide:chevron-down" size="size-3.5" class="text-fg-muted shrink-0")
14
+ Calendar(
15
+ bind:value
16
+ min!="{min}"
17
+ max!="{max}"
18
+ onchange!="{handleChange}"
19
+ )
20
+
21
+ <script>
22
+ import { getContext } from 'svelte'
23
+ import Popover from './Popover.n'
24
+ import Icon from './Icon.n'
25
+ import Calendar from './Calendar.n'
26
+
27
+ {
28
+ value = $bindable('')
29
+ open = $bindable(false)
30
+ min = ''
31
+ max = ''
32
+ placeholder = 'Pick a date'
33
+ disabled = false
34
+ closeOnSelect = true
35
+ label
36
+ name
37
+ id: providedId
38
+ error: explicitError = false
39
+ } .= $props()
40
+
41
+ field := getContext 'norns-ui:field'
42
+ resolvedId := providedId ?? field?.id
43
+ hasError := explicitError || field?.hasError || false
44
+
45
+ formatter := new Intl.DateTimeFormat undefined, { dateStyle: 'medium' }
46
+
47
+ formatted := $derived.by =>
48
+ return '' unless value
49
+ try
50
+ formatter.format new Date(`${value}T00:00:00`)
51
+ catch _
52
+ value
53
+
54
+ handleChange := =>
55
+ open = false if closeOnSelect
56
+ </script>
@@ -0,0 +1,47 @@
1
+ Popover(bind:open class="p-3 w-[20rem]")
2
+ +snippet('trigger')
3
+ button.input.date-picker-trigger(type="button" disabled!="{disabled}" aria-label="Pick a date range")
4
+ Icon(name="lucide:calendar-range" size="size-4" class="text-fg-muted shrink-0")
5
+ span.flex-1.text-left.truncate
6
+ | {label}
7
+ Icon(name="lucide:chevron-down" size="size-3.5" class="text-fg-muted shrink-0")
8
+ Calendar(
9
+ bind:rangeValue!="{value}"
10
+ range!="{true}"
11
+ min!="{min}"
12
+ max!="{max}"
13
+ )
14
+ .flex.justify-end.gap-2.mt-3
15
+ button.btn.btn-ghost.btn-sm(type="button" onclick!="{clear}") Clear
16
+ button.btn.btn-primary.btn-sm(type="button" onclick!="{() => open = false}") Done
17
+
18
+ <script>
19
+ import Popover from './Popover.n'
20
+ import Icon from './Icon.n'
21
+ import Calendar from './Calendar.n'
22
+
23
+ {
24
+ value = $bindable({ start: '', end: '' })
25
+ open = $bindable(false)
26
+ min = ''
27
+ max = ''
28
+ placeholder = 'Pick a range'
29
+ disabled = false
30
+ } .= $props()
31
+
32
+ formatter := new Intl.DateTimeFormat undefined, { dateStyle: 'medium' }
33
+
34
+ fmt := (s) =>
35
+ return '' unless s
36
+ try
37
+ formatter.format new Date(`${s}T00:00:00`)
38
+ catch _
39
+ s
40
+
41
+ label := $derived.by =>
42
+ return placeholder unless value?.start
43
+ return `${fmt(value.start)} – …` unless value?.end
44
+ `${fmt(value.start)} – ${fmt(value.end)}`
45
+
46
+ clear := => value = { start: '', end: '' }
47
+ </script>
@@ -1,15 +1,28 @@
1
- DialogRoot(bind:open)
2
- +if('trigger')
3
- DialogTrigger(class!="{triggerClass ?? undefined}")
4
- | {@render trigger()}
5
- DialogPortal
6
- DialogOverlay(class!="{overlayClasses}")
7
- DialogContent(class!="{contentClasses}")
1
+ +if('trigger')
2
+ span(class!="{triggerClass ?? ''}" onclick!="{() => open = true}")
3
+ | {@render trigger()}
4
+
5
+ +if('open')
6
+ div(use:portal use:scrollLock)
7
+ div(
8
+ data-state="open"
9
+ class!="{overlayClasses}"
10
+ onclick!="{onOverlayClick}"
11
+ )
12
+ div(
13
+ role="dialog"
14
+ aria-modal="true"
15
+ aria-label!="{title ?? ariaLabel}"
16
+ class!="{contentClasses}"
17
+ data-state="open"
18
+ use:focusTrap
19
+ use:escape!="{[onClose, open]}"
20
+ )
8
21
  +if('title')
9
- DialogTitle.dialog-title
22
+ h2.dialog-title
10
23
  | {title}
11
24
  +if('description')
12
- DialogDescription.dialog-description
25
+ p.dialog-description
13
26
  | {description}
14
27
  +if('children')
15
28
  .dialog-body
@@ -18,29 +31,23 @@ DialogRoot(bind:open)
18
31
  .dialog-actions
19
32
  | {@render actions()}
20
33
  +if('!hideClose')
21
- DialogClose.dialog-close(aria-label="Close")
34
+ button.dialog-close(type="button" aria-label="Close" onclick!="{onClose}")
22
35
  | ✕
23
36
 
24
37
  <script>
25
- import { Dialog } from 'bits-ui'
26
38
  import { cn } from '@human-synthesis/norns-ui/cn'
27
-
28
- {
29
- Root: DialogRoot
30
- Trigger: DialogTrigger
31
- Portal: DialogPortal
32
- Overlay: DialogOverlay
33
- Content: DialogContent
34
- Title: DialogTitle
35
- Description: DialogDescription
36
- Close: DialogClose
37
- } := Dialog
39
+ import { portal } from '@human-synthesis/norns-ui/behaviors/portal.svelte.js'
40
+ import { scrollLock } from '@human-synthesis/norns-ui/behaviors/scrollLock.svelte.js'
41
+ import { focusTrap } from '@human-synthesis/norns-ui/behaviors/focusTrap.svelte.js'
42
+ import { escape } from '@human-synthesis/norns-ui/behaviors/escape.svelte.js'
38
43
 
39
44
  {
40
45
  open = $bindable(false)
41
46
  title
42
47
  description
43
48
  hideClose = false
49
+ dismissable = true
50
+ ariaLabel
44
51
  trigger
45
52
  actions
46
53
  children
@@ -51,4 +58,10 @@ DialogRoot(bind:open)
51
58
 
52
59
  overlayClasses := cn 'dialog-overlay', overlayClass
53
60
  contentClasses := cn 'dialog-content', extra
61
+
62
+ onClose := => open = false
63
+
64
+ onOverlayClick := (e) =>
65
+ return unless dismissable
66
+ open = false if e.target === e.currentTarget
54
67
  </script>
@@ -1,29 +1,44 @@
1
- MenuRoot(bind:open)
2
- +if('trigger')
3
- MenuTrigger(class!="{triggerClass ?? undefined}")
4
- | {@render trigger()}
5
- MenuPortal
6
- MenuContent(class!="{contentClasses}" side!="{side}" align!="{align}" sideOffset!="{sideOffset}")
7
- +each('items as item, i')
8
- +if('item.separator')
9
- MenuSeparator.dropdown-separator
10
- +if('!item.separator')
11
- MenuItem.dropdown-item(disabled!="{item.disabled}" onSelect!="{item.onSelect}")
12
- | {item.label}
13
- | {@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
+ use:rovingTabindex!="{{ orientation: 'vertical', selector: '[data-roving]' }}"
12
+ class!="{contentClasses}"
13
+ data-state="open"
14
+ role="menu"
15
+ )
16
+ +each('items as item, i')
17
+ +if('item.separator')
18
+ .dropdown-separator(role="separator")
19
+ +if('!item.separator')
20
+ button.dropdown-item(
21
+ type="button"
22
+ role="menuitem"
23
+ data-roving
24
+ data-disabled!="{item.disabled ? '' : undefined}"
25
+ disabled!="{item.disabled}"
26
+ onclick!="{() => onItem(item)}"
27
+ )
28
+ +if('item.icon')
29
+ Icon(name!="{item.icon}" size="size-3.5" class="text-fg-muted shrink-0")
30
+ span.flex-1 {item.label}
31
+ +if('children')
32
+ | {@render children()}
14
33
 
15
34
  <script>
16
- import { DropdownMenu } from 'bits-ui'
17
35
  import { cn } from '@human-synthesis/norns-ui/cn'
18
-
19
- {
20
- Root: MenuRoot
21
- Trigger: MenuTrigger
22
- Portal: MenuPortal
23
- Content: MenuContent
24
- Item: MenuItem
25
- Separator: MenuSeparator
26
- } := DropdownMenu
36
+ import { portal } from '@human-synthesis/norns-ui/behaviors/portal.svelte.js'
37
+ import { clickOutside } from '@human-synthesis/norns-ui/behaviors/clickOutside.svelte.js'
38
+ import { escape } from '@human-synthesis/norns-ui/behaviors/escape.svelte.js'
39
+ import { rovingTabindex } from '@human-synthesis/norns-ui/behaviors/rovingTabindex.svelte.js'
40
+ import { useFloating } from '@human-synthesis/norns-ui/behaviors/floating.svelte.js'
41
+ import Icon from './Icon.n'
27
42
 
28
43
  {
29
44
  open = $bindable(false)
@@ -37,5 +52,21 @@ MenuRoot(bind:open)
37
52
  class: extra = ''
38
53
  } .= $props()
39
54
 
55
+ placement := align === 'center' ? side : `${side}-${align}`
56
+
57
+ { reference: refAction, floating: floatAction } := useFloating
58
+ placement: placement
59
+ offset: sideOffset
60
+
40
61
  contentClasses := cn 'dropdown-content', extra
62
+
63
+ triggerEl .= $state null
64
+
65
+ toggle := => open = !open
66
+ onClose := => open = false
67
+
68
+ onItem := (item) =>
69
+ return if item.disabled
70
+ item.onSelect?.()
71
+ open = false
41
72
  </script>
@@ -0,0 +1,21 @@
1
+ span(class!="{classes}" style!="{styleStr}")
2
+ | {@render children?.()}
3
+
4
+ <script>
5
+ import { cn } from '@human-synthesis/norns-ui/cn'
6
+
7
+ {
8
+ from = 'var(--color-primary-500)'
9
+ to = 'var(--color-info-500)'
10
+ via
11
+ animate = true
12
+ angle = 90
13
+ children
14
+ class: extra = ''
15
+ } := $props()
16
+
17
+ classes := cn 'gradient-text', animate && 'gradient-text-anim', extra
18
+
19
+ stops := $derived via ? `${from}, ${via}, ${to}` : `${from}, ${to}`
20
+ styleStr := $derived `background-image: linear-gradient(${angle}deg, ${stops})`
21
+ </script>
@@ -0,0 +1,23 @@
1
+ header(class!="{classes}")
2
+ .norns-header-brand
3
+ | {@render brand?.()}
4
+ +if('nav')
5
+ nav.norns-header-nav
6
+ | {@render nav()}
7
+ .norns-header-actions
8
+ | {@render actions?.()}
9
+
10
+ <script>
11
+ import { cn } from '@human-synthesis/norns-ui/cn'
12
+
13
+ {
14
+ sticky = false
15
+ brand
16
+ nav
17
+ actions
18
+ class: extra = ''
19
+ ...rest
20
+ } := $props()
21
+
22
+ classes := cn 'norns-header', sticky && 'norns-header-sticky', extra
23
+ </script>
@@ -0,0 +1,37 @@
1
+ section(class!="{classes}")
2
+ +if('image')
3
+ img.hero-image(src!="{image}" alt="" loading="lazy")
4
+ .relative
5
+ h1.hero-title
6
+ +if('gradient')
7
+ GradientText
8
+ | {title}
9
+ +if('!gradient')
10
+ | {title}
11
+ +if('description')
12
+ p.hero-description
13
+ | {description}
14
+ +if('children')
15
+ .mt-4
16
+ | {@render children()}
17
+ +if('actions')
18
+ .hero-actions
19
+ | {@render actions()}
20
+
21
+ <script>
22
+ import { cn } from '@human-synthesis/norns-ui/cn'
23
+ import GradientText from './GradientText.n'
24
+
25
+ {
26
+ title = ''
27
+ description
28
+ image
29
+ align = 'left'
30
+ gradient = false
31
+ actions
32
+ children
33
+ class: extra = ''
34
+ } := $props()
35
+
36
+ classes := cn 'hero', align === 'center' && 'hero-center', extra
37
+ </script>
@@ -0,0 +1,38 @@
1
+ nav.nav-menu(class!="{classes}" aria-label!="{label}")
2
+ ul.nav-menu-list
3
+ +each('items as item')
4
+ li.nav-menu-item
5
+ +if('item.children')
6
+ button.nav-menu-trigger(type="button" aria-haspopup="true" aria-expanded="false")
7
+ span {item.label}
8
+ Icon(name="lucide:chevron-down" size="size-3.5" class="ml-1 nav-menu-chevron")
9
+ .nav-menu-content
10
+ ul.flex.flex-col.gap-0.5.p-1(class="min-w-[12rem]")
11
+ +each('item.children as child')
12
+ li
13
+ a.nav-menu-link(href!="{child.href ?? '#'}" onclick!="{(e) => onClickItem(e, child)}")
14
+ +if('child.icon')
15
+ Icon(name!="{child.icon}" size="size-4" class="text-fg-muted shrink-0")
16
+ span.flex-1 {child.label}
17
+ +if('!item.children')
18
+ a.nav-menu-trigger(href!="{item.href ?? '#'}" onclick!="{(e) => onClickItem(e, item)}")
19
+ span {item.label}
20
+
21
+ <script>
22
+ import { cn } from '@human-synthesis/norns-ui/cn'
23
+ import Icon from './Icon.n'
24
+
25
+ {
26
+ items = []
27
+ label = 'Main navigation'
28
+ onSelect
29
+ class: extra = ''
30
+ } := $props()
31
+
32
+ classes := cn '', extra
33
+
34
+ onClickItem := (e, item) =>
35
+ if onSelect
36
+ e.preventDefault()
37
+ onSelect item
38
+ </script>
@@ -0,0 +1,16 @@
1
+ IconifyIcon(icon!="{name}" class!="{classes}" flip!="{flip}" rotate!="{rotate}")
2
+
3
+ <script>
4
+ import IconifyIcon from '@iconify/svelte'
5
+ import { cn } from '@human-synthesis/norns-ui/cn'
6
+
7
+ {
8
+ name
9
+ size = 'size-4'
10
+ flip
11
+ rotate
12
+ class: extra = ''
13
+ } := $props()
14
+
15
+ classes := cn 'inline-block shrink-0 align-[-0.125em]', size, extra
16
+ </script>