@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/Sheet.n
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
+if('trigger')
|
|
2
|
+
span(class!="{triggerClass ?? ''}" onclick!="{() => open = true}")
|
|
3
|
+
| {@render trigger()}
|
|
4
|
+
|
|
5
|
+
+if('open')
|
|
6
|
+
div(use:portal use:scrollLock)
|
|
7
|
+
div(
|
|
8
|
+
data-state="open"
|
|
9
|
+
class!="{overlayClasses}"
|
|
10
|
+
onclick!="{onOverlayClick}"
|
|
11
|
+
)
|
|
12
|
+
div(
|
|
13
|
+
role="dialog"
|
|
14
|
+
aria-modal="true"
|
|
15
|
+
aria-label!="{title ?? ariaLabel}"
|
|
16
|
+
class!="{contentClasses}"
|
|
17
|
+
data-state="open"
|
|
18
|
+
data-side!="{side}"
|
|
19
|
+
use:focusTrap
|
|
20
|
+
use:escape!="{[onClose, open]}"
|
|
21
|
+
)
|
|
8
22
|
+if('title')
|
|
9
|
-
|
|
23
|
+
h2.dialog-title
|
|
10
24
|
| {title}
|
|
11
25
|
+if('description')
|
|
12
|
-
|
|
26
|
+
p.dialog-description
|
|
13
27
|
| {description}
|
|
14
28
|
+if('children')
|
|
15
29
|
.sheet-body
|
|
@@ -18,23 +32,15 @@ DialogRoot(bind:open)
|
|
|
18
32
|
.dialog-actions
|
|
19
33
|
| {@render actions()}
|
|
20
34
|
+if('!hideClose')
|
|
21
|
-
|
|
35
|
+
button.dialog-close(type="button" aria-label="Close" onclick!="{onClose}")
|
|
22
36
|
| ✕
|
|
23
37
|
|
|
24
38
|
<script>
|
|
25
|
-
import { Dialog } from 'bits-ui'
|
|
26
39
|
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Portal: DialogPortal
|
|
32
|
-
Overlay: DialogOverlay
|
|
33
|
-
Content: DialogContent
|
|
34
|
-
Title: DialogTitle
|
|
35
|
-
Description: DialogDescription
|
|
36
|
-
Close: DialogClose
|
|
37
|
-
} := Dialog
|
|
40
|
+
import { portal } from '@human-synthesis/norns-ui/behaviors/portal.svelte.js'
|
|
41
|
+
import { scrollLock } from '@human-synthesis/norns-ui/behaviors/scrollLock.svelte.js'
|
|
42
|
+
import { focusTrap } from '@human-synthesis/norns-ui/behaviors/focusTrap.svelte.js'
|
|
43
|
+
import { escape } from '@human-synthesis/norns-ui/behaviors/escape.svelte.js'
|
|
38
44
|
|
|
39
45
|
{
|
|
40
46
|
open = $bindable(false)
|
|
@@ -42,6 +48,8 @@ DialogRoot(bind:open)
|
|
|
42
48
|
title
|
|
43
49
|
description
|
|
44
50
|
hideClose = false
|
|
51
|
+
dismissable = true
|
|
52
|
+
ariaLabel
|
|
45
53
|
trigger
|
|
46
54
|
actions
|
|
47
55
|
children
|
|
@@ -52,4 +60,10 @@ DialogRoot(bind:open)
|
|
|
52
60
|
|
|
53
61
|
overlayClasses := cn 'dialog-overlay', overlayClass
|
|
54
62
|
contentClasses := cn 'sheet-content', `sheet-${side}`, extra
|
|
63
|
+
|
|
64
|
+
onClose := => open = false
|
|
65
|
+
|
|
66
|
+
onOverlayClick := (e) =>
|
|
67
|
+
return unless dismissable
|
|
68
|
+
open = false if e.target === e.currentTarget
|
|
55
69
|
</script>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
button(
|
|
2
|
+
type!="{type}"
|
|
3
|
+
class!="{classes}"
|
|
4
|
+
disabled!="{disabled}"
|
|
5
|
+
onclick!="{onclick}"
|
|
6
|
+
)
|
|
7
|
+
span.shiny-btn-shine(aria-hidden="true")
|
|
8
|
+
+if('icon')
|
|
9
|
+
Icon(name!="{icon}" size!="{iconSize}")
|
|
10
|
+
+if('children')
|
|
11
|
+
| {@render children()}
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
15
|
+
import Icon from './Icon.n'
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
variant = 'primary'
|
|
19
|
+
size = 'md'
|
|
20
|
+
type = 'button'
|
|
21
|
+
disabled = false
|
|
22
|
+
onclick
|
|
23
|
+
icon
|
|
24
|
+
children
|
|
25
|
+
class: extra = ''
|
|
26
|
+
} := $props()
|
|
27
|
+
|
|
28
|
+
iconSize := size === 'sm' ? 'size-3.5' : size === 'lg' ? 'size-5' : 'size-4'
|
|
29
|
+
|
|
30
|
+
classes := cn
|
|
31
|
+
'btn'
|
|
32
|
+
`btn-${variant}`
|
|
33
|
+
size !== 'md' && `btn-${size}`
|
|
34
|
+
'shiny-btn'
|
|
35
|
+
extra
|
|
36
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
span(class!="{classes}" style!="{style}" aria-hidden="true")
|
|
2
|
+
|
|
3
|
+
<script>
|
|
4
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
5
|
+
|
|
6
|
+
{
|
|
7
|
+
variant = 'rect'
|
|
8
|
+
width
|
|
9
|
+
height
|
|
10
|
+
class: extra = ''
|
|
11
|
+
...rest
|
|
12
|
+
} := $props()
|
|
13
|
+
|
|
14
|
+
classes := cn 'skeleton', variant === 'circle' && 'skeleton-circle', extra
|
|
15
|
+
|
|
16
|
+
style := $derived.by =>
|
|
17
|
+
parts := []
|
|
18
|
+
if width
|
|
19
|
+
parts.push `width: ${typeof width === 'number' ? `${width}px` : width}`
|
|
20
|
+
if height
|
|
21
|
+
parts.push `height: ${typeof height === 'number' ? `${height}px` : height}`
|
|
22
|
+
else if variant === 'text'
|
|
23
|
+
parts.push 'height: 0.875rem'
|
|
24
|
+
else if variant === 'circle' && !height
|
|
25
|
+
parts.push 'height: 2.5rem; width: 2.5rem'
|
|
26
|
+
parts.join '; '
|
|
27
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
ol(class!="{classes}")
|
|
2
|
+
+each('steps as step, i')
|
|
3
|
+
li.stepper-step(class!="{stepClass(i)}")
|
|
4
|
+
.stepper-marker
|
|
5
|
+
+if('isComplete(i)')
|
|
6
|
+
Icon(name="lucide:check" size="size-3.5")
|
|
7
|
+
+if('!isComplete(i)')
|
|
8
|
+
| {i + 1}
|
|
9
|
+
.stepper-label
|
|
10
|
+
| {step.label}
|
|
11
|
+
+if('i < steps.length - 1')
|
|
12
|
+
.stepper-divider
|
|
13
|
+
|
|
14
|
+
<script>
|
|
15
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
16
|
+
import Icon from './Icon.n'
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
steps = []
|
|
20
|
+
current = 0
|
|
21
|
+
orientation = 'horizontal'
|
|
22
|
+
class: extra = ''
|
|
23
|
+
...rest
|
|
24
|
+
} := $props()
|
|
25
|
+
|
|
26
|
+
classes := cn 'stepper', orientation === 'vertical' && 'stepper-vertical', extra
|
|
27
|
+
|
|
28
|
+
isComplete := (i) => steps[i]?.complete ?? i < current
|
|
29
|
+
isCurrent := (i) => steps[i]?.current ?? i === current
|
|
30
|
+
|
|
31
|
+
stepClass := (i) =>
|
|
32
|
+
cn
|
|
33
|
+
isCurrent(i) && 'stepper-step-current'
|
|
34
|
+
isComplete(i) && 'stepper-step-complete'
|
|
35
|
+
</script>
|
package/src/components/Tabs.n
CHANGED
|
@@ -1,28 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
.tabs-root(class!="{rootClasses}")
|
|
2
|
+
.tabs-list(
|
|
3
|
+
role="tablist"
|
|
4
|
+
aria-label!="{label}"
|
|
5
|
+
use:rovingTabindex!="{{ orientation: 'horizontal' }}"
|
|
6
|
+
)
|
|
3
7
|
+each('items as item (item.value)')
|
|
4
|
-
|
|
8
|
+
button.tabs-trigger(
|
|
9
|
+
type="button"
|
|
10
|
+
role="tab"
|
|
11
|
+
id!="{tabId(item.value)}"
|
|
12
|
+
aria-selected!="{value === item.value}"
|
|
13
|
+
aria-controls!="{panelId(item.value)}"
|
|
14
|
+
data-state!="{value === item.value ? 'active' : 'inactive'}"
|
|
15
|
+
data-roving
|
|
16
|
+
disabled!="{item.disabled}"
|
|
17
|
+
onclick!="{() => select(item.value)}"
|
|
18
|
+
)
|
|
5
19
|
| {item.label}
|
|
6
20
|
+each('items as item (item.value)')
|
|
7
|
-
|
|
8
|
-
|
|
21
|
+
.tabs-content(
|
|
22
|
+
role="tabpanel"
|
|
23
|
+
id!="{panelId(item.value)}"
|
|
24
|
+
aria-labelledby!="{tabId(item.value)}"
|
|
25
|
+
tabindex="0"
|
|
26
|
+
hidden!="{value !== item.value ? 'until-found' : undefined}"
|
|
27
|
+
)
|
|
28
|
+
+if('value === item.value')
|
|
29
|
+
| {@render item.panel?.()}
|
|
9
30
|
|
|
10
31
|
<script>
|
|
11
|
-
import { Tabs } from 'bits-ui'
|
|
12
32
|
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
13
|
-
|
|
14
|
-
{
|
|
15
|
-
Root: TabsRoot
|
|
16
|
-
List: TabsList
|
|
17
|
-
Trigger: TabsTrigger
|
|
18
|
-
Content: TabsContent
|
|
19
|
-
} := Tabs
|
|
33
|
+
import { rovingTabindex } from '@human-synthesis/norns-ui/behaviors/rovingTabindex.svelte.js'
|
|
20
34
|
|
|
21
35
|
{
|
|
22
36
|
value = $bindable()
|
|
23
37
|
items = []
|
|
38
|
+
label = 'Tabs'
|
|
24
39
|
class: extra = ''
|
|
25
40
|
} .= $props()
|
|
26
41
|
|
|
27
42
|
rootClasses := cn 'tabs-root', extra
|
|
43
|
+
|
|
44
|
+
// Lazy default to first tab if no value bound.
|
|
45
|
+
$effect =>
|
|
46
|
+
if value === undefined && items.length > 0 && !items[0].disabled
|
|
47
|
+
value = items[0].value
|
|
48
|
+
|
|
49
|
+
uid := Math.random().toString(36).slice(2, 8)
|
|
50
|
+
tabId := (v) => `tab-${uid}-${v}`
|
|
51
|
+
panelId := (v) => `tabpanel-${uid}-${v}`
|
|
52
|
+
|
|
53
|
+
select := (v) => value = v
|
|
28
54
|
</script>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
.tags-input(class!="{wrapClasses}" onclick!="{focusInput}")
|
|
2
|
+
+each('value as tag, i')
|
|
3
|
+
span.chip
|
|
4
|
+
| {tag}
|
|
5
|
+
button.chip-remove(type="button" aria-label!="{`Remove ${tag}`}" onclick!="{(e) => { e.stopPropagation(); remove(i) }}")
|
|
6
|
+
Icon(name="lucide:x" size="size-3")
|
|
7
|
+
input.tags-input-field(
|
|
8
|
+
bind:this!="{inputEl}"
|
|
9
|
+
bind:value!="{draft}"
|
|
10
|
+
type="text"
|
|
11
|
+
placeholder!="{placeholder}"
|
|
12
|
+
disabled!="{disabled}"
|
|
13
|
+
onkeydown!="{onKey}"
|
|
14
|
+
onblur!="{commitOnBlur ? commit : undefined}"
|
|
15
|
+
name!="{name}"
|
|
16
|
+
id!="{resolvedId}"
|
|
17
|
+
aria-invalid!="{hasError ? 'true' : undefined}"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
<script>
|
|
21
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
22
|
+
import { getContext } from 'svelte'
|
|
23
|
+
import Icon from './Icon.n'
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
value = $bindable([])
|
|
27
|
+
max
|
|
28
|
+
placeholder = 'Add a tag…'
|
|
29
|
+
commitOnBlur = true
|
|
30
|
+
separators = [',', 'Enter']
|
|
31
|
+
disabled = false
|
|
32
|
+
name
|
|
33
|
+
id: providedId
|
|
34
|
+
error: explicitError = false
|
|
35
|
+
class: extra = ''
|
|
36
|
+
} .= $props()
|
|
37
|
+
|
|
38
|
+
field := getContext 'norns-ui:field'
|
|
39
|
+
resolvedId := providedId ?? field?.id
|
|
40
|
+
hasError := explicitError || field?.hasError || false
|
|
41
|
+
|
|
42
|
+
draft .= $state ''
|
|
43
|
+
inputEl .= $state null
|
|
44
|
+
|
|
45
|
+
wrapClasses := cn 'tags-input-wrap', hasError && 'tags-input-err', extra
|
|
46
|
+
|
|
47
|
+
focusInput := => inputEl?.focus()
|
|
48
|
+
|
|
49
|
+
commit := =>
|
|
50
|
+
t := draft.trim()
|
|
51
|
+
return unless t
|
|
52
|
+
return if max && value.length >= max
|
|
53
|
+
return if value.includes(t)
|
|
54
|
+
value = [...value, t]
|
|
55
|
+
draft = ''
|
|
56
|
+
|
|
57
|
+
remove := (i) =>
|
|
58
|
+
value = value.filter (_, idx) => idx !== i
|
|
59
|
+
|
|
60
|
+
onKey := (e) =>
|
|
61
|
+
if separators.includes(e.key)
|
|
62
|
+
e.preventDefault()
|
|
63
|
+
commit()
|
|
64
|
+
else if e.key === 'Backspace' && !draft && value.length > 0
|
|
65
|
+
remove(value.length - 1)
|
|
66
|
+
</script>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
button(
|
|
2
|
+
type="button"
|
|
3
|
+
class!="{classes}"
|
|
4
|
+
onclick!="{toggle}"
|
|
5
|
+
aria-label="Toggle theme"
|
|
6
|
+
title="Toggle theme"
|
|
7
|
+
)
|
|
8
|
+
Icon(name!="{lightIcon}" size!="{iconSize}" class="theme-toggle-light")
|
|
9
|
+
Icon(name!="{darkIcon}" size!="{iconSize}" class="theme-toggle-dark")
|
|
10
|
+
+if('showLabel')
|
|
11
|
+
span.theme-toggle-light
|
|
12
|
+
| {labelLight}
|
|
13
|
+
span.theme-toggle-dark
|
|
14
|
+
| {labelDark}
|
|
15
|
+
|
|
16
|
+
<script>
|
|
17
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
18
|
+
import Icon from './Icon.n'
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
variant = 'ghost'
|
|
22
|
+
size = 'md'
|
|
23
|
+
showLabel = false
|
|
24
|
+
labelLight = 'Dark'
|
|
25
|
+
labelDark = 'Light'
|
|
26
|
+
lightIcon = 'lucide:moon'
|
|
27
|
+
darkIcon = 'lucide:sun'
|
|
28
|
+
storageKey = 'norns-theme'
|
|
29
|
+
class: extra = ''
|
|
30
|
+
} := $props()
|
|
31
|
+
|
|
32
|
+
iconSize := size === 'sm' ? 'size-3.5' : size === 'lg' ? 'size-5' : 'size-4'
|
|
33
|
+
|
|
34
|
+
classes := cn 'btn' , `btn-${variant}`, size !== 'md' && `btn-${size}`, extra
|
|
35
|
+
|
|
36
|
+
toggle := =>
|
|
37
|
+
root := document.documentElement
|
|
38
|
+
next := root.dataset.theme === 'dark' ? 'light' : 'dark'
|
|
39
|
+
root.dataset.theme = next === 'dark' ? 'dark' : ''
|
|
40
|
+
try
|
|
41
|
+
localStorage.setItem storageKey, next
|
|
42
|
+
catch _
|
|
43
|
+
</script>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Popover(bind:open class="p-2 time-picker-popover")
|
|
2
|
+
+snippet('trigger')
|
|
3
|
+
button.input.date-picker-trigger(
|
|
4
|
+
type="button"
|
|
5
|
+
disabled!="{disabled}"
|
|
6
|
+
id!="{resolvedId}"
|
|
7
|
+
aria-label!="{label ?? 'Pick a time'}"
|
|
8
|
+
class!="{hasError ? 'input-err' : ''}"
|
|
9
|
+
)
|
|
10
|
+
Icon(name="lucide:clock" size="size-4" class="text-fg-muted shrink-0")
|
|
11
|
+
span.flex-1.text-left.font-mono.tabular-nums
|
|
12
|
+
| {value || placeholder}
|
|
13
|
+
Icon(name="lucide:chevron-down" size="size-3.5" class="text-fg-muted shrink-0")
|
|
14
|
+
.time-picker-grid
|
|
15
|
+
.time-picker-col(role="listbox" aria-label="Hour" bind:this!="{hourCol}")
|
|
16
|
+
+each('hours as h')
|
|
17
|
+
button.time-picker-cell(
|
|
18
|
+
type="button"
|
|
19
|
+
class!="{h === currentHour ? 'time-picker-cell-selected' : ''}"
|
|
20
|
+
onclick!="{() => setHour(h)}"
|
|
21
|
+
data-time-h!="{h}"
|
|
22
|
+
)
|
|
23
|
+
| {pad(h)}
|
|
24
|
+
.time-picker-sep :
|
|
25
|
+
.time-picker-col(role="listbox" aria-label="Minute" bind:this!="{minuteCol}")
|
|
26
|
+
+each('minutes as m')
|
|
27
|
+
button.time-picker-cell(
|
|
28
|
+
type="button"
|
|
29
|
+
class!="{m === currentMinute ? 'time-picker-cell-selected' : ''}"
|
|
30
|
+
onclick!="{() => setMinute(m)}"
|
|
31
|
+
data-time-m!="{m}"
|
|
32
|
+
)
|
|
33
|
+
| {pad(m)}
|
|
34
|
+
+if('hourCycle === 12')
|
|
35
|
+
.time-picker-col(role="listbox" aria-label="AM/PM")
|
|
36
|
+
button.time-picker-cell(
|
|
37
|
+
type="button"
|
|
38
|
+
class!="{period === 'AM' ? 'time-picker-cell-selected' : ''}"
|
|
39
|
+
onclick!="{() => setPeriod('AM')}"
|
|
40
|
+
)
|
|
41
|
+
| AM
|
|
42
|
+
button.time-picker-cell(
|
|
43
|
+
type="button"
|
|
44
|
+
class!="{period === 'PM' ? 'time-picker-cell-selected' : ''}"
|
|
45
|
+
onclick!="{() => setPeriod('PM')}"
|
|
46
|
+
)
|
|
47
|
+
| PM
|
|
48
|
+
.flex.justify-between.gap-2.mt-2.pt-2(class="border-t border-border")
|
|
49
|
+
button.btn.btn-ghost.btn-sm(type="button" onclick!="{clearValue}") Clear
|
|
50
|
+
button.btn.btn-primary.btn-sm(type="button" onclick!="{() => open = false}") Done
|
|
51
|
+
|
|
52
|
+
<script>
|
|
53
|
+
import { getContext } from 'svelte'
|
|
54
|
+
import Popover from './Popover.n'
|
|
55
|
+
import Icon from './Icon.n'
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
value = $bindable('')
|
|
59
|
+
open = $bindable(false)
|
|
60
|
+
minuteStep = 5
|
|
61
|
+
hourCycle = 24
|
|
62
|
+
placeholder = 'Pick a time'
|
|
63
|
+
disabled = false
|
|
64
|
+
label
|
|
65
|
+
name
|
|
66
|
+
id: providedId
|
|
67
|
+
error: explicitError = false
|
|
68
|
+
} .= $props()
|
|
69
|
+
|
|
70
|
+
field := getContext 'norns-ui:field'
|
|
71
|
+
resolvedId := providedId ?? field?.id
|
|
72
|
+
hasError := explicitError || field?.hasError || false
|
|
73
|
+
|
|
74
|
+
pad := (n) => String(n).padStart 2, '0'
|
|
75
|
+
|
|
76
|
+
parts := $derived.by =>
|
|
77
|
+
if !value
|
|
78
|
+
return { h: 0, m: 0 }
|
|
79
|
+
[hStr, mStr] := value.split ':'
|
|
80
|
+
{ h: parseInt(hStr ?? '0', 10), m: parseInt(mStr ?? '0', 10) }
|
|
81
|
+
|
|
82
|
+
hours := $derived.by =>
|
|
83
|
+
out := []
|
|
84
|
+
if hourCycle === 12
|
|
85
|
+
out.push 12
|
|
86
|
+
i .= 1
|
|
87
|
+
while i <= 11
|
|
88
|
+
out.push i
|
|
89
|
+
i++
|
|
90
|
+
else
|
|
91
|
+
i .= 0
|
|
92
|
+
while i < 24
|
|
93
|
+
out.push i
|
|
94
|
+
i++
|
|
95
|
+
out
|
|
96
|
+
|
|
97
|
+
minutes := $derived.by =>
|
|
98
|
+
out := []
|
|
99
|
+
i .= 0
|
|
100
|
+
while i < 60
|
|
101
|
+
out.push i
|
|
102
|
+
i += minuteStep
|
|
103
|
+
out
|
|
104
|
+
|
|
105
|
+
currentHour := $derived.by =>
|
|
106
|
+
h := parts.h
|
|
107
|
+
if hourCycle === 12
|
|
108
|
+
h === 0 ? 12 : (h > 12 ? h - 12 : h)
|
|
109
|
+
else
|
|
110
|
+
h
|
|
111
|
+
|
|
112
|
+
currentMinute := $derived parts.m
|
|
113
|
+
period := $derived parts.h >= 12 ? 'PM' : 'AM'
|
|
114
|
+
|
|
115
|
+
emit := (h24, m) =>
|
|
116
|
+
value = `${pad(h24)}:${pad(m)}`
|
|
117
|
+
|
|
118
|
+
setHour := (displayed) =>
|
|
119
|
+
nextH .= displayed
|
|
120
|
+
if hourCycle === 12
|
|
121
|
+
if period === 'PM'
|
|
122
|
+
nextH = displayed === 12 ? 12 : displayed + 12
|
|
123
|
+
else
|
|
124
|
+
nextH = displayed === 12 ? 0 : displayed
|
|
125
|
+
emit nextH, parts.m
|
|
126
|
+
|
|
127
|
+
setMinute := (m) => emit parts.h, m
|
|
128
|
+
|
|
129
|
+
setPeriod := (p) =>
|
|
130
|
+
nextH .= parts.h
|
|
131
|
+
if p === 'PM' && parts.h < 12
|
|
132
|
+
nextH = parts.h + 12
|
|
133
|
+
else if p === 'AM' && parts.h >= 12
|
|
134
|
+
nextH = parts.h - 12
|
|
135
|
+
emit nextH, parts.m
|
|
136
|
+
|
|
137
|
+
clearValue := => value = ''
|
|
138
|
+
|
|
139
|
+
hourCol .= $state null
|
|
140
|
+
minuteCol .= $state null
|
|
141
|
+
|
|
142
|
+
$effect =>
|
|
143
|
+
return unless open && (hourCol || minuteCol)
|
|
144
|
+
queueMicrotask =>
|
|
145
|
+
scrollSelected hourCol, currentHour, 'data-time-h'
|
|
146
|
+
scrollSelected minuteCol, currentMinute, 'data-time-m'
|
|
147
|
+
|
|
148
|
+
scrollSelected := (col, val, attr) =>
|
|
149
|
+
return unless col
|
|
150
|
+
target := col.querySelector `[${attr}="${val}"]`
|
|
151
|
+
return unless target
|
|
152
|
+
col.scrollTop = target.offsetTop - col.clientHeight / 2 + target.clientHeight / 2
|
|
153
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
ol(class!="{classes}")
|
|
2
|
+
+each('items as item, i')
|
|
3
|
+
li.timeline-item
|
|
4
|
+
.timeline-dot(class!="{item.variant ? `timeline-dot-${item.variant}` : ''}")
|
|
5
|
+
+if('item.icon')
|
|
6
|
+
Icon(name!="{item.icon}" size="size-3")
|
|
7
|
+
+if('i < items.length - 1')
|
|
8
|
+
.timeline-line
|
|
9
|
+
.timeline-body
|
|
10
|
+
+if('item.time')
|
|
11
|
+
.timeline-time {item.time}
|
|
12
|
+
+if('item.title')
|
|
13
|
+
.timeline-title {item.title}
|
|
14
|
+
+if('item.description')
|
|
15
|
+
.timeline-description {item.description}
|
|
16
|
+
|
|
17
|
+
<script>
|
|
18
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
19
|
+
import Icon from './Icon.n'
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
items = []
|
|
23
|
+
class: extra = ''
|
|
24
|
+
} := $props()
|
|
25
|
+
|
|
26
|
+
classes := cn 'timeline', extra
|
|
27
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
button(
|
|
2
|
+
type="button"
|
|
3
|
+
class!="{classes}"
|
|
4
|
+
disabled!="{disabled}"
|
|
5
|
+
data-pressed!="{pressed ? 'true' : undefined}"
|
|
6
|
+
aria-pressed!="{pressed}"
|
|
7
|
+
onclick!="{toggle}"
|
|
8
|
+
)
|
|
9
|
+
+if('icon')
|
|
10
|
+
Icon(name!="{icon}" size!="{iconSize}")
|
|
11
|
+
+if('children')
|
|
12
|
+
| {@render children()}
|
|
13
|
+
|
|
14
|
+
<script>
|
|
15
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
16
|
+
import Icon from './Icon.n'
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
pressed = $bindable(false)
|
|
20
|
+
disabled = false
|
|
21
|
+
icon
|
|
22
|
+
variant = 'secondary'
|
|
23
|
+
size = 'md'
|
|
24
|
+
onpressedchange
|
|
25
|
+
children
|
|
26
|
+
class: extra = ''
|
|
27
|
+
} .= $props()
|
|
28
|
+
|
|
29
|
+
classes := cn 'btn', `btn-${variant}`, size !== 'md' && `btn-${size}`, extra
|
|
30
|
+
iconSize := size === 'sm' ? 'size-3.5' : size === 'lg' ? 'size-5' : 'size-4'
|
|
31
|
+
|
|
32
|
+
toggle := =>
|
|
33
|
+
pressed = !pressed
|
|
34
|
+
onpressedchange?.(pressed)
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
.btn-group(
|
|
2
|
+
role="group"
|
|
3
|
+
aria-label!="{label}"
|
|
4
|
+
class!="{classes}"
|
|
5
|
+
use:rovingTabindex!="{{ orientation: 'horizontal' }}"
|
|
6
|
+
)
|
|
7
|
+
+each('items as item')
|
|
8
|
+
button(
|
|
9
|
+
type="button"
|
|
10
|
+
class!="{itemClasses(item.value)}"
|
|
11
|
+
data-roving
|
|
12
|
+
data-pressed!="{isActive(item.value) ? 'true' : undefined}"
|
|
13
|
+
aria-pressed!="{isActive(item.value)}"
|
|
14
|
+
disabled!="{disabled || item.disabled}"
|
|
15
|
+
aria-label!="{item.label}"
|
|
16
|
+
onclick!="{() => toggle(item.value)}"
|
|
17
|
+
)
|
|
18
|
+
+if('item.icon')
|
|
19
|
+
Icon(name!="{item.icon}" size="size-4")
|
|
20
|
+
+if('item.label && !item.iconOnly')
|
|
21
|
+
span {item.label}
|
|
22
|
+
|
|
23
|
+
<script>
|
|
24
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
25
|
+
import { rovingTabindex } from '@human-synthesis/norns-ui/behaviors/rovingTabindex.svelte.js'
|
|
26
|
+
import Icon from './Icon.n'
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
items = []
|
|
30
|
+
value = $bindable(undefined)
|
|
31
|
+
multiple = false
|
|
32
|
+
disabled = false
|
|
33
|
+
label = 'Toggle group'
|
|
34
|
+
class: extra = ''
|
|
35
|
+
} .= $props()
|
|
36
|
+
|
|
37
|
+
classes := cn extra
|
|
38
|
+
|
|
39
|
+
isActive := (v) =>
|
|
40
|
+
if multiple
|
|
41
|
+
Array.isArray(value) && value.includes v
|
|
42
|
+
else
|
|
43
|
+
value === v
|
|
44
|
+
|
|
45
|
+
itemClasses := (v) => cn 'btn btn-secondary'
|
|
46
|
+
|
|
47
|
+
toggle := (v) =>
|
|
48
|
+
return if disabled
|
|
49
|
+
if multiple
|
|
50
|
+
cur := Array.isArray(value) ? value : []
|
|
51
|
+
value = cur.includes(v) ? cur.filter((x) => x !== v) : [...cur, v]
|
|
52
|
+
else
|
|
53
|
+
value = value === v ? undefined : v
|
|
54
|
+
</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>
|