@noxlovette/material 0.7.1 → 0.8.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 (34) 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/components/time/TimeField.svelte +1 -4
  28. package/dist/styles/components.css +6 -0
  29. package/dist/styles/icon-font.css +17 -0
  30. package/dist/styles/motion.css +35 -0
  31. package/dist/utils/icon/MaterialSymbolsProvider.svelte +7 -1
  32. package/dist/utils/icon/MaterialSymbolsProvider.svelte.d.ts +3 -0
  33. package/dist/utils/icon/types.d.ts +11 -0
  34. package/package.json +2 -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',
@@ -98,10 +98,7 @@ It combines a Material 3 Textfield with a TimepickerInput for intuitive time sel
98
98
  <div {...wrapperProps}>
99
99
  <div
100
100
  {...props}
101
- class={[
102
- props.class,
103
- 'max-h-(--bits-floating-available-height) min-w-(--bits-floating-anchor-width) overflow-auto'
104
- ]}
101
+ class={[props.class, 'min-w-(--bits-floating-anchor-width)']}
105
102
  {@attach presence(() => open, enterExit.scale)}
106
103
  >
107
104
  <TimepickerInput
@@ -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
  /*
@@ -0,0 +1,17 @@
1
+ /*
2
+ Optional, for `<App iconProviderProps={{ selfHosted: true }}>`. Google's stylesheet normally
3
+ binds each `.material-symbols-*` class to its font family; a self-hoster imports this instead and
4
+ writes only the `@font-face` rules (variable, with the opsz, wght, FILL and GRAD axes).
5
+ The rest of the class metrics (line-height, ligatures, smoothing…) ship in `./styles`.
6
+ */
7
+ .material-symbols-rounded {
8
+ font-family: 'Material Symbols Rounded';
9
+ }
10
+
11
+ .material-symbols-outlined {
12
+ font-family: 'Material Symbols Outlined';
13
+ }
14
+
15
+ .material-symbols-sharp {
16
+ font-family: 'Material Symbols Sharp';
17
+ }
@@ -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
+ }
@@ -2,12 +2,16 @@
2
2
  @component
3
3
  A helper that handles google icons. Should not be used directly.
4
4
  Use the App.svelte component instead
5
+
6
+ With `selfHosted` it renders nothing, so no request goes to fonts.googleapis.com. The consumer then
7
+ supplies the `@font-face` rules; see `@noxlovette/material/styles/icon-font.css`.
5
8
  -->
6
9
  <script lang="ts">
7
10
  import { baseIcons } from './base-icons.js';
8
11
  import type { MaterialSymbolsProviderProps } from './types.js';
9
12
 
10
13
  const {
14
+ selfHosted = false,
11
15
  extraIcons = [],
12
16
  display = 'swap',
13
17
  subset = true,
@@ -41,5 +45,7 @@ Use the App.svelte component instead
41
45
  </script>
42
46
 
43
47
  <svelte:head>
44
- <link rel="stylesheet" {href} />
48
+ {#if !selfHosted}
49
+ <link rel="stylesheet" {href} />
50
+ {/if}
45
51
  </svelte:head>
@@ -2,6 +2,9 @@ import type { MaterialSymbolsProviderProps } from './types.js';
2
2
  /**
3
3
  * A helper that handles google icons. Should not be used directly.
4
4
  * Use the App.svelte component instead
5
+ *
6
+ * With `selfHosted` it renders nothing, so no request goes to fonts.googleapis.com. The consumer then
7
+ * supplies the `@font-face` rules; see `@noxlovette/material/styles/icon-font.css`.
5
8
  */
6
9
  declare const MaterialSymbolsProvider: import("svelte").Component<MaterialSymbolsProviderProps, {}, "">;
7
10
  type MaterialSymbolsProvider = ReturnType<typeof MaterialSymbolsProvider>;
@@ -59,6 +59,17 @@ export type LoadingIndicatorProps = HTMLAttributes<SVGAElement> & LoadingIndicat
59
59
  center?: boolean;
60
60
  };
61
61
  export type MaterialSymbolsProviderProps = {
62
+ /**
63
+ * Skip the Google Fonts `<link>` so the page makes no request to `fonts.googleapis.com`, and
64
+ * supply the font yourself. Define an `@font-face` for each family in use (`"Material Symbols
65
+ * Rounded"`, `"Material Symbols Outlined"`, `"Material Symbols Sharp"`), variable, with the
66
+ * `opsz`, `wght`, `FILL` and `GRAD` axes that `Icon` sets through `font-variation-settings`, and
67
+ * import `@noxlovette/material/styles/icon-font.css` for the `font-family` of each
68
+ * `.material-symbols-*` class. The remaining class metrics ship with `@noxlovette/material/styles`.
69
+ * With this set, `icons`, `extraIcons`, `subset`, `families` and `display` have no effect.
70
+ * @default false
71
+ */
72
+ selfHosted?: boolean;
62
73
  icons?: MaterialSymbolName[];
63
74
  /** Icon names to load on top of `baseIcons`. Ignored when `subset` is false. */
64
75
  extraIcons?: MaterialSymbolName[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noxlovette/material",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "description": "A Material Design 3 component library for Svelte, built on Bits UI.",
6
6
  "keywords": [
@@ -57,6 +57,7 @@
57
57
  "svelte": "./dist/index.js"
58
58
  },
59
59
  "./styles": "./dist/index.css",
60
+ "./styles/icon-font.css": "./dist/styles/icon-font.css",
60
61
  "./theme/*": "./dist/styles/theme/*.css"
61
62
  },
62
63
  "peerDependencies": {