@noxlovette/material 0.7.1 → 0.8.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 (29) hide show
  1. package/claude-skill/material-design/references/motion-guide.md +9 -9
  2. package/dist/animation/containerTransform.d.ts +0 -27
  3. package/dist/animation/containerTransform.js +40 -9
  4. package/dist/components/containers/popover/theme.d.ts +3 -3
  5. package/dist/components/forms/checkbox/Checkbox.svelte +1 -1
  6. package/dist/components/forms/checkbox/theme.d.ts +0 -12
  7. package/dist/components/forms/checkbox/theme.js +2 -7
  8. package/dist/components/forms/search/Search.mdx +76 -8
  9. package/dist/components/forms/search/Search.stories.svelte +107 -0
  10. package/dist/components/forms/search/Search.stories.svelte.d.ts +2 -17
  11. package/dist/components/forms/search/Search.svelte +55 -2
  12. package/dist/components/forms/search/Search.svelte.d.ts +4 -1
  13. package/dist/components/forms/search/SearchView.svelte +269 -0
  14. package/dist/components/forms/search/SearchView.svelte.d.ts +21 -0
  15. package/dist/components/forms/search/index.d.ts +1 -0
  16. package/dist/components/forms/search/index.js +1 -0
  17. package/dist/components/forms/search/theme.d.ts +68 -1
  18. package/dist/components/forms/search/theme.js +91 -2
  19. package/dist/components/forms/search/types.d.ts +75 -2
  20. package/dist/components/nav/appbar/AppBar.mdx +5 -1
  21. package/dist/components/nav/appbar/AppBar.stories.svelte +38 -0
  22. package/dist/components/nav/appbar/AppBar.svelte +47 -2
  23. package/dist/components/nav/appbar/AppBar.svelte.d.ts +3 -2
  24. package/dist/components/nav/appbar/theme.js +1 -1
  25. package/dist/components/nav/appbar/types.d.ts +18 -1
  26. package/dist/components/table/theme.js +3 -1
  27. package/dist/styles/components.css +6 -0
  28. package/dist/styles/motion.css +35 -0
  29. package/package.json +1 -1
@@ -8,7 +8,8 @@ They’re used for branding, screen titles, navigation, and actions.
8
8
  Sizes: `small` (64dp, one line) and the Expressive `medium`/`large` flexible bars, whose
9
9
  headline block wraps and grows. `size` also takes one value per breakpoint
10
10
  (`{ small: 'small', large: 'large' }`), switched in CSS so server rendering needs no JS.
11
- `align="center"` centres the title. Setting `search` makes it a search app bar.
11
+ `align="center"` centres the title. Setting `search` makes it a search app bar; add
12
+ `searchResults` and selecting the field opens the search view, as with `Search`.
12
13
 
13
14
  The bar takes its on-scroll color once `scrollContainer` (default: the page) scrolls.
14
15
 
@@ -28,6 +29,7 @@ A `ButtonIcon` anywhere inside it defaults to `variant="standard"`, per M3; pass
28
29
  import clsx from 'clsx';
29
30
  import ButtonIcon from '../../buttons/ButtonIcon.svelte';
30
31
  import { setButtonIconVariant } from '../../buttons/context.js';
32
+ import SearchView from '../../forms/search/SearchView.svelte';
31
33
 
32
34
  let {
33
35
  children,
@@ -47,6 +49,9 @@ A `ButtonIcon` anywhere inside it defaults to `variant="standard"`, per M3; pass
47
49
  query = $bindable(''),
48
50
  searchProps,
49
51
  searchTrailing,
52
+ searchResults,
53
+ searchOpen = $bindable(false),
54
+ searchLayout,
50
55
  scrollContainer,
51
56
  ghost = false,
52
57
  ...rest
@@ -69,6 +74,33 @@ A `ButtonIcon` anywhere inside it defaults to `variant="standard"`, per M3; pass
69
74
  });
70
75
 
71
76
  const isSearch = $derived(search !== undefined);
77
+
78
+ let searchBar = $state<HTMLElement>();
79
+
80
+ // Same as Search: click, typing or ↓ opens the view; focus alone doesn't.
81
+ const opener = $derived(
82
+ searchResults
83
+ ? {
84
+ 'aria-haspopup': 'dialog' as const,
85
+ 'aria-expanded': searchOpen,
86
+ onclick: (e: MouseEvent & { currentTarget: HTMLInputElement }) => {
87
+ searchProps?.onclick?.(e);
88
+ if (!e.defaultPrevented) searchOpen = true;
89
+ },
90
+ oninput: (e: Event & { currentTarget: HTMLInputElement }) => {
91
+ searchProps?.oninput?.(e);
92
+ if (!e.defaultPrevented) searchOpen = true;
93
+ },
94
+ onkeydown: (e: KeyboardEvent & { currentTarget: HTMLInputElement }) => {
95
+ searchProps?.onkeydown?.(e);
96
+ if (!e.defaultPrevented && e.key === 'ArrowDown') {
97
+ e.preventDefault();
98
+ searchOpen = true;
99
+ }
100
+ }
101
+ }
102
+ : {}
103
+ );
72
104
  const noLeading = $derived(!leading && !showBack);
73
105
 
74
106
  const s = $derived(
@@ -118,10 +150,11 @@ A `ButtonIcon` anywhere inside it defaults to `variant="standard"`, per M3; pass
118
150
  {#if title}
119
151
  <h1 {...titleProps} class={clsx('sr-only', titleProps?.class)}>{title}</h1>
120
152
  {/if}
121
- <label class={s.search()}>
153
+ <label class={s.search()} bind:this={searchBar}>
122
154
  <input
123
155
  type="search"
124
156
  {...searchProps}
157
+ {...opener}
125
158
  placeholder={search}
126
159
  aria-label={searchProps?.['aria-label'] ?? search}
127
160
  bind:value={query}
@@ -156,6 +189,18 @@ A `ButtonIcon` anywhere inside it defaults to `variant="standard"`, per M3; pass
156
189
  {/if}
157
190
  </nav>
158
191
 
192
+ {#if isSearch && searchResults}
193
+ <SearchView
194
+ bind:open={searchOpen}
195
+ bind:value={query}
196
+ anchor={searchBar}
197
+ results={searchResults}
198
+ layout={searchLayout}
199
+ placeholder={search}
200
+ trailing={searchTrailing}
201
+ />
202
+ {/if}
203
+
159
204
  {#if ghost}
160
205
  <div class={s.ghost()} style="height: {barHeight}px" aria-hidden="true"></div>
161
206
  {/if}
@@ -8,7 +8,8 @@ import type { AppBarProps } from './types.js';
8
8
  * Sizes: `small` (64dp, one line) and the Expressive `medium`/`large` flexible bars, whose
9
9
  * headline block wraps and grows. `size` also takes one value per breakpoint
10
10
  * (`{ small: 'small', large: 'large' }`), switched in CSS so server rendering needs no JS.
11
- * `align="center"` centres the title. Setting `search` makes it a search app bar.
11
+ * `align="center"` centres the title. Setting `search` makes it a search app bar; add
12
+ * `searchResults` and selecting the field opens the search view, as with `Search`.
12
13
  *
13
14
  * The bar takes its on-scroll color once `scrollContainer` (default: the page) scrolls.
14
15
  *
@@ -22,6 +23,6 @@ import type { AppBarProps } from './types.js';
22
23
  *
23
24
  * @see https://m3.material.io/components/app-bars/specs
24
25
  */
25
- declare const AppBar: import("svelte").Component<AppBarProps, {}, "query">;
26
+ declare const AppBar: import("svelte").Component<AppBarProps, {}, "query" | "searchOpen">;
26
27
  type AppBar = ReturnType<typeof AppBar>;
27
28
  export default AppBar;
@@ -131,7 +131,7 @@ export const appbar = tv({
131
131
  subtitle: 'text-md-sys-color-on-surface-variant',
132
132
  leading: 'text-md-sys-color-on-surface col-start-1 row-start-1 flex shrink-0 items-center justify-self-start',
133
133
  trailing: 'text-md-sys-color-on-surface-variant col-start-3 row-start-1 flex shrink-0 items-center justify-self-end',
134
- search: 'state-layer relative flex h-spacing-700 w-full min-w-spacing-0 cursor-text items-center rounded-full ps-spacing-200 pe-spacing-200 transition-colors md-sys-motion-effects before:rounded-full hover:before:bg-md-sys-color-on-surface/8 focus-within:outline-3 focus-within:outline-offset-2 focus-within:outline-md-sys-color-secondary',
134
+ search: 'state-layer relative flex h-spacing-700 w-full min-w-spacing-0 cursor-text items-center rounded-xl ps-spacing-200 pe-spacing-200 transition-colors md-sys-motion-effects before:rounded-xl hover:before:bg-md-sys-color-on-surface/8 focus-within:outline-3 focus-within:outline-offset-2 focus-within:outline-md-sys-color-secondary [:root:active-view-transition_&]:outline-0',
135
135
  searchInput: 'md-sys-typescale-body-large text-md-sys-color-on-surface placeholder:text-md-sys-color-on-surface-variant w-full min-w-spacing-0 bg-transparent text-start outline-none placeholder-shown:text-center [&::-webkit-search-cancel-button]:hidden',
136
136
  searchTrailing: 'text-md-sys-color-on-surface-variant flex shrink-0 items-center'
137
137
  },
@@ -1,6 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes, HTMLInputAttributes } from 'svelte/elements';
3
3
  import type { Responsive } from '../../containers/pane/theme.js';
4
+ import type { SearchLayout, SearchResultsProps } from '../../forms/search/types.js';
4
5
  import type { AppBarSize, AppbarVariants } from './theme.js';
5
6
  type AppBarBaseProps = Pick<AppbarVariants, 'align'> & HTMLAttributes<HTMLElementTagNameMap['nav']> & {
6
7
  /** Extra content rendered in a full-width row below the title, e.g. a Breadcrumb trail. */
@@ -43,6 +44,9 @@ export type TitleAppBarProps = AppBarBaseProps & {
43
44
  query?: never;
44
45
  searchProps?: never;
45
46
  searchTrailing?: never;
47
+ searchResults?: never;
48
+ searchOpen?: never;
49
+ searchLayout?: never;
46
50
  };
47
51
  /** A search app bar: a search field in place of the title, always 64dp. */
48
52
  export type SearchAppBarProps = AppBarBaseProps & {
@@ -54,8 +58,21 @@ export type SearchAppBarProps = AppBarBaseProps & {
54
58
  title?: string;
55
59
  /** Props for the search `<input>`, e.g. `onfocus` to open a search view. */
56
60
  searchProps?: HTMLInputAttributes;
57
- /** An action inside the search field, e.g. a mic icon button. */
61
+ /** An action inside the search field, e.g. a mic icon button. Also shown in the search view. */
58
62
  searchTrailing?: Snippet;
63
+ /**
64
+ * Suggestions and results. Setting it gives the bar a search view (`SearchView`), opened by
65
+ * clicking or typing in the field. Spread the snippet's argument onto a `List` of
66
+ * `role="option"` items.
67
+ */
68
+ searchResults?: Snippet<[SearchResultsProps]>;
69
+ /** Whether the search view is open. Only has an effect with `searchResults`. */
70
+ searchOpen?: boolean;
71
+ /**
72
+ * The search view's layout, one value or one per window tier.
73
+ * @default { small: 'fullScreen', medium: 'docked' }
74
+ */
75
+ searchLayout?: Responsive<SearchLayout>;
59
76
  size?: never;
60
77
  };
61
78
  /**
@@ -1,7 +1,9 @@
1
1
  import { tv } from '../../utils/tv.js';
2
2
  export const table = tv({
3
3
  slots: {
4
- root: 'w-full border-collapse text-left md-sys-typescale-body-medium text-md-sys-color-on-surface',
4
+ // `isolate` scopes the sticky head's z-10 to the table, so it only covers the rows
5
+ // scrolling beneath it and never app bars, menus or sheets outside the table.
6
+ root: 'isolate w-full border-collapse text-left md-sys-typescale-body-medium text-md-sys-color-on-surface',
5
7
  head: '',
6
8
  body: '[&_tr:last-child]:border-b-0',
7
9
  foot: 'border-t border-md-sys-color-outline-variant',
@@ -11,6 +11,12 @@
11
11
  --md-comp-nav-rail-expanded-top-space: 2.75rem; /* 44dp */
12
12
  --md-comp-nav-rail-expanded-container-width-minimum: 13.75rem; /* 220dp */
13
13
  --md-comp-nav-rail-expanded-container-width-maximum: 22.5rem; /* 360dp */
14
+
15
+ /* Search view, contained style, docked layout: https://m3.material.io/components/search/specs */
16
+ --md-comp-search-view-contained-docked-container-width-minimum: 22.5rem; /* 360dp */
17
+ --md-comp-search-view-contained-docked-container-width-maximum: 45rem; /* 720dp */
18
+ --md-comp-search-view-contained-docked-container-height-minimum: 15rem; /* 240dp */
19
+ --md-comp-search-view-contained-docked-container-height-maximum: calc(100dvh * 2 / 3);
14
20
  }
15
21
 
16
22
  /*
@@ -331,3 +331,38 @@
331
331
  transition-duration: var(--md-sys-motion-duration-slow-effects-spring);
332
332
  transition-timing-function: var(--md-sys-motion-timing-function-slow-effects-spring);
333
333
  }
334
+
335
+ /*
336
+ Container transform (animation/containerTransform.ts): both snapshots keep their own width and
337
+ sit at the container's top, clipped by it as it grows or shrinks. Motion's crop would otherwise
338
+ `object-fit: cover` them, which blows a short bar's snapshot up to the height of a full view.
339
+ Motion injects its per-name rule at runtime, after this sheet and at the same specificity, so
340
+ these need `!important` to win.
341
+ */
342
+ ::view-transition-old(.md-container-transform),
343
+ ::view-transition-new(.md-container-transform) {
344
+ top: 0 !important;
345
+ width: 100% !important;
346
+ height: auto !important;
347
+ object-fit: fill !important;
348
+ }
349
+
350
+ /*
351
+ Outgoing on top, fading off the incoming snapshot underneath, which stays opaque. Normal
352
+ blending: the browser runs `plus-lighter` on the pair as an animation, which only
353
+ `!important` outranks, and adding the outgoing bar onto the opaque view flashed it white.
354
+ The incoming snapshot also drops the browser's default fade-in.
355
+ */
356
+ ::view-transition-old(.md-container-transform) {
357
+ z-index: 1;
358
+ mix-blend-mode: normal !important;
359
+ }
360
+ ::view-transition-new(.md-container-transform) {
361
+ mix-blend-mode: normal !important;
362
+ animation-name: none !important;
363
+ }
364
+
365
+ /* Filled with the destination's own background while it morphs (set by containerTransform). */
366
+ ::view-transition-group(.md-container-transform) {
367
+ background-color: var(--md-container-transform-color, transparent);
368
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noxlovette/material",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "A Material Design 3 component library for Svelte, built on Bits UI.",
6
6
  "keywords": [