@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.
Files changed (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -129
  3. package/package.json +23 -15
  4. package/src/auto-import.js +96 -22
  5. package/src/components/Accordion.n +38 -0
  6. package/src/components/Audio.n +24 -0
  7. package/src/components/Autocomplete.n +59 -0
  8. package/src/components/Avatar.n +27 -0
  9. package/src/components/AvatarGroup.n +25 -0
  10. package/src/components/Badge.n +20 -0
  11. package/src/components/Banner.n +25 -0
  12. package/src/components/Breadcrumbs.n +22 -0
  13. package/src/components/Btn.n +29 -8
  14. package/src/components/ButtonGroup.n +14 -0
  15. package/src/components/Calendar.n +153 -0
  16. package/src/components/Card.n +34 -0
  17. package/src/components/Carousel.n +66 -0
  18. package/src/components/Chip.n +27 -0
  19. package/src/components/Collapsible.n +32 -0
  20. package/src/components/ColorPicker.n +62 -0
  21. package/src/components/ContextMenu.n +48 -0
  22. package/src/components/CopyButton.n +50 -0
  23. package/src/components/DataTable.n +61 -0
  24. package/src/components/DatePicker.n +68 -0
  25. package/src/components/DateRangePicker.n +59 -0
  26. package/src/components/Dialog.n +54 -0
  27. package/src/components/Drawer.n +49 -0
  28. package/src/components/Dropdown.n +41 -0
  29. package/src/components/Field.n +29 -20
  30. package/src/components/Form.n +22 -0
  31. package/src/components/GradientText.n +21 -0
  32. package/src/components/Header.n +23 -0
  33. package/src/components/HeroBanner.n +37 -0
  34. package/src/components/HierarchicalMenu.n +38 -0
  35. package/src/components/Icon.n +16 -0
  36. package/src/components/Image.n +31 -0
  37. package/src/components/MegaMenu.n +36 -0
  38. package/src/components/MultiSelect.n +56 -0
  39. package/src/components/NumberInput.n +49 -0
  40. package/src/components/OtpField.n +67 -0
  41. package/src/components/Pagination.n +73 -0
  42. package/src/components/Popover.n +32 -0
  43. package/src/components/PreviewCard.n +30 -0
  44. package/src/components/Progress.n +26 -0
  45. package/src/components/ProgressCircular.n +52 -0
  46. package/src/components/RichTooltip.n +33 -0
  47. package/src/components/RippleButton.n +56 -0
  48. package/src/components/ScrollArea.n +31 -0
  49. package/src/components/Separator.n +12 -0
  50. package/src/components/Sheet.n +55 -0
  51. package/src/components/ShinyButton.n +36 -0
  52. package/src/components/Skeleton.n +27 -0
  53. package/src/components/Stepper.n +35 -0
  54. package/src/components/Surface.n +10 -0
  55. package/src/components/Tabs.n +28 -0
  56. package/src/components/TagsInput.n +66 -0
  57. package/src/components/ThemeToggler.n +43 -0
  58. package/src/components/TimePicker.n +166 -0
  59. package/src/components/Timeline.n +27 -0
  60. package/src/components/ToastProvider.n +16 -0
  61. package/src/components/ToggleButton.n +29 -0
  62. package/src/components/ToggleButtonGroup.n +34 -0
  63. package/src/components/Toolbar.n +15 -0
  64. package/src/components/Tooltip.n +34 -0
  65. package/src/components/Tree.n +57 -0
  66. package/src/components/Uploader.n +88 -0
  67. package/src/components/Video.n +26 -0
  68. package/src/index.js +76 -0
  69. package/src/lib/toast.svelte.js +42 -0
  70. package/src/motion/AnimatedNumber.n +55 -0
  71. package/src/motion/GradientBackground.n +25 -0
  72. package/src/motion/LiquidButton.n +48 -0
  73. package/src/motion/Reveal.n +37 -0
  74. package/src/motion/Sparkles.n +53 -0
  75. package/src/motion/index.js +18 -0
  76. package/src/styles/atoms.css +1794 -71
  77. package/src/styles/tokens.css +171 -40
  78. package/src/types/Accordion.d.ts +17 -0
  79. package/src/types/AnimatedNumber.d.ts +15 -0
  80. package/src/types/Audio.d.ts +18 -0
  81. package/src/types/Autocomplete.d.ts +17 -0
  82. package/src/types/Avatar.d.ts +14 -0
  83. package/src/types/AvatarGroup.d.ts +15 -0
  84. package/src/types/Badge.d.ts +14 -0
  85. package/src/types/Banner.d.ts +15 -0
  86. package/src/types/Breadcrumbs.d.ts +15 -0
  87. package/src/types/Btn.d.ts +10 -0
  88. package/src/types/ButtonGroup.d.ts +10 -0
  89. package/src/types/Calendar.d.ts +20 -0
  90. package/src/types/Card.d.ts +19 -0
  91. package/src/types/Carousel.d.ts +23 -0
  92. package/src/types/Chip.d.ts +12 -0
  93. package/src/types/Collapsible.d.ts +12 -0
  94. package/src/types/ColorPicker.d.ts +15 -0
  95. package/src/types/ContextMenu.d.ts +19 -0
  96. package/src/types/CopyButton.d.ts +16 -0
  97. package/src/types/DataTable.d.ts +27 -0
  98. package/src/types/DatePicker.d.ts +18 -0
  99. package/src/types/DateRangePicker.d.ts +17 -0
  100. package/src/types/Dialog.d.ts +17 -0
  101. package/src/types/Drawer.d.ts +18 -0
  102. package/src/types/Dropdown.d.ts +23 -0
  103. package/src/types/Field.d.ts +14 -3
  104. package/src/types/Form.d.ts +17 -0
  105. package/src/types/GradientBackground.d.ts +14 -0
  106. package/src/types/GradientText.d.ts +19 -0
  107. package/src/types/Header.d.ts +12 -0
  108. package/src/types/HeroBanner.d.ts +17 -0
  109. package/src/types/HierarchicalMenu.d.ts +20 -0
  110. package/src/types/Icon.d.ts +21 -0
  111. package/src/types/Image.d.ts +15 -0
  112. package/src/types/LiquidButton.d.ts +15 -0
  113. package/src/types/MegaMenu.d.ts +29 -0
  114. package/src/types/MultiSelect.d.ts +13 -0
  115. package/src/types/NumberInput.d.ts +17 -0
  116. package/src/types/OtpField.d.ts +13 -0
  117. package/src/types/Pagination.d.ts +13 -0
  118. package/src/types/Popover.d.ts +18 -0
  119. package/src/types/PreviewCard.d.ts +16 -0
  120. package/src/types/Progress.d.ts +14 -0
  121. package/src/types/ProgressCircular.d.ts +14 -0
  122. package/src/types/Reveal.d.ts +20 -0
  123. package/src/types/RichTooltip.d.ts +16 -0
  124. package/src/types/RippleButton.d.ts +15 -0
  125. package/src/types/ScrollArea.d.ts +11 -0
  126. package/src/types/Separator.d.ts +9 -0
  127. package/src/types/Sheet.d.ts +20 -0
  128. package/src/types/ShinyButton.d.ts +15 -0
  129. package/src/types/Skeleton.d.ts +13 -0
  130. package/src/types/Sparkles.d.ts +13 -0
  131. package/src/types/Stepper.d.ts +19 -0
  132. package/src/types/Surface.d.ts +10 -0
  133. package/src/types/Tabs.d.ts +17 -0
  134. package/src/types/TagsInput.d.ts +17 -0
  135. package/src/types/ThemeToggler.d.ts +17 -0
  136. package/src/types/TimePicker.d.ts +20 -0
  137. package/src/types/Timeline.d.ts +17 -0
  138. package/src/types/ToastProvider.d.ts +15 -0
  139. package/src/types/ToggleButton.d.ts +15 -0
  140. package/src/types/ToggleButtonGroup.d.ts +20 -0
  141. package/src/types/Toolbar.d.ts +12 -0
  142. package/src/types/Tooltip.d.ts +15 -0
  143. package/src/types/Tree.d.ts +20 -0
  144. package/src/types/Uploader.d.ts +15 -0
  145. 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>
@@ -1,46 +1,55 @@
1
1
  .field(class!="{classes}")
2
2
  +if('label')
3
- label.field-label(for!="{ctx.id}")
3
+ label.field-label(for!="{resolvedId}")
4
4
  | {label}
5
5
  +if('required')
6
6
  span.field-required *
7
7
  | {@render children?.()}
8
- +if('error')
9
- p.field-error(role="alert" id!="{ctx.id ? `${ctx.id}-error` : undefined}")
10
- | {error}
11
- +if('!error && help')
12
- p.field-help(id!="{ctx.id ? `${ctx.id}-help` : undefined}")
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
- // Deterministic id derivation:
30
- // 1. explicit prop wins
31
- // 2. otherwise slugify the label so SSR + hydrate produce the same id
32
- // 3. otherwise undefined — without a label, no <label for=> association
33
- // is needed anyway
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 (Input, Textarea, Select, etc.) so they pick up
38
- // the id automatically and switch to error styling. $state proxy so
39
- // children's reads are reactive when `error` changes.
40
- ctx := $state({ id: resolvedId, hasError: !!error })
41
- $effect ->
42
- ctx.id = resolvedId
43
- ctx.hasError = !!error
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
@@ -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>