@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,30 @@
1
+ LinkPreviewRoot(openDelay!="{openDelay}")
2
+ LinkPreviewTrigger(href!="{href}" class!="{triggerClass ?? ''}")
3
+ | {@render trigger?.()}
4
+ LinkPreviewContent.popover-content(side!="{side}" align!="{align}" sideOffset!="{sideOffset}" class!="{contentClass}")
5
+ | {@render children?.()}
6
+
7
+ <script>
8
+ import { LinkPreview } from 'bits-ui'
9
+ import { cn } from '@human-synthesis/norns-ui/cn'
10
+
11
+ {
12
+ Root: LinkPreviewRoot
13
+ Trigger: LinkPreviewTrigger
14
+ Content: LinkPreviewContent
15
+ } := LinkPreview
16
+
17
+ {
18
+ href
19
+ side = 'top'
20
+ align = 'center'
21
+ sideOffset = 8
22
+ openDelay = 700
23
+ trigger
24
+ children
25
+ triggerClass
26
+ class: extra = ''
27
+ } := $props()
28
+
29
+ contentClass := cn 'w-72 p-3', extra
30
+ </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,33 @@
1
+ TooltipProvider(delayDuration!="{openDelay}")
2
+ TooltipRoot
3
+ TooltipTrigger(class!="{triggerClass ?? ''}")
4
+ | {@render trigger?.()}
5
+ TooltipPortal
6
+ TooltipContent.popover-content(side!="{side}" align!="{align}" sideOffset!="{sideOffset}" class!="{contentClass}")
7
+ | {@render children?.()}
8
+
9
+ <script>
10
+ import { Tooltip } from 'bits-ui'
11
+ import { cn } from '@human-synthesis/norns-ui/cn'
12
+
13
+ {
14
+ Provider: TooltipProvider
15
+ Root: TooltipRoot
16
+ Trigger: TooltipTrigger
17
+ Portal: TooltipPortal
18
+ Content: TooltipContent
19
+ } := Tooltip
20
+
21
+ {
22
+ side = 'top'
23
+ align = 'center'
24
+ sideOffset = 6
25
+ openDelay = 300
26
+ trigger
27
+ children
28
+ triggerClass
29
+ class: extra = ''
30
+ } := $props()
31
+
32
+ contentClass := cn 'max-w-xs p-3 text-sm', extra
33
+ </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,31 @@
1
+ ScrollAreaRoot.scroll-area(class!="{classes}" type!="{type}")
2
+ ScrollAreaViewport.scroll-area-viewport
3
+ | {@render children?.()}
4
+ ScrollAreaScrollbar.scroll-area-scrollbar(orientation="vertical")
5
+ ScrollAreaThumb.scroll-area-thumb
6
+ +if('horizontal')
7
+ ScrollAreaScrollbar.scroll-area-scrollbar.scroll-area-scrollbar-h(orientation="horizontal")
8
+ ScrollAreaThumb.scroll-area-thumb
9
+ ScrollAreaCorner
10
+
11
+ <script>
12
+ import { ScrollArea } from 'bits-ui'
13
+ import { cn } from '@human-synthesis/norns-ui/cn'
14
+
15
+ {
16
+ Root: ScrollAreaRoot
17
+ Viewport: ScrollAreaViewport
18
+ Scrollbar: ScrollAreaScrollbar
19
+ Thumb: ScrollAreaThumb
20
+ Corner: ScrollAreaCorner
21
+ } := ScrollArea
22
+
23
+ {
24
+ type = 'hover'
25
+ horizontal = false
26
+ children
27
+ class: extra = ''
28
+ } := $props()
29
+
30
+ classes := cn 'relative overflow-hidden', extra
31
+ </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>
@@ -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>
@@ -0,0 +1,66 @@
1
+ .tags-input(class!="{wrapClasses}" onclick!="{focusInput}")
2
+ +each('value as tag, i')
3
+ span.chip
4
+ | {tag}
5
+ button.chip-remove(type="button" aria-label!="{`Remove ${tag}`}" onclick!="{(e) => { e.stopPropagation(); remove(i) }}")
6
+ Icon(name="lucide:x" size="size-3")
7
+ input.tags-input-field(
8
+ bind:this!="{inputEl}"
9
+ bind:value!="{draft}"
10
+ type="text"
11
+ placeholder!="{placeholder}"
12
+ disabled!="{disabled}"
13
+ onkeydown!="{onKey}"
14
+ onblur!="{commitOnBlur ? commit : undefined}"
15
+ name!="{name}"
16
+ id!="{resolvedId}"
17
+ aria-invalid!="{hasError ? 'true' : undefined}"
18
+ )
19
+
20
+ <script>
21
+ import { cn } from '@human-synthesis/norns-ui/cn'
22
+ import { getContext } from 'svelte'
23
+ import Icon from './Icon.n'
24
+
25
+ {
26
+ value = $bindable([])
27
+ max
28
+ placeholder = 'Add a tag…'
29
+ commitOnBlur = true
30
+ separators = [',', 'Enter']
31
+ disabled = false
32
+ name
33
+ id: providedId
34
+ error: explicitError = false
35
+ class: extra = ''
36
+ } .= $props()
37
+
38
+ field := getContext 'norns-ui:field'
39
+ resolvedId := providedId ?? field?.id
40
+ hasError := explicitError || field?.hasError || false
41
+
42
+ draft .= $state ''
43
+ inputEl .= $state null
44
+
45
+ wrapClasses := cn 'tags-input-wrap', hasError && 'tags-input-err', extra
46
+
47
+ focusInput := => inputEl?.focus()
48
+
49
+ commit := =>
50
+ t := draft.trim()
51
+ return unless t
52
+ return if max && value.length >= max
53
+ return if value.includes(t)
54
+ value = [...value, t]
55
+ draft = ''
56
+
57
+ remove := (i) =>
58
+ value = value.filter (_, idx) => idx !== i
59
+
60
+ onKey := (e) =>
61
+ if separators.includes(e.key)
62
+ e.preventDefault()
63
+ commit()
64
+ else if e.key === 'Backspace' && !draft && value.length > 0
65
+ remove(value.length - 1)
66
+ </script>
@@ -0,0 +1,43 @@
1
+ button(
2
+ type="button"
3
+ class!="{classes}"
4
+ onclick!="{toggle}"
5
+ aria-label="Toggle theme"
6
+ title="Toggle theme"
7
+ )
8
+ Icon(name!="{lightIcon}" size!="{iconSize}" class="theme-toggle-light")
9
+ Icon(name!="{darkIcon}" size!="{iconSize}" class="theme-toggle-dark")
10
+ +if('showLabel')
11
+ span.theme-toggle-light
12
+ | {labelLight}
13
+ span.theme-toggle-dark
14
+ | {labelDark}
15
+
16
+ <script>
17
+ import { cn } from '@human-synthesis/norns-ui/cn'
18
+ import Icon from './Icon.n'
19
+
20
+ {
21
+ variant = 'ghost'
22
+ size = 'md'
23
+ showLabel = false
24
+ labelLight = 'Dark'
25
+ labelDark = 'Light'
26
+ lightIcon = 'lucide:moon'
27
+ darkIcon = 'lucide:sun'
28
+ storageKey = 'norns-theme'
29
+ class: extra = ''
30
+ } := $props()
31
+
32
+ iconSize := size === 'sm' ? 'size-3.5' : size === 'lg' ? 'size-5' : 'size-4'
33
+
34
+ classes := cn 'btn' , `btn-${variant}`, size !== 'md' && `btn-${size}`, extra
35
+
36
+ toggle := =>
37
+ root := document.documentElement
38
+ next := root.dataset.theme === 'dark' ? 'light' : 'dark'
39
+ root.dataset.theme = next === 'dark' ? 'dark' : ''
40
+ try
41
+ localStorage.setItem storageKey, next
42
+ catch _
43
+ </script>
@@ -0,0 +1,166 @@
1
+ PopoverRoot(bind:open)
2
+ PopoverTrigger.input.date-picker-trigger(
3
+ type="button"
4
+ disabled!="{disabled}"
5
+ id!="{resolvedId}"
6
+ aria-label!="{label ?? 'Pick a time'}"
7
+ class!="{hasError ? 'input-err' : ''}"
8
+ )
9
+ Icon(name="lucide:clock" size="size-4" class="text-fg-muted shrink-0")
10
+ span.flex-1.text-left.font-mono.tabular-nums
11
+ | {value || placeholder}
12
+ Icon(name="lucide:chevron-down" size="size-3.5" class="text-fg-muted shrink-0")
13
+ PopoverPortal
14
+ PopoverContent.popover-content(
15
+ class="p-2 time-picker-popover"
16
+ onOpenAutoFocus!="{(e) => e.preventDefault()}"
17
+ onCloseAutoFocus!="{(e) => e.preventDefault()}"
18
+ )
19
+ .time-picker-grid
20
+ .time-picker-col(role="listbox" aria-label="Hour" bind:this!="{hourCol}")
21
+ +each('hours as h')
22
+ button.time-picker-cell(
23
+ type="button"
24
+ class!="{h === currentHour ? 'time-picker-cell-selected' : ''}"
25
+ onclick!="{() => setHour(h)}"
26
+ data-time-h!="{h}"
27
+ )
28
+ | {pad(h)}
29
+ .time-picker-sep :
30
+ .time-picker-col(role="listbox" aria-label="Minute" bind:this!="{minuteCol}")
31
+ +each('minutes as m')
32
+ button.time-picker-cell(
33
+ type="button"
34
+ class!="{m === currentMinute ? 'time-picker-cell-selected' : ''}"
35
+ onclick!="{() => setMinute(m)}"
36
+ data-time-m!="{m}"
37
+ )
38
+ | {pad(m)}
39
+ +if('hourCycle === 12')
40
+ .time-picker-col(role="listbox" aria-label="AM/PM")
41
+ button.time-picker-cell(
42
+ type="button"
43
+ class!="{period === 'AM' ? 'time-picker-cell-selected' : ''}"
44
+ onclick!="{() => setPeriod('AM')}"
45
+ )
46
+ | AM
47
+ button.time-picker-cell(
48
+ type="button"
49
+ class!="{period === 'PM' ? 'time-picker-cell-selected' : ''}"
50
+ onclick!="{() => setPeriod('PM')}"
51
+ )
52
+ | PM
53
+ .flex.justify-between.gap-2.mt-2.pt-2(class="border-t border-border")
54
+ button.btn.btn-ghost.btn-sm(type="button" onclick!="{clearValue}") Clear
55
+ button.btn.btn-primary.btn-sm(type="button" onclick!="{() => open = false}") Done
56
+
57
+ <script>
58
+ import { Popover } from 'bits-ui'
59
+ import { getContext } from 'svelte'
60
+ import Icon from './Icon.n'
61
+
62
+ {
63
+ Root: PopoverRoot
64
+ Trigger: PopoverTrigger
65
+ Portal: PopoverPortal
66
+ Content: PopoverContent
67
+ } := Popover
68
+
69
+ {
70
+ value = $bindable('')
71
+ open = $bindable(false)
72
+ minuteStep = 5
73
+ hourCycle = 24
74
+ placeholder = 'Pick a time'
75
+ disabled = false
76
+ label
77
+ name
78
+ id: providedId
79
+ error: explicitError = false
80
+ } .= $props()
81
+
82
+ field := getContext 'norns-ui:field'
83
+ resolvedId := providedId ?? field?.id
84
+ hasError := explicitError || field?.hasError || false
85
+
86
+ pad := (n) => String(n).padStart 2, '0'
87
+
88
+ parts := $derived.by =>
89
+ if !value
90
+ return { h: 0, m: 0 }
91
+ [hStr, mStr] := value.split ':'
92
+ { h: parseInt(hStr ?? '0', 10), m: parseInt(mStr ?? '0', 10) }
93
+
94
+ hours := $derived.by =>
95
+ out := []
96
+ if hourCycle === 12
97
+ out.push 12
98
+ i .= 1
99
+ while i <= 11
100
+ out.push i
101
+ i++
102
+ else
103
+ i .= 0
104
+ while i < 24
105
+ out.push i
106
+ i++
107
+ out
108
+
109
+ minutes := $derived.by =>
110
+ out := []
111
+ i .= 0
112
+ while i < 60
113
+ out.push i
114
+ i += minuteStep
115
+ out
116
+
117
+ currentHour := $derived.by =>
118
+ h := parts.h
119
+ if hourCycle === 12
120
+ h === 0 ? 12 : (h > 12 ? h - 12 : h)
121
+ else
122
+ h
123
+
124
+ currentMinute := $derived parts.m
125
+ period := $derived parts.h >= 12 ? 'PM' : 'AM'
126
+
127
+ emit := (h24, m) =>
128
+ value = `${pad(h24)}:${pad(m)}`
129
+
130
+ setHour := (displayed) =>
131
+ nextH .= displayed
132
+ if hourCycle === 12
133
+ if period === 'PM'
134
+ nextH = displayed === 12 ? 12 : displayed + 12
135
+ else
136
+ nextH = displayed === 12 ? 0 : displayed
137
+ emit nextH, parts.m
138
+
139
+ setMinute := (m) => emit parts.h, m
140
+
141
+ setPeriod := (p) =>
142
+ nextH .= parts.h
143
+ if p === 'PM' && parts.h < 12
144
+ nextH = parts.h + 12
145
+ else if p === 'AM' && parts.h >= 12
146
+ nextH = parts.h - 12
147
+ emit nextH, parts.m
148
+
149
+ clearValue := => value = ''
150
+
151
+ hourCol .= $state null
152
+ minuteCol .= $state null
153
+
154
+ // When the popover opens, scroll each column to its currently-selected cell.
155
+ $effect =>
156
+ return unless open && (hourCol || minuteCol)
157
+ queueMicrotask =>
158
+ scrollSelected hourCol, currentHour, 'data-time-h'
159
+ scrollSelected minuteCol, currentMinute, 'data-time-m'
160
+
161
+ scrollSelected := (col, value, attr) =>
162
+ return unless col
163
+ target := col.querySelector `[${attr}="${value}"]`
164
+ return unless target
165
+ col.scrollTop = target.offsetTop - col.clientHeight / 2 + target.clientHeight / 2
166
+ </script>
@@ -0,0 +1,27 @@
1
+ ol(class!="{classes}")
2
+ +each('items as item, i')
3
+ li.timeline-item
4
+ .timeline-dot(class!="{item.variant ? `timeline-dot-${item.variant}` : ''}")
5
+ +if('item.icon')
6
+ Icon(name!="{item.icon}" size="size-3")
7
+ +if('i < items.length - 1')
8
+ .timeline-line
9
+ .timeline-body
10
+ +if('item.time')
11
+ .timeline-time {item.time}
12
+ +if('item.title')
13
+ .timeline-title {item.title}
14
+ +if('item.description')
15
+ .timeline-description {item.description}
16
+
17
+ <script>
18
+ import { cn } from '@human-synthesis/norns-ui/cn'
19
+ import Icon from './Icon.n'
20
+
21
+ {
22
+ items = []
23
+ class: extra = ''
24
+ } := $props()
25
+
26
+ classes := cn 'timeline', extra
27
+ </script>
@@ -0,0 +1,29 @@
1
+ ToggleRoot(bind:pressed class!="{classes}" disabled!="{disabled}" onPressedChange!="{onpressedchange}")
2
+ +if('icon')
3
+ Icon(name!="{icon}" size="size-4")
4
+ | {@render children?.()}
5
+
6
+ <script>
7
+ import { Toggle } from 'bits-ui'
8
+ import { cn } from '@human-synthesis/norns-ui/cn'
9
+ import Icon from './Icon.n'
10
+
11
+ { Root: ToggleRoot } := Toggle
12
+
13
+ {
14
+ pressed = $bindable(false)
15
+ disabled = false
16
+ icon
17
+ variant = 'default'
18
+ size = 'md'
19
+ onpressedchange
20
+ children
21
+ class: extra = ''
22
+ } .= $props()
23
+
24
+ classes := cn
25
+ 'toggle-btn'
26
+ variant !== 'default' && `toggle-btn-${variant}`
27
+ size !== 'md' && `toggle-btn-${size}`
28
+ extra
29
+ </script>