@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
package/src/components/Tooltip.n
CHANGED
|
@@ -1,34 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
span.tooltip-trigger-wrap(
|
|
2
|
+
use:refAction
|
|
3
|
+
class!="{triggerClass ?? ''}"
|
|
4
|
+
onmouseenter!="{onEnter}"
|
|
5
|
+
onmouseleave!="{onLeave}"
|
|
6
|
+
onfocusin!="{onEnter}"
|
|
7
|
+
onfocusout!="{onLeave}"
|
|
8
|
+
)
|
|
9
|
+
| {@render trigger?.()}
|
|
10
|
+
|
|
11
|
+
+if('open')
|
|
12
|
+
div(
|
|
13
|
+
use:portal
|
|
14
|
+
use:floatAction
|
|
15
|
+
role="tooltip"
|
|
16
|
+
class!="{contentClasses}"
|
|
17
|
+
data-state="open"
|
|
18
|
+
)
|
|
19
|
+
+if('content')
|
|
20
|
+
| {content}
|
|
21
|
+
+if('children')
|
|
22
|
+
| {@render children()}
|
|
9
23
|
|
|
10
24
|
<script>
|
|
11
|
-
import { Tooltip } from 'bits-ui'
|
|
12
25
|
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
13
|
-
|
|
14
|
-
{
|
|
15
|
-
Provider: TooltipProvider
|
|
16
|
-
Root: TooltipRoot
|
|
17
|
-
Trigger: TooltipTrigger
|
|
18
|
-
Portal: TooltipPortal
|
|
19
|
-
Content: TooltipContent
|
|
20
|
-
} := Tooltip
|
|
26
|
+
import { portal } from '@human-synthesis/norns-ui/behaviors/portal.svelte.js'
|
|
27
|
+
import { useFloating } from '@human-synthesis/norns-ui/behaviors/floating.svelte.js'
|
|
21
28
|
|
|
22
29
|
{
|
|
23
30
|
content
|
|
24
31
|
side = 'top'
|
|
32
|
+
align = 'center'
|
|
25
33
|
sideOffset = 6
|
|
26
34
|
delay = 300
|
|
35
|
+
rich = false
|
|
27
36
|
trigger
|
|
28
37
|
children
|
|
29
38
|
triggerClass
|
|
30
39
|
class: extra = ''
|
|
31
|
-
}
|
|
40
|
+
} := $props()
|
|
41
|
+
|
|
42
|
+
open .= $state false
|
|
43
|
+
openTimer .= null
|
|
44
|
+
closeTimer .= null
|
|
45
|
+
|
|
46
|
+
placement := align === 'center' ? side : `${side}-${align}`
|
|
47
|
+
|
|
48
|
+
{ reference: refAction, floating: floatAction } := useFloating
|
|
49
|
+
placement: placement
|
|
50
|
+
offset: sideOffset
|
|
51
|
+
|
|
52
|
+
contentClasses := cn rich ? 'tooltip-rich' : 'tooltip-content', extra
|
|
53
|
+
|
|
54
|
+
clear := =>
|
|
55
|
+
clearTimeout openTimer if openTimer
|
|
56
|
+
clearTimeout closeTimer if closeTimer
|
|
57
|
+
openTimer = null
|
|
58
|
+
closeTimer = null
|
|
59
|
+
|
|
60
|
+
onEnter := =>
|
|
61
|
+
clear()
|
|
62
|
+
openTimer = setTimeout (=> open = true), delay
|
|
32
63
|
|
|
33
|
-
|
|
64
|
+
onLeave := =>
|
|
65
|
+
clear()
|
|
66
|
+
closeTimer = setTimeout (=> open = false), 100
|
|
34
67
|
</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
|
@@ -21,7 +21,7 @@ export { default as Select } from './components/Select.n';
|
|
|
21
21
|
export { default as Switch } from './components/Switch.n';
|
|
22
22
|
export { default as Textarea } from './components/Textarea.n';
|
|
23
23
|
|
|
24
|
-
// Phase 3 — behavior tier (
|
|
24
|
+
// Phase 3 — behavior tier (custom on @floating-ui/dom + behaviors module)
|
|
25
25
|
export { default as Dialog } from './components/Dialog.n';
|
|
26
26
|
export { default as Sheet } from './components/Sheet.n';
|
|
27
27
|
export { default as Popover } from './components/Popover.n';
|
|
@@ -30,6 +30,69 @@ 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 HeroBanner } from './components/HeroBanner.n';
|
|
48
|
+
export { default as Stepper } from './components/Stepper.n';
|
|
49
|
+
export { default as Breadcrumbs } from './components/Breadcrumbs.n';
|
|
50
|
+
export { default as Pagination } from './components/Pagination.n';
|
|
51
|
+
export { default as Accordion } from './components/Accordion.n';
|
|
52
|
+
export { default as Carousel } from './components/Carousel.n';
|
|
53
|
+
export { default as Tree } from './components/Tree.n';
|
|
54
|
+
|
|
55
|
+
// 0.0.4 — wave A
|
|
56
|
+
export { default as Separator } from './components/Separator.n';
|
|
57
|
+
export { default as Toolbar } from './components/Toolbar.n';
|
|
58
|
+
export { default as ButtonGroup } from './components/ButtonGroup.n';
|
|
59
|
+
export { default as ToggleButton } from './components/ToggleButton.n';
|
|
60
|
+
export { default as ToggleButtonGroup } from './components/ToggleButtonGroup.n';
|
|
61
|
+
export { default as Collapsible } from './components/Collapsible.n';
|
|
62
|
+
export { default as NumberInput } from './components/NumberInput.n';
|
|
63
|
+
export { default as OtpField } from './components/OtpField.n';
|
|
64
|
+
export { default as TagsInput } from './components/TagsInput.n';
|
|
65
|
+
export { default as Image } from './components/Image.n';
|
|
66
|
+
export { default as Audio } from './components/Audio.n';
|
|
67
|
+
export { default as Video } from './components/Video.n';
|
|
68
|
+
export { default as Timeline } from './components/Timeline.n';
|
|
69
|
+
|
|
70
|
+
// 0.0.4 — wave B
|
|
71
|
+
export { default as Autocomplete } from './components/Autocomplete.n';
|
|
72
|
+
export { default as MultiSelect } from './components/MultiSelect.n';
|
|
73
|
+
export { default as ContextMenu } from './components/ContextMenu.n';
|
|
74
|
+
export { default as ScrollArea } from './components/ScrollArea.n';
|
|
75
|
+
export { default as ColorPicker } from './components/ColorPicker.n';
|
|
76
|
+
export { default as Uploader } from './components/Uploader.n';
|
|
77
|
+
export { default as HierarchicalMenu } from './components/HierarchicalMenu.n';
|
|
78
|
+
export { default as MegaMenu } from './components/MegaMenu.n';
|
|
79
|
+
|
|
80
|
+
// 0.0.4 — wave C
|
|
81
|
+
export { default as DatePicker } from './components/DatePicker.n';
|
|
82
|
+
export { default as DateRangePicker } from './components/DateRangePicker.n';
|
|
83
|
+
export { default as TimePicker } from './components/TimePicker.n';
|
|
84
|
+
export { default as Calendar } from './components/Calendar.n';
|
|
85
|
+
export { default as DataTable } from './components/DataTable.n';
|
|
86
|
+
|
|
87
|
+
// 0.0.5 — CSS-only motion components
|
|
88
|
+
export { default as AvatarGroup } from './components/AvatarGroup.n';
|
|
89
|
+
export { default as GradientText } from './components/GradientText.n';
|
|
90
|
+
export { default as CopyButton } from './components/CopyButton.n';
|
|
91
|
+
export { default as ThemeToggler } from './components/ThemeToggler.n';
|
|
92
|
+
export { default as ShinyButton } from './components/ShinyButton.n';
|
|
93
|
+
export { default as RippleButton } from './components/RippleButton.n';
|
|
94
|
+
// Motion-driven: `import … from '@human-synthesis/norns-ui/motion'`.
|
|
95
|
+
|
|
33
96
|
// Helpers
|
|
34
97
|
export { cn } from './lib/cn.js';
|
|
35
98
|
export { variantClasses } from './lib/variants.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte action: invoke `handler` when a pointerdown occurs outside `node`.
|
|
3
|
+
*
|
|
4
|
+
* div(use:clickOutside={onClose})
|
|
5
|
+
* div(use:clickOutside={[onClose, open]})
|
|
6
|
+
* div(use:clickOutside={[onClose, open, triggerEl]})
|
|
7
|
+
*
|
|
8
|
+
* Three parameter shapes:
|
|
9
|
+
* - `handler` alone
|
|
10
|
+
* - `[handler, enabled]` — pass the open flag so the action no-ops while closed
|
|
11
|
+
* - `[handler, enabled, ignore]` — `ignore` is an element (or array of elements)
|
|
12
|
+
* whose subtree also counts as "inside." Use this to exclude the trigger
|
|
13
|
+
* button so clicking it doesn't close-then-reopen the overlay.
|
|
14
|
+
*
|
|
15
|
+
* Listens on `pointerdown` (capture) so it fires before inner handlers get
|
|
16
|
+
* a chance to stop propagation.
|
|
17
|
+
*/
|
|
18
|
+
export function clickOutside(node, paramOrHandler) {
|
|
19
|
+
let handler;
|
|
20
|
+
let enabled = true;
|
|
21
|
+
let ignoreEls = [];
|
|
22
|
+
const set = (value) => {
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
handler = value[0];
|
|
25
|
+
enabled = value[1] !== false;
|
|
26
|
+
const ig = value[2];
|
|
27
|
+
ignoreEls = ig == null ? [] : Array.isArray(ig) ? ig : [ig];
|
|
28
|
+
} else {
|
|
29
|
+
handler = value;
|
|
30
|
+
enabled = true;
|
|
31
|
+
ignoreEls = [];
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
set(paramOrHandler);
|
|
35
|
+
|
|
36
|
+
const onPointerDown = (e) => {
|
|
37
|
+
if (!enabled || !handler) return;
|
|
38
|
+
if (node.contains(e.target)) return;
|
|
39
|
+
for (const el of ignoreEls) {
|
|
40
|
+
if (el && typeof el.contains === 'function' && el.contains(e.target)) return;
|
|
41
|
+
}
|
|
42
|
+
handler(e);
|
|
43
|
+
};
|
|
44
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
update(value) {
|
|
48
|
+
set(value);
|
|
49
|
+
},
|
|
50
|
+
destroy() {
|
|
51
|
+
document.removeEventListener('pointerdown', onPointerDown, true);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte action: invoke `handler` on Escape keydown while the action is
|
|
3
|
+
* mounted. Honors disabled state via the second-tuple parameter form:
|
|
4
|
+
*
|
|
5
|
+
* div(use:escape={[onClose, open]})
|
|
6
|
+
*/
|
|
7
|
+
export function escape(node, paramOrHandler) {
|
|
8
|
+
let handler;
|
|
9
|
+
let enabled = true;
|
|
10
|
+
const set = (value) => {
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
handler = value[0];
|
|
13
|
+
enabled = value[1] !== false;
|
|
14
|
+
} else {
|
|
15
|
+
handler = value;
|
|
16
|
+
enabled = true;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
set(paramOrHandler);
|
|
20
|
+
|
|
21
|
+
const onKeyDown = (e) => {
|
|
22
|
+
if (!enabled || !handler) return;
|
|
23
|
+
if (e.key !== 'Escape') return;
|
|
24
|
+
handler(e);
|
|
25
|
+
};
|
|
26
|
+
document.addEventListener('keydown', onKeyDown);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
update(value) {
|
|
30
|
+
set(value);
|
|
31
|
+
},
|
|
32
|
+
destroy() {
|
|
33
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floating-element positioning, built on `@floating-ui/dom`.
|
|
3
|
+
*
|
|
4
|
+
* import { useFloating } from '$lib/behaviors/floating.svelte.js'
|
|
5
|
+
*
|
|
6
|
+
* floating := useFloating
|
|
7
|
+
* placement: 'bottom-start'
|
|
8
|
+
* offset: 8
|
|
9
|
+
*
|
|
10
|
+
* ...
|
|
11
|
+
* div(use:floating.reference)
|
|
12
|
+
* div(use:floating.floating)
|
|
13
|
+
*
|
|
14
|
+
* The action sets `position: absolute` + `left`/`top` on the floating
|
|
15
|
+
* element, plus a `data-placement` attribute reflecting the actually-used
|
|
16
|
+
* placement after collision flips.
|
|
17
|
+
*
|
|
18
|
+
* Imperative `update()` is exposed too — call it after content size changes.
|
|
19
|
+
*/
|
|
20
|
+
import { computePosition, flip, shift, offset as offsetMiddleware, autoUpdate, size } from '@floating-ui/dom';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} [opts]
|
|
24
|
+
* @param {import('@floating-ui/dom').Placement} [opts.placement] — e.g. 'bottom-start'
|
|
25
|
+
* @param {number} [opts.offset] — gap from reference, in px
|
|
26
|
+
* @param {number} [opts.padding] — viewport padding for collision
|
|
27
|
+
* @param {boolean} [opts.matchWidth] — match floating's min-width to reference width
|
|
28
|
+
* @param {boolean} [opts.flip] — enable collision flip (default true)
|
|
29
|
+
*/
|
|
30
|
+
export function useFloating(opts = {}) {
|
|
31
|
+
const {
|
|
32
|
+
placement = 'bottom',
|
|
33
|
+
offset: gap = 8,
|
|
34
|
+
padding = 8,
|
|
35
|
+
matchWidth = false,
|
|
36
|
+
flip: flipOnCollision = true
|
|
37
|
+
} = opts;
|
|
38
|
+
|
|
39
|
+
let referenceEl = null;
|
|
40
|
+
let floatingEl = null;
|
|
41
|
+
let cleanupAutoUpdate = null;
|
|
42
|
+
|
|
43
|
+
const middleware = [offsetMiddleware(gap)];
|
|
44
|
+
if (flipOnCollision) middleware.push(flip({ padding }));
|
|
45
|
+
middleware.push(shift({ padding }));
|
|
46
|
+
if (matchWidth) {
|
|
47
|
+
middleware.push(
|
|
48
|
+
size({
|
|
49
|
+
apply({ rects, elements }) {
|
|
50
|
+
Object.assign(elements.floating.style, {
|
|
51
|
+
minWidth: `${rects.reference.width}px`
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function update() {
|
|
59
|
+
if (!referenceEl || !floatingEl) return;
|
|
60
|
+
const { x, y, placement: actualPlacement } = await computePosition(referenceEl, floatingEl, {
|
|
61
|
+
placement,
|
|
62
|
+
middleware,
|
|
63
|
+
strategy: 'absolute'
|
|
64
|
+
});
|
|
65
|
+
Object.assign(floatingEl.style, {
|
|
66
|
+
position: 'absolute',
|
|
67
|
+
left: `${x}px`,
|
|
68
|
+
top: `${y}px`
|
|
69
|
+
});
|
|
70
|
+
floatingEl.setAttribute('data-placement', actualPlacement);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function bind() {
|
|
74
|
+
if (cleanupAutoUpdate) cleanupAutoUpdate();
|
|
75
|
+
if (referenceEl && floatingEl) {
|
|
76
|
+
cleanupAutoUpdate = autoUpdate(referenceEl, floatingEl, update);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const reference = (node) => {
|
|
81
|
+
referenceEl = node;
|
|
82
|
+
bind();
|
|
83
|
+
return {
|
|
84
|
+
destroy() {
|
|
85
|
+
if (referenceEl === node) referenceEl = null;
|
|
86
|
+
if (cleanupAutoUpdate) cleanupAutoUpdate();
|
|
87
|
+
cleanupAutoUpdate = null;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const floating = (node) => {
|
|
93
|
+
floatingEl = node;
|
|
94
|
+
bind();
|
|
95
|
+
return {
|
|
96
|
+
destroy() {
|
|
97
|
+
if (floatingEl === node) floatingEl = null;
|
|
98
|
+
if (cleanupAutoUpdate) cleanupAutoUpdate();
|
|
99
|
+
cleanupAutoUpdate = null;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return { reference, floating, update };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Position a floating element relative to an arbitrary `{ x, y }` virtual
|
|
109
|
+
* reference (e.g. a right-click position for a context menu).
|
|
110
|
+
*/
|
|
111
|
+
export function positionAt(floatingEl, x, y, opts = {}) {
|
|
112
|
+
const virtual = {
|
|
113
|
+
getBoundingClientRect: () => ({
|
|
114
|
+
x,
|
|
115
|
+
y,
|
|
116
|
+
width: 0,
|
|
117
|
+
height: 0,
|
|
118
|
+
left: x,
|
|
119
|
+
top: y,
|
|
120
|
+
right: x,
|
|
121
|
+
bottom: y
|
|
122
|
+
})
|
|
123
|
+
};
|
|
124
|
+
const { placement = 'right-start', offset: gap = 0, padding = 8 } = opts;
|
|
125
|
+
return computePosition(virtual, floatingEl, {
|
|
126
|
+
placement,
|
|
127
|
+
middleware: [offsetMiddleware(gap), flip({ padding }), shift({ padding })],
|
|
128
|
+
strategy: 'absolute'
|
|
129
|
+
}).then(({ x: fx, y: fy, placement: p }) => {
|
|
130
|
+
Object.assign(floatingEl.style, {
|
|
131
|
+
position: 'absolute',
|
|
132
|
+
left: `${fx}px`,
|
|
133
|
+
top: `${fy}px`
|
|
134
|
+
});
|
|
135
|
+
floatingEl.setAttribute('data-placement', p);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte action: trap Tab/Shift+Tab focus inside `node`.
|
|
3
|
+
*
|
|
4
|
+
* div(use:focusTrap)
|
|
5
|
+
*
|
|
6
|
+
* On mount, focuses the first focusable child (or the node itself if it's
|
|
7
|
+
* tabbable). On destroy, returns focus to the previously focused element
|
|
8
|
+
* outside the trap.
|
|
9
|
+
*
|
|
10
|
+
* Disable via tuple form:
|
|
11
|
+
* div(use:focusTrap={open})
|
|
12
|
+
*/
|
|
13
|
+
const FOCUSABLE = [
|
|
14
|
+
'a[href]',
|
|
15
|
+
'button:not([disabled])',
|
|
16
|
+
'input:not([disabled]):not([type="hidden"])',
|
|
17
|
+
'select:not([disabled])',
|
|
18
|
+
'textarea:not([disabled])',
|
|
19
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
20
|
+
'audio[controls]',
|
|
21
|
+
'video[controls]',
|
|
22
|
+
'[contenteditable]:not([contenteditable="false"])'
|
|
23
|
+
].join(',');
|
|
24
|
+
|
|
25
|
+
function focusable(node) {
|
|
26
|
+
return Array.from(node.querySelectorAll(FOCUSABLE)).filter(
|
|
27
|
+
(el) => !el.hasAttribute('disabled') && el.offsetParent !== null
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function focusTrap(node, enabled = true) {
|
|
32
|
+
let active = enabled !== false;
|
|
33
|
+
const previouslyFocused = document.activeElement;
|
|
34
|
+
|
|
35
|
+
const focusFirst = () => {
|
|
36
|
+
const items = focusable(node);
|
|
37
|
+
if (items.length > 0) {
|
|
38
|
+
items[0].focus();
|
|
39
|
+
} else {
|
|
40
|
+
node.setAttribute('tabindex', '-1');
|
|
41
|
+
node.focus();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const onKeyDown = (e) => {
|
|
46
|
+
if (!active || e.key !== 'Tab') return;
|
|
47
|
+
const items = focusable(node);
|
|
48
|
+
if (items.length === 0) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const first = items[0];
|
|
53
|
+
const last = items[items.length - 1];
|
|
54
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
last.focus();
|
|
57
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
first.focus();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (active) {
|
|
64
|
+
// Defer one frame so the node is actually in the DOM after portal moves.
|
|
65
|
+
queueMicrotask(focusFirst);
|
|
66
|
+
}
|
|
67
|
+
node.addEventListener('keydown', onKeyDown);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
update(value) {
|
|
71
|
+
active = value !== false;
|
|
72
|
+
},
|
|
73
|
+
destroy() {
|
|
74
|
+
node.removeEventListener('keydown', onKeyDown);
|
|
75
|
+
if (previouslyFocused && typeof previouslyFocused.focus === 'function') {
|
|
76
|
+
previouslyFocused.focus();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public re-exports of the behavior actions used by the library's components.
|
|
3
|
+
* Consumers can use these directly in their own components for consistency
|
|
4
|
+
* with the library's interaction model.
|
|
5
|
+
*
|
|
6
|
+
* import { portal, clickOutside, escape, focusTrap, useFloating } from '@human-synthesis/norns-ui/behaviors'
|
|
7
|
+
*/
|
|
8
|
+
export { portal } from './portal.svelte.js';
|
|
9
|
+
export { clickOutside } from './clickOutside.svelte.js';
|
|
10
|
+
export { escape } from './escape.svelte.js';
|
|
11
|
+
export { focusTrap } from './focusTrap.svelte.js';
|
|
12
|
+
export { lock as scrollLockAcquire, unlock as scrollLockRelease, scrollLock } from './scrollLock.svelte.js';
|
|
13
|
+
export { useFloating, positionAt } from './floating.svelte.js';
|
|
14
|
+
export { rovingTabindex } from './rovingTabindex.svelte.js';
|