noph-ui 0.45.0 → 0.45.1

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 (46) hide show
  1. package/dist/animation.d.ts +0 -15
  2. package/dist/animation.js +0 -15
  3. package/dist/app-bar/AppBar.svelte +2 -52
  4. package/dist/app-bar/types.d.ts +0 -22
  5. package/dist/button/SplitButton.svelte +0 -17
  6. package/dist/button/types.d.ts +0 -6
  7. package/dist/checkbox/Checkbox.svelte +0 -2
  8. package/dist/chip/FilterChip.svelte +0 -2
  9. package/dist/chip/types.d.ts +0 -8
  10. package/dist/date-picker/DatePickerDialog.svelte +0 -10
  11. package/dist/date-picker/DateRangePicker.svelte +0 -5
  12. package/dist/date-picker/DockedDatePicker.svelte +0 -11
  13. package/dist/date-picker/DockedDateTimePicker.svelte +0 -8
  14. package/dist/date-picker/exitVisibility.svelte.d.ts +0 -5
  15. package/dist/date-picker/exitVisibility.svelte.js +0 -7
  16. package/dist/date-picker/optionList.d.ts +0 -12
  17. package/dist/date-picker/optionList.js +0 -12
  18. package/dist/dialog/Dialog.svelte +0 -22
  19. package/dist/dialog/types.d.ts +0 -1
  20. package/dist/fab/ExtendedFab.svelte +0 -26
  21. package/dist/fab/Fab.svelte +0 -28
  22. package/dist/fab/FabMenu.svelte +0 -48
  23. package/dist/fab/types.d.ts +0 -24
  24. package/dist/loading-indicator/shapes.js +0 -7
  25. package/dist/navigation-bar/NavigationBar.svelte +0 -7
  26. package/dist/navigation-bar/NavigationBarItem.svelte +0 -22
  27. package/dist/navigation-bar/types.d.ts +0 -10
  28. package/dist/navigation-drawer/NavigationDrawer.svelte +0 -21
  29. package/dist/navigation-drawer/types.d.ts +0 -3
  30. package/dist/popover.svelte.d.ts +0 -9
  31. package/dist/popover.svelte.js +0 -9
  32. package/dist/press.svelte.d.ts +0 -13
  33. package/dist/press.svelte.js +0 -13
  34. package/dist/search/Search.svelte +0 -81
  35. package/dist/search/types.d.ts +0 -20
  36. package/dist/sheet/Sheet.svelte +0 -25
  37. package/dist/sheet/types.d.ts +0 -8
  38. package/dist/slider/Slider.svelte +0 -36
  39. package/dist/snackbar/Snackbar.svelte +3 -17
  40. package/dist/tabs/Tab.svelte +0 -4
  41. package/dist/tabs/Tabs.svelte +2 -2
  42. package/dist/text-field/TextField.svelte +0 -2
  43. package/dist/toolbar/Toolbar.svelte +0 -12
  44. package/dist/toolbar/types.d.ts +0 -7
  45. package/dist/tooltip/RichTooltip.svelte +0 -7
  46. package/package.json +1 -1
@@ -29,18 +29,9 @@
29
29
  const uid = $props.id()
30
30
  const resultsId = `np-search-results-${uid}`
31
31
 
32
- /*
33
- * A combobox has to own a popup ARIA recognises, so the field claims to be one only when the
34
- * results say what they are. Left alone they are ordinary content that appears below the
35
- * field, which is what a list, a set of categories or a row of chips actually is.
36
- */
37
32
  const comboboxPopups = ['listbox', 'tree', 'grid', 'dialog']
38
33
  const isCombobox = $derived(comboboxPopups.includes(String(resultsAttributes?.role)))
39
34
 
40
- /*
41
- * A focused text input always matches `:focus-visible`, pointer or not, so the ring is tracked
42
- * by hand: it shows only when focus did not arrive under a finger or a mouse.
43
- */
44
35
  let pointerFocus = false
45
36
  let focusRing = $state(false)
46
37
  </script>
@@ -56,19 +47,16 @@
56
47
  attributes.class,
57
48
  ]}
58
49
  onkeydown={(event) => {
59
- // A key press means whatever takes focus next is being reached with the keyboard.
60
50
  pointerFocus = false
61
51
  attributes.onkeydown?.(event)
62
52
  }}
63
53
  onfocusout={(event) => {
64
- // The open view floats over the page, so it closes the moment focus leaves it.
65
54
  const next = event.relatedTarget
66
55
  if (expanded && !(next instanceof Node && element?.contains(next))) expanded = false
67
56
  attributes.onfocusout?.(event)
68
57
  }}
69
58
  >
70
59
  <div class="np-search-container">
71
- <!-- The click only forwards focus to the input below, which is the actual control. -->
72
60
  <!-- svelte-ignore a11y_click_events_have_key_events -->
73
61
  <!-- svelte-ignore a11y_no_static_element_interactions -->
74
62
  <div
@@ -84,7 +72,6 @@
84
72
  <IconButton
85
73
  title={backLabel}
86
74
  onclick={(event) => {
87
- // Bubbling to the bar's own onclick would refocus the input and reopen the view.
88
75
  event.stopPropagation()
89
76
  expanded = false
90
77
  }}
@@ -139,7 +126,6 @@
139
126
  title={clearLabel}
140
127
  onclick={() => {
141
128
  value = ''
142
- // The focus below is a pointer's doing, so it must not raise the ring.
143
129
  pointerFocus = true
144
130
  inputElement?.focus()
145
131
  }}
@@ -153,11 +139,6 @@
153
139
  </span>
154
140
  </div>
155
141
 
156
- <!--
157
- The container brings no semantics of its own: what goes in it is a list one time and
158
- categories, avatars and chips the next. `resultsAttributes` is where a role belongs when
159
- the content has earned one.
160
- -->
161
142
  <div
162
143
  {...resultsAttributes}
163
144
  id={resultsId}
@@ -170,13 +151,7 @@
170
151
 
171
152
  <style>
172
153
  .np-search {
173
- /* SearchBarTokens: 56dp tall, fully rounded, surface-container-high. */
174
154
  --_bar-height: 3.5rem;
175
- /*
176
- * M3 Expressive grows the bar by shrinking the margins it keeps to its pane, from 24dp at
177
- * rest to 12dp once the view opens. Owning the margin is what makes that growth work
178
- * without the consumer having to name two widths.
179
- */
180
155
  --_pane-margin: var(--np-search-pane-margin, 1.5rem);
181
156
  display: flex;
182
157
  flex-direction: column;
@@ -195,27 +170,18 @@
195
170
  }
196
171
  }
197
172
 
198
- /*
199
- * Docked, the bar is all the component takes up in the layout: the view it opens floats over
200
- * the page rather than pushing it down, so the margins are the container's own insets.
201
- */
202
173
  .np-search-docked {
203
174
  position: relative;
204
175
  block-size: var(--_bar-height);
205
176
  padding-inline: 0;
206
177
  }
207
178
 
208
- /*
209
- * The container is only a surface in the divided style, where the bar and the results share
210
- * one. Contained keeps them apart, 2dp of gap between the two.
211
- */
212
179
  .np-search-container {
213
180
  display: flex;
214
181
  flex-direction: column;
215
182
  box-sizing: border-box;
216
183
  gap: 0.125rem;
217
184
  min-height: 0;
218
- /* Container width is 360dp at the least and 720dp at the most. */
219
185
  width: 100%;
220
186
  min-width: min(22.5rem, 100%);
221
187
  max-width: var(--np-search-width, 45rem);
@@ -225,18 +191,7 @@
225
191
  position: absolute;
226
192
  inset-block-start: 0;
227
193
  inset-inline: var(--_pane-margin);
228
- /*
229
- * A definite `width` rather than `auto`, which resolves outside the transition engine and
230
- * would snap the pane to its grown width the instant the class changes instead of easing
231
- * it in with the insets.
232
- */
233
194
  width: calc(100% - 2 * var(--_pane-margin));
234
- /*
235
- * An absolutely positioned box measures a percentage against the whole component, margins
236
- * included, so the floor has to leave room for them or a narrow host overflows. Also
237
- * transitioned below, in step with `width`, or it would snap to its own end value early
238
- * and clamp the still-transitioning `width` to it for the whole animation.
239
- */
240
195
  min-width: min(22.5rem, calc(100% - 2 * var(--_pane-margin)));
241
196
  z-index: var(--np-search-z-index, 3);
242
197
  }
@@ -250,11 +205,6 @@
250
205
  min-width var(--np-motion-expressive-default-spatial);
251
206
  }
252
207
 
253
- /*
254
- * The view unrolls from under the bar rather than arriving whole. Clipping is what does it,
255
- * so nothing about the bar moves and the pane's shape, rule and shadow are all uncovered
256
- * together, however tall the list turns out to be.
257
- */
258
208
  .np-search-docked.np-search-expanded .np-search-container {
259
209
  animation: unroll var(--np-motion-expressive-default-spatial);
260
210
  }
@@ -262,7 +212,6 @@
262
212
  animation: fadeIn var(--np-motion-expressive-default-effects);
263
213
  }
264
214
  }
265
- /* The clip is held off the edges so that it never cuts the shadow while it plays. */
266
215
  @keyframes unroll {
267
216
  from {
268
217
  clip-path: inset(-1.5rem -1.5rem calc(100% - var(--_bar-height)) -1.5rem);
@@ -283,12 +232,6 @@
283
232
  align-items: center;
284
233
  box-sizing: border-box;
285
234
  flex: none;
286
- /*
287
- * 8dp of padding and 8dp of gap put the glyphs 16dp inside the bar and the input text 16dp
288
- * past them, which is what the spec measures. The spec reaches those numbers with 48dp tap
289
- * targets and 4dp of space; the library's icon buttons are 40dp, so the space absorbs the
290
- * difference.
291
- */
292
235
  gap: 0.5rem;
293
236
  padding-inline: 0.5rem;
294
237
  height: var(--_bar-height);
@@ -308,7 +251,6 @@
308
251
  }
309
252
  }
310
253
 
311
- /* The bar is a tap target at rest. Once the view is open it is a header, and takes no states. */
312
254
  .np-search-state-layer {
313
255
  position: absolute;
314
256
  inset: 0;
@@ -326,7 +268,6 @@
326
268
  opacity: 0.1;
327
269
  }
328
270
 
329
- /* Positioned, so the bar's content paints over the state layer rather than under it. */
330
271
  .np-search-leading,
331
272
  .np-search-trailing,
332
273
  .np-search-input {
@@ -339,18 +280,15 @@
339
280
  align-items: center;
340
281
  flex: none;
341
282
  }
342
- /* Trailing actions sit flush against one another. */
343
283
  .np-search-trailing {
344
284
  gap: 0;
345
285
  }
346
- /* LeadingIconColor is on-surface; trailing is on-surface-variant. */
347
286
  .np-search-leading {
348
287
  --np-icon-button-icon-color: var(--np-color-on-surface);
349
288
  }
350
289
  .np-search-trailing {
351
290
  --np-icon-button-icon-color: var(--np-color-on-surface-variant);
352
291
  }
353
- /* Sized like the icon button that replaces it, so the glyph holds still as the view opens. */
354
292
  .np-search-leading-icon {
355
293
  display: flex;
356
294
  align-items: center;
@@ -368,7 +306,6 @@
368
306
  outline: none;
369
307
  background: none;
370
308
  font: inherit;
371
- /* InputTextFont is body-large. */
372
309
  font-size: 1rem;
373
310
  line-height: 1.5rem;
374
311
  letter-spacing: 0.03125rem;
@@ -380,7 +317,6 @@
380
317
  color: var(--np-color-on-surface-variant);
381
318
  opacity: 1;
382
319
  }
383
- /* The type="search" clear affordance would duplicate the component's own. */
384
320
  .np-search-input::-webkit-search-cancel-button {
385
321
  appearance: none;
386
322
  }
@@ -397,7 +333,6 @@
397
333
  }
398
334
  }
399
335
 
400
- /* Results are only in the layout, and only in the accessibility tree, once expanded. */
401
336
  .np-search-results {
402
337
  display: none;
403
338
  box-sizing: border-box;
@@ -408,20 +343,14 @@
408
343
  display: block;
409
344
  }
410
345
 
411
- /* Docked: the results sit under the field, growing to two thirds of the screen at the most. */
412
346
  .np-search-docked .np-search-results {
413
347
  max-height: var(--np-search-results-max-height, 66.6667dvh);
414
348
  }
415
- /* Contained docks them as their own 12dp pane. */
416
349
  .np-search-contained.np-search-docked .np-search-results {
417
350
  border-radius: var(--np-search-results-shape, var(--np-shape-corner-medium));
418
351
  background-color: var(--np-search-container-color, var(--np-color-surface-container-high));
419
352
  }
420
353
 
421
- /*
422
- * Divided (baseline) folds the field and the results into one 28dp container, separated by a
423
- * rule. Contained keeps the pill, which is what M3 recommends.
424
- */
425
354
  .np-search-divided.np-search-expanded .np-search-bar {
426
355
  border-radius: 0;
427
356
  background-color: transparent;
@@ -432,21 +361,15 @@
432
361
  border-radius: var(--np-search-results-shape, var(--np-shape-corner-extra-large));
433
362
  background-color: var(--np-search-container-color, var(--np-color-surface-container-high));
434
363
  }
435
- /*
436
- * The results round off the container's bottom corners themselves, rather than the container
437
- * clipping them, which would take the header's focus ring with it.
438
- */
439
364
  .np-search-divided.np-search-docked.np-search-expanded .np-search-results {
440
365
  border-end-start-radius: inherit;
441
366
  border-end-end-radius: inherit;
442
367
  }
443
368
 
444
- /* Full screen: the field and its results take over the viewport, and the view has no shape. */
445
369
  .np-search-full-screen.np-search-expanded {
446
370
  position: fixed;
447
371
  inset: 0;
448
372
  z-index: var(--np-search-z-index, 24);
449
- /* The pill needs the same room above it that it keeps at its sides. */
450
373
  padding-block-start: var(--_pane-margin);
451
374
  background-color: var(--np-search-view-background-color, var(--np-color-surface-container-low));
452
375
  }
@@ -458,10 +381,6 @@
458
381
  flex: 1;
459
382
  max-height: none;
460
383
  }
461
- /*
462
- * Contained holds the 56dp bar in full screen too. Only the divided header grows to 72dp, and
463
- * it runs edge to edge on the view's own surface.
464
- */
465
384
  .np-search-divided.np-search-full-screen.np-search-expanded {
466
385
  padding: 0;
467
386
  background-color: var(--np-search-container-color, var(--np-color-surface-container-high));
@@ -1,38 +1,18 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes, HTMLInputAttributes } from 'svelte/elements';
3
3
  export interface SearchProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onsearch'> {
4
- /** Bindable. The query. */
5
4
  value?: string;
6
- /** Hinted search text, shown while the field is empty. */
7
5
  placeholder?: string;
8
- /** Bindable. Whether the results are showing. */
9
6
  expanded?: boolean;
10
- /**
11
- * `'contained'` keeps the field's pill shape and colour while expanded, which M3 recommends.
12
- * `'divided'` squares the field off against the results and separates them with a divider.
13
- */
14
7
  variant?: 'contained' | 'divided';
15
- /** `'docked'` drops the results under the field. `'full-screen'` covers the viewport. */
16
8
  view?: 'docked' | 'full-screen';
17
- /** Leading slot. Defaults to a search icon, or a back arrow once expanded. */
18
9
  leading?: Snippet;
19
- /** Trailing slot, for an avatar or actions. A clear button is shown alongside it. */
20
10
  trailing?: Snippet;
21
- /** Accessible name for the field. */
22
11
  label?: string;
23
- /** Accessible name for the clear button. */
24
12
  clearLabel?: string;
25
- /** Accessible name for the back action shown while expanded. */
26
13
  backLabel?: string;
27
- /** Fired on the Enter key, with the current query. */
28
14
  onsearch?: (value: string) => void;
29
- /** Extra attributes for the underlying `input`. */
30
15
  inputAttributes?: HTMLInputAttributes;
31
- /**
32
- * Extra attributes for the container holding the suggestions or results. It carries no role of
33
- * its own, so this is where the content's semantics go: `role="listbox"` when the children
34
- * really are options, nothing when they are a list, categories or chips.
35
- */
36
16
  resultsAttributes?: HTMLAttributes<HTMLDivElement>;
37
17
  element?: HTMLDivElement;
38
18
  inputElement?: HTMLInputElement;
@@ -37,14 +37,6 @@
37
37
  )
38
38
  </script>
39
39
 
40
- <!--
41
- A `dialog` rather than a popover. `showModal()` keeps everything outside the sheet inert for as
42
- long as it is open, including anything added while it is open, and restores focus on close. A
43
- hand-rolled trap can only inert what exists at the moment it runs.
44
-
45
- `closedby="any"` is the light dismiss a popover gets for free. Where it is unsupported the click
46
- handler below covers it: a click on the backdrop is reported against the dialog itself.
47
- -->
48
40
  <dialog
49
41
  {...attributes}
50
42
  bind:this={element}
@@ -58,11 +50,6 @@
58
50
  }}
59
51
  ontoggle={(event) => {
60
52
  open = event.newState === 'open'
61
- /*
62
- * Without this the browser autofocuses the first focusable descendant, which is the close
63
- * button when `action` puts one in the header, firing its tooltip on open. Focus the sheet
64
- * itself instead, so nothing is focused that the user didn't choose to interact with.
65
- */
66
53
  if (event.newState === 'open') element?.focus()
67
54
  ontoggle?.(event)
68
55
  }}
@@ -91,13 +78,7 @@
91
78
  <style>
92
79
  .np-sheet {
93
80
  position: fixed;
94
- /* Focus lands here on open only to stay off the header action, not as something to show. */
95
81
  outline: none;
96
- /*
97
- * `showModal()` puts a modal sheet in the top layer, above normal stacking order regardless
98
- * of z-index. A non-modal sheet (`.show()`) stays in normal flow, so it needs a z-index of
99
- * its own to clear app bars and other overlays.
100
- */
101
82
  z-index: var(--np-sheet-z-index, 24);
102
83
  box-sizing: border-box;
103
84
  margin: 0;
@@ -113,7 +94,6 @@
113
94
  box-shadow: var(--np-sheet-elevation, var(--np-elevation-1));
114
95
  }
115
96
 
116
- /* A closed dialog is `display: none` from the user agent, which the enter transition needs. */
117
97
  .np-sheet:not([open]) {
118
98
  display: none;
119
99
  }
@@ -193,16 +173,11 @@
193
173
  padding: 1rem 1.5rem 1.5rem;
194
174
  }
195
175
 
196
- /* `::backdrop` is the scrim, so a modal sheet needs no backdrop element of its own. */
197
176
  .np-sheet-modal::backdrop {
198
177
  background-color: var(--np-color-scrim);
199
178
  opacity: 0.32;
200
179
  }
201
180
 
202
- /*
203
- * `display` and `overlay` are discrete properties, so the sheet would appear and vanish without
204
- * `allow-discrete`. `@starting-style` gives the entry a value to move from.
205
- */
206
181
  @media (prefers-reduced-motion: no-preference) {
207
182
  .np-sheet {
208
183
  transition:
@@ -1,19 +1,11 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLDialogAttributes } from 'svelte/elements';
3
3
  export interface SheetProps extends Omit<HTMLDialogAttributes, 'open'> {
4
- /** Bindable. Whether the sheet is showing. */
5
4
  open?: boolean;
6
- /**
7
- * A modal sheet takes focus and blocks the page behind it; a standard one sits alongside the
8
- * content and leaves it usable.
9
- */
10
5
  modal?: boolean;
11
- /** Which edge the sheet is docked to. `'bottom'` is M3's bottom sheet, the sides are side sheets. */
12
6
  placement?: 'bottom' | 'top' | 'start' | 'end';
13
- /** The drag handle M3 puts on a bottom sheet. Ignored on the other edges. */
14
7
  handle?: boolean;
15
8
  headline?: string;
16
- /** Trailing action in the header, usually a close button. */
17
9
  action?: Snippet;
18
10
  element?: HTMLDialogElement;
19
11
  }
@@ -115,13 +115,6 @@
115
115
 
116
116
  const DRAG_THRESHOLD = 3
117
117
 
118
- /*
119
- * The value label is a `manual` popover rather than a plain absolutely positioned element, so it
120
- * renders in the top layer and isn't clipped by an ancestor that scrolls or clips overflow, the
121
- * way a `DemoContainer` or any card with `overflow: hidden` would otherwise cut it off. `manual`
122
- * rather than `hint`, whose open and close the browser drives on its own schedule; this one has
123
- * to follow the drag exactly.
124
- */
125
118
  let startLabelElement = $state<HTMLDivElement>()
126
119
  let endLabelElement = $state<HTMLDivElement>()
127
120
  let startHandleElement = $state<HTMLDivElement>()
@@ -129,13 +122,6 @@
129
122
  let hovered = $state<'start' | 'end' | null>(null)
130
123
  let focused = $state<'start' | 'end' | null>(null)
131
124
 
132
- /*
133
- * One handle carries the label at a time, the one being interacted with: a drag wins, then the
134
- * pointer, then the keyboard. Focus only counts when it did not come from a pointer, since a
135
- * drag focuses the input and `:focus-visible` does match a range input focused that way, which
136
- * is why the focus ring is suppressed by `np-pointer-focused` too. Without that gate the label
137
- * would stay up after a drag for as long as the input keeps focus, however far the pointer went.
138
- */
139
125
  let labelled = $derived(
140
126
  disabled ? null : (dragging ?? hovered ?? (pointerFocused ? null : focused)),
141
127
  )
@@ -193,16 +179,8 @@
193
179
  }
194
180
  }
195
181
 
196
- /*
197
- * Hover is derived from where the pointer actually is rather than from the handle's own
198
- * mouseenter/mouseleave. A handle is only a few pixels wide, so those events are easy to miss
199
- * between frames, and while a drag holds pointer capture they are routed to the capturing
200
- * element and never reach the handle at all, which left the label stuck open. The tolerance
201
- * gives the thin handle a hit area worth aiming at.
202
- */
203
182
  const HOVER_TOLERANCE = 12
204
183
 
205
- /** Distance from the pointer to a handle's box, zero while inside it. */
206
184
  const distanceTo = (handle: HTMLDivElement | undefined, e: PointerEvent) => {
207
185
  if (!handle) return Infinity
208
186
  const r = handle.getBoundingClientRect()
@@ -597,15 +575,6 @@
597
575
  block-size var(--np-motion-expressive-fast-effects);
598
576
  }
599
577
 
600
- /*
601
- * A `manual` popover rather than a plain absolutely positioned element: it renders in the top
602
- * layer, anchored to the handle by `position-anchor`/`position-area` rather than by an inset
603
- * computed from the handle's own box, so it isn't clipped by an ancestor that scrolls or hides
604
- * overflow. It stays a wrapper around the visible label rather than the popover itself carrying
605
- * the styling, so it can keep resolving `position-area` in the slider's own writing mode while
606
- * the label inside always renders horizontally, the same split the previous absolute-positioned
607
- * version needed for a vertical slider's rotated writing mode.
608
- */
609
578
  .np-slider-label-anchor[popover] {
610
579
  position-area: block-start;
611
580
  justify-self: anchor-center;
@@ -617,11 +586,6 @@
617
586
  pointer-events: none;
618
587
  }
619
588
 
620
- /*
621
- * `display` only while open. Setting it on the base rule would beat the user agent's
622
- * `display: none` for a closed popover, because author styles win over user agent styles
623
- * whatever the specificity, leaving the label on screen the whole time.
624
- */
625
589
  .np-slider-label-anchor:popover-open {
626
590
  display: flex;
627
591
  opacity: 1;
@@ -23,16 +23,12 @@
23
23
  let hovered = $state(false)
24
24
  let focused = $state(false)
25
25
 
26
+ const uid = $props.id()
26
27
  const controller = popoverController(() => element)
27
28
 
28
29
  export const show = () => controller.show()
29
30
  export const close = () => controller.close()
30
31
 
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
32
  syncOpenEffect(
37
33
  () => element,
38
34
  () => open,
@@ -40,11 +36,6 @@
40
36
  close,
41
37
  )
42
38
 
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
39
  $effect(() => {
49
40
  if (!open || hovered || focused || timeout <= 0) return
50
41
  const timeoutId = setTimeout(close, timeout)
@@ -52,18 +43,13 @@
52
43
  })
53
44
  </script>
54
45
 
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
- -->
61
46
  <div
62
47
  {...attributes}
63
48
  {popover}
64
49
  class={['np-snackbar', attributes.class]}
65
50
  bind:this={element}
66
51
  role="alert"
52
+ aria-labelledby="np-snackbar-label-{uid}"
67
53
  onpointerenter={() => (hovered = true)}
68
54
  onpointerleave={() => (hovered = false)}
69
55
  onfocusin={() => (focused = true)}
@@ -75,7 +61,7 @@
75
61
  >
76
62
  <div class="np-snackbar-inner">
77
63
  <div class="np-snackbar-label-container">
78
- <div class="np-snackbar-label">{label}</div>
64
+ <div id="np-snackbar-label-{uid}" class="np-snackbar-label">{label}</div>
79
65
  {#if supportingText}
80
66
  <div class="np-snackbar-supporting-text">{supportingText}</div>
81
67
  {/if}
@@ -23,10 +23,6 @@
23
23
 
24
24
  const tabsContext = getTabsContext()
25
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
- */
30
26
  $effect(() => {
31
27
  if (tabsContext.value === value) {
32
28
  tabsContext.indicatorValue = value
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { untrack } from 'svelte'
2
3
  import Divider from '../divider/Divider.svelte'
3
4
  import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
4
5
  import { setTabsContext } from './context.js'
@@ -12,11 +13,10 @@
12
13
  ...attributes
13
14
  }: TabsProps = $props()
14
15
 
15
- // svelte-ignore state_referenced_locally
16
16
  let tabsContext = $state<TabsContext>({
17
17
  value,
18
18
  indicatorValue: value,
19
- variant,
19
+ variant: untrack(() => variant),
20
20
  })
21
21
  $effect(() => {
22
22
  value = tabsContext.value
@@ -33,8 +33,6 @@
33
33
  const uid = $props.id()
34
34
  let errorText = $derived(issues?.map((issue) => issue.message).join(', '))
35
35
 
36
- // Merged rather than set before the spread, so a consumer's own aria wiring adds to the
37
- // supporting-text wiring instead of silently replacing it.
38
36
  const supportingTextId = `supporting-text-${uid}`
39
37
  const ids = (...values: (string | undefined | null | false)[]) =>
40
38
  values.filter(Boolean).join(' ') || undefined
@@ -54,12 +54,6 @@
54
54
 
55
55
  .np-toolbar-vertical {
56
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
57
  --np-tooltip-position-area: inline-end;
64
58
  --np-tooltip-justify-self: auto;
65
59
  --np-tooltip-align-self: anchor-center;
@@ -72,14 +66,12 @@
72
66
  --np-rich-tooltip-position-try-fallbacks: flip-inline;
73
67
  }
74
68
 
75
- /* Docked: full width, flush to the edge, no corner rounding. */
76
69
  .np-toolbar-docked {
77
70
  width: 100%;
78
71
  min-height: 4rem;
79
72
  justify-content: center;
80
73
  padding-inline: 0.5rem;
81
74
  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
75
  padding-block-end: max(0px, env(safe-area-inset-bottom));
84
76
  }
85
77
  .np-toolbar-docked.np-toolbar-vertical {
@@ -91,10 +83,6 @@
91
83
  padding-inline: 0;
92
84
  }
93
85
 
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
86
  .np-toolbar-floating {
99
87
  position: sticky;
100
88
  z-index: 8;
@@ -1,15 +1,8 @@
1
1
  import type { HTMLAttributes } from 'svelte/elements';
2
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
3
  variant?: 'docked' | 'floating';
9
- /** Which edge a floating toolbar hovers against. Ignored when docked. */
10
4
  placement?: 'bottom' | 'top' | 'start' | 'end';
11
5
  orientation?: 'horizontal' | 'vertical';
12
- /** Colour role of a floating toolbar's container. */
13
6
  color?: 'standard' | 'vibrant';
14
7
  element?: HTMLElement;
15
8
  }
@@ -13,13 +13,6 @@
13
13
  ...attributes
14
14
  }: RichTooltipProps = $props()
15
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
- */
23
16
  const findControl = () =>
24
17
  id
25
18
  ? (document.querySelector<HTMLElement>(`[commandfor="${id}"], [popovertarget="${id}"]`) ??
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noph-ui",
3
- "version": "0.45.0",
3
+ "version": "0.45.1",
4
4
  "license": "MIT",
5
5
  "homepage": "https://noph.dev",
6
6
  "repository": {