noph-ui 0.45.0 → 0.46.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 (47) 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 +1 -11
  13. package/dist/date-picker/DockedDateTimePicker.svelte +1 -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 +23 -81
  35. package/dist/search/Search.svelte.d.ts +4 -1
  36. package/dist/search/types.d.ts +0 -20
  37. package/dist/sheet/Sheet.svelte +0 -25
  38. package/dist/sheet/types.d.ts +0 -8
  39. package/dist/slider/Slider.svelte +0 -36
  40. package/dist/snackbar/Snackbar.svelte +3 -17
  41. package/dist/tabs/Tab.svelte +0 -4
  42. package/dist/tabs/Tabs.svelte +2 -2
  43. package/dist/text-field/TextField.svelte +0 -2
  44. package/dist/toolbar/Toolbar.svelte +0 -12
  45. package/dist/toolbar/types.d.ts +0 -7
  46. package/dist/tooltip/RichTooltip.svelte +0 -7
  47. package/package.json +1 -1
@@ -1,17 +1,2 @@
1
- /**
2
- * Runs `fn` after two frames, so a class or attribute change written just before it has been through
3
- * one style recalculation. Returns a canceller.
4
- */
5
1
  export declare const afterTwoFrames: (fn: () => void) => () => void;
6
- /**
7
- * Runs `fn` once `element` has finished the transitions it is playing, so content gated on `open`
8
- * can stay mounted for as long as its container is still on screen. A container that animates its
9
- * exit is still painted after `open` has flipped to `false`, and content dropped on that flip empties
10
- * the container a frame before it starts to leave.
11
- *
12
- * The wait starts two frames in because a transition does not exist until the browser has
13
- * recalculated styles, so `getAnimations()` reads empty straight after the change that starts it.
14
- * An element with no exit animation therefore finishes on the second frame. Returns a canceller for
15
- * the container reopening mid exit.
16
- */
17
2
  export declare const afterExit: (element: Element | undefined, fn: () => void) => () => void;
package/dist/animation.js CHANGED
@@ -1,7 +1,3 @@
1
- /**
2
- * Runs `fn` after two frames, so a class or attribute change written just before it has been through
3
- * one style recalculation. Returns a canceller.
4
- */
5
1
  export const afterTwoFrames = (fn) => {
6
2
  let inner = 0;
7
3
  const outer = requestAnimationFrame(() => {
@@ -12,17 +8,6 @@ export const afterTwoFrames = (fn) => {
12
8
  cancelAnimationFrame(inner);
13
9
  };
14
10
  };
15
- /**
16
- * Runs `fn` once `element` has finished the transitions it is playing, so content gated on `open`
17
- * can stay mounted for as long as its container is still on screen. A container that animates its
18
- * exit is still painted after `open` has flipped to `false`, and content dropped on that flip empties
19
- * the container a frame before it starts to leave.
20
- *
21
- * The wait starts two frames in because a transition does not exist until the browser has
22
- * recalculated styles, so `getAnimations()` reads empty straight after the change that starts it.
23
- * An element with no exit animation therefore finishes on the second frame. Returns a canceller for
24
- * the container reopening mid exit.
25
- */
26
11
  export const afterExit = (element, fn) => {
27
12
  if (!element) {
28
13
  fn();
@@ -17,7 +17,6 @@
17
17
 
18
18
  const twoLine = $derived(variant === 'medium' || variant === 'large')
19
19
  const isSearch = $derived(variant === 'search')
20
- // Only a two-line bar has a second row to collapse.
21
20
  const collapses = $derived(collapsible && twoLine)
22
21
  </script>
23
22
 
@@ -46,13 +45,8 @@
46
45
  <div class="np-app-bar-leading">{@render leading()}</div>
47
46
  {/if}
48
47
  {#if isSearch}
49
- <!-- A search app bar carries a field in place of a headline. -->
50
48
  <div class="np-app-bar-search-field">{@render search?.()}</div>
51
49
  {:else if twoLine}
52
- <!--
53
- The collapsed copy of the headline and subtitle. Hidden from assistive technology so they
54
- are announced once; the real ones below stay in the accessibility tree even at height zero.
55
- -->
56
50
  <div class="np-app-bar-inline" aria-hidden="true">{@render titles(true)}</div>
57
51
  {:else}
58
52
  {@render titles(false)}
@@ -83,12 +77,6 @@
83
77
  --np-icon-button-icon-color: var(--np-color-on-surface-variant);
84
78
  }
85
79
 
86
- /*
87
- * An expanded search floats past the bar's own bounds, into the space of whatever comes
88
- * after it in the document. Sibling bars share the same z-index, so without this the one
89
- * holding the expanded search would lose to a later sibling and the view would open behind
90
- * it rather than over it.
91
- */
92
80
  .np-app-bar:has(:global(.np-search-expanded)) {
93
81
  z-index: 9;
94
82
  }
@@ -98,7 +86,6 @@
98
86
  align-items: center;
99
87
  gap: 0.5rem;
100
88
  flex: none;
101
- /* `min-height` rather than `height`, so a subtitle on a one-row bar grows it. */
102
89
  min-height: 4rem;
103
90
  padding-inline: 0.25rem;
104
91
  }
@@ -131,7 +118,6 @@
131
118
  text-overflow: ellipsis;
132
119
  white-space: nowrap;
133
120
  }
134
- /* The collapsed copy sits on one row, so its subtitle stays at the small size. */
135
121
  .np-app-bar-titles-inline .np-app-bar-headline {
136
122
  font-size: 1.375rem;
137
123
  }
@@ -147,16 +133,11 @@
147
133
  min-width: 0;
148
134
  }
149
135
 
150
- /* One-row bars: the headline sits in the action row. */
151
136
  .np-app-bar-small,
152
137
  .np-app-bar-search {
153
138
  --_headline-size: 1.375rem;
154
139
  }
155
140
 
156
- /*
157
- * The field takes the row, so a search bar has no headline to size. Named `-search-field`
158
- * rather than `-search`, which is already the variant class on the header.
159
- */
160
141
  .np-app-bar-search-field {
161
142
  flex: 1;
162
143
  min-width: 0;
@@ -169,21 +150,15 @@
169
150
  flex: 1;
170
151
  }
171
152
 
172
- /* Two-row bars: the headline sits below, and the inline copy is revealed on collapse. */
173
153
  .np-app-bar-medium {
174
154
  --_headline-size: 1.5rem;
175
- --_second-row-height: 3rem; /* medium: 64 + 48 = 112 */
155
+ --_second-row-height: 3rem;
176
156
  }
177
157
  .np-app-bar-large {
178
158
  --_headline-size: 1.75rem;
179
- --_second-row-height: 5.5rem; /* large: 64 + 88 = 152 */
159
+ --_second-row-height: 5.5rem;
180
160
  }
181
161
 
182
- /*
183
- * Sized by its content with a floor, not a fixed height, so a headline plus a subtitle is not
184
- * clipped. `interpolate-size` lets the collapse animate that intrinsic height down to zero,
185
- * the same way the extended FAB animates its width.
186
- */
187
162
  .np-app-bar-second-row {
188
163
  interpolate-size: allow-keywords;
189
164
  display: flex;
@@ -207,41 +182,17 @@
207
182
  overflow: hidden;
208
183
  text-overflow: ellipsis;
209
184
  white-space: nowrap;
210
- /* Hidden until the bar collapses. */
211
185
  opacity: 0;
212
186
  }
213
187
 
214
- /*
215
- * The collapse. `animation-timeline: scroll()` drives these keyframes from the scroll position
216
- * itself, so there is no scroll listener, no measurement and no state. `animation-range` ends
217
- * the collapse once the page has scrolled past the second row's height.
218
- */
219
188
  @supports (animation-timeline: scroll()) {
220
- /*
221
- * Scroll anchoring has to be off on whichever element scrolls, or the collapse cannot
222
- * happen at all: the bar shrinking above the viewport is exactly the content shift that
223
- * anchoring exists to compensate for, so the browser pushes the scroll position back and
224
- * the two fight to a standstill. `:has()` reaches the scroller so a consumer does not have
225
- * to know this, and only ever matches when a collapsible bar is actually present.
226
- */
227
189
  :global(html:has(.np-app-bar-collapsible)) {
228
190
  overflow-anchor: none;
229
191
  }
230
- /*
231
- * `scroll(nearest block)` resolves to the nearest scrollable ancestor at any depth, and CSS
232
- * cannot single that one out, so every ancestor of the bar is covered. Turning anchoring off
233
- * on an ancestor that does not scroll costs nothing.
234
- */
235
192
  :global(:has(.np-app-bar-scroller-nearest)) {
236
193
  overflow-anchor: none;
237
194
  }
238
195
 
239
- /*
240
- * Deliberately not gated on `prefers-reduced-motion`. The collapse is a layout state tied
241
- * one to one to the scroll position, the way `position: sticky` is, not motion that plays on
242
- * its own. Turning it off would leave the bar permanently eating a third of a small screen
243
- * rather than removing an animation.
244
- */
245
196
  .np-app-bar-collapsible .np-app-bar-second-row {
246
197
  animation: np-app-bar-collapse linear both;
247
198
  animation-timeline: scroll(root block);
@@ -268,7 +219,6 @@
268
219
  }
269
220
 
270
221
  @keyframes np-app-bar-reveal {
271
- /* Held hidden for the first half, so the two headlines cross rather than overlap. */
272
222
  0%,
273
223
  50% {
274
224
  opacity: 0;
@@ -1,35 +1,13 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  export interface AppBarProps extends HTMLAttributes<HTMLElement> {
4
- /**
5
- * M3 Expressive's four: `'search'` carries a search field instead of a headline and is the
6
- * emphasised, global entry point to search. `'small'` is one row. `'medium'` and `'large'` are
7
- * the flexible variants, putting the headline on a second line below the action row.
8
- */
9
4
  variant?: 'search' | 'small' | 'medium' | 'large';
10
- /** Named `headline` rather than `title`, which is the native tooltip attribute. */
11
5
  headline?: string;
12
- /** Secondary line under the headline. */
13
6
  subtitle?: string;
14
- /** Leading action, usually a navigation or back icon button. */
15
7
  leading?: Snippet;
16
- /**
17
- * The search field for `variant="search"`. Put a `Search` here. Replaces the headline, which a
18
- * search app bar does not show.
19
- */
20
8
  search?: Snippet;
21
- /** Trailing actions. */
22
9
  trailing?: Snippet;
23
- /**
24
- * Collapses a `medium` or `large` bar down to one row as the page scrolls. Driven by
25
- * `animation-timeline: scroll()`, so there is no scroll listener. Where that is unsupported the
26
- * bar simply stays expanded.
27
- */
28
10
  collapsible?: boolean;
29
- /**
30
- * The element the collapse is tied to. `'root'` (the default) follows the page scroller; pass a
31
- * different scroll container's axis with `'nearest'` when the bar sits inside one.
32
- */
33
11
  scroller?: 'root' | 'nearest';
34
12
  element?: HTMLElement;
35
13
  }
@@ -22,7 +22,6 @@
22
22
  const uid = $props.id()
23
23
  const menuId = `np-split-button-menu-${uid}`
24
24
 
25
- // The trailing half is the menu's invoker, so it is also the popover's implicit anchor.
26
25
  let trigger: HTMLElement | undefined = $state()
27
26
  </script>
28
27
 
@@ -37,10 +36,6 @@
37
36
  class={['np-split-button', attributes.class]}
38
37
  >
39
38
  <Button {variant} {size} {disabled} {onclick} start={icon}>{label}</Button>
40
- <!--
41
- A `Button` rather than an `IconButton`, because an icon button has no `elevated` variant and
42
- a split button's two halves have to match.
43
- -->
44
39
  <Button
45
40
  {variant}
46
41
  {size}
@@ -60,29 +55,17 @@
60
55
  </ButtonGroup>
61
56
 
62
57
  <style>
63
- /* The caret turns over while the menu is open, the way the select arrow does. */
64
58
  :global(.np-split-button-trigger svg) {
65
59
  transition: rotate var(--np-motion-expressive-fast-effects);
66
60
  }
67
61
  :global(.np-split-button-trigger[aria-expanded='true'] svg) {
68
62
  rotate: 180deg;
69
63
  }
70
- /*
71
- The trailing half only holds a caret, so it does not need a full button's width. It is
72
- square rather than merely narrow, because every size sets its round corner to half its
73
- height, and only on a square does that corner come out as a true circle.
74
- */
75
64
  :global(.np-split-button > .np-split-button-trigger) {
76
65
  --np-button-padding: 0;
77
66
  aspect-ratio: 1;
78
67
  justify-content: center;
79
68
  }
80
- /*
81
- While the menu is open the trailing half reads as selected: its inner corner rounds out to
82
- the outer one, so the half becomes a circle, and it keeps a state layer up. Overriding the
83
- group's inner corner rather than `border-radius` leaves ButtonGroup's own corner rules in
84
- charge of which corner is which.
85
- */
86
69
  :global(.np-split-button > .np-split-button-trigger[aria-expanded='true']) {
87
70
  --np-button-group-inner-corner: var(--_round-radius, var(--np-shape-corner-full));
88
71
  }
@@ -29,20 +29,14 @@ export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
29
29
  element?: HTMLElement;
30
30
  }
31
31
  export interface SplitButtonProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onclick'> {
32
- /** Text of the leading action. */
33
32
  label?: string;
34
- /** Leading icon on the action half. */
35
33
  icon?: Snippet;
36
- /** Menu items for the trailing half. Rendered inside a `Menu`. */
37
34
  menu?: Snippet<[string]>;
38
35
  variant?: Exclude<ButtonProps['variant'], 'text'>;
39
36
  size?: BaseButtonProps['size'];
40
37
  disabled?: boolean | null;
41
- /** Bindable. Whether the menu is open. */
42
38
  open?: boolean;
43
- /** Accessible name for the trailing half, which shows only a caret. */
44
39
  menuLabel?: string;
45
- /** The action half's click handler. The trailing half opens the menu instead. */
46
40
  onclick?: MouseEventHandler<ButtonElement>;
47
41
  element?: HTMLElement;
48
42
  }
@@ -21,8 +21,6 @@
21
21
  let groupValue = $derived(
22
22
  typeof value === 'string' || typeof value === 'number' ? value : undefined,
23
23
  )
24
- // A grouped checkbox takes its checked state from the group, so it cannot also `bind:checked`.
25
- // One controlled input covers both, with the change handler writing back whichever is in play.
26
24
  let isChecked = $derived(
27
25
  Array.isArray(group) && groupValue !== undefined ? group.includes(groupValue) : checked,
28
26
  )
@@ -29,8 +29,6 @@
29
29
 
30
30
  let chipLabel: HTMLLabelElement | undefined = $state()
31
31
 
32
- // A grouped chip takes its checked state from the group, so it cannot also `bind:checked`. One
33
- // controlled input covers both, with the change handler writing back whichever is in play.
34
32
  let isChecked = $derived(
35
33
  Array.isArray(group) && value !== undefined ? group.includes(value) : selected,
36
34
  )
@@ -4,10 +4,6 @@ import type { HTMLAnchorAttributes, HTMLAttributes, HTMLButtonAttributes, MouseE
4
4
  export interface ChipSetProps extends HTMLAttributes<HTMLDivElement> {
5
5
  element?: HTMLDivElement;
6
6
  }
7
- /**
8
- * Declared from the element attributes rather than `ButtonProps`, which leaked `size`, `shape`,
9
- * `toggle`, `loading` and `selected` onto a chip. A chip has one size and one shape.
10
- */
11
7
  export interface AssistChipProps extends HTMLAttributes<ButtonElement>, Omit<HTMLButtonAttributes, keyof HTMLAttributes<HTMLButtonElement> | 'type'>, Omit<HTMLAnchorAttributes, keyof HTMLAttributes<HTMLAnchorElement> | 'type'> {
12
8
  variant?: 'outlined' | 'elevated';
13
9
  disabled?: boolean | null;
@@ -16,10 +12,6 @@ export interface AssistChipProps extends HTMLAttributes<ButtonElement>, Omit<HTM
16
12
  element?: HTMLElement;
17
13
  type?: 'submit' | 'reset' | 'button' | (string & {}) | null;
18
14
  }
19
- /**
20
- * A suggestion chip carries a generated suggestion rather than an action, so it is text-first and
21
- * takes no leading icon. Everything else matches an assist chip.
22
- */
23
15
  export type SuggestionChipProps = Omit<AssistChipProps, 'icon'>;
24
16
  export interface FilterChipProps extends HTMLAttributes<HTMLDivElement> {
25
17
  selected?: boolean;
@@ -130,11 +130,6 @@
130
130
  cancelYearEnter = afterTwoFrames(() => (yearEnter = false))
131
131
  }
132
132
 
133
- /*
134
- * The same pair every overlay in the library exports. A trigger with `command="show-modal"` and
135
- * `commandfor` needs neither, so these are for the times there is no trigger to point at the
136
- * dialog. `open` follows either way, written back from the dialog's own toggle event.
137
- */
138
133
  export const show = () => {
139
134
  if (element && !element.open) element.showModal()
140
135
  }
@@ -227,8 +222,6 @@
227
222
  } else {
228
223
  if (!confirming) oncancel?.()
229
224
  confirming = false
230
- // Handing the month back before the dialog has gone would step the calendar to another
231
- // month in front of the user, so that waits for the fade as well.
232
225
  exit.scheduleExit(element, () => (displayMonth = monthBeforeOpen))
233
226
  }
234
227
  }}
@@ -246,9 +239,6 @@
246
239
  type="button"
247
240
  aria-label={mode === 'calendar' ? inputModeLabel : calendarModeLabel}
248
241
  onclick={() => {
249
- // The calendar moves `pending` on without touching the field, so the text has to
250
- // catch up before it is shown again. Otherwise the field contradicts the headline
251
- // and OK confirms a date the user never saw there.
252
242
  if (mode === 'calendar') syncText()
253
243
  setMode(mode === 'calendar' ? 'input' : 'calendar')
254
244
  showYearPicker(false)
@@ -116,11 +116,6 @@
116
116
  if (target && scroller) scroller.scrollTop = target.offsetTop
117
117
  }
118
118
 
119
- /*
120
- * The same pair every overlay in the library exports. A trigger with `command="show-modal"` and
121
- * `commandfor` needs neither, so these are for the times there is no trigger to point at the
122
- * dialog. `open` follows either way, written back from the dialog's own toggle event.
123
- */
124
119
  export const show = () => {
125
120
  if (element && !element.open) element.showModal()
126
121
  }
@@ -35,7 +35,7 @@
35
35
  let {
36
36
  value = $bindable(),
37
37
  defaultValue,
38
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- absorbed on purpose
38
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- pulled out of the rest props on purpose
39
39
  type,
40
40
  'aria-invalid': ariaInvalid,
41
41
  style,
@@ -215,11 +215,6 @@
215
215
  focusCalendar()
216
216
  }
217
217
 
218
- /*
219
- * The same pair every overlay in the library exports, for the times there is no trigger to point
220
- * at the picker. `close()` leaves focus where it is; only a close the user asked for, through the
221
- * field or the cancel button, hands focus back to the input.
222
- */
223
218
  export const show = () => openPicker()
224
219
 
225
220
  export const close = () => {
@@ -269,7 +264,6 @@
269
264
  const parsed = usableDate(next)
270
265
  commit(parsed ? toISODate(parsed) : undefined)
271
266
  if (parsed) setMonth(parsed)
272
- // Recorded after the commit, so that it is remembered against the value it just produced.
273
267
  typed = { source: current, text: next }
274
268
  invalidText = undefined
275
269
  }
@@ -281,8 +275,6 @@
281
275
  }
282
276
  const parsed = usableDate(text)
283
277
  invalidText = parsed ? undefined : text
284
- // Leaving the field tidies a loose entry: dropping what was typed falls back to the
285
- // canonical formatting of the value it produced.
286
278
  if (parsed) typed = undefined
287
279
  }
288
280
 
@@ -299,8 +291,6 @@
299
291
  }
300
292
 
301
293
  const chooseYear = (year: number) => {
302
- // A year is offered as soon as any of its days is in range, so keeping the month the list was
303
- // opened from can land on a month where nothing at all is selectable.
304
294
  setMonth(new Date(year, clampMonthInYear(year, monthDate.getMonth(), minDate, maxDate), 1))
305
295
  leaveList()
306
296
  }
@@ -49,7 +49,7 @@
49
49
  let {
50
50
  value = $bindable(),
51
51
  defaultValue,
52
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- absorbed on purpose
52
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- pulled out of the rest props on purpose
53
53
  type,
54
54
  'aria-invalid': ariaInvalid,
55
55
  style,
@@ -119,8 +119,6 @@
119
119
  let week = $derived(firstDayOfWeek ?? getFirstDayOfWeek(locale))
120
120
  let minDate = $derived(parseISODateTime(min))
121
121
  let maxDate = $derived(parseISODateTime(max, MINUTES_IN_DAY - 1))
122
- // `as('datetime-local')` types its value as `string | number` and carries the last submission in
123
- // `defaultValue`, so both are funnelled into one ISO string the rest of the component reads.
124
122
  let current = $derived<string | undefined>(
125
123
  typeof value === 'string' ? value : typeof defaultValue === 'string' ? defaultValue : undefined,
126
124
  )
@@ -298,11 +296,6 @@
298
296
  focusCalendar()
299
297
  }
300
298
 
301
- /*
302
- * The same pair every overlay in the library exports, for the times there is no trigger to point
303
- * at the picker. `close()` leaves focus where it is; only a close the user asked for, through the
304
- * field or the cancel button, hands focus back to the input.
305
- */
306
299
  export const show = () => openPicker()
307
300
 
308
301
  export const close = () => {
@@ -1,8 +1,3 @@
1
- /**
2
- * `visible` for a dialog that fades itself out, so it stays on screen after `open` has flipped to
3
- * `false`. Content tied to `open` would leave the fading dialog empty a frame before it starts to
4
- * leave, so this holds `visible` true until the exit animation has actually finished.
5
- */
6
1
  export declare const exitVisibility: () => {
7
2
  readonly visible: boolean;
8
3
  show: () => void;
@@ -1,14 +1,7 @@
1
1
  import { afterExit } from '../animation.js';
2
- /**
3
- * `visible` for a dialog that fades itself out, so it stays on screen after `open` has flipped to
4
- * `false`. Content tied to `open` would leave the fading dialog empty a frame before it starts to
5
- * leave, so this holds `visible` true until the exit animation has actually finished.
6
- */
7
2
  export const exitVisibility = () => {
8
3
  let visible = $state(false);
9
4
  let cancelExit;
10
- // A dialog torn down mid fade has nothing left to reveal, and running a stale `onDone` against a
11
- // parent that has already dropped its state is worse than not running it at all.
12
5
  $effect(() => () => cancelExit?.());
13
6
  return {
14
7
  get visible() {
@@ -1,16 +1,4 @@
1
1
  import type { Attachment } from 'svelte/attachments';
2
2
  import type { TimeOption } from './types.ts';
3
- /**
4
- * The option that carries the list's single tab stop: the selected one, or else the offered option
5
- * nearest to the value, so arriving with the keyboard lands next to where the value already is.
6
- */
7
3
  export declare const tabStop: (options: TimeOption[], value: number | undefined) => number | undefined;
8
- /**
9
- * Scrolls the selected option into view inside its own list, rather than through `scrollIntoView`,
10
- * which takes every scrollable ancestor with it and would move the page behind the picker.
11
- *
12
- * `centre` puts the option in the middle of the list every time it changes. Otherwise the list is
13
- * centred once, on the option it opens with, and afterwards the selection is only pulled to the
14
- * nearest edge when it has moved out of view, so stepping through options does not jump the list.
15
- */
16
4
  export declare const revealSelected: (centre?: boolean) => Attachment<HTMLElement>;
@@ -1,7 +1,3 @@
1
- /**
2
- * The option that carries the list's single tab stop: the selected one, or else the offered option
3
- * nearest to the value, so arriving with the keyboard lands next to where the value already is.
4
- */
5
1
  export const tabStop = (options, value) => {
6
2
  if (options.some((option) => option.value === value))
7
3
  return value;
@@ -13,14 +9,6 @@ export const tabStop = (options, value) => {
13
9
  return pool[0].value;
14
10
  return pool.reduce((nearest, option) => Math.abs(option.value - value) < Math.abs(nearest.value - value) ? option : nearest).value;
15
11
  };
16
- /**
17
- * Scrolls the selected option into view inside its own list, rather than through `scrollIntoView`,
18
- * which takes every scrollable ancestor with it and would move the page behind the picker.
19
- *
20
- * `centre` puts the option in the middle of the list every time it changes. Otherwise the list is
21
- * centred once, on the option it opens with, and afterwards the selection is only pulled to the
22
- * nearest edge when it has moved out of view, so stepping through options does not jump the list.
23
- */
24
12
  export const revealSelected = (centre = false) => {
25
13
  let centred = false;
26
14
  return (element) => {
@@ -20,10 +20,6 @@
20
20
 
21
21
  const uid = $props.id()
22
22
 
23
- /*
24
- * A modal dialog can only be opened from script or from a `command` button; `<dialog open>` on
25
- * its own is the non-modal kind, with no focus trap and no backdrop.
26
- */
27
23
  export const show = () => {
28
24
  if (element && !element.open) element.showModal()
29
25
  }
@@ -40,13 +36,6 @@
40
36
  )
41
37
  </script>
42
38
 
43
- <!--
44
- A `dialog`, not a popover. `showModal()` keeps everything outside inert for as long as the dialog
45
- is open, including anything added to the page while it is open, and hands focus back on close.
46
- `::backdrop` is the scrim and `closedby="any"` the click-outside-to-close. Where `closedby` is
47
- unsupported the click handler below covers it: a click on the backdrop is reported against the
48
- dialog itself.
49
- -->
50
39
  <dialog
51
40
  bind:this={element}
52
41
  {...attributes}
@@ -57,11 +46,6 @@
57
46
  class={['np-dialog-container', !quick && 'np-animate', attributes.class]}
58
47
  ontoggle={(event) => {
59
48
  open = event.newState === 'open'
60
- /*
61
- * Without this the browser autofocuses the first focusable descendant, one of the actions,
62
- * which can be a destructive one. Focus the dialog itself instead, so nothing is focused
63
- * that the user didn't choose to interact with.
64
- */
65
49
  if (event.newState === 'open') element?.focus()
66
50
  ontoggle?.(event)
67
51
  }}
@@ -108,7 +92,6 @@
108
92
  .np-dialog-container {
109
93
  background: transparent;
110
94
  border: none;
111
- /* Focus lands here on open only to stay off the actions, not as something to show. */
112
95
  outline: none;
113
96
  margin: auto;
114
97
  padding: var(--np-dialog-inset, 2rem 1rem);
@@ -120,14 +103,9 @@
120
103
  overflow: visible;
121
104
  color: var(--np-color-on-surface);
122
105
  }
123
- /* A closed dialog is `display: none` from the user agent, which the enter transition needs. */
124
106
  .np-dialog-container:not([open]) {
125
107
  display: none;
126
108
  }
127
- /*
128
- * The exit transition holds the dialog in the top layer after it has closed, where it would go on
129
- * taking the clicks meant for the page behind it.
130
- */
131
109
  .np-animate:not([open]) {
132
110
  pointer-events: none;
133
111
  }
@@ -8,6 +8,5 @@ export interface DialogProps extends Omit<HTMLDialogAttributes, 'open'> {
8
8
  actions?: Snippet;
9
9
  quick?: boolean;
10
10
  element?: HTMLDialogElement;
11
- /** Bindable. Whether the dialog is showing. */
12
11
  open?: boolean;
13
12
  }
@@ -22,7 +22,6 @@
22
22
  const uid = $props.id()
23
23
 
24
24
  const isLink = $derived(attributes.href != null && !disabled)
25
- // Collapsed, the text is hidden, so the label has to be announced some other way.
26
25
  const tooltipId = $derived(collapsed && label && !disabled ? uid : undefined)
27
26
 
28
27
  const morph = pressMorph()
@@ -97,10 +96,6 @@
97
96
  {/if}
98
97
 
99
98
  <style>
100
- /*
101
- * `interpolate-size` lets the intrinsic width animate, so collapsing is a plain CSS
102
- * transition on `width` rather than a measured pixel value.
103
- */
104
99
  .np-extended-fab {
105
100
  interpolate-size: allow-keywords;
106
101
  position: relative;
@@ -128,27 +123,12 @@
128
123
  padding-inline: var(--_padding);
129
124
  }
130
125
 
131
- /*
132
- * `calc(var(--_height) / 2)` renders identically to `--np-shape-corner-full` (9999px) at
133
- * rest, since border-radius clamps an oversized radius down to exactly half the height
134
- * anyway. It matters for the pressed transition: animating from a huge fixed value spends
135
- * nearly the whole transition on radii that still render as fully round, so the morph never
136
- * becomes visible during a normal, brief click. `50%` isn't a substitute here, since the box
137
- * isn't square, it would resolve to an elliptical corner rather than a round pill end.
138
- */
139
126
  .round {
140
127
  border-radius: var(--np-fab-shape, calc(var(--_height) / 2));
141
128
  }
142
129
  .square {
143
130
  border-radius: var(--np-fab-shape, var(--_square-radius));
144
131
  }
145
- /*
146
- * A press morphs the corner towards the other shape. `:active` alone drops the moment the
147
- * pointer lifts, which on a normal click is long before the morph has played, so the short
148
- * `pressed` class holds the shape until the whole round trip is visible, the way Button does
149
- * it. Both are gated on the motion preference, since without the transition below this is
150
- * only an instant snap.
151
- */
152
132
  @media (prefers-reduced-motion: no-preference) {
153
133
  .round:not(:disabled):is(:active, .pressed) {
154
134
  border-radius: var(--np-fab-pressed-shape, var(--_square-radius));
@@ -158,7 +138,6 @@
158
138
  }
159
139
  }
160
140
 
161
- /* The same three sizes as `Fab`, which is what M3 Expressive aligned the two on. */
162
141
  .s {
163
142
  --_height: 3.5rem;
164
143
  --_square-radius: 1rem;
@@ -184,7 +163,6 @@
184
163
  --np-icon-size: 2.25rem;
185
164
  }
186
165
 
187
- /* Collapsed is the icon-only FAB: square box, no text, no padding around the label. */
188
166
  .np-fab-collapsed {
189
167
  width: var(--_height);
190
168
  padding-inline: 0;
@@ -219,10 +197,6 @@
219
197
  }
220
198
  }
221
199
 
222
- /*
223
- * M3's colour styles, named after the tokens they map to. The tone styles are the stronger
224
- * pair, the container styles the softer one. The surface style M3 Expressive dropped is gone.
225
- */
226
200
  .np-fab-primary {
227
201
  --_container-color: var(--np-color-primary);
228
202
  --_icon-color: var(--np-color-on-primary);