@human-synthesis/norns-ui 0.0.2 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +77 -129
- package/package.json +23 -15
- package/src/auto-import.js +96 -22
- package/src/components/Accordion.n +38 -0
- package/src/components/Audio.n +24 -0
- package/src/components/Autocomplete.n +59 -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 +32 -0
- package/src/components/ColorPicker.n +62 -0
- package/src/components/ContextMenu.n +48 -0
- package/src/components/CopyButton.n +50 -0
- package/src/components/DataTable.n +61 -0
- package/src/components/DatePicker.n +68 -0
- package/src/components/DateRangePicker.n +59 -0
- package/src/components/Dialog.n +54 -0
- package/src/components/Drawer.n +49 -0
- package/src/components/Dropdown.n +41 -0
- package/src/components/Field.n +29 -20
- package/src/components/Form.n +22 -0
- 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 +56 -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 +32 -0
- package/src/components/PreviewCard.n +30 -0
- package/src/components/Progress.n +26 -0
- package/src/components/ProgressCircular.n +52 -0
- package/src/components/RichTooltip.n +33 -0
- package/src/components/RippleButton.n +56 -0
- package/src/components/ScrollArea.n +31 -0
- package/src/components/Separator.n +12 -0
- package/src/components/Sheet.n +55 -0
- 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 +28 -0
- package/src/components/TagsInput.n +66 -0
- package/src/components/ThemeToggler.n +43 -0
- package/src/components/TimePicker.n +166 -0
- package/src/components/Timeline.n +27 -0
- package/src/components/ToastProvider.n +16 -0
- package/src/components/ToggleButton.n +29 -0
- package/src/components/ToggleButtonGroup.n +34 -0
- package/src/components/Toolbar.n +15 -0
- package/src/components/Tooltip.n +34 -0
- 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 +76 -0
- package/src/lib/toast.svelte.js +42 -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 +1794 -71
- package/src/styles/tokens.css +171 -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 +12 -0
- package/src/types/ColorPicker.d.ts +15 -0
- package/src/types/ContextMenu.d.ts +19 -0
- package/src/types/CopyButton.d.ts +16 -0
- package/src/types/DataTable.d.ts +27 -0
- package/src/types/DatePicker.d.ts +18 -0
- package/src/types/DateRangePicker.d.ts +17 -0
- package/src/types/Dialog.d.ts +17 -0
- package/src/types/Drawer.d.ts +18 -0
- package/src/types/Dropdown.d.ts +23 -0
- package/src/types/Field.d.ts +14 -3
- package/src/types/Form.d.ts +17 -0
- 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 +13 -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/Popover.d.ts +18 -0
- package/src/types/PreviewCard.d.ts +16 -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/RichTooltip.d.ts +16 -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 +20 -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 +17 -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/ToastProvider.d.ts +15 -0
- package/src/types/ToggleButton.d.ts +15 -0
- package/src/types/ToggleButtonGroup.d.ts +20 -0
- package/src/types/Toolbar.d.ts +12 -0
- package/src/types/Tooltip.d.ts +15 -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,61 @@
|
|
|
1
|
+
.data-table-wrap(class!="{wrapClasses}")
|
|
2
|
+
table.data-table
|
|
3
|
+
thead
|
|
4
|
+
tr
|
|
5
|
+
+each('columns as col')
|
|
6
|
+
th(
|
|
7
|
+
class!="{col.class ?? ''}"
|
|
8
|
+
style!="{col.width ? `width: ${col.width}` : undefined}"
|
|
9
|
+
)
|
|
10
|
+
+if('col.sortable')
|
|
11
|
+
button.data-table-sort(type="button" onclick!="{() => onSort(col.key)}")
|
|
12
|
+
span {col.label ?? col.key}
|
|
13
|
+
Icon(name!="{sortIcon(col.key)}" size="size-3.5" class="text-fg-muted")
|
|
14
|
+
+if('!col.sortable')
|
|
15
|
+
| {col.label ?? col.key}
|
|
16
|
+
tbody
|
|
17
|
+
+if('rows.length === 0')
|
|
18
|
+
tr.data-table-empty
|
|
19
|
+
td(colspan!="{columns.length}")
|
|
20
|
+
| {emptyMessage}
|
|
21
|
+
+each('rows as row, i')
|
|
22
|
+
tr(onclick!="{onrowclick ? () => onrowclick(row, i) : undefined}" class!="{onrowclick ? 'data-table-row-clickable' : ''}")
|
|
23
|
+
+each('columns as col')
|
|
24
|
+
td(class!="{col.cellClass ?? ''}")
|
|
25
|
+
| {row[col.key]}
|
|
26
|
+
|
|
27
|
+
<script>
|
|
28
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
29
|
+
import Icon from './Icon.n'
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
columns = []
|
|
33
|
+
rows = []
|
|
34
|
+
striped = false
|
|
35
|
+
dense = false
|
|
36
|
+
stickyHeader = false
|
|
37
|
+
emptyMessage = 'No data'
|
|
38
|
+
sortKey = $bindable(undefined)
|
|
39
|
+
sortDir = $bindable('asc')
|
|
40
|
+
onrowclick
|
|
41
|
+
class: extra = ''
|
|
42
|
+
} .= $props()
|
|
43
|
+
|
|
44
|
+
wrapClasses := cn
|
|
45
|
+
'data-table-root'
|
|
46
|
+
striped && 'data-table-striped'
|
|
47
|
+
dense && 'data-table-dense'
|
|
48
|
+
stickyHeader && 'data-table-sticky'
|
|
49
|
+
extra
|
|
50
|
+
|
|
51
|
+
onSort := (key) =>
|
|
52
|
+
if sortKey === key
|
|
53
|
+
sortDir = sortDir === 'asc' ? 'desc' : 'asc'
|
|
54
|
+
else
|
|
55
|
+
sortKey = key
|
|
56
|
+
sortDir = 'asc'
|
|
57
|
+
|
|
58
|
+
sortIcon := (key) =>
|
|
59
|
+
return 'lucide:chevrons-up-down' unless sortKey === key
|
|
60
|
+
sortDir === 'asc' ? 'lucide:chevron-up' : 'lucide:chevron-down'
|
|
61
|
+
</script>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
PopoverRoot(bind:open)
|
|
2
|
+
PopoverTrigger.input.date-picker-trigger(
|
|
3
|
+
type="button"
|
|
4
|
+
disabled!="{disabled}"
|
|
5
|
+
id!="{resolvedId}"
|
|
6
|
+
aria-label!="{label ?? 'Pick a date'}"
|
|
7
|
+
class!="{hasError ? 'input-err' : ''}"
|
|
8
|
+
)
|
|
9
|
+
Icon(name="lucide:calendar" size="size-4" class="text-fg-muted shrink-0")
|
|
10
|
+
span.flex-1.text-left.truncate
|
|
11
|
+
| {value ? formatted : placeholder}
|
|
12
|
+
Icon(name="lucide:chevron-down" size="size-3.5" class="text-fg-muted shrink-0")
|
|
13
|
+
PopoverPortal
|
|
14
|
+
PopoverContent.popover-content(
|
|
15
|
+
class="p-3 w-[20rem]"
|
|
16
|
+
onOpenAutoFocus!="{(e) => e.preventDefault()}"
|
|
17
|
+
onCloseAutoFocus!="{(e) => e.preventDefault()}"
|
|
18
|
+
)
|
|
19
|
+
Calendar(
|
|
20
|
+
bind:value
|
|
21
|
+
min!="{min}"
|
|
22
|
+
max!="{max}"
|
|
23
|
+
onchange!="{handleChange}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
<script>
|
|
27
|
+
import { Popover } from 'bits-ui'
|
|
28
|
+
import { getContext } from 'svelte'
|
|
29
|
+
import Icon from './Icon.n'
|
|
30
|
+
import Calendar from './Calendar.n'
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
Root: PopoverRoot
|
|
34
|
+
Trigger: PopoverTrigger
|
|
35
|
+
Portal: PopoverPortal
|
|
36
|
+
Content: PopoverContent
|
|
37
|
+
} := Popover
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
value = $bindable('')
|
|
41
|
+
open = $bindable(false)
|
|
42
|
+
min = ''
|
|
43
|
+
max = ''
|
|
44
|
+
placeholder = 'Pick a date'
|
|
45
|
+
disabled = false
|
|
46
|
+
closeOnSelect = true
|
|
47
|
+
label
|
|
48
|
+
name
|
|
49
|
+
id: providedId
|
|
50
|
+
error: explicitError = false
|
|
51
|
+
} .= $props()
|
|
52
|
+
|
|
53
|
+
field := getContext 'norns-ui:field'
|
|
54
|
+
resolvedId := providedId ?? field?.id
|
|
55
|
+
hasError := explicitError || field?.hasError || false
|
|
56
|
+
|
|
57
|
+
formatter := new Intl.DateTimeFormat undefined, { dateStyle: 'medium' }
|
|
58
|
+
|
|
59
|
+
formatted := $derived.by =>
|
|
60
|
+
return '' unless value
|
|
61
|
+
try
|
|
62
|
+
formatter.format new Date(`${value}T00:00:00`)
|
|
63
|
+
catch _
|
|
64
|
+
value
|
|
65
|
+
|
|
66
|
+
handleChange := =>
|
|
67
|
+
open = false if closeOnSelect
|
|
68
|
+
</script>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
PopoverRoot(bind:open)
|
|
2
|
+
PopoverTrigger.input.date-picker-trigger(type="button" disabled!="{disabled}" aria-label="Pick a date range")
|
|
3
|
+
Icon(name="lucide:calendar-range" size="size-4" class="text-fg-muted shrink-0")
|
|
4
|
+
span.flex-1.text-left.truncate
|
|
5
|
+
| {label}
|
|
6
|
+
Icon(name="lucide:chevron-down" size="size-3.5" class="text-fg-muted shrink-0")
|
|
7
|
+
PopoverPortal
|
|
8
|
+
PopoverContent.popover-content(
|
|
9
|
+
class="p-3 w-[20rem]"
|
|
10
|
+
onOpenAutoFocus!="{(e) => e.preventDefault()}"
|
|
11
|
+
onCloseAutoFocus!="{(e) => e.preventDefault()}"
|
|
12
|
+
)
|
|
13
|
+
Calendar(
|
|
14
|
+
bind:rangeValue!="{value}"
|
|
15
|
+
range!="{true}"
|
|
16
|
+
min!="{min}"
|
|
17
|
+
max!="{max}"
|
|
18
|
+
)
|
|
19
|
+
.flex.justify-end.gap-2.mt-3
|
|
20
|
+
button.btn.btn-ghost.btn-sm(type="button" onclick!="{clear}") Clear
|
|
21
|
+
button.btn.btn-primary.btn-sm(type="button" onclick!="{() => open = false}") Done
|
|
22
|
+
|
|
23
|
+
<script>
|
|
24
|
+
import { Popover } from 'bits-ui'
|
|
25
|
+
import Icon from './Icon.n'
|
|
26
|
+
import Calendar from './Calendar.n'
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
Root: PopoverRoot
|
|
30
|
+
Trigger: PopoverTrigger
|
|
31
|
+
Portal: PopoverPortal
|
|
32
|
+
Content: PopoverContent
|
|
33
|
+
} := Popover
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
value = $bindable({ start: '', end: '' })
|
|
37
|
+
open = $bindable(false)
|
|
38
|
+
min = ''
|
|
39
|
+
max = ''
|
|
40
|
+
placeholder = 'Pick a range'
|
|
41
|
+
disabled = false
|
|
42
|
+
} .= $props()
|
|
43
|
+
|
|
44
|
+
formatter := new Intl.DateTimeFormat undefined, { dateStyle: 'medium' }
|
|
45
|
+
|
|
46
|
+
fmt := (s) =>
|
|
47
|
+
return '' unless s
|
|
48
|
+
try
|
|
49
|
+
formatter.format new Date(`${s}T00:00:00`)
|
|
50
|
+
catch _
|
|
51
|
+
s
|
|
52
|
+
|
|
53
|
+
label := $derived.by =>
|
|
54
|
+
return placeholder unless value?.start
|
|
55
|
+
return `${fmt(value.start)} – …` unless value?.end
|
|
56
|
+
`${fmt(value.start)} – ${fmt(value.end)}`
|
|
57
|
+
|
|
58
|
+
clear := => value = { start: '', end: '' }
|
|
59
|
+
</script>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
DialogRoot(bind:open)
|
|
2
|
+
+if('trigger')
|
|
3
|
+
DialogTrigger(class!="{triggerClass ?? undefined}")
|
|
4
|
+
| {@render trigger()}
|
|
5
|
+
DialogPortal
|
|
6
|
+
DialogOverlay(class!="{overlayClasses}")
|
|
7
|
+
DialogContent(class!="{contentClasses}")
|
|
8
|
+
+if('title')
|
|
9
|
+
DialogTitle.dialog-title
|
|
10
|
+
| {title}
|
|
11
|
+
+if('description')
|
|
12
|
+
DialogDescription.dialog-description
|
|
13
|
+
| {description}
|
|
14
|
+
+if('children')
|
|
15
|
+
.dialog-body
|
|
16
|
+
| {@render children()}
|
|
17
|
+
+if('actions')
|
|
18
|
+
.dialog-actions
|
|
19
|
+
| {@render actions()}
|
|
20
|
+
+if('!hideClose')
|
|
21
|
+
DialogClose.dialog-close(aria-label="Close")
|
|
22
|
+
| ✕
|
|
23
|
+
|
|
24
|
+
<script>
|
|
25
|
+
import { Dialog } from 'bits-ui'
|
|
26
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
Root: DialogRoot
|
|
30
|
+
Trigger: DialogTrigger
|
|
31
|
+
Portal: DialogPortal
|
|
32
|
+
Overlay: DialogOverlay
|
|
33
|
+
Content: DialogContent
|
|
34
|
+
Title: DialogTitle
|
|
35
|
+
Description: DialogDescription
|
|
36
|
+
Close: DialogClose
|
|
37
|
+
} := Dialog
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
open = $bindable(false)
|
|
41
|
+
title
|
|
42
|
+
description
|
|
43
|
+
hideClose = false
|
|
44
|
+
trigger
|
|
45
|
+
actions
|
|
46
|
+
children
|
|
47
|
+
triggerClass
|
|
48
|
+
overlayClass = ''
|
|
49
|
+
class: extra = ''
|
|
50
|
+
} .= $props()
|
|
51
|
+
|
|
52
|
+
overlayClasses := cn 'dialog-overlay', overlayClass
|
|
53
|
+
contentClasses := cn 'dialog-content', extra
|
|
54
|
+
</script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
DialogRoot(bind:open)
|
|
2
|
+
+if('trigger')
|
|
3
|
+
DialogTrigger(class!="{triggerClass ?? undefined}")
|
|
4
|
+
| {@render trigger()}
|
|
5
|
+
DialogPortal
|
|
6
|
+
DialogOverlay.dialog-overlay
|
|
7
|
+
DialogContent(class!="{contentClasses}" data-side!="{side}")
|
|
8
|
+
+if('title')
|
|
9
|
+
DialogTitle.dialog-title
|
|
10
|
+
| {title}
|
|
11
|
+
+if('description')
|
|
12
|
+
DialogDescription.dialog-description
|
|
13
|
+
| {description}
|
|
14
|
+
+if('children')
|
|
15
|
+
.sheet-body
|
|
16
|
+
| {@render children()}
|
|
17
|
+
+if('!hideClose')
|
|
18
|
+
DialogClose.dialog-close(aria-label="Close")
|
|
19
|
+
| ✕
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
import { Dialog } from 'bits-ui'
|
|
23
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
Root: DialogRoot
|
|
27
|
+
Trigger: DialogTrigger
|
|
28
|
+
Portal: DialogPortal
|
|
29
|
+
Overlay: DialogOverlay
|
|
30
|
+
Content: DialogContent
|
|
31
|
+
Title: DialogTitle
|
|
32
|
+
Description: DialogDescription
|
|
33
|
+
Close: DialogClose
|
|
34
|
+
} := Dialog
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
open = $bindable(false)
|
|
38
|
+
side = 'left'
|
|
39
|
+
title
|
|
40
|
+
description
|
|
41
|
+
hideClose = false
|
|
42
|
+
trigger
|
|
43
|
+
children
|
|
44
|
+
triggerClass
|
|
45
|
+
class: extra = ''
|
|
46
|
+
} .= $props()
|
|
47
|
+
|
|
48
|
+
contentClasses := cn 'sheet-content', `sheet-${side}`, extra
|
|
49
|
+
</script>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
MenuRoot(bind:open)
|
|
2
|
+
+if('trigger')
|
|
3
|
+
MenuTrigger(class!="{triggerClass ?? undefined}")
|
|
4
|
+
| {@render trigger()}
|
|
5
|
+
MenuPortal
|
|
6
|
+
MenuContent(class!="{contentClasses}" side!="{side}" align!="{align}" sideOffset!="{sideOffset}")
|
|
7
|
+
+each('items as item, i')
|
|
8
|
+
+if('item.separator')
|
|
9
|
+
MenuSeparator.dropdown-separator
|
|
10
|
+
+if('!item.separator')
|
|
11
|
+
MenuItem.dropdown-item(disabled!="{item.disabled}" onSelect!="{item.onSelect}")
|
|
12
|
+
| {item.label}
|
|
13
|
+
| {@render children?.()}
|
|
14
|
+
|
|
15
|
+
<script>
|
|
16
|
+
import { DropdownMenu } from 'bits-ui'
|
|
17
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
Root: MenuRoot
|
|
21
|
+
Trigger: MenuTrigger
|
|
22
|
+
Portal: MenuPortal
|
|
23
|
+
Content: MenuContent
|
|
24
|
+
Item: MenuItem
|
|
25
|
+
Separator: MenuSeparator
|
|
26
|
+
} := DropdownMenu
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
open = $bindable(false)
|
|
30
|
+
side = 'bottom'
|
|
31
|
+
align = 'start'
|
|
32
|
+
sideOffset = 4
|
|
33
|
+
items = []
|
|
34
|
+
trigger
|
|
35
|
+
children
|
|
36
|
+
triggerClass
|
|
37
|
+
class: extra = ''
|
|
38
|
+
} .= $props()
|
|
39
|
+
|
|
40
|
+
contentClasses := cn 'dropdown-content', extra
|
|
41
|
+
</script>
|
package/src/components/Field.n
CHANGED
|
@@ -1,46 +1,55 @@
|
|
|
1
1
|
.field(class!="{classes}")
|
|
2
2
|
+if('label')
|
|
3
|
-
label.field-label(for!="{
|
|
3
|
+
label.field-label(for!="{resolvedId}")
|
|
4
4
|
| {label}
|
|
5
5
|
+if('required')
|
|
6
6
|
span.field-required *
|
|
7
7
|
| {@render children?.()}
|
|
8
|
-
+if('
|
|
9
|
-
p.field-error(role="alert" id!="{
|
|
10
|
-
| {
|
|
11
|
-
+if('!
|
|
12
|
-
p.field-help(id!="{
|
|
8
|
+
+if('resolvedError')
|
|
9
|
+
p.field-error(role="alert" id!="{resolvedId ? `${resolvedId}-error` : undefined}")
|
|
10
|
+
| {resolvedError}
|
|
11
|
+
+if('!resolvedError && help')
|
|
12
|
+
p.field-help(id!="{resolvedId ? `${resolvedId}-help` : undefined}")
|
|
13
13
|
| {help}
|
|
14
14
|
|
|
15
15
|
<script>
|
|
16
16
|
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
17
|
-
import { setContext } from 'svelte'
|
|
17
|
+
import { setContext, getContext } from 'svelte'
|
|
18
18
|
|
|
19
19
|
{
|
|
20
20
|
label
|
|
21
21
|
help
|
|
22
22
|
error
|
|
23
|
+
name
|
|
23
24
|
required = false
|
|
24
25
|
id: providedId
|
|
25
26
|
children
|
|
26
27
|
class: extra = ''
|
|
27
28
|
} := $props()
|
|
28
29
|
|
|
29
|
-
//
|
|
30
|
-
// 1. explicit prop wins
|
|
31
|
-
// 2.
|
|
32
|
-
// 3.
|
|
33
|
-
|
|
30
|
+
// Error resolution order:
|
|
31
|
+
// 1. explicit `error` prop wins (caller has full control)
|
|
32
|
+
// 2. else look up `name` in the parent <Form>'s errors map (auto-wired)
|
|
33
|
+
// 3. else no error
|
|
34
|
+
formCtx := getContext 'norns-ui:form'
|
|
35
|
+
resolvedError := $derived error ?? (name && formCtx?.errors?.[name]) ?? undefined
|
|
36
|
+
|
|
37
|
+
// Deterministic id derivation. SSR-safe (no random fallback):
|
|
38
|
+
// 1. explicit `id` prop wins
|
|
39
|
+
// 2. else `name` (single source of truth for submission + id)
|
|
40
|
+
// 3. else slugify the label
|
|
41
|
+
// 4. else undefined — no label, no <label for=> association needed
|
|
34
42
|
slugify := (s) => s.toLowerCase().trim().replace(/[^\w]+/g, '-').replace(/^-+|-+$/g, '')
|
|
35
|
-
resolvedId := providedId ?? (label && `field-${slugify(label)}`) ?? undefined
|
|
43
|
+
resolvedId := $derived providedId ?? name ?? (label && `field-${slugify(label)}`) ?? undefined
|
|
36
44
|
|
|
37
|
-
// Shared with descendants
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
// Shared with descendants. Getters wrap $derived so context reads
|
|
46
|
+
// remain reactive in both SSR (synchronous) and client (post-mount)
|
|
47
|
+
// — `$effect` would only fire client-side, leaving SSR with stale
|
|
48
|
+
// initial values.
|
|
49
|
+
ctx := {
|
|
50
|
+
get id() { resolvedId }
|
|
51
|
+
get hasError() { !!resolvedError }
|
|
52
|
+
}
|
|
44
53
|
setContext 'norns-ui:field', ctx
|
|
45
54
|
|
|
46
55
|
classes := cn 'field', extra
|
package/src/components/Form.n
CHANGED
|
@@ -9,15 +9,37 @@ form(
|
|
|
9
9
|
|
|
10
10
|
<script>
|
|
11
11
|
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
12
|
+
import { setContext } from 'svelte'
|
|
12
13
|
|
|
13
14
|
{
|
|
14
15
|
method = 'POST'
|
|
15
16
|
action
|
|
16
17
|
enctype
|
|
17
18
|
onsubmit
|
|
19
|
+
form
|
|
18
20
|
children
|
|
19
21
|
class: extra = ''
|
|
20
22
|
} := $props()
|
|
21
23
|
|
|
24
|
+
// Errors-by-name map. Built reactively from the valibot issue list so
|
|
25
|
+
// descendant Fields can look up their own error by `name` without a
|
|
26
|
+
// per-page $derived helper.
|
|
27
|
+
//
|
|
28
|
+
// $derived (not $state + $effect) is critical for SSR — $effect runs
|
|
29
|
+
// only client-side, so an effect-based context would produce empty
|
|
30
|
+
// errors during initial server render.
|
|
31
|
+
errorsMap := $derived.by =>
|
|
32
|
+
out := {}
|
|
33
|
+
if form?.errors
|
|
34
|
+
for issue of form.errors
|
|
35
|
+
name := issue.path?.[0]?.key
|
|
36
|
+
if name && !out[name]
|
|
37
|
+
out[name] = issue.message
|
|
38
|
+
out
|
|
39
|
+
|
|
40
|
+
// Getter wraps the $derived so context consumers re-evaluate on read.
|
|
41
|
+
ctx := { get errors() { errorsMap } }
|
|
42
|
+
setContext 'norns-ui:form', ctx
|
|
43
|
+
|
|
22
44
|
classes := cn 'form', extra
|
|
23
45
|
</script>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
span(class!="{classes}" style!="{styleStr}")
|
|
2
|
+
| {@render children?.()}
|
|
3
|
+
|
|
4
|
+
<script>
|
|
5
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
from = 'var(--color-primary-500)'
|
|
9
|
+
to = 'var(--color-info-500)'
|
|
10
|
+
via
|
|
11
|
+
animate = true
|
|
12
|
+
angle = 90
|
|
13
|
+
children
|
|
14
|
+
class: extra = ''
|
|
15
|
+
} := $props()
|
|
16
|
+
|
|
17
|
+
classes := cn 'gradient-text', animate && 'gradient-text-anim', extra
|
|
18
|
+
|
|
19
|
+
stops := $derived via ? `${from}, ${via}, ${to}` : `${from}, ${to}`
|
|
20
|
+
styleStr := $derived `background-image: linear-gradient(${angle}deg, ${stops})`
|
|
21
|
+
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
header(class!="{classes}")
|
|
2
|
+
.norns-header-brand
|
|
3
|
+
| {@render brand?.()}
|
|
4
|
+
+if('nav')
|
|
5
|
+
nav.norns-header-nav
|
|
6
|
+
| {@render nav()}
|
|
7
|
+
.norns-header-actions
|
|
8
|
+
| {@render actions?.()}
|
|
9
|
+
|
|
10
|
+
<script>
|
|
11
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
sticky = false
|
|
15
|
+
brand
|
|
16
|
+
nav
|
|
17
|
+
actions
|
|
18
|
+
class: extra = ''
|
|
19
|
+
...rest
|
|
20
|
+
} := $props()
|
|
21
|
+
|
|
22
|
+
classes := cn 'norns-header', sticky && 'norns-header-sticky', extra
|
|
23
|
+
</script>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
section(class!="{classes}")
|
|
2
|
+
+if('image')
|
|
3
|
+
img.hero-image(src!="{image}" alt="" loading="lazy")
|
|
4
|
+
.relative
|
|
5
|
+
h1.hero-title
|
|
6
|
+
+if('gradient')
|
|
7
|
+
GradientText
|
|
8
|
+
| {title}
|
|
9
|
+
+if('!gradient')
|
|
10
|
+
| {title}
|
|
11
|
+
+if('description')
|
|
12
|
+
p.hero-description
|
|
13
|
+
| {description}
|
|
14
|
+
+if('children')
|
|
15
|
+
.mt-4
|
|
16
|
+
| {@render children()}
|
|
17
|
+
+if('actions')
|
|
18
|
+
.hero-actions
|
|
19
|
+
| {@render actions()}
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
23
|
+
import GradientText from './GradientText.n'
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
title = ''
|
|
27
|
+
description
|
|
28
|
+
image
|
|
29
|
+
align = 'left'
|
|
30
|
+
gradient = false
|
|
31
|
+
actions
|
|
32
|
+
children
|
|
33
|
+
class: extra = ''
|
|
34
|
+
} := $props()
|
|
35
|
+
|
|
36
|
+
classes := cn 'hero', align === 'center' && 'hero-center', extra
|
|
37
|
+
</script>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
nav.nav-menu(class!="{classes}" aria-label!="{label}")
|
|
2
|
+
ul.nav-menu-list
|
|
3
|
+
+each('items as item')
|
|
4
|
+
li.nav-menu-item
|
|
5
|
+
+if('item.children')
|
|
6
|
+
button.nav-menu-trigger(type="button" aria-haspopup="true" aria-expanded="false")
|
|
7
|
+
span {item.label}
|
|
8
|
+
Icon(name="lucide:chevron-down" size="size-3.5" class="ml-1 nav-menu-chevron")
|
|
9
|
+
.nav-menu-content
|
|
10
|
+
ul.flex.flex-col.gap-0.5.p-1(class="min-w-[12rem]")
|
|
11
|
+
+each('item.children as child')
|
|
12
|
+
li
|
|
13
|
+
a.nav-menu-link(href!="{child.href ?? '#'}" onclick!="{(e) => onClickItem(e, child)}")
|
|
14
|
+
+if('child.icon')
|
|
15
|
+
Icon(name!="{child.icon}" size="size-4" class="text-fg-muted shrink-0")
|
|
16
|
+
span.flex-1 {child.label}
|
|
17
|
+
+if('!item.children')
|
|
18
|
+
a.nav-menu-trigger(href!="{item.href ?? '#'}" onclick!="{(e) => onClickItem(e, item)}")
|
|
19
|
+
span {item.label}
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
23
|
+
import Icon from './Icon.n'
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
items = []
|
|
27
|
+
label = 'Main navigation'
|
|
28
|
+
onSelect
|
|
29
|
+
class: extra = ''
|
|
30
|
+
} := $props()
|
|
31
|
+
|
|
32
|
+
classes := cn '', extra
|
|
33
|
+
|
|
34
|
+
onClickItem := (e, item) =>
|
|
35
|
+
if onSelect
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
onSelect item
|
|
38
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
IconifyIcon(icon!="{name}" class!="{classes}" flip!="{flip}" rotate!="{rotate}")
|
|
2
|
+
|
|
3
|
+
<script>
|
|
4
|
+
import IconifyIcon from '@iconify/svelte'
|
|
5
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
6
|
+
|
|
7
|
+
{
|
|
8
|
+
name
|
|
9
|
+
size = 'size-4'
|
|
10
|
+
flip
|
|
11
|
+
rotate
|
|
12
|
+
class: extra = ''
|
|
13
|
+
} := $props()
|
|
14
|
+
|
|
15
|
+
classes := cn 'inline-block shrink-0 align-[-0.125em]', size, extra
|
|
16
|
+
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
img(
|
|
2
|
+
src!="{src}"
|
|
3
|
+
alt!="{alt}"
|
|
4
|
+
loading!="{loading}"
|
|
5
|
+
decoding="async"
|
|
6
|
+
class!="{classes}"
|
|
7
|
+
width!="{width}"
|
|
8
|
+
height!="{height}"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
<script>
|
|
12
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
src
|
|
16
|
+
alt = ''
|
|
17
|
+
loading = 'lazy'
|
|
18
|
+
fit = 'cover'
|
|
19
|
+
rounded = false
|
|
20
|
+
width
|
|
21
|
+
height
|
|
22
|
+
class: extra = ''
|
|
23
|
+
} := $props()
|
|
24
|
+
|
|
25
|
+
classes := cn
|
|
26
|
+
'block max-w-full'
|
|
27
|
+
fit && `object-${fit}`
|
|
28
|
+
rounded === true && 'rounded'
|
|
29
|
+
typeof rounded === 'string' && `rounded-${rounded}`
|
|
30
|
+
extra
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
nav.nav-menu(class!="{classes}" aria-label!="{label}")
|
|
2
|
+
ul.nav-menu-list
|
|
3
|
+
+each('sections as section')
|
|
4
|
+
li.nav-menu-item
|
|
5
|
+
button.nav-menu-trigger(type="button" aria-haspopup="true" aria-expanded="false")
|
|
6
|
+
span {section.label}
|
|
7
|
+
Icon(name="lucide:chevron-down" size="size-3.5" class="ml-1 nav-menu-chevron")
|
|
8
|
+
.nav-menu-content
|
|
9
|
+
.mega-menu-grid(class!="{`grid-cols-${section.cols ?? 3}`}")
|
|
10
|
+
+each('section.columns as column')
|
|
11
|
+
.mega-menu-column
|
|
12
|
+
+if('column.title')
|
|
13
|
+
.mega-menu-title {column.title}
|
|
14
|
+
ul.flex.flex-col.gap-0.5
|
|
15
|
+
+each('column.items as item')
|
|
16
|
+
li
|
|
17
|
+
a.nav-menu-link(href!="{item.href ?? '#'}")
|
|
18
|
+
+if('item.icon')
|
|
19
|
+
Icon(name!="{item.icon}" size="size-4" class="text-fg-muted shrink-0")
|
|
20
|
+
.flex-1
|
|
21
|
+
.text-sm.font-medium {item.label}
|
|
22
|
+
+if('item.description')
|
|
23
|
+
.text-xs(class="text-fg-muted") {item.description}
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
import { cn } from '@human-synthesis/norns-ui/cn'
|
|
27
|
+
import Icon from './Icon.n'
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
sections = []
|
|
31
|
+
label = 'Site navigation'
|
|
32
|
+
class: extra = ''
|
|
33
|
+
} := $props()
|
|
34
|
+
|
|
35
|
+
classes := cn '', extra
|
|
36
|
+
</script>
|