noph-ui 0.44.0 → 0.45.0

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 (135) hide show
  1. package/README.md +10 -1
  2. package/dist/animation.d.ts +17 -0
  3. package/dist/animation.js +48 -0
  4. package/dist/app-bar/AppBar.svelte +280 -0
  5. package/dist/app-bar/AppBar.svelte.d.ts +4 -0
  6. package/dist/app-bar/index.d.ts +1 -0
  7. package/dist/app-bar/index.js +1 -0
  8. package/dist/app-bar/types.d.ts +35 -0
  9. package/dist/app-bar/types.js +1 -0
  10. package/dist/autocomplete/AutoComplete.svelte +10 -11
  11. package/dist/autocomplete/AutoComplete.svelte.d.ts +1 -1
  12. package/dist/autocomplete/types.d.ts +1 -1
  13. package/dist/badge/Badge.svelte +6 -1
  14. package/dist/badge/types.d.ts +0 -1
  15. package/dist/button/Button.svelte +5 -15
  16. package/dist/button/IconButton.svelte +5 -15
  17. package/dist/button/SegmentedButton.svelte +15 -1
  18. package/dist/button/SplitButton.svelte +92 -0
  19. package/dist/button/SplitButton.svelte.d.ts +4 -0
  20. package/dist/button/index.d.ts +1 -0
  21. package/dist/button/index.js +1 -0
  22. package/dist/button/types.d.ts +23 -4
  23. package/dist/card/types.d.ts +1 -1
  24. package/dist/checkbox/Checkbox.svelte +49 -58
  25. package/dist/chip/AssistChip.svelte +5 -5
  26. package/dist/chip/ChipSet.svelte +3 -11
  27. package/dist/chip/FilterChip.svelte +35 -27
  28. package/dist/chip/InputChip.svelte +13 -2
  29. package/dist/chip/SuggestionChip.svelte +29 -0
  30. package/dist/chip/SuggestionChip.svelte.d.ts +4 -0
  31. package/dist/chip/index.d.ts +1 -0
  32. package/dist/chip/index.js +1 -0
  33. package/dist/chip/types.d.ts +24 -9
  34. package/dist/date-picker/Calendar.svelte +1 -1
  35. package/dist/date-picker/DatePickerDialog.svelte +30 -8
  36. package/dist/date-picker/DatePickerDialog.svelte.d.ts +4 -1
  37. package/dist/date-picker/DateRangePicker.svelte +45 -10
  38. package/dist/date-picker/DateRangePicker.svelte.d.ts +4 -1
  39. package/dist/date-picker/DockedDatePicker.svelte +18 -4
  40. package/dist/date-picker/DockedDatePicker.svelte.d.ts +4 -1
  41. package/dist/date-picker/DockedDateTimePicker.svelte +18 -4
  42. package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +4 -1
  43. package/dist/date-picker/SelectionList.svelte +6 -32
  44. package/dist/date-picker/SelectionList.svelte.d.ts +2 -12
  45. package/dist/date-picker/TimeColumn.svelte +4 -29
  46. package/dist/date-picker/exitVisibility.svelte.d.ts +10 -0
  47. package/dist/date-picker/exitVisibility.svelte.js +30 -0
  48. package/dist/date-picker/optionList.d.ts +16 -0
  49. package/dist/date-picker/optionList.js +43 -0
  50. package/dist/date-picker/types.d.ts +11 -4
  51. package/dist/dialog/Dialog.svelte +77 -68
  52. package/dist/dialog/Dialog.svelte.d.ts +3 -3
  53. package/dist/dialog/types.d.ts +5 -3
  54. package/dist/fab/ExtendedFab.svelte +281 -0
  55. package/dist/fab/ExtendedFab.svelte.d.ts +5 -0
  56. package/dist/fab/Fab.svelte +239 -0
  57. package/dist/fab/Fab.svelte.d.ts +5 -0
  58. package/dist/fab/FabMenu.svelte +255 -0
  59. package/dist/fab/FabMenu.svelte.d.ts +7 -0
  60. package/dist/fab/index.d.ts +3 -0
  61. package/dist/fab/index.js +3 -0
  62. package/dist/fab/types.d.ts +58 -0
  63. package/dist/fab/types.js +1 -0
  64. package/dist/icons/ArrowBackIcon.svelte +13 -0
  65. package/dist/icons/ArrowBackIcon.svelte.d.ts +7 -0
  66. package/dist/icons/ChevronDownIcon.svelte +3 -0
  67. package/dist/icons/ChevronDownIcon.svelte.d.ts +26 -0
  68. package/dist/icons/SearchIcon.svelte +15 -0
  69. package/dist/icons/SearchIcon.svelte.d.ts +7 -0
  70. package/dist/icons/index.d.ts +3 -0
  71. package/dist/icons/index.js +3 -0
  72. package/dist/index.d.ts +6 -0
  73. package/dist/index.js +6 -0
  74. package/dist/internal/focus-ring.css +12 -0
  75. package/dist/menu/Menu.svelte +12 -7
  76. package/dist/menu/Menu.svelte.d.ts +2 -2
  77. package/dist/menu/types.d.ts +0 -2
  78. package/dist/navigation-bar/NavigationBar.svelte +66 -0
  79. package/dist/navigation-bar/NavigationBar.svelte.d.ts +4 -0
  80. package/dist/navigation-bar/NavigationBarItem.svelte +261 -0
  81. package/dist/navigation-bar/NavigationBarItem.svelte.d.ts +5 -0
  82. package/dist/navigation-bar/index.d.ts +2 -0
  83. package/dist/navigation-bar/index.js +2 -0
  84. package/dist/navigation-bar/types.d.ts +29 -0
  85. package/dist/navigation-bar/types.js +1 -0
  86. package/dist/navigation-drawer/NavigationDrawer.svelte +128 -76
  87. package/dist/navigation-drawer/NavigationDrawer.svelte.d.ts +4 -1
  88. package/dist/navigation-drawer/NavigationDrawerItem.svelte +10 -3
  89. package/dist/navigation-drawer/types.d.ts +6 -1
  90. package/dist/navigation-rail/NavigationRailItem.svelte +16 -0
  91. package/dist/navigation-rail/types.d.ts +3 -0
  92. package/dist/popover.svelte.d.ts +14 -0
  93. package/dist/popover.svelte.js +31 -0
  94. package/dist/press.svelte.d.ts +19 -0
  95. package/dist/press.svelte.js +45 -0
  96. package/dist/radio/Radio.svelte +26 -7
  97. package/dist/radio/types.d.ts +3 -0
  98. package/dist/search/Search.svelte +472 -0
  99. package/dist/search/Search.svelte.d.ts +5 -0
  100. package/dist/search/index.d.ts +1 -0
  101. package/dist/search/index.js +1 -0
  102. package/dist/search/types.d.ts +39 -0
  103. package/dist/search/types.js +1 -0
  104. package/dist/select/Select.svelte +48 -87
  105. package/dist/sheet/Sheet.svelte +260 -0
  106. package/dist/sheet/Sheet.svelte.d.ts +7 -0
  107. package/dist/sheet/index.d.ts +1 -0
  108. package/dist/sheet/index.js +1 -0
  109. package/dist/sheet/types.d.ts +19 -0
  110. package/dist/sheet/types.js +1 -0
  111. package/dist/slider/Slider.svelte +154 -30
  112. package/dist/slider/types.d.ts +3 -0
  113. package/dist/snackbar/Snackbar.svelte +48 -37
  114. package/dist/snackbar/Snackbar.svelte.d.ts +2 -2
  115. package/dist/snackbar/types.d.ts +3 -2
  116. package/dist/switch/Switch.svelte +51 -20
  117. package/dist/switch/types.d.ts +3 -0
  118. package/dist/tabs/Tab.svelte +8 -12
  119. package/dist/tabs/Tabs.svelte +3 -3
  120. package/dist/text-field/TextField.svelte +2 -2
  121. package/dist/toolbar/Toolbar.svelte +133 -0
  122. package/dist/toolbar/Toolbar.svelte.d.ts +4 -0
  123. package/dist/toolbar/index.d.ts +1 -0
  124. package/dist/toolbar/index.js +1 -0
  125. package/dist/toolbar/types.d.ts +15 -0
  126. package/dist/toolbar/types.js +1 -0
  127. package/dist/tooltip/RichTooltip.svelte +25 -12
  128. package/dist/tooltip/RichTooltip.svelte.d.ts +2 -2
  129. package/dist/tooltip/Tooltip.svelte +22 -11
  130. package/dist/tooltip/Tooltip.svelte.d.ts +4 -1
  131. package/dist/types.d.ts +6 -0
  132. package/dist/types.js +6 -0
  133. package/package.json +13 -13
  134. package/dist/date-picker/enterAnimation.d.ts +0 -1
  135. package/dist/date-picker/enterAnimation.js +0 -10
@@ -1,12 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
3
+ import { syncOpenEffect } from '../popover.svelte.js'
3
4
  import type { NavigationDrawerProps } from './types.js'
5
+
4
6
  let {
5
7
  modal = false,
6
8
  backdrop = false,
9
+ open = $bindable(false),
7
10
  element = $bindable(),
8
11
  direction = 'ltr',
9
- popover,
10
12
  children,
11
13
  onkeydown: userKeydown,
12
14
  ontoggle,
@@ -21,83 +23,123 @@
21
23
  if (!event.defaultPrevented) arrowHandler(event)
22
24
  }
23
25
 
24
- let previouslyFocused: HTMLElement | undefined
25
- let inerted: HTMLElement[] = []
26
+ /* Only a modal drawer is a dialog; a standard one is a nav that is always in the layout. */
27
+ const asDialog = () => (element instanceof HTMLDialogElement ? element : undefined)
26
28
 
27
- const trapFocus = () => {
28
- if (!element) return
29
- previouslyFocused = document.activeElement as HTMLElement | undefined
30
- for (let node: HTMLElement | null = element; node && node !== document.body;) {
31
- const parent: HTMLElement | null = node.parentElement
32
- if (!parent) break
33
- for (const sibling of parent.children) {
34
- if (sibling === node || !(sibling instanceof HTMLElement) || sibling.inert) continue
35
- sibling.inert = true
36
- inerted.push(sibling)
37
- }
38
- node = parent
39
- }
40
- element.querySelector<HTMLElement>('.np-navigation-drawer-item')?.focus()
29
+ export const show = () => {
30
+ const dialog = asDialog()
31
+ if (dialog && !dialog.open) dialog.showModal()
41
32
  }
42
33
 
43
- const releaseFocus = () => {
44
- for (const node of inerted) node.inert = false
45
- inerted = []
46
- previouslyFocused?.focus?.()
47
- previouslyFocused = undefined
34
+ export const close = () => {
35
+ asDialog()?.close()
48
36
  }
37
+
38
+ syncOpenEffect(
39
+ () => (modal ? element : undefined),
40
+ () => open,
41
+ show,
42
+ close,
43
+ )
44
+
45
+ const start = $derived(direction === 'ltr' ? 'translateX(-100%)' : 'translateX(100%)')
49
46
  </script>
50
47
 
51
- <nav
52
- {...attributes}
53
- {@attach attach}
54
- bind:this={element}
55
- popover={modal ? popover || 'auto' : undefined}
56
- style="--np-navigation-drawer-start: {direction === 'ltr'
57
- ? 'translateX(-100%)'
58
- : 'translateX(100%)'}"
59
- class={[
60
- 'np-navigation-drawer-container',
61
- modal && 'np-navigation-drawer-container-modal',
62
- backdrop && 'np-navigation-drawer-backdrop',
63
- attributes.class,
64
- ]}
65
- onkeydown={handleKeydown}
66
- ontoggle={(event) => {
67
- if (modal) {
68
- if (event.newState === 'open') trapFocus()
69
- else releaseFocus()
70
- }
71
- ontoggle?.(event)
72
- }}
73
- >
74
- {#if backdrop}
75
- <div
76
- role="none"
77
- class="np-backdrop"
78
- onclick={() => {
79
- element?.hidePopover()
80
- }}
81
- ></div>
82
- {/if}
83
- <div class={['np-navigation-wrapper', modal && 'np-navigation-drawer-shade']}>
84
- <div class="np-navigation-drawer">
85
- {#if children}
86
- {@render children()}
87
- {/if}
48
+ <!--
49
+ A modal drawer is a `dialog` holding a nav landmark, so the browser keeps the page behind it
50
+ inert for as long as it is open and hands focus back on close. `::backdrop` is the scrim and
51
+ `closedby="any"` the tap-outside-to-close. Where `closedby` is unsupported the click handler
52
+ below covers it: a click on the backdrop is reported against the dialog itself.
53
+ -->
54
+ {#if modal}
55
+ <dialog
56
+ {...attributes}
57
+ {@attach attach}
58
+ bind:this={element}
59
+ tabindex="-1"
60
+ aria-label={null}
61
+ aria-labelledby={null}
62
+ closedby="any"
63
+ style="--np-navigation-drawer-start: {start}; {attributes.style ?? ''}"
64
+ class={[
65
+ 'np-navigation-drawer-container',
66
+ 'np-navigation-drawer-container-modal',
67
+ backdrop && 'np-navigation-drawer-backdrop',
68
+ attributes.class,
69
+ ]}
70
+ onkeydown={handleKeydown}
71
+ ontoggle={(event) => {
72
+ open = event.newState === 'open'
73
+ /*
74
+ * Without this the browser autofocuses the first focusable descendant, the first nav
75
+ * item. Focus the drawer itself instead, so nothing is focused that the user didn't
76
+ * choose to interact with.
77
+ */
78
+ if (event.newState === 'open') element?.focus()
79
+ ontoggle?.(event)
80
+ }}
81
+ onclick={(event) => {
82
+ attributes.onclick?.(event)
83
+ if (event.target === element) close()
84
+ }}
85
+ >
86
+ <!--
87
+ The label belongs to the navigation landmark, not to the dialog wrapping it, so it is
88
+ pulled back off the dialog above and put here. Everything else stays on the dialog: it
89
+ is the container the layout and the custom properties are written against, and the
90
+ element `commandfor` has to reach.
91
+ -->
92
+ <nav
93
+ aria-label={attributes['aria-label']}
94
+ aria-labelledby={attributes['aria-labelledby']}
95
+ class="np-navigation-wrapper np-navigation-drawer-shade"
96
+ >
97
+ <div class="np-navigation-drawer">
98
+ {@render children?.()}
99
+ </div>
100
+ </nav>
101
+ </dialog>
102
+ {:else}
103
+ <nav
104
+ {...attributes}
105
+ {@attach attach}
106
+ bind:this={element}
107
+ class={['np-navigation-drawer-container', attributes.class]}
108
+ onkeydown={handleKeydown}
109
+ >
110
+ <div class="np-navigation-wrapper">
111
+ <div class="np-navigation-drawer">
112
+ {@render children?.()}
113
+ </div>
88
114
  </div>
89
- </div>
90
- </nav>
115
+ </nav>
116
+ {/if}
91
117
 
92
118
  <style>
93
119
  .np-navigation-drawer-container {
94
120
  color: var(--np-color-on-surface-variant);
95
121
  width: calc(var(--np-navigation-drawer-width, 22.5rem) + 3px);
96
122
  border: 0;
123
+ /* Focus lands here on open only to stay off the first nav item, not as something to show. */
124
+ outline: none;
97
125
  margin: 0;
98
126
  padding: 0;
99
127
  background-color: transparent;
128
+ max-width: none;
129
+ max-height: none;
100
130
  }
131
+
132
+ /* The modal drawer is docked to the inline edge rather than centred as a dialog would be. */
133
+ .np-navigation-drawer-container-modal {
134
+ position: fixed;
135
+ inset-block: 0;
136
+ height: 100dvh;
137
+ overflow: visible;
138
+ }
139
+ .np-navigation-drawer-container-modal:not([open]) {
140
+ display: none;
141
+ }
142
+
101
143
  .np-navigation-wrapper {
102
144
  background-color: var(--np-navigation-drawer-background, var(--np-color-surface-container-low));
103
145
  border-start-end-radius: var(--np-shape-corner-large);
@@ -106,33 +148,32 @@
106
148
  height: var(--np-navigation-drawer-height, 100dvh);
107
149
  overflow-y: auto;
108
150
  scrollbar-width: thin;
151
+ display: block;
109
152
  }
110
153
 
111
- .np-navigation-drawer-container[popover] .np-navigation-wrapper {
154
+ .np-navigation-drawer-container-modal .np-navigation-wrapper {
112
155
  transform: var(--np-navigation-drawer-start, translateX(-100%));
113
156
  }
114
-
115
- .np-navigation-drawer-container:popover-open .np-navigation-wrapper {
157
+ .np-navigation-drawer-container-modal[open] .np-navigation-wrapper {
116
158
  transform: translateX(0);
117
159
  }
118
160
 
119
161
  @media (prefers-reduced-motion: no-preference) {
120
- .np-navigation-drawer-container[popover] .np-navigation-wrapper {
162
+ .np-navigation-drawer-container-modal .np-navigation-wrapper {
121
163
  transition: transform var(--np-motion-standard-slow-spatial);
122
164
  }
123
-
124
- .np-navigation-drawer-container[popover] {
165
+ .np-navigation-drawer-container-modal {
125
166
  transition:
126
167
  overlay var(--np-motion-standard-slow-spatial) allow-discrete,
127
168
  display var(--np-motion-standard-slow-spatial) allow-discrete;
128
169
  }
129
-
130
- .np-navigation-drawer-container:popover-open .np-navigation-wrapper {
170
+ .np-navigation-drawer-container-modal[open] .np-navigation-wrapper {
131
171
  @starting-style {
132
172
  transform: var(--np-navigation-drawer-start, translateX(-100%));
133
173
  }
134
174
  }
135
175
  }
176
+
136
177
  .np-navigation-drawer {
137
178
  display: flex;
138
179
  padding: var(--np-navigation-drawer-padding, 1.25rem 0.75rem);
@@ -142,17 +183,28 @@
142
183
  box-shadow: var(--np-elevation-1);
143
184
  }
144
185
 
145
- .np-navigation-drawer-backdrop[popover] .np-backdrop {
146
- inset: 0;
147
- position: fixed;
186
+ /* `backdrop` decides whether the scrim is painted; a modal dialog always has the layer. */
187
+ .np-navigation-drawer-container-modal::backdrop {
188
+ background-color: transparent;
189
+ }
190
+ .np-navigation-drawer-backdrop::backdrop {
148
191
  background-color: var(--np-color-scrim);
149
192
  opacity: 0;
150
- transition: opacity var(--np-motion-expressive-slow-effects);
151
193
  }
152
- .np-navigation-drawer-backdrop[popover]:popover-open .np-backdrop {
194
+ .np-navigation-drawer-backdrop[open]::backdrop {
153
195
  opacity: 0.32;
154
- @starting-style {
155
- opacity: 0;
196
+ }
197
+ @media (prefers-reduced-motion: no-preference) {
198
+ .np-navigation-drawer-backdrop::backdrop {
199
+ transition:
200
+ opacity var(--np-motion-expressive-slow-effects),
201
+ overlay var(--np-motion-expressive-slow-effects) allow-discrete,
202
+ display var(--np-motion-expressive-slow-effects) allow-discrete;
203
+ }
204
+ .np-navigation-drawer-backdrop[open]::backdrop {
205
+ @starting-style {
206
+ opacity: 0;
207
+ }
156
208
  }
157
209
  }
158
210
  </style>
@@ -1,4 +1,7 @@
1
1
  import type { NavigationDrawerProps } from './types.ts';
2
- declare const NavigationDrawer: import("svelte").Component<NavigationDrawerProps, {}, "element">;
2
+ declare const NavigationDrawer: import("svelte").Component<NavigationDrawerProps, {
3
+ show: () => void;
4
+ close: () => void;
5
+ }, "element" | "open">;
3
6
  type NavigationDrawer = ReturnType<typeof NavigationDrawer>;
4
7
  export default NavigationDrawer;
@@ -7,7 +7,8 @@
7
7
  let {
8
8
  selected = false,
9
9
  label,
10
- badgeLabelText,
10
+ badgeLabel,
11
+ badgeAriaLabel,
11
12
  icon,
12
13
  element = $bindable(),
13
14
  ...attributes
@@ -24,8 +25,14 @@
24
25
  {/if}
25
26
  </div>
26
27
  <div class="np-navigation-drawer-item-label">{label}</div>
27
- {#if badgeLabelText}
28
- <div class="np-navigation-drawer-item-badge">{badgeLabelText}</div>
28
+ {#if badgeLabel}
29
+ <div
30
+ class="np-navigation-drawer-item-badge"
31
+ role={badgeAriaLabel ? 'status' : undefined}
32
+ aria-label={badgeAriaLabel}
33
+ >
34
+ {badgeLabel}
35
+ </div>
29
36
  {/if}
30
37
  {/snippet}
31
38
 
@@ -1,8 +1,12 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAnchorAttributes, HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements';
3
3
  export interface NavigationDrawerProps extends HTMLAttributes<HTMLElement> {
4
+ /** A modal drawer is a `dialog` over the page; a standard one is part of the layout. */
4
5
  modal?: boolean;
6
+ /** Paints the scrim behind a modal drawer. */
5
7
  backdrop?: boolean;
8
+ /** Bindable. Whether a modal drawer is showing. Ignored by a standard drawer. */
9
+ open?: boolean;
6
10
  element?: HTMLElement;
7
11
  direction?: 'rtl' | 'ltr';
8
12
  }
@@ -11,7 +15,8 @@ export interface NavigationDrawerItemProps extends HTMLAttributes<NavigationDraw
11
15
  icon?: Snippet;
12
16
  label: string;
13
17
  selected?: boolean;
14
- badgeLabelText?: string;
18
+ badgeLabel?: string;
19
+ badgeAriaLabel?: string;
15
20
  type?: 'submit' | 'reset' | 'button' | (string & {}) | null;
16
21
  element?: NavigationDrawerItemElement;
17
22
  }
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import '../internal/focus-ring.css'
3
+ import Badge from '../badge/Badge.svelte'
3
4
  import Ripple from '../ripple/Ripple.svelte'
4
5
  import type { HTMLButtonAttributes } from 'svelte/elements'
5
6
  import type { NavigationRailItemProps } from './types.js'
@@ -8,6 +9,9 @@
8
9
  selected,
9
10
  icon,
10
11
  label,
12
+ badge = false,
13
+ badgeLabel,
14
+ badgeAriaLabel,
11
15
  element = $bindable(),
12
16
  ...attributes
13
17
  }: NavigationRailItemProps = $props()
@@ -20,6 +24,11 @@
20
24
  <div class="np-navigation-action-icon">
21
25
  <Ripple forElement={touchEl} />
22
26
  {@render icon()}
27
+ {#if badge}
28
+ <div class="np-navigation-action-badge">
29
+ <Badge label={badgeLabel} aria-label={badgeAriaLabel} />
30
+ </div>
31
+ {/if}
23
32
  </div>
24
33
  <div class="np-navigation-action-label">{label}</div>
25
34
  <span class="np-touch" bind:this={touchEl}></span>
@@ -92,6 +101,13 @@
92
101
  transition: color var(--np-motion-expressive-default-effects);
93
102
  color: var(--np-color-on-surface-variant);
94
103
  }
104
+ .np-navigation-action-badge {
105
+ --np-badge-top: 0;
106
+ --np-badge-end: 0.75rem;
107
+ position: absolute;
108
+ inset: 0;
109
+ pointer-events: none;
110
+ }
95
111
  .np-navigation-action-selected .np-navigation-action-icon {
96
112
  color: var(--np-color-on-secondary-container);
97
113
  --np-icon-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
@@ -8,6 +8,9 @@ export interface NavigationRailItemProps extends HTMLAttributes<NavigationRailIt
8
8
  icon: Snippet;
9
9
  label: string;
10
10
  selected?: boolean;
11
+ badge?: boolean;
12
+ badgeLabel?: string | number;
13
+ badgeAriaLabel?: string;
11
14
  type?: 'submit' | 'reset' | 'button' | (string & {}) | null;
12
15
  element?: NavigationRailItemElement;
13
16
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `show()`/`close()` for a popover-backed overlay, guarded both ways. `showPopover()` throws if
3
+ * the popover is already showing and `hidePopover()` throws if it is already hidden, so both
4
+ * directions need the `:popover-open` check, not just the one that happens to get exercised first.
5
+ */
6
+ export declare const popoverController: (getElement: () => HTMLElement | undefined) => {
7
+ show: () => void;
8
+ close: () => void;
9
+ };
10
+ /**
11
+ * Keeps a bindable `open` prop in sync with an overlay's own `show`/`close`, so a caller can drive
12
+ * the overlay either by calling the exported methods or by writing to the bound prop.
13
+ */
14
+ export declare const syncOpenEffect: (getElement: () => HTMLElement | undefined, getOpen: () => boolean | undefined, show: () => void, close: () => void) => void;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `show()`/`close()` for a popover-backed overlay, guarded both ways. `showPopover()` throws if
3
+ * the popover is already showing and `hidePopover()` throws if it is already hidden, so both
4
+ * directions need the `:popover-open` check, not just the one that happens to get exercised first.
5
+ */
6
+ export const popoverController = (getElement) => ({
7
+ show: () => {
8
+ const element = getElement();
9
+ if (element && !element.matches(':popover-open'))
10
+ element.showPopover();
11
+ },
12
+ close: () => {
13
+ const element = getElement();
14
+ if (element?.matches(':popover-open'))
15
+ element.hidePopover();
16
+ },
17
+ });
18
+ /**
19
+ * Keeps a bindable `open` prop in sync with an overlay's own `show`/`close`, so a caller can drive
20
+ * the overlay either by calling the exported methods or by writing to the bound prop.
21
+ */
22
+ export const syncOpenEffect = (getElement, getOpen, show, close) => {
23
+ $effect(() => {
24
+ if (!getElement())
25
+ return;
26
+ if (getOpen() === true)
27
+ show();
28
+ else if (getOpen() === false)
29
+ close();
30
+ });
31
+ };
@@ -0,0 +1,19 @@
1
+ export declare const PRESS_DURATION = 100;
2
+ /**
3
+ * The corner morph a Button, an IconButton or a FAB plays when it is activated. `:active` covers
4
+ * the press itself, but it drops the moment the pointer lifts, so a quick click would show only a
5
+ * frame or two of the morph. The `pressed` this returns is held for as long as the round trip
6
+ * takes to be seen, and is set from `click`, which arrives for the keyboard as well as the pointer.
7
+ */
8
+ export declare const pressMorph: () => {
9
+ readonly pressed: boolean;
10
+ press: () => void;
11
+ /**
12
+ * A change of `shape` supersedes the morph. Both describe the same corner, and the pressed
13
+ * radius is the shape the element is not, so holding the press across a shape change cancels
14
+ * the change out and stalls it until the timer runs down. FabMenu's trigger does exactly
15
+ * that, flipping to `square` on the click that opens the menu; dropping the press there
16
+ * leaves that click one continuous morph.
17
+ */
18
+ supersededBy: (shape: () => unknown) => void;
19
+ };
@@ -0,0 +1,45 @@
1
+ import { onMount } from 'svelte';
2
+ export const PRESS_DURATION = 100;
3
+ /**
4
+ * The corner morph a Button, an IconButton or a FAB plays when it is activated. `:active` covers
5
+ * the press itself, but it drops the moment the pointer lifts, so a quick click would show only a
6
+ * frame or two of the morph. The `pressed` this returns is held for as long as the round trip
7
+ * takes to be seen, and is set from `click`, which arrives for the keyboard as well as the pointer.
8
+ */
9
+ export const pressMorph = () => {
10
+ let pressed = $state(false);
11
+ let timeout;
12
+ const cancel = () => {
13
+ clearTimeout(timeout);
14
+ pressed = false;
15
+ };
16
+ onMount(() => cancel);
17
+ return {
18
+ get pressed() {
19
+ return pressed;
20
+ },
21
+ press: () => {
22
+ clearTimeout(timeout);
23
+ pressed = true;
24
+ timeout = setTimeout(() => (pressed = false), PRESS_DURATION);
25
+ },
26
+ /**
27
+ * A change of `shape` supersedes the morph. Both describe the same corner, and the pressed
28
+ * radius is the shape the element is not, so holding the press across a shape change cancels
29
+ * the change out and stalls it until the timer runs down. FabMenu's trigger does exactly
30
+ * that, flipping to `square` on the click that opens the menu; dropping the press there
31
+ * leaves that click one continuous morph.
32
+ */
33
+ supersededBy: (shape) => {
34
+ let previous;
35
+ $effect(() => {
36
+ const next = shape();
37
+ const was = previous;
38
+ previous = next;
39
+ if (was === undefined || was === next)
40
+ return;
41
+ cancel();
42
+ });
43
+ },
44
+ };
45
+ };
@@ -9,14 +9,20 @@
9
9
  element = $bindable(),
10
10
  group = $bindable(),
11
11
  style,
12
+ issues,
12
13
  ...attributes
13
14
  }: RadioProps = $props()
14
15
 
15
16
  let inputEl = $state<HTMLInputElement>()
16
17
  const uid = $props.id()
18
+ let hasError = $derived(!!issues?.length)
17
19
  </script>
18
20
 
19
- <div {style} class={['np-container', attributes.class]} bind:this={element}>
21
+ <div
22
+ {style}
23
+ class={['np-radio-container', hasError && !attributes.disabled && 'np-error', attributes.class]}
24
+ bind:this={element}
25
+ >
20
26
  {#if !attributes.disabled}
21
27
  <Ripple forElement={inputEl} class="np-radio-ripple" />
22
28
  {/if}
@@ -70,7 +76,7 @@
70
76
  -webkit-tap-highlight-color: transparent;
71
77
  }
72
78
 
73
- .np-container {
79
+ .np-radio-container {
74
80
  margin: max(0px, (40px - var(--np-radio-icon-size, 20px))/2);
75
81
  display: inline-flex;
76
82
  vertical-align: top;
@@ -84,7 +90,7 @@
84
90
  cursor: pointer;
85
91
  }
86
92
 
87
- .np-container:has(input:disabled) {
93
+ .np-radio-container:has(input:disabled) {
88
94
  cursor: default;
89
95
  }
90
96
 
@@ -94,11 +100,11 @@
94
100
  position: absolute;
95
101
  }
96
102
 
97
- .np-container:has(input:checked) .np-radio-icon {
103
+ .np-radio-container:has(input:checked) .np-radio-icon {
98
104
  fill: var(--np-radio-selected-icon-color, var(--np-color-primary));
99
105
  }
100
106
 
101
- .np-container:has(input:disabled) .np-radio-icon {
107
+ .np-radio-container:has(input:disabled) .np-radio-icon {
102
108
  fill: var(--np-color-on-surface);
103
109
  opacity: 0.38;
104
110
  }
@@ -109,7 +115,7 @@
109
115
  transition: opacity 50ms linear;
110
116
  }
111
117
 
112
- .np-container:has(input:checked) .inner.circle {
118
+ .np-radio-container:has(input:checked) .inner.circle {
113
119
  animation: 300ms cubic-bezier(0.05, 0.7, 0.1, 1) 0s 1 normal none running inner-circle-grow;
114
120
  opacity: 1;
115
121
  }
@@ -123,7 +129,20 @@
123
129
  }
124
130
  }
125
131
 
126
- .np-container:has(input:focus-visible) {
132
+ .np-error .np-radio-icon {
133
+ fill: var(--np-color-error);
134
+ }
135
+
136
+ .np-error:has(input:checked) .np-radio-icon {
137
+ fill: var(--np-color-error);
138
+ }
139
+
140
+ .np-error {
141
+ --np-ripple-hover-color: var(--np-color-error);
142
+ --np-ripple-pressed-color: var(--np-color-error);
143
+ }
144
+
145
+ .np-radio-container:has(input:focus-visible) {
127
146
  outline-style: solid;
128
147
  outline-color: var(--np-color-secondary);
129
148
  outline-width: 3px;
@@ -2,4 +2,7 @@ import type { HTMLInputAttributes } from 'svelte/elements';
2
2
  export interface RadioProps extends Omit<HTMLInputAttributes, 'type'> {
3
3
  element?: HTMLElement;
4
4
  group?: string | number | null;
5
+ issues?: {
6
+ message: string;
7
+ }[];
5
8
  }