@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.
- package/LICENSE +21 -0
- package/README.md +81 -225
- package/package.json +13 -3
- package/src/auto-import.js +81 -19
- package/src/components/Accordion.n +39 -0
- package/src/components/Audio.n +24 -0
- package/src/components/Autocomplete.n +134 -0
- package/src/components/Avatar.n +27 -0
- package/src/components/AvatarGroup.n +25 -0
- package/src/components/Badge.n +20 -0
- package/src/components/Banner.n +25 -0
- package/src/components/Breadcrumbs.n +22 -0
- package/src/components/Btn.n +29 -8
- package/src/components/ButtonGroup.n +14 -0
- package/src/components/Calendar.n +153 -0
- package/src/components/Card.n +34 -0
- package/src/components/Carousel.n +66 -0
- package/src/components/Chip.n +27 -0
- package/src/components/Collapsible.n +29 -0
- package/src/components/ColorPicker.n +62 -0
- package/src/components/ContextMenu.n +67 -0
- package/src/components/CopyButton.n +50 -0
- package/src/components/DataTable.n +61 -0
- package/src/components/DatePicker.n +56 -0
- package/src/components/DateRangePicker.n +47 -0
- package/src/components/Dialog.n +35 -22
- package/src/components/Dropdown.n +54 -23
- package/src/components/GradientText.n +21 -0
- package/src/components/Header.n +23 -0
- package/src/components/HeroBanner.n +37 -0
- package/src/components/HierarchicalMenu.n +38 -0
- package/src/components/Icon.n +16 -0
- package/src/components/Image.n +31 -0
- package/src/components/MegaMenu.n +36 -0
- package/src/components/MultiSelect.n +158 -0
- package/src/components/NumberInput.n +49 -0
- package/src/components/OtpField.n +67 -0
- package/src/components/Pagination.n +73 -0
- package/src/components/Popover.n +30 -15
- package/src/components/Progress.n +26 -0
- package/src/components/ProgressCircular.n +52 -0
- package/src/components/RippleButton.n +56 -0
- package/src/components/ScrollArea.n +20 -0
- package/src/components/Separator.n +12 -0
- package/src/components/Sheet.n +36 -22
- package/src/components/ShinyButton.n +36 -0
- package/src/components/Skeleton.n +27 -0
- package/src/components/Stepper.n +35 -0
- package/src/components/Surface.n +10 -0
- package/src/components/Tabs.n +39 -13
- package/src/components/TagsInput.n +66 -0
- package/src/components/ThemeToggler.n +43 -0
- package/src/components/TimePicker.n +153 -0
- package/src/components/Timeline.n +27 -0
- package/src/components/ToggleButton.n +35 -0
- package/src/components/ToggleButtonGroup.n +54 -0
- package/src/components/Toolbar.n +15 -0
- package/src/components/Tooltip.n +52 -19
- package/src/components/Tree.n +57 -0
- package/src/components/Uploader.n +88 -0
- package/src/components/Video.n +26 -0
- package/src/index.js +64 -1
- package/src/lib/behaviors/clickOutside.svelte.js +54 -0
- package/src/lib/behaviors/escape.svelte.js +36 -0
- package/src/lib/behaviors/floating.svelte.js +137 -0
- package/src/lib/behaviors/focusTrap.svelte.js +80 -0
- package/src/lib/behaviors/index.js +14 -0
- package/src/lib/behaviors/portal.svelte.js +32 -0
- package/src/lib/behaviors/rovingTabindex.svelte.js +76 -0
- package/src/lib/behaviors/scrollLock.svelte.js +56 -0
- package/src/motion/AnimatedNumber.n +55 -0
- package/src/motion/GradientBackground.n +25 -0
- package/src/motion/LiquidButton.n +48 -0
- package/src/motion/Reveal.n +37 -0
- package/src/motion/Sparkles.n +53 -0
- package/src/motion/index.js +18 -0
- package/src/styles/atoms.css +1481 -223
- package/src/styles/tokens.css +204 -40
- package/src/types/Accordion.d.ts +17 -0
- package/src/types/AnimatedNumber.d.ts +15 -0
- package/src/types/Audio.d.ts +18 -0
- package/src/types/Autocomplete.d.ts +17 -0
- package/src/types/Avatar.d.ts +14 -0
- package/src/types/AvatarGroup.d.ts +15 -0
- package/src/types/Badge.d.ts +14 -0
- package/src/types/Banner.d.ts +15 -0
- package/src/types/Breadcrumbs.d.ts +15 -0
- package/src/types/Btn.d.ts +10 -0
- package/src/types/ButtonGroup.d.ts +10 -0
- package/src/types/Calendar.d.ts +20 -0
- package/src/types/Card.d.ts +19 -0
- package/src/types/Carousel.d.ts +23 -0
- package/src/types/Chip.d.ts +12 -0
- package/src/types/Collapsible.d.ts +13 -0
- package/src/types/ColorPicker.d.ts +15 -0
- package/src/types/ContextMenu.d.ts +24 -0
- package/src/types/CopyButton.d.ts +16 -0
- package/src/types/DataTable.d.ts +27 -0
- package/src/types/DatePicker.d.ts +23 -0
- package/src/types/DateRangePicker.d.ts +17 -0
- package/src/types/Dialog.d.ts +4 -0
- package/src/types/Dropdown.d.ts +2 -1
- package/src/types/GradientBackground.d.ts +14 -0
- package/src/types/GradientText.d.ts +19 -0
- package/src/types/Header.d.ts +12 -0
- package/src/types/HeroBanner.d.ts +17 -0
- package/src/types/HierarchicalMenu.d.ts +20 -0
- package/src/types/Icon.d.ts +21 -0
- package/src/types/Image.d.ts +15 -0
- package/src/types/LiquidButton.d.ts +15 -0
- package/src/types/MegaMenu.d.ts +29 -0
- package/src/types/MultiSelect.d.ts +16 -0
- package/src/types/NumberInput.d.ts +17 -0
- package/src/types/OtpField.d.ts +13 -0
- package/src/types/Pagination.d.ts +13 -0
- package/src/types/Progress.d.ts +14 -0
- package/src/types/ProgressCircular.d.ts +14 -0
- package/src/types/Reveal.d.ts +20 -0
- package/src/types/RippleButton.d.ts +15 -0
- package/src/types/ScrollArea.d.ts +11 -0
- package/src/types/Separator.d.ts +9 -0
- package/src/types/Sheet.d.ts +3 -0
- package/src/types/ShinyButton.d.ts +15 -0
- package/src/types/Skeleton.d.ts +13 -0
- package/src/types/Sparkles.d.ts +13 -0
- package/src/types/Stepper.d.ts +19 -0
- package/src/types/Surface.d.ts +10 -0
- package/src/types/Tabs.d.ts +2 -0
- package/src/types/TagsInput.d.ts +17 -0
- package/src/types/ThemeToggler.d.ts +17 -0
- package/src/types/TimePicker.d.ts +20 -0
- package/src/types/Timeline.d.ts +17 -0
- package/src/types/ToggleButton.d.ts +16 -0
- package/src/types/ToggleButtonGroup.d.ts +21 -0
- package/src/types/Toolbar.d.ts +12 -0
- package/src/types/Tooltip.d.ts +3 -0
- package/src/types/Tree.d.ts +20 -0
- package/src/types/Uploader.d.ts +15 -0
- 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';
|