@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,34 @@
1
+ ToggleGroupRoot(
2
+ type!="{multiple ? 'multiple' : 'single'}"
3
+ bind:value
4
+ class!="{classes}"
5
+ disabled!="{disabled}"
6
+ )
7
+ +each('items as item')
8
+ ToggleGroupItem.toggle-btn(
9
+ value!="{item.value}"
10
+ disabled!="{item.disabled}"
11
+ aria-label!="{item.label}"
12
+ )
13
+ +if('item.icon')
14
+ Icon(name!="{item.icon}" size="size-4")
15
+ +if('item.label && !item.iconOnly')
16
+ span {item.label}
17
+
18
+ <script>
19
+ import { ToggleGroup } from 'bits-ui'
20
+ import { cn } from '@human-synthesis/norns-ui/cn'
21
+ import Icon from './Icon.n'
22
+
23
+ { Root: ToggleGroupRoot, Item: ToggleGroupItem } := ToggleGroup
24
+
25
+ {
26
+ items = []
27
+ value = $bindable(undefined)
28
+ multiple = false
29
+ disabled = false
30
+ class: extra = ''
31
+ } .= $props()
32
+
33
+ classes := cn 'btn-group toggle-group', extra
34
+ </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>
@@ -0,0 +1,57 @@
1
+ ul.tree(class!="{classes}" role="tree")
2
+ +each('items as node')
3
+ | {@render renderNode(node, 0)}
4
+
5
+ +snippet('renderNode', node, depth)
6
+ li.tree-node(
7
+ role="treeitem"
8
+ aria-expanded!="{hasChildren(node) ? isExpanded(node.id) : undefined}"
9
+ aria-selected!="{node.id === selected}"
10
+ )
11
+ button.tree-row(
12
+ type="button"
13
+ class!="{node.id === selected ? 'tree-row-selected' : ''}"
14
+ style!="{`padding-left: ${0.5 + depth * 1.25}rem`}"
15
+ onclick!="{() => onRowClick(node)}"
16
+ )
17
+ +if('hasChildren(node)')
18
+ span.tree-chevron(class!="{isExpanded(node.id) ? 'tree-chevron-open' : ''}")
19
+ Icon(name="lucide:chevron-right" size="size-3.5")
20
+ +if('!hasChildren(node)')
21
+ span.tree-chevron-spacer
22
+ +if('node.icon')
23
+ Icon(name!="{node.icon}" size="size-4" class="text-fg-muted shrink-0")
24
+ span.tree-label
25
+ | {node.label}
26
+ +if('hasChildren(node) && isExpanded(node.id)')
27
+ ul(role="group")
28
+ +each('node.children as child')
29
+ | {@render renderNode(child, depth + 1)}
30
+
31
+ <script>
32
+ import { cn } from '@human-synthesis/norns-ui/cn'
33
+ import Icon from './Icon.n'
34
+
35
+ {
36
+ items = []
37
+ expanded = $bindable([])
38
+ selected = $bindable(undefined)
39
+ class: extra = ''
40
+ } .= $props()
41
+
42
+ classes := cn 'tree', extra
43
+
44
+ hasChildren := (node) => Array.isArray(node.children) && node.children.length > 0
45
+ isExpanded := (id) => expanded.includes(id)
46
+
47
+ toggle := (id) =>
48
+ if isExpanded(id)
49
+ expanded = expanded.filter (x) => x !== id
50
+ else
51
+ expanded = [...expanded, id]
52
+
53
+ onRowClick := (node) =>
54
+ if hasChildren(node)
55
+ toggle node.id
56
+ selected = node.id
57
+ </script>
@@ -0,0 +1,88 @@
1
+ .uploader(
2
+ class!="{classes}"
3
+ ondragover!="{onDragOver}"
4
+ ondragleave!="{onDragLeave}"
5
+ ondrop!="{onDrop}"
6
+ role="button"
7
+ tabindex="0"
8
+ onclick!="{() => inputEl?.click()}"
9
+ onkeydown!="{(e) => { if (e.key === 'Enter' || e.key === ' ') inputEl?.click() }}"
10
+ )
11
+ input(
12
+ type="file"
13
+ bind:this!="{inputEl}"
14
+ multiple!="{multiple}"
15
+ accept!="{accept}"
16
+ disabled!="{disabled}"
17
+ class="sr-only"
18
+ onchange!="{(e) => onPick(e.currentTarget.files)}"
19
+ )
20
+ Icon(name="lucide:upload-cloud" size="size-8" class="text-fg-muted")
21
+ .uploader-text
22
+ span.uploader-primary
23
+ | {placeholder}
24
+ +if('helper')
25
+ span.uploader-helper
26
+ | {helper}
27
+
28
+ +if('files.length > 0')
29
+ ul.uploader-list
30
+ +each('files as file, i')
31
+ li.uploader-item
32
+ Icon(name="lucide:file" size="size-4" class="text-fg-muted shrink-0")
33
+ .flex-1.min-w-0
34
+ .text-sm.truncate {file.name}
35
+ .text-xs(class="text-fg-muted") {humanSize(file.size)}
36
+ button.btn.btn-ghost.btn-sm(type="button" aria-label="Remove" onclick!="{() => remove(i)}")
37
+ Icon(name="lucide:x" size="size-3.5")
38
+
39
+ <script>
40
+ import { cn } from '@human-synthesis/norns-ui/cn'
41
+ import Icon from './Icon.n'
42
+
43
+ {
44
+ files = $bindable([])
45
+ multiple = true
46
+ accept
47
+ disabled = false
48
+ placeholder = 'Drop files here or click to browse'
49
+ helper
50
+ onchange
51
+ class: extra = ''
52
+ } .= $props()
53
+
54
+ inputEl .= $state null
55
+ dragging .= $state false
56
+
57
+ classes := cn 'uploader-root', dragging && 'uploader-dragging', extra
58
+
59
+ humanSize := (n) =>
60
+ return '' unless n
61
+ if n < 1024
62
+ `${n} B`
63
+ else if n < 1024 * 1024
64
+ `${(n / 1024).toFixed(1)} KB`
65
+ else
66
+ `${(n / 1024 / 1024).toFixed(1)} MB`
67
+
68
+ onPick := (list) =>
69
+ return unless list
70
+ next := Array.from list
71
+ files = multiple ? [...files, ...next] : next.slice(0, 1)
72
+ onchange?.(files)
73
+
74
+ remove := (i) =>
75
+ files = files.filter (_, idx) => idx !== i
76
+ onchange?.(files)
77
+
78
+ onDragOver := (e) =>
79
+ e.preventDefault()
80
+ dragging = true
81
+
82
+ onDragLeave := => dragging = false
83
+
84
+ onDrop := (e) =>
85
+ e.preventDefault()
86
+ dragging = false
87
+ onPick e.dataTransfer?.files
88
+ </script>
@@ -0,0 +1,26 @@
1
+ video(src!="{src}" controls!="{controls}" autoplay!="{autoplay}" loop!="{looping}" muted!="{muted}" playsinline!="{playsinline}" preload!="{preload}" poster!="{poster}" class!="{classes}")
2
+ +each('sources as source')
3
+ source(src!="{source.src}" type!="{source.type}")
4
+ | {fallback}
5
+
6
+ <script>
7
+ import { cn } from '@human-synthesis/norns-ui/cn'
8
+
9
+ // Note: HTML attribute `loop` becomes `looping` here because `loop` is a
10
+ // Civet reserved word.
11
+ {
12
+ src
13
+ sources = []
14
+ controls = true
15
+ autoplay = false
16
+ loop: looping = false
17
+ muted = false
18
+ playsinline = true
19
+ preload = 'metadata'
20
+ poster
21
+ fallback = 'Your browser does not support the video element.'
22
+ class: extra = ''
23
+ } := $props()
24
+
25
+ classes := cn 'video-player block w-full rounded-md', extra
26
+ </script>
package/src/index.js CHANGED
@@ -30,6 +30,72 @@ export { default as Tooltip } from './components/Tooltip.n';
30
30
  export { default as Tabs } from './components/Tabs.n';
31
31
  export { default as ToastProvider } from './components/ToastProvider.n';
32
32
 
33
+ // 0.0.4 — display layer
34
+ export { default as Icon } from './components/Icon.n';
35
+ export { default as Card } from './components/Card.n';
36
+ export { default as Surface } from './components/Surface.n';
37
+ export { default as Banner } from './components/Banner.n';
38
+ export { default as Badge } from './components/Badge.n';
39
+ export { default as Chip } from './components/Chip.n';
40
+ export { default as Avatar } from './components/Avatar.n';
41
+ export { default as Skeleton } from './components/Skeleton.n';
42
+ export { default as Progress } from './components/Progress.n';
43
+ export { default as ProgressCircular } from './components/ProgressCircular.n';
44
+
45
+ // 0.0.4 — composite layer
46
+ export { default as Header } from './components/Header.n';
47
+ export { default as Drawer } from './components/Drawer.n';
48
+ export { default as HeroBanner } from './components/HeroBanner.n';
49
+ export { default as Stepper } from './components/Stepper.n';
50
+ export { default as Breadcrumbs } from './components/Breadcrumbs.n';
51
+ export { default as Pagination } from './components/Pagination.n';
52
+ export { default as Accordion } from './components/Accordion.n';
53
+ export { default as Carousel } from './components/Carousel.n';
54
+ export { default as Tree } from './components/Tree.n';
55
+
56
+ // 0.0.4 — wave A
57
+ export { default as Separator } from './components/Separator.n';
58
+ export { default as Toolbar } from './components/Toolbar.n';
59
+ export { default as ButtonGroup } from './components/ButtonGroup.n';
60
+ export { default as ToggleButton } from './components/ToggleButton.n';
61
+ export { default as ToggleButtonGroup } from './components/ToggleButtonGroup.n';
62
+ export { default as Collapsible } from './components/Collapsible.n';
63
+ export { default as NumberInput } from './components/NumberInput.n';
64
+ export { default as OtpField } from './components/OtpField.n';
65
+ export { default as TagsInput } from './components/TagsInput.n';
66
+ export { default as Image } from './components/Image.n';
67
+ export { default as Audio } from './components/Audio.n';
68
+ export { default as Video } from './components/Video.n';
69
+ export { default as Timeline } from './components/Timeline.n';
70
+
71
+ // 0.0.4 — wave B
72
+ export { default as Autocomplete } from './components/Autocomplete.n';
73
+ export { default as MultiSelect } from './components/MultiSelect.n';
74
+ export { default as ContextMenu } from './components/ContextMenu.n';
75
+ export { default as RichTooltip } from './components/RichTooltip.n';
76
+ export { default as PreviewCard } from './components/PreviewCard.n';
77
+ export { default as ScrollArea } from './components/ScrollArea.n';
78
+ export { default as ColorPicker } from './components/ColorPicker.n';
79
+ export { default as Uploader } from './components/Uploader.n';
80
+ export { default as HierarchicalMenu } from './components/HierarchicalMenu.n';
81
+ export { default as MegaMenu } from './components/MegaMenu.n';
82
+
83
+ // 0.0.4 — wave C
84
+ export { default as DatePicker } from './components/DatePicker.n';
85
+ export { default as DateRangePicker } from './components/DateRangePicker.n';
86
+ export { default as TimePicker } from './components/TimePicker.n';
87
+ export { default as Calendar } from './components/Calendar.n';
88
+ export { default as DataTable } from './components/DataTable.n';
89
+
90
+ // 0.0.5 — CSS-only motion components
91
+ export { default as AvatarGroup } from './components/AvatarGroup.n';
92
+ export { default as GradientText } from './components/GradientText.n';
93
+ export { default as CopyButton } from './components/CopyButton.n';
94
+ export { default as ThemeToggler } from './components/ThemeToggler.n';
95
+ export { default as ShinyButton } from './components/ShinyButton.n';
96
+ export { default as RippleButton } from './components/RippleButton.n';
97
+ // Motion-driven: `import … from '@human-synthesis/norns-ui/motion'`.
98
+
33
99
  // Helpers
34
100
  export { cn } from './lib/cn.js';
35
101
  export { variantClasses } from './lib/variants.js';
@@ -0,0 +1,55 @@
1
+ span(class!="{classes}" bind:this!="{el}")
2
+ | {display}
3
+
4
+ <script>
5
+ import { onMount } from 'svelte'
6
+ import { cn } from '@human-synthesis/norns-ui/cn'
7
+
8
+ {
9
+ value = 0
10
+ from = 0
11
+ duration = 0.9
12
+ decimals = 0
13
+ format
14
+ class: extra = ''
15
+ } := $props()
16
+
17
+ display .= $state String((from ?? value).toFixed(decimals))
18
+ el .= $state null
19
+ hasMounted .= $state false
20
+
21
+ classes := cn 'tabular-nums', extra
22
+
23
+ doFormat := (n) =>
24
+ if format
25
+ format(n)
26
+ else if decimals === 0
27
+ Math.round(n).toLocaleString()
28
+ else
29
+ n.toFixed(decimals)
30
+
31
+ $effect =>
32
+ return unless hasMounted
33
+ // Re-runs whenever `value` changes (after mount).
34
+ v := value
35
+ (async =>
36
+ { animate } := await import('motion')
37
+ anim := animate from, v, {
38
+ duration
39
+ onUpdate: (latest) => display = doFormat(latest)
40
+ onComplete: => display = doFormat(v)
41
+ }
42
+ anim
43
+ )()
44
+
45
+ onMount =>
46
+ hasMounted = true
47
+ (async =>
48
+ { animate } := await import('motion')
49
+ animate (from ?? 0), value, {
50
+ duration
51
+ onUpdate: (latest) => display = doFormat(latest)
52
+ onComplete: => display = doFormat(value)
53
+ }
54
+ )()
55
+ </script>
@@ -0,0 +1,25 @@
1
+ .gradient-bg(class!="{classes}" style!="{styleStr}")
2
+ +if('children')
3
+ .relative
4
+ | {@render children()}
5
+
6
+ <script>
7
+ import { cn } from '@human-synthesis/norns-ui/cn'
8
+
9
+ {
10
+ colors = [
11
+ 'oklch(60% 0.25 290)'
12
+ 'oklch(60% 0.18 240)'
13
+ 'oklch(70% 0.18 200)'
14
+ 'oklch(60% 0.25 320)'
15
+ ]
16
+ duration = 18
17
+ angle = 130
18
+ children
19
+ class: extra = ''
20
+ } := $props()
21
+
22
+ classes := cn 'gradient-bg', extra
23
+ stops := $derived colors.join ', '
24
+ styleStr := $derived `--gradient-bg-stops: ${stops}; --gradient-bg-angle: ${angle}deg; --gradient-bg-duration: ${duration}s`
25
+ </script>
@@ -0,0 +1,48 @@
1
+ button(
2
+ type!="{type}"
3
+ class!="{classes}"
4
+ disabled!="{disabled}"
5
+ onclick!="{onclick}"
6
+ bind:this!="{el}"
7
+ )
8
+ span.liquid-btn-blob(aria-hidden="true")
9
+ span.liquid-btn-content
10
+ +if('icon')
11
+ Icon(name!="{icon}" size!="{iconSize}")
12
+ +if('children')
13
+ | {@render children()}
14
+
15
+ <script>
16
+ import { onMount } from 'svelte'
17
+ import { cn } from '@human-synthesis/norns-ui/cn'
18
+ import Icon from './../components/Icon.n'
19
+
20
+ {
21
+ variant = 'primary'
22
+ size = 'md'
23
+ type = 'button'
24
+ disabled = false
25
+ onclick
26
+ icon
27
+ children
28
+ class: extra = ''
29
+ } := $props()
30
+
31
+ iconSize := size === 'sm' ? 'size-3.5' : size === 'lg' ? 'size-5' : 'size-4'
32
+
33
+ classes := cn 'btn', `btn-${variant}`, size !== 'md' && `btn-${size}`, 'liquid-btn', extra
34
+
35
+ el .= $state null
36
+
37
+ onMount =>
38
+ return unless el
39
+ (async =>
40
+ { animate } := await import('motion')
41
+ blob := el.querySelector '.liquid-btn-blob'
42
+ return unless blob
43
+ el.addEventListener 'mouseenter', =>
44
+ animate blob, { transform: ['scale(0.6)', 'scale(1.4)'] }, { duration: 0.6, easing: [0.34, 1.56, 0.64, 1] }
45
+ el.addEventListener 'mouseleave', =>
46
+ animate blob, { transform: 'scale(0.6)' }, { duration: 0.4 }
47
+ )()
48
+ </script>
@@ -0,0 +1,37 @@
1
+ div(class!="{classes}" style!="{initialStyle}" bind:this!="{el}")
2
+ | {@render children?.()}
3
+
4
+ <script>
5
+ import { onMount } from 'svelte'
6
+ import { cn } from '@human-synthesis/norns-ui/cn'
7
+
8
+ {
9
+ direction = 'up'
10
+ distance = 12
11
+ duration = 0.5
12
+ delay = 0
13
+ once = true
14
+ threshold = 0.15
15
+ children
16
+ class: extra = ''
17
+ } := $props()
18
+
19
+ classes := cn 'will-change-transform', extra
20
+
21
+ axis := direction === 'left' || direction === 'right' ? 'X' : 'Y'
22
+ sign := direction === 'down' || direction === 'right' ? -distance : distance
23
+
24
+ initialStyle := `opacity: 0; transform: translate${axis}(${sign}px)`
25
+
26
+ el .= $state null
27
+
28
+ onMount =>
29
+ return unless el
30
+ (async =>
31
+ { inView, animate } := await import('motion')
32
+ inView el, =>
33
+ animate el, { opacity: [0, 1], [`transform`]: [`translate${axis}(${sign}px)`, `translate${axis}(0)`] }, { duration, delay }
34
+ once ? false : (=> animate(el, { opacity: 0 }, { duration: 0 }))
35
+ , { amount: threshold }
36
+ )()
37
+ </script>
@@ -0,0 +1,53 @@
1
+ .sparkles-wrap(class!="{classes}" bind:this!="{wrap}")
2
+ +if('children')
3
+ | {@render children()}
4
+ .sparkles-overlay(aria-hidden="true")
5
+ +each('seeds as s')
6
+ span.sparkle(style!="{`left: ${s.x}%; top: ${s.y}%; width: ${s.size}px; height: ${s.size}px; --sparkle-color: ${s.color}; animation-delay: ${s.delay}s; animation-duration: ${s.duration}s`}")
7
+
8
+ <script>
9
+ import { onMount, onDestroy } from 'svelte'
10
+ import { cn } from '@human-synthesis/norns-ui/cn'
11
+
12
+ {
13
+ density = 14
14
+ colors = ['var(--color-primary-400)', 'var(--color-info-400)', 'var(--color-warning-400)']
15
+ minSize = 4
16
+ maxSize = 10
17
+ children
18
+ class: extra = ''
19
+ } := $props()
20
+
21
+ classes := cn 'sparkles-wrap', extra
22
+
23
+ rand := (min, max) => min + Math.random() * (max - min)
24
+
25
+ seeds := $derived.by =>
26
+ out := []
27
+ i .= 0
28
+ while i < density
29
+ out.push {
30
+ x: rand(2, 96)
31
+ y: rand(2, 96)
32
+ size: Math.round rand(minSize, maxSize)
33
+ color: colors[i % colors.length]
34
+ delay: rand(0, 3)
35
+ duration: rand(1.6, 3.2)
36
+ }
37
+ i++
38
+ out
39
+
40
+ wrap .= $state null
41
+ stopObserver .= $state null
42
+
43
+ onMount =>
44
+ return unless wrap
45
+ // Pause CSS animations off-screen via animation-play-state.
46
+ io := new IntersectionObserver (entries) =>
47
+ for entry of entries
48
+ wrap.classList.toggle 'sparkles-paused', !entry.isIntersecting
49
+ io.observe wrap
50
+ stopObserver = => io.disconnect()
51
+
52
+ onDestroy => stopObserver?.()
53
+ </script>
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `@human-synthesis/norns-ui/motion` — opt-in motion subpath.
3
+ *
4
+ * Importing from this path pulls in [Motion One](https://motion.dev/) (~3 KB
5
+ * gzipped). The default barrel at the package root stays motion-free; if you
6
+ * never import from `/motion` your bundle stays unaffected.
7
+ *
8
+ * Auto-import (via `presetUI()`) deliberately does NOT register these names —
9
+ * we don't want every page to pay the Motion One cost. Import explicitly:
10
+ *
11
+ * import AnimatedNumber from '@human-synthesis/norns-ui/motion/AnimatedNumber';
12
+ * import Reveal from '@human-synthesis/norns-ui/motion/Reveal';
13
+ */
14
+ export { default as AnimatedNumber } from './AnimatedNumber.n';
15
+ export { default as Reveal } from './Reveal.n';
16
+ export { default as LiquidButton } from './LiquidButton.n';
17
+ export { default as Sparkles } from './Sparkles.n';
18
+ export { default as GradientBackground } from './GradientBackground.n';