@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
@@ -1,15 +1,29 @@
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}" data-side!="{side}")
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
+ data-side!="{side}"
19
+ use:focusTrap
20
+ use:escape!="{[onClose, open]}"
21
+ )
8
22
  +if('title')
9
- DialogTitle.dialog-title
23
+ h2.dialog-title
10
24
  | {title}
11
25
  +if('description')
12
- DialogDescription.dialog-description
26
+ p.dialog-description
13
27
  | {description}
14
28
  +if('children')
15
29
  .sheet-body
@@ -18,23 +32,15 @@ DialogRoot(bind:open)
18
32
  .dialog-actions
19
33
  | {@render actions()}
20
34
  +if('!hideClose')
21
- DialogClose.dialog-close(aria-label="Close")
35
+ button.dialog-close(type="button" aria-label="Close" onclick!="{onClose}")
22
36
  | ✕
23
37
 
24
38
  <script>
25
- import { Dialog } from 'bits-ui'
26
39
  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
40
+ import { portal } from '@human-synthesis/norns-ui/behaviors/portal.svelte.js'
41
+ import { scrollLock } from '@human-synthesis/norns-ui/behaviors/scrollLock.svelte.js'
42
+ import { focusTrap } from '@human-synthesis/norns-ui/behaviors/focusTrap.svelte.js'
43
+ import { escape } from '@human-synthesis/norns-ui/behaviors/escape.svelte.js'
38
44
 
39
45
  {
40
46
  open = $bindable(false)
@@ -42,6 +48,8 @@ DialogRoot(bind:open)
42
48
  title
43
49
  description
44
50
  hideClose = false
51
+ dismissable = true
52
+ ariaLabel
45
53
  trigger
46
54
  actions
47
55
  children
@@ -52,4 +60,10 @@ DialogRoot(bind:open)
52
60
 
53
61
  overlayClasses := cn 'dialog-overlay', overlayClass
54
62
  contentClasses := cn 'sheet-content', `sheet-${side}`, extra
63
+
64
+ onClose := => open = false
65
+
66
+ onOverlayClick := (e) =>
67
+ return unless dismissable
68
+ open = false if e.target === e.currentTarget
55
69
  </script>
@@ -0,0 +1,36 @@
1
+ button(
2
+ type!="{type}"
3
+ class!="{classes}"
4
+ disabled!="{disabled}"
5
+ onclick!="{onclick}"
6
+ )
7
+ span.shiny-btn-shine(aria-hidden="true")
8
+ +if('icon')
9
+ Icon(name!="{icon}" size!="{iconSize}")
10
+ +if('children')
11
+ | {@render children()}
12
+
13
+ <script>
14
+ import { cn } from '@human-synthesis/norns-ui/cn'
15
+ import Icon from './Icon.n'
16
+
17
+ {
18
+ variant = 'primary'
19
+ size = 'md'
20
+ type = 'button'
21
+ disabled = false
22
+ onclick
23
+ icon
24
+ children
25
+ class: extra = ''
26
+ } := $props()
27
+
28
+ iconSize := size === 'sm' ? 'size-3.5' : size === 'lg' ? 'size-5' : 'size-4'
29
+
30
+ classes := cn
31
+ 'btn'
32
+ `btn-${variant}`
33
+ size !== 'md' && `btn-${size}`
34
+ 'shiny-btn'
35
+ extra
36
+ </script>
@@ -0,0 +1,27 @@
1
+ span(class!="{classes}" style!="{style}" aria-hidden="true")
2
+
3
+ <script>
4
+ import { cn } from '@human-synthesis/norns-ui/cn'
5
+
6
+ {
7
+ variant = 'rect'
8
+ width
9
+ height
10
+ class: extra = ''
11
+ ...rest
12
+ } := $props()
13
+
14
+ classes := cn 'skeleton', variant === 'circle' && 'skeleton-circle', extra
15
+
16
+ style := $derived.by =>
17
+ parts := []
18
+ if width
19
+ parts.push `width: ${typeof width === 'number' ? `${width}px` : width}`
20
+ if height
21
+ parts.push `height: ${typeof height === 'number' ? `${height}px` : height}`
22
+ else if variant === 'text'
23
+ parts.push 'height: 0.875rem'
24
+ else if variant === 'circle' && !height
25
+ parts.push 'height: 2.5rem; width: 2.5rem'
26
+ parts.join '; '
27
+ </script>
@@ -0,0 +1,35 @@
1
+ ol(class!="{classes}")
2
+ +each('steps as step, i')
3
+ li.stepper-step(class!="{stepClass(i)}")
4
+ .stepper-marker
5
+ +if('isComplete(i)')
6
+ Icon(name="lucide:check" size="size-3.5")
7
+ +if('!isComplete(i)')
8
+ | {i + 1}
9
+ .stepper-label
10
+ | {step.label}
11
+ +if('i < steps.length - 1')
12
+ .stepper-divider
13
+
14
+ <script>
15
+ import { cn } from '@human-synthesis/norns-ui/cn'
16
+ import Icon from './Icon.n'
17
+
18
+ {
19
+ steps = []
20
+ current = 0
21
+ orientation = 'horizontal'
22
+ class: extra = ''
23
+ ...rest
24
+ } := $props()
25
+
26
+ classes := cn 'stepper', orientation === 'vertical' && 'stepper-vertical', extra
27
+
28
+ isComplete := (i) => steps[i]?.complete ?? i < current
29
+ isCurrent := (i) => steps[i]?.current ?? i === current
30
+
31
+ stepClass := (i) =>
32
+ cn
33
+ isCurrent(i) && 'stepper-step-current'
34
+ isComplete(i) && 'stepper-step-complete'
35
+ </script>
@@ -0,0 +1,10 @@
1
+ div(class!="{classes}")
2
+ | {@render children?.()}
3
+
4
+ <script>
5
+ import { cn } from '@human-synthesis/norns-ui/cn'
6
+
7
+ { tone, children, class: extra = '' } := $props()
8
+
9
+ classes := cn 'surface p-4', tone === 'glass' && 'surface-glass', extra
10
+ </script>
@@ -1,28 +1,54 @@
1
- TabsRoot(bind:value class!="{rootClasses}")
2
- TabsList.tabs-list
1
+ .tabs-root(class!="{rootClasses}")
2
+ .tabs-list(
3
+ role="tablist"
4
+ aria-label!="{label}"
5
+ use:rovingTabindex!="{{ orientation: 'horizontal' }}"
6
+ )
3
7
  +each('items as item (item.value)')
4
- TabsTrigger.tabs-trigger(value!="{item.value}" disabled!="{item.disabled}")
8
+ button.tabs-trigger(
9
+ type="button"
10
+ role="tab"
11
+ id!="{tabId(item.value)}"
12
+ aria-selected!="{value === item.value}"
13
+ aria-controls!="{panelId(item.value)}"
14
+ data-state!="{value === item.value ? 'active' : 'inactive'}"
15
+ data-roving
16
+ disabled!="{item.disabled}"
17
+ onclick!="{() => select(item.value)}"
18
+ )
5
19
  | {item.label}
6
20
  +each('items as item (item.value)')
7
- TabsContent.tabs-content(value!="{item.value}")
8
- | {@render item.panel?.()}
21
+ .tabs-content(
22
+ role="tabpanel"
23
+ id!="{panelId(item.value)}"
24
+ aria-labelledby!="{tabId(item.value)}"
25
+ tabindex="0"
26
+ hidden!="{value !== item.value ? 'until-found' : undefined}"
27
+ )
28
+ +if('value === item.value')
29
+ | {@render item.panel?.()}
9
30
 
10
31
  <script>
11
- import { Tabs } from 'bits-ui'
12
32
  import { cn } from '@human-synthesis/norns-ui/cn'
13
-
14
- {
15
- Root: TabsRoot
16
- List: TabsList
17
- Trigger: TabsTrigger
18
- Content: TabsContent
19
- } := Tabs
33
+ import { rovingTabindex } from '@human-synthesis/norns-ui/behaviors/rovingTabindex.svelte.js'
20
34
 
21
35
  {
22
36
  value = $bindable()
23
37
  items = []
38
+ label = 'Tabs'
24
39
  class: extra = ''
25
40
  } .= $props()
26
41
 
27
42
  rootClasses := cn 'tabs-root', extra
43
+
44
+ // Lazy default to first tab if no value bound.
45
+ $effect =>
46
+ if value === undefined && items.length > 0 && !items[0].disabled
47
+ value = items[0].value
48
+
49
+ uid := Math.random().toString(36).slice(2, 8)
50
+ tabId := (v) => `tab-${uid}-${v}`
51
+ panelId := (v) => `tabpanel-${uid}-${v}`
52
+
53
+ select := (v) => value = v
28
54
  </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,153 @@
1
+ Popover(bind:open class="p-2 time-picker-popover")
2
+ +snippet('trigger')
3
+ button.input.date-picker-trigger(
4
+ type="button"
5
+ disabled!="{disabled}"
6
+ id!="{resolvedId}"
7
+ aria-label!="{label ?? 'Pick a time'}"
8
+ class!="{hasError ? 'input-err' : ''}"
9
+ )
10
+ Icon(name="lucide:clock" size="size-4" class="text-fg-muted shrink-0")
11
+ span.flex-1.text-left.font-mono.tabular-nums
12
+ | {value || placeholder}
13
+ Icon(name="lucide:chevron-down" size="size-3.5" class="text-fg-muted shrink-0")
14
+ .time-picker-grid
15
+ .time-picker-col(role="listbox" aria-label="Hour" bind:this!="{hourCol}")
16
+ +each('hours as h')
17
+ button.time-picker-cell(
18
+ type="button"
19
+ class!="{h === currentHour ? 'time-picker-cell-selected' : ''}"
20
+ onclick!="{() => setHour(h)}"
21
+ data-time-h!="{h}"
22
+ )
23
+ | {pad(h)}
24
+ .time-picker-sep :
25
+ .time-picker-col(role="listbox" aria-label="Minute" bind:this!="{minuteCol}")
26
+ +each('minutes as m')
27
+ button.time-picker-cell(
28
+ type="button"
29
+ class!="{m === currentMinute ? 'time-picker-cell-selected' : ''}"
30
+ onclick!="{() => setMinute(m)}"
31
+ data-time-m!="{m}"
32
+ )
33
+ | {pad(m)}
34
+ +if('hourCycle === 12')
35
+ .time-picker-col(role="listbox" aria-label="AM/PM")
36
+ button.time-picker-cell(
37
+ type="button"
38
+ class!="{period === 'AM' ? 'time-picker-cell-selected' : ''}"
39
+ onclick!="{() => setPeriod('AM')}"
40
+ )
41
+ | AM
42
+ button.time-picker-cell(
43
+ type="button"
44
+ class!="{period === 'PM' ? 'time-picker-cell-selected' : ''}"
45
+ onclick!="{() => setPeriod('PM')}"
46
+ )
47
+ | PM
48
+ .flex.justify-between.gap-2.mt-2.pt-2(class="border-t border-border")
49
+ button.btn.btn-ghost.btn-sm(type="button" onclick!="{clearValue}") Clear
50
+ button.btn.btn-primary.btn-sm(type="button" onclick!="{() => open = false}") Done
51
+
52
+ <script>
53
+ import { getContext } from 'svelte'
54
+ import Popover from './Popover.n'
55
+ import Icon from './Icon.n'
56
+
57
+ {
58
+ value = $bindable('')
59
+ open = $bindable(false)
60
+ minuteStep = 5
61
+ hourCycle = 24
62
+ placeholder = 'Pick a time'
63
+ disabled = false
64
+ label
65
+ name
66
+ id: providedId
67
+ error: explicitError = false
68
+ } .= $props()
69
+
70
+ field := getContext 'norns-ui:field'
71
+ resolvedId := providedId ?? field?.id
72
+ hasError := explicitError || field?.hasError || false
73
+
74
+ pad := (n) => String(n).padStart 2, '0'
75
+
76
+ parts := $derived.by =>
77
+ if !value
78
+ return { h: 0, m: 0 }
79
+ [hStr, mStr] := value.split ':'
80
+ { h: parseInt(hStr ?? '0', 10), m: parseInt(mStr ?? '0', 10) }
81
+
82
+ hours := $derived.by =>
83
+ out := []
84
+ if hourCycle === 12
85
+ out.push 12
86
+ i .= 1
87
+ while i <= 11
88
+ out.push i
89
+ i++
90
+ else
91
+ i .= 0
92
+ while i < 24
93
+ out.push i
94
+ i++
95
+ out
96
+
97
+ minutes := $derived.by =>
98
+ out := []
99
+ i .= 0
100
+ while i < 60
101
+ out.push i
102
+ i += minuteStep
103
+ out
104
+
105
+ currentHour := $derived.by =>
106
+ h := parts.h
107
+ if hourCycle === 12
108
+ h === 0 ? 12 : (h > 12 ? h - 12 : h)
109
+ else
110
+ h
111
+
112
+ currentMinute := $derived parts.m
113
+ period := $derived parts.h >= 12 ? 'PM' : 'AM'
114
+
115
+ emit := (h24, m) =>
116
+ value = `${pad(h24)}:${pad(m)}`
117
+
118
+ setHour := (displayed) =>
119
+ nextH .= displayed
120
+ if hourCycle === 12
121
+ if period === 'PM'
122
+ nextH = displayed === 12 ? 12 : displayed + 12
123
+ else
124
+ nextH = displayed === 12 ? 0 : displayed
125
+ emit nextH, parts.m
126
+
127
+ setMinute := (m) => emit parts.h, m
128
+
129
+ setPeriod := (p) =>
130
+ nextH .= parts.h
131
+ if p === 'PM' && parts.h < 12
132
+ nextH = parts.h + 12
133
+ else if p === 'AM' && parts.h >= 12
134
+ nextH = parts.h - 12
135
+ emit nextH, parts.m
136
+
137
+ clearValue := => value = ''
138
+
139
+ hourCol .= $state null
140
+ minuteCol .= $state null
141
+
142
+ $effect =>
143
+ return unless open && (hourCol || minuteCol)
144
+ queueMicrotask =>
145
+ scrollSelected hourCol, currentHour, 'data-time-h'
146
+ scrollSelected minuteCol, currentMinute, 'data-time-m'
147
+
148
+ scrollSelected := (col, val, attr) =>
149
+ return unless col
150
+ target := col.querySelector `[${attr}="${val}"]`
151
+ return unless target
152
+ col.scrollTop = target.offsetTop - col.clientHeight / 2 + target.clientHeight / 2
153
+ </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,35 @@
1
+ button(
2
+ type="button"
3
+ class!="{classes}"
4
+ disabled!="{disabled}"
5
+ data-pressed!="{pressed ? 'true' : undefined}"
6
+ aria-pressed!="{pressed}"
7
+ onclick!="{toggle}"
8
+ )
9
+ +if('icon')
10
+ Icon(name!="{icon}" size!="{iconSize}")
11
+ +if('children')
12
+ | {@render children()}
13
+
14
+ <script>
15
+ import { cn } from '@human-synthesis/norns-ui/cn'
16
+ import Icon from './Icon.n'
17
+
18
+ {
19
+ pressed = $bindable(false)
20
+ disabled = false
21
+ icon
22
+ variant = 'secondary'
23
+ size = 'md'
24
+ onpressedchange
25
+ children
26
+ class: extra = ''
27
+ } .= $props()
28
+
29
+ classes := cn 'btn', `btn-${variant}`, size !== 'md' && `btn-${size}`, extra
30
+ iconSize := size === 'sm' ? 'size-3.5' : size === 'lg' ? 'size-5' : 'size-4'
31
+
32
+ toggle := =>
33
+ pressed = !pressed
34
+ onpressedchange?.(pressed)
35
+ </script>
@@ -0,0 +1,54 @@
1
+ .btn-group(
2
+ role="group"
3
+ aria-label!="{label}"
4
+ class!="{classes}"
5
+ use:rovingTabindex!="{{ orientation: 'horizontal' }}"
6
+ )
7
+ +each('items as item')
8
+ button(
9
+ type="button"
10
+ class!="{itemClasses(item.value)}"
11
+ data-roving
12
+ data-pressed!="{isActive(item.value) ? 'true' : undefined}"
13
+ aria-pressed!="{isActive(item.value)}"
14
+ disabled!="{disabled || item.disabled}"
15
+ aria-label!="{item.label}"
16
+ onclick!="{() => toggle(item.value)}"
17
+ )
18
+ +if('item.icon')
19
+ Icon(name!="{item.icon}" size="size-4")
20
+ +if('item.label && !item.iconOnly')
21
+ span {item.label}
22
+
23
+ <script>
24
+ import { cn } from '@human-synthesis/norns-ui/cn'
25
+ import { rovingTabindex } from '@human-synthesis/norns-ui/behaviors/rovingTabindex.svelte.js'
26
+ import Icon from './Icon.n'
27
+
28
+ {
29
+ items = []
30
+ value = $bindable(undefined)
31
+ multiple = false
32
+ disabled = false
33
+ label = 'Toggle group'
34
+ class: extra = ''
35
+ } .= $props()
36
+
37
+ classes := cn extra
38
+
39
+ isActive := (v) =>
40
+ if multiple
41
+ Array.isArray(value) && value.includes v
42
+ else
43
+ value === v
44
+
45
+ itemClasses := (v) => cn 'btn btn-secondary'
46
+
47
+ toggle := (v) =>
48
+ return if disabled
49
+ if multiple
50
+ cur := Array.isArray(value) ? value : []
51
+ value = cur.includes(v) ? cur.filter((x) => x !== v) : [...cur, v]
52
+ else
53
+ value = value === v ? undefined : v
54
+ </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>