@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,32 @@
1
+ /**
2
+ * Svelte action: move a node out of its DOM parent into a target element
3
+ * (default: <body>) for the lifetime of the action. Used by overlays
4
+ * (Dialog, Sheet, Popover, Dropdown, Tooltip) so they're not constrained
5
+ * by parent `overflow` or `transform` stacking contexts.
6
+ *
7
+ * Usage in Pug + Civet:
8
+ * div(use:portal class!="dialog-content")
9
+ *
10
+ * Cleanup note: `destroy` only removes the moved node + the placeholder
11
+ * comment we inserted. It does NOT attempt to move the node back into the
12
+ * original tree. Svelte already iterates its own tracked DOM (and finds
13
+ * the moved node by reference) when the branch unmounts; if we re-inserted
14
+ * the node here, Svelte would leave it in place because it had already
15
+ * "removed" it via the body reference — the overlay would stay visible.
16
+ *
17
+ * @param {HTMLElement} node
18
+ * @param {HTMLElement | string} [target]
19
+ */
20
+ export function portal(node, target = document.body) {
21
+ const dest = typeof target === 'string' ? document.querySelector(target) : target;
22
+ if (!dest) return {};
23
+ const placeholder = document.createComment('portal');
24
+ node.parentNode?.replaceChild(placeholder, node);
25
+ dest.appendChild(node);
26
+ return {
27
+ destroy() {
28
+ node.parentNode?.removeChild(node);
29
+ placeholder.parentNode?.removeChild(placeholder);
30
+ }
31
+ };
32
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Keyboard navigation for menu/tab/listbox patterns.
3
+ *
4
+ * Attach to a container; the action queries focusable items via `selector`
5
+ * (default `[data-roving]`) and routes ArrowDown/Up/Home/End between them.
6
+ * Items get `tabindex=-1` except the active one (`tabindex=0`).
7
+ *
8
+ * ul(use:rovingTabindex={{ orientation: 'vertical', selector: '[data-item]' }})
9
+ *
10
+ * Use `orientation: 'horizontal'` for ArrowLeft/Right (Tabs).
11
+ * Pass `loop: true` to wrap around at the ends.
12
+ */
13
+ export function rovingTabindex(node, opts = {}) {
14
+ let { orientation = 'vertical', selector = '[data-roving]', loop = true } = opts;
15
+ const isVertical = () => orientation === 'vertical';
16
+ const isHorizontal = () => orientation === 'horizontal';
17
+
18
+ const items = () => Array.from(node.querySelectorAll(selector)).filter((el) => !el.disabled);
19
+
20
+ const setTabindex = () => {
21
+ const list = items();
22
+ if (list.length === 0) return;
23
+ const activeIdx = list.indexOf(document.activeElement);
24
+ list.forEach((el, i) => {
25
+ el.setAttribute('tabindex', i === Math.max(0, activeIdx) ? '0' : '-1');
26
+ });
27
+ };
28
+
29
+ const move = (delta) => {
30
+ const list = items();
31
+ if (list.length === 0) return;
32
+ const cur = list.indexOf(document.activeElement);
33
+ let next = cur + delta;
34
+ if (next < 0) next = loop ? list.length - 1 : 0;
35
+ if (next >= list.length) next = loop ? 0 : list.length - 1;
36
+ list[next].focus();
37
+ };
38
+
39
+ const onKeyDown = (e) => {
40
+ const { key } = e;
41
+ if (isVertical() && (key === 'ArrowDown' || key === 'ArrowUp')) {
42
+ e.preventDefault();
43
+ move(key === 'ArrowDown' ? 1 : -1);
44
+ } else if (isHorizontal() && (key === 'ArrowRight' || key === 'ArrowLeft')) {
45
+ e.preventDefault();
46
+ move(key === 'ArrowRight' ? 1 : -1);
47
+ } else if (key === 'Home') {
48
+ e.preventDefault();
49
+ items()[0]?.focus();
50
+ } else if (key === 'End') {
51
+ e.preventDefault();
52
+ const list = items();
53
+ list[list.length - 1]?.focus();
54
+ }
55
+ };
56
+
57
+ const onFocusIn = () => setTabindex();
58
+
59
+ node.addEventListener('keydown', onKeyDown);
60
+ node.addEventListener('focusin', onFocusIn);
61
+ queueMicrotask(setTabindex);
62
+
63
+ return {
64
+ update(next) {
65
+ if (next) {
66
+ orientation = next.orientation ?? orientation;
67
+ selector = next.selector ?? selector;
68
+ loop = next.loop ?? loop;
69
+ }
70
+ },
71
+ destroy() {
72
+ node.removeEventListener('keydown', onKeyDown);
73
+ node.removeEventListener('focusin', onFocusIn);
74
+ }
75
+ };
76
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Refcounted scroll lock for the document body. Multiple overlays may stack
3
+ * (Dialog over Drawer over Popover); each acquires a lock and releases on
4
+ * unmount. The body stays locked until the last release.
5
+ *
6
+ * Preserves the scroll position by setting `position: fixed; top: -<scrollY>`
7
+ * and restoring it on full release.
8
+ */
9
+ let count = 0;
10
+ let savedScrollY = 0;
11
+ let savedStyles = null;
12
+
13
+ export function lock() {
14
+ if (count === 0 && typeof document !== 'undefined') {
15
+ savedScrollY = window.scrollY;
16
+ const body = document.body;
17
+ savedStyles = {
18
+ position: body.style.position,
19
+ top: body.style.top,
20
+ width: body.style.width,
21
+ overflow: body.style.overflow
22
+ };
23
+ body.style.position = 'fixed';
24
+ body.style.top = `-${savedScrollY}px`;
25
+ body.style.width = '100%';
26
+ body.style.overflow = 'hidden';
27
+ }
28
+ count++;
29
+ }
30
+
31
+ export function unlock() {
32
+ if (count === 0) return;
33
+ count--;
34
+ if (count === 0 && savedStyles && typeof document !== 'undefined') {
35
+ const body = document.body;
36
+ body.style.position = savedStyles.position;
37
+ body.style.top = savedStyles.top;
38
+ body.style.width = savedStyles.width;
39
+ body.style.overflow = savedStyles.overflow;
40
+ window.scrollTo(0, savedScrollY);
41
+ savedStyles = null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Svelte action — locks while mounted, releases on destroy.
47
+ * div(use:scrollLock)
48
+ */
49
+ export function scrollLock(_node) {
50
+ lock();
51
+ return {
52
+ destroy() {
53
+ unlock();
54
+ }
55
+ };
56
+ }
@@ -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';