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,70 +1,81 @@
1
1
  <script lang="ts">
2
2
  import Button from '../button/Button.svelte'
3
3
  import IconButton from '../button/IconButton.svelte'
4
+ import { popoverController, syncOpenEffect } from '../popover.svelte.js'
4
5
  import type { SnackbarProps } from './types.js'
5
6
 
6
7
  let {
7
8
  label,
8
9
  supportingText,
9
10
  actionLabel,
10
- onActionClick,
11
+ onactionclick,
11
12
  icon,
12
- onIconClick = () => {
13
- element?.hidePopover()
14
- },
13
+ iconAriaLabel = 'Close',
14
+ oniconclick = () => close(),
15
15
  open = $bindable(false),
16
- onbeforetoggle,
16
+ ontoggle,
17
17
  timeout = 4000,
18
18
  element = $bindable(),
19
19
  popover = 'manual',
20
20
  ...attributes
21
21
  }: SnackbarProps = $props()
22
22
 
23
- let timeoutId: ReturnType<typeof setTimeout> | undefined
24
- const uid = $props.id()
23
+ let hovered = $state(false)
24
+ let focused = $state(false)
25
25
 
26
- export const showPopover = () => {
27
- element?.showPopover()
28
- }
29
-
30
- export const hidePopover = () => {
31
- element?.hidePopover()
32
- }
26
+ const controller = popoverController(() => element)
33
27
 
34
- const startTimer = () => {
35
- clearTimeout(timeoutId)
36
- if (!open || timeout <= 0) return
37
- timeoutId = setTimeout(() => {
38
- element?.hidePopover()
39
- }, timeout)
40
- }
28
+ export const show = () => controller.show()
29
+ export const close = () => controller.close()
41
30
 
42
- const pauseTimer = () => clearTimeout(timeoutId)
31
+ /*
32
+ * `toggle` fires once the popover has settled, so `open` coming back from the DOM already
33
+ * matches it and this does nothing. Syncing on `beforetoggle` instead would land in the middle
34
+ * of the browser's show operation, where `showPopover()` throws.
35
+ */
36
+ syncOpenEffect(
37
+ () => element,
38
+ () => open,
39
+ show,
40
+ close,
41
+ )
43
42
 
44
- $effect(() => () => clearTimeout(timeoutId))
43
+ /*
44
+ * The timeout lives and dies with the snackbar being open, so there is no timer to clear by
45
+ * hand: closing early or unmounting runs the cleanup. It holds while the snackbar is hovered or
46
+ * keeps focus, and starts over once the snackbar is left alone, so an action stays reachable.
47
+ */
48
+ $effect(() => {
49
+ if (!open || hovered || focused || timeout <= 0) return
50
+ const timeoutId = setTimeout(close, timeout)
51
+ return () => clearTimeout(timeoutId)
52
+ })
45
53
  </script>
46
54
 
55
+ <!--
56
+ `alert` is an atomic live region, so a screen reader reads the whole snackbar out when it appears,
57
+ label and supporting text together. It needs no name of its own for that, and the ARIA alert
58
+ pattern gives it none: naming the region after an element inside it only risks that text being
59
+ announced twice. A caller's own `aria-label` reaches the element through `attributes`.
60
+ -->
47
61
  <div
48
62
  {...attributes}
49
63
  {popover}
50
64
  class={['np-snackbar', attributes.class]}
51
65
  bind:this={element}
52
66
  role="alert"
53
- aria-labelledby="np-snackbar-label-{uid}"
54
- onpointerenter={pauseTimer}
55
- onpointerleave={startTimer}
56
- onfocusin={pauseTimer}
57
- onfocusout={startTimer}
58
- onbeforetoggle={(event) => {
59
- let { newState } = event
60
- open = newState === 'open'
61
- startTimer()
62
- onbeforetoggle?.(event)
67
+ onpointerenter={() => (hovered = true)}
68
+ onpointerleave={() => (hovered = false)}
69
+ onfocusin={() => (focused = true)}
70
+ onfocusout={() => (focused = false)}
71
+ ontoggle={(event) => {
72
+ open = event.newState === 'open'
73
+ ontoggle?.(event)
63
74
  }}
64
75
  >
65
76
  <div class="np-snackbar-inner">
66
77
  <div class="np-snackbar-label-container">
67
- <div id="np-snackbar-label-{uid}" class="np-snackbar-label">{label}</div>
78
+ <div class="np-snackbar-label">{label}</div>
68
79
  {#if supportingText}
69
80
  <div class="np-snackbar-supporting-text">{supportingText}</div>
70
81
  {/if}
@@ -74,7 +85,7 @@
74
85
  variant="text"
75
86
  --np-text-button-label-text-color="var(--np-snackbar-action-color, var(--np-color-inverse-primary))"
76
87
  aria-label={actionLabel}
77
- onclick={onActionClick}
88
+ onclick={onactionclick}
78
89
  >
79
90
  {actionLabel}
80
91
  </Button>
@@ -83,8 +94,8 @@
83
94
  <div class="np-snackbar-icon-container">
84
95
  <IconButton
85
96
  --np-icon-button-icon-color="var(--np-snackbar-text-color, var(--np-color-inverse-on-surface))"
86
- aria-label="Close"
87
- onclick={onIconClick}
97
+ aria-label={iconAriaLabel}
98
+ onclick={oniconclick}
88
99
  >
89
100
  {@render icon()}</IconButton
90
101
  >
@@ -1,7 +1,7 @@
1
1
  import type { SnackbarProps } from './types.ts';
2
2
  declare const Snackbar: import("svelte").Component<SnackbarProps, {
3
- showPopover: () => void;
4
- hidePopover: () => void;
3
+ show: () => void;
4
+ close: () => void;
5
5
  }, "element" | "open">;
6
6
  type Snackbar = ReturnType<typeof Snackbar>;
7
7
  export default Snackbar;
@@ -5,9 +5,10 @@ export interface SnackbarProps extends HTMLAttributes<HTMLDivElement> {
5
5
  open?: boolean;
6
6
  supportingText?: string | undefined;
7
7
  actionLabel?: string | undefined;
8
- onActionClick?: (event: Event) => void;
8
+ onactionclick?: (event: Event) => void;
9
9
  icon?: Snippet | undefined;
10
- onIconClick?: (event: Event) => void;
10
+ oniconclick?: (event: Event) => void;
11
+ iconAriaLabel?: string;
11
12
  timeout?: number;
12
13
  element?: HTMLElement;
13
14
  popover?: 'auto' | 'manual';
@@ -10,11 +10,23 @@
10
10
  inputElement = $bindable(),
11
11
  icons,
12
12
  onkeyup,
13
+ issues,
14
+ 'aria-invalid': ariaInvalid,
13
15
  ...attributes
14
16
  }: SwitchProps = $props()
17
+
18
+ let hasError = $derived(!!issues?.length)
15
19
  </script>
16
20
 
17
- <div bind:this={element} class={['np-switch', disabled && 'np-disabled', attributes.class]}>
21
+ <div
22
+ bind:this={element}
23
+ class={[
24
+ 'np-switch',
25
+ disabled && 'np-disabled',
26
+ hasError && !disabled && 'np-error',
27
+ attributes.class,
28
+ ]}
29
+ >
18
30
  <div class="np-track"></div>
19
31
  <div class={['np-handle', icons === 'both' && 'both-icons']}>
20
32
  {#if icons}
@@ -57,6 +69,7 @@
57
69
  bind:this={inputElement}
58
70
  bind:checked={selected}
59
71
  {disabled}
72
+ aria-invalid={hasError ? 'true' : ariaInvalid}
60
73
  />
61
74
  </div>
62
75
 
@@ -78,7 +91,7 @@
78
91
  width: 3.25rem;
79
92
  height: 2rem;
80
93
  background-color: var(
81
- --np-comp-switch-unselected-track-color,
94
+ --np-switch-unselected-track-color,
82
95
  var(--np-color-surface-container-highest)
83
96
  );
84
97
  border-radius: var(--np-shape-corner-full);
@@ -86,7 +99,7 @@
86
99
  background-color 0.2s,
87
100
  outline-color 0.2s;
88
101
  outline-width: 0.125rem;
89
- outline-color: var(--np-comp-switch-unselected-track-outline-color, var(--np-color-outline));
102
+ outline-color: var(--np-switch-unselected-track-outline-color, var(--np-color-outline));
90
103
  outline-style: solid;
91
104
  outline-offset: -0.125rem;
92
105
  }
@@ -100,8 +113,8 @@
100
113
  transition:
101
114
  transform var(--np-motion-expressive-fast-spatial),
102
115
  background-color var(--np-motion-expressive-default-effects);
103
- background-color: var(--np-comp-switch-unselected-handle-color, var(--np-color-outline));
104
- border-radius: var(--np-comp-switch-handle-shape, var(--np-shape-corner-full));
116
+ background-color: var(--np-switch-unselected-handle-color, var(--np-color-outline));
117
+ border-radius: var(--np-switch-handle-shape, var(--np-shape-corner-full));
105
118
  }
106
119
 
107
120
  .np-handle.both-icons {
@@ -110,12 +123,12 @@
110
123
 
111
124
  .np-state-layer {
112
125
  position: relative;
113
- width: var(--np-comp-switch-state-layer-size, 2.5rem);
114
- height: var(--np-comp-switch-state-layer-size, 2.5rem);
126
+ width: var(--np-switch-state-layer-size, 2.5rem);
127
+ height: var(--np-switch-state-layer-size, 2.5rem);
115
128
  margin-block-start: -0.25rem;
116
129
  margin-inline-start: -0.25rem;
117
130
  transition: var(--np-motion-expressive-fast-spatial);
118
- border-radius: var(--np-comp-switch-state-layer-shape, var(--np-shape-corner-full));
131
+ border-radius: var(--np-switch-state-layer-shape, var(--np-shape-corner-full));
119
132
  }
120
133
 
121
134
  .np-switch:has(input:checked) .np-state-layer {
@@ -152,37 +165,55 @@
152
165
 
153
166
  .np-switch:has(input:checked) .np-handle {
154
167
  transform: translateX(1.25rem) scale(1.5);
155
- color: var(--np-comp-switch-selected-icon-color, var(--np-color-primary));
156
- background-color: var(--np-comp-switch-selected-handle-color, var(--np-color-on-primary));
168
+ color: var(--np-switch-selected-icon-color, var(--np-color-primary));
169
+ background-color: var(--np-switch-selected-handle-color, var(--np-color-on-primary));
157
170
  }
158
171
 
159
172
  .np-disabled .np-handle {
160
- opacity: var(--np-comp-switch-disabled-unselected-handle-opacity, 0.38);
173
+ opacity: var(--np-switch-disabled-unselected-handle-opacity, 0.38);
161
174
  }
162
175
 
163
176
  .np-switch.np-disabled:has(input:checked) .np-handle {
164
- opacity: var(--np-comp-switch-disabled-selected-handle-opacity, 1);
165
- background-color: var(--np-comp-switch-selected-handle-color, var(--np-color-surface));
177
+ opacity: var(--np-switch-disabled-selected-handle-opacity, 1);
178
+ background-color: var(--np-switch-selected-handle-color, var(--np-color-surface));
166
179
  }
167
180
 
168
181
  .np-switch:has(input:checked) .np-track {
169
- background-color: var(--np-comp-switch-selected-track-color, var(--np-color-primary));
182
+ background-color: var(--np-switch-selected-track-color, var(--np-color-primary));
170
183
  outline-color: transparent;
171
184
  }
172
185
 
186
+ .np-error .np-track {
187
+ outline-color: var(--np-color-error);
188
+ }
189
+
190
+ .np-error:has(input:checked) .np-track {
191
+ background-color: var(--np-color-error);
192
+ }
193
+
194
+ .np-error:has(input:checked) .np-handle {
195
+ background-color: var(--np-color-on-error);
196
+ }
197
+
198
+ .np-error:has(input:checked) .np-switch-icon {
199
+ fill: var(--np-color-error);
200
+ }
201
+
202
+ .np-error {
203
+ --np-ripple-hover-color: var(--np-color-error);
204
+ --np-ripple-pressed-color: var(--np-color-error);
205
+ }
206
+
173
207
  .np-disabled .np-track {
174
- opacity: var(--np-comp-switch-disabled-track-opacity, 0.12);
208
+ opacity: var(--np-switch-disabled-track-opacity, 0.12);
175
209
  background-color: var(
176
- --np-comp-switch-disabled-unselected-track-color,
210
+ --np-switch-disabled-unselected-track-color,
177
211
  var(--np-color-surface-container-highest)
178
212
  );
179
213
  }
180
214
 
181
215
  .np-switch.np-disabled:has(input:checked) .np-track {
182
- background-color: var(
183
- --np-comp-switch-disabled-selected-track-color,
184
- var(--np-color-on-surface)
185
- );
216
+ background-color: var(--np-switch-disabled-selected-track-color, var(--np-color-on-surface));
186
217
  }
187
218
 
188
219
  .np-disabled {
@@ -5,4 +5,7 @@ export interface SwitchProps extends Omit<HTMLInputAttributes, 'type' | 'role' |
5
5
  icons?: 'selected' | 'both';
6
6
  inputElement?: HTMLInputElement;
7
7
  element?: HTMLDivElement;
8
+ issues?: {
9
+ message: string;
10
+ }[];
8
11
  }
@@ -2,7 +2,6 @@
2
2
  import '../internal/focus-ring.css'
3
3
  import Badge from '../badge/Badge.svelte'
4
4
  import Ripple from '../ripple/Ripple.svelte'
5
- import { tick } from 'svelte'
6
5
  import { getTabsContext } from './context.js'
7
6
  import type { TabProps } from './types.js'
8
7
 
@@ -24,16 +23,15 @@
24
23
 
25
24
  const tabsContext = getTabsContext()
26
25
 
26
+ /*
27
+ * The indicator follows the selection one flush behind, so the anchor it is tied to has moved
28
+ * to the tab that is already marked selected rather than to one still being rendered.
29
+ */
27
30
  $effect(() => {
28
31
  if (tabsContext.value === value) {
29
32
  tabsContext.indicatorValue = value
30
33
  }
31
34
  })
32
-
33
- const setIndicatorValue = async () => {
34
- await tick()
35
- tabsContext.indicatorValue = value
36
- }
37
35
  </script>
38
36
 
39
37
  {#snippet content()}
@@ -47,7 +45,7 @@
47
45
  {#if icon}
48
46
  {#if badge && tabsContext.variant === 'primary' && !inlineIcon}
49
47
  <div class="np-tab-icon-badge">
50
- <Badge label={badgeLabel} ariaLabel={badgeAriaLabel} />
48
+ <Badge label={badgeLabel} aria-label={badgeAriaLabel} />
51
49
  {@render icon?.()}
52
50
  </div>
53
51
  {:else}
@@ -58,7 +56,7 @@
58
56
  <div style="--np-badge-position:static;">
59
57
  <span class="np-tab-label-badge">{@render children?.()}</span><Badge
60
58
  label={badgeLabel}
61
- ariaLabel={badgeAriaLabel}
59
+ aria-label={badgeAriaLabel}
62
60
  />
63
61
  </div>
64
62
  {:else}
@@ -116,13 +114,11 @@
116
114
  ]}
117
115
  onclick={(event) => {
118
116
  tabsContext.value = value
119
- setIndicatorValue()
120
117
  onclick?.(event)
121
118
  }}
122
119
  onkeydown={(event) => {
123
120
  if (event.key === 'Enter' || event.key === ' ') {
124
121
  tabsContext.value = value
125
- setIndicatorValue()
126
122
  }
127
123
  onkeydown?.(event)
128
124
  }}
@@ -205,8 +201,8 @@
205
201
  @supports not (anchor-name: --np-tab-indicator) {
206
202
  .np-tab-content-active .np-indicator {
207
203
  background-color: var(--np-color-primary);
208
- border-start-start-radius: var(--np-indicator-radius, var(--np-shape-corner-full));
209
- border-start-end-radius: var(--np-indicator-radius, var(--np-shape-corner-full));
204
+ border-start-start-radius: var(--np-tabs-indicator-radius, var(--np-shape-corner-full));
205
+ border-start-end-radius: var(--np-tabs-indicator-radius, var(--np-shape-corner-full));
210
206
  }
211
207
  }
212
208
 
@@ -30,7 +30,7 @@
30
30
 
31
31
  const secondaryStyle = $derived(
32
32
  tabsContext.variant === 'secondary'
33
- ? '--np-indicator-radius: 0;--_indicator-gap: 0;--_indicator-height: 2px'
33
+ ? '--np-tabs-indicator-radius: 0;--_indicator-gap: 0;--_indicator-height: 2px'
34
34
  : '',
35
35
  )
36
36
 
@@ -77,8 +77,8 @@
77
77
  right: anchor(right);
78
78
  bottom: anchor(bottom);
79
79
  background-color: var(--np-color-primary);
80
- border-start-start-radius: var(--np-indicator-radius, var(--np-shape-corner-full));
81
- border-start-end-radius: var(--np-indicator-radius, var(--np-shape-corner-full));
80
+ border-start-start-radius: var(--np-tabs-indicator-radius, var(--np-shape-corner-full));
81
+ border-start-end-radius: var(--np-tabs-indicator-radius, var(--np-shape-corner-full));
82
82
  position-anchor: --np-tab-indicator;
83
83
 
84
84
  @media (prefers-reduced-motion: no-preference) {
@@ -54,7 +54,7 @@
54
54
  (children ? '--top-space:2rem;--bottom-space:1rem;' : '')) +
55
55
  `--_min-height:calc(${minLines} * 1lh);--_max-height:${`calc(${maxLines} * 1lh)`};` +
56
56
  style}
57
- class={['text-field', attributes.class]}
57
+ class={['np-text-field', attributes.class]}
58
58
  bind:this={element}
59
59
  bind:clientWidth
60
60
  bind:clientHeight
@@ -279,7 +279,7 @@
279
279
  height: 100%;
280
280
  position: relative;
281
281
  }
282
- .text-field {
282
+ .np-text-field {
283
283
  display: inline-flex;
284
284
  text-align: start;
285
285
  }
@@ -0,0 +1,133 @@
1
+ <script lang="ts">
2
+ import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
3
+ import type { ToolbarProps } from './types.js'
4
+
5
+ let {
6
+ variant = 'docked',
7
+ placement = 'bottom',
8
+ orientation = 'horizontal',
9
+ color = 'standard',
10
+ children,
11
+ element = $bindable(),
12
+ onkeydown: userKeydown,
13
+ ...attributes
14
+ }: ToolbarProps = $props()
15
+
16
+ const ITEMS = 'button, a[href], input:not([type="hidden"])'
17
+ const attach = rovingTabindex(ITEMS)
18
+ const arrowHandler = $derived(arrowKeyNav(ITEMS, orientation))
19
+
20
+ const handleKeydown = (event: KeyboardEvent & { currentTarget: EventTarget & HTMLElement }) => {
21
+ userKeydown?.(event)
22
+ if (!event.defaultPrevented) arrowHandler(event)
23
+ }
24
+ </script>
25
+
26
+ <div
27
+ {...attributes}
28
+ {@attach attach}
29
+ bind:this={element}
30
+ role="toolbar"
31
+ aria-orientation={orientation}
32
+ class={[
33
+ 'np-toolbar',
34
+ `np-toolbar-${variant}`,
35
+ variant === 'floating' && `np-toolbar-${placement}`,
36
+ variant === 'floating' && color === 'vibrant' && 'np-toolbar-vibrant',
37
+ orientation === 'vertical' && 'np-toolbar-vertical',
38
+ attributes.class,
39
+ ]}
40
+ onkeydown={handleKeydown}
41
+ >
42
+ {@render children?.()}
43
+ </div>
44
+
45
+ <style>
46
+ .np-toolbar {
47
+ display: flex;
48
+ align-items: center;
49
+ box-sizing: border-box;
50
+ gap: var(--np-toolbar-gap, 0.25rem);
51
+ color: var(--np-toolbar-color, var(--np-color-on-surface));
52
+ --np-icon-button-icon-color: var(--np-toolbar-color, var(--np-color-on-surface-variant));
53
+ }
54
+
55
+ .np-toolbar-vertical {
56
+ flex-direction: column;
57
+ /*
58
+ * Stacked actions sit right above one another, so a tooltip in its default place covers the
59
+ * button before it. Moving it to the trailing side keeps every action readable, and
60
+ * flip-inline sends it the other way when the window edge really is in the way. Material's
61
+ * Compose API exposes the same idea as TooltipAnchorPosition.End.
62
+ */
63
+ --np-tooltip-position-area: inline-end;
64
+ --np-tooltip-justify-self: auto;
65
+ --np-tooltip-align-self: anchor-center;
66
+ --np-tooltip-margin: 0 4px;
67
+ --np-tooltip-position-try-fallbacks: flip-inline;
68
+ --np-rich-tooltip-position-area: inline-end;
69
+ --np-rich-tooltip-justify-self: auto;
70
+ --np-rich-tooltip-align-self: anchor-center;
71
+ --np-rich-tooltip-margin: 0 4px;
72
+ --np-rich-tooltip-position-try-fallbacks: flip-inline;
73
+ }
74
+
75
+ /* Docked: full width, flush to the edge, no corner rounding. */
76
+ .np-toolbar-docked {
77
+ width: 100%;
78
+ min-height: 4rem;
79
+ justify-content: center;
80
+ padding-inline: 0.5rem;
81
+ background-color: var(--np-toolbar-container-color, var(--np-color-surface-container));
82
+ /* Clears the home inset when docked to the bottom of a phone screen. */
83
+ padding-block-end: max(0px, env(safe-area-inset-bottom));
84
+ }
85
+ .np-toolbar-docked.np-toolbar-vertical {
86
+ width: auto;
87
+ height: 100%;
88
+ min-height: 0;
89
+ min-width: 4rem;
90
+ padding-block: 0.5rem;
91
+ padding-inline: 0;
92
+ }
93
+
94
+ /*
95
+ * Floating: an island above the content. `position: sticky` keeps it in flow so it never
96
+ * covers the end of the page, which `fixed` would.
97
+ */
98
+ .np-toolbar-floating {
99
+ position: sticky;
100
+ z-index: 8;
101
+ width: fit-content;
102
+ margin-inline: auto;
103
+ padding: 0.5rem;
104
+ border-radius: var(--np-toolbar-shape, var(--np-shape-corner-full));
105
+ background-color: var(--np-toolbar-container-color, var(--np-color-surface-container));
106
+ box-shadow: var(--np-toolbar-elevation, var(--np-elevation-3));
107
+ }
108
+
109
+ .np-toolbar-bottom {
110
+ inset-block-end: var(--np-toolbar-inset, 1rem);
111
+ }
112
+ .np-toolbar-top {
113
+ inset-block-start: var(--np-toolbar-inset, 1rem);
114
+ }
115
+ .np-toolbar-start,
116
+ .np-toolbar-end {
117
+ margin-inline: 0;
118
+ margin-block: auto;
119
+ }
120
+ .np-toolbar-start {
121
+ inset-inline-start: var(--np-toolbar-inset, 1rem);
122
+ }
123
+ .np-toolbar-end {
124
+ inset-inline-end: var(--np-toolbar-inset, 1rem);
125
+ margin-inline-start: auto;
126
+ }
127
+
128
+ .np-toolbar-vibrant {
129
+ background-color: var(--np-toolbar-container-color, var(--np-color-primary-container));
130
+ --np-toolbar-color: var(--np-color-on-primary-container);
131
+ color: var(--np-toolbar-color);
132
+ }
133
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { ToolbarProps } from './types.ts';
2
+ declare const Toolbar: import("svelte").Component<ToolbarProps, {}, "element">;
3
+ type Toolbar = ReturnType<typeof Toolbar>;
4
+ export default Toolbar;
@@ -0,0 +1 @@
1
+ export { default as Toolbar } from './Toolbar.svelte';
@@ -0,0 +1 @@
1
+ export { default as Toolbar } from './Toolbar.svelte';
@@ -0,0 +1,15 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ export interface ToolbarProps extends HTMLAttributes<HTMLElement> {
3
+ /**
4
+ * `'docked'` spans the full width of its container and sits flush against an edge, for actions
5
+ * that stay the same across pages. It replaces M3's deprecated bottom app bar. `'floating'`
6
+ * hovers above the content as a rounded island, for actions specific to the page.
7
+ */
8
+ variant?: 'docked' | 'floating';
9
+ /** Which edge a floating toolbar hovers against. Ignored when docked. */
10
+ placement?: 'bottom' | 'top' | 'start' | 'end';
11
+ orientation?: 'horizontal' | 'vertical';
12
+ /** Colour role of a floating toolbar's container. */
13
+ color?: 'standard' | 'vibrant';
14
+ element?: HTMLElement;
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { popoverController, syncOpenEffect } from '../popover.svelte.js'
2
3
  import type { RichTooltipProps } from './types.js'
3
4
 
4
5
  let {
@@ -12,24 +13,35 @@
12
13
  ...attributes
13
14
  }: RichTooltipProps = $props()
14
15
 
16
+ /*
17
+ * The control the tooltip is opened from, passed to `showPopover()` as its source so a
18
+ * programmatic open gets the invoker relationship the declarative one gets for free: the
19
+ * tooltip counts as nested inside any popover its control sits in, rather than dismissing it.
20
+ * Either attribute names a control, since `commandfor` and `popovertarget` are separate
21
+ * properties that set the same relationship.
22
+ */
15
23
  const findControl = () =>
16
- id ? (document.querySelector<HTMLElement>(`[popovertarget="${id}"]`) ?? undefined) : undefined
24
+ id
25
+ ? (document.querySelector<HTMLElement>(`[commandfor="${id}"], [popovertarget="${id}"]`) ??
26
+ undefined)
27
+ : undefined
17
28
 
18
- export const showPopover = () => {
29
+ const controller = popoverController(() => element)
30
+
31
+ export const show = () => {
19
32
  if (!element || element.matches(':popover-open')) return
20
33
  const source = findControl()
21
34
  element.showPopover(source ? { source } : undefined)
22
35
  }
23
36
 
24
- export const hidePopover = () => {
25
- element?.hidePopover()
26
- }
37
+ export const close = () => controller.close()
27
38
 
28
- $effect(() => {
29
- if (!element) return
30
- if (open === true) showPopover()
31
- else if (open === false) hidePopover()
32
- })
39
+ syncOpenEffect(
40
+ () => element,
41
+ () => open,
42
+ show,
43
+ close,
44
+ )
33
45
  </script>
34
46
 
35
47
  <div
@@ -68,7 +80,7 @@
68
80
  max-width: 20rem;
69
81
  word-wrap: break-word;
70
82
  overflow-wrap: break-word;
71
- margin: 4px 0;
83
+ margin: var(--np-rich-tooltip-margin, 4px 0);
72
84
  padding: 0.25rem 1rem;
73
85
  border: none;
74
86
  border-radius: var(--np-shape-corner-medium);
@@ -78,8 +90,9 @@
78
90
  font-size: 0.875rem;
79
91
  line-height: 1.25rem;
80
92
  justify-self: var(--np-rich-tooltip-justify-self, anchor-center);
93
+ align-self: var(--np-rich-tooltip-align-self, auto);
81
94
  position-area: var(--np-rich-tooltip-position-area, bottom);
82
- position-try-fallbacks: flip-block;
95
+ position-try-fallbacks: var(--np-rich-tooltip-position-try-fallbacks, flip-block);
83
96
  }
84
97
  .np-rich-tooltip-with-subhead[popover] {
85
98
  padding-block: 0.75rem 1rem;
@@ -1,7 +1,7 @@
1
1
  import type { RichTooltipProps } from './types.ts';
2
2
  declare const RichTooltip: import("svelte").Component<RichTooltipProps, {
3
- showPopover: () => void;
4
- hidePopover: () => void;
3
+ show: () => void;
4
+ close: () => void;
5
5
  }, "element" | "open">;
6
6
  type RichTooltip = ReturnType<typeof RichTooltip>;
7
7
  export default RichTooltip;