@noxlovette/material 0.6.0 → 0.7.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 (176) hide show
  1. package/claude-skill/material-design/SKILL.md +5 -2
  2. package/claude-skill/material-design/references/accessibility-checklist.md +5 -0
  3. package/claude-skill/material-design/references/component-patterns.md +24 -3
  4. package/claude-skill/material-design/references/motion-guide.md +53 -11
  5. package/claude-skill/material-design/references/tokens-and-styles.md +4 -0
  6. package/dist/animation/TransitionPatterns.stories.svelte +50 -0
  7. package/dist/animation/containerTransform.js +10 -4
  8. package/dist/animation/reducedMotion.d.ts +2 -0
  9. package/dist/animation/reducedMotion.js +2 -0
  10. package/dist/animation/sharedAxisTransition.d.ts +20 -3
  11. package/dist/animation/sharedAxisTransition.js +67 -8
  12. package/dist/attachments/drag.d.ts +52 -0
  13. package/dist/attachments/drag.js +217 -0
  14. package/dist/attachments/index.d.ts +1 -0
  15. package/dist/attachments/index.js +1 -0
  16. package/dist/components/badge/theme.js +1 -1
  17. package/dist/components/buttons/button-group/theme.d.ts +1 -1
  18. package/dist/components/buttons/button-group/theme.js +1 -1
  19. package/dist/components/buttons/connected/theme.d.ts +1 -1
  20. package/dist/components/buttons/connected/theme.js +1 -1
  21. package/dist/components/buttons/split-button/SplitButton.svelte +1 -1
  22. package/dist/components/buttons/split-button/theme.d.ts +1 -1
  23. package/dist/components/buttons/split-button/theme.js +1 -1
  24. package/dist/components/buttons/theme.d.ts +1 -1
  25. package/dist/components/buttons/theme.js +3 -3
  26. package/dist/components/cards/theme.js +1 -1
  27. package/dist/components/chips/Chip.mdx +24 -11
  28. package/dist/components/chips/Chip.stories.svelte +10 -0
  29. package/dist/components/chips/Chip.svelte +58 -18
  30. package/dist/components/chips/Chip.svelte.d.ts +3 -1
  31. package/dist/components/chips/ChipGroup.stories.svelte +73 -0
  32. package/dist/components/chips/ChipGroup.stories.svelte.d.ts +4 -0
  33. package/dist/components/chips/ChipGroup.svelte +242 -0
  34. package/dist/components/chips/ChipGroup.svelte.d.ts +33 -0
  35. package/dist/components/chips/index.d.ts +1 -0
  36. package/dist/components/chips/index.js +1 -0
  37. package/dist/components/chips/theme.d.ts +183 -22
  38. package/dist/components/chips/theme.js +117 -26
  39. package/dist/components/chips/types.d.ts +36 -2
  40. package/dist/components/containers/action-rail/theme.d.ts +1 -1
  41. package/dist/components/containers/action-rail/theme.js +2 -2
  42. package/dist/components/containers/app/theme.d.ts +2 -2
  43. package/dist/components/containers/app/theme.js +2 -2
  44. package/dist/components/containers/bottom-sheet/BottomSheet.stories.svelte +1 -1
  45. package/dist/components/containers/bottom-sheet/theme.d.ts +1 -1
  46. package/dist/components/containers/bottom-sheet/theme.js +2 -2
  47. package/dist/components/containers/carousel/theme.d.ts +1 -1
  48. package/dist/components/containers/carousel/theme.js +1 -1
  49. package/dist/components/containers/context-menu/theme.d.ts +1 -1
  50. package/dist/components/containers/context-menu/theme.js +2 -2
  51. package/dist/components/containers/dialogue/Dialogue.svelte +1 -1
  52. package/dist/components/containers/dialogue/theme.d.ts +1 -1
  53. package/dist/components/containers/dialogue/theme.js +3 -3
  54. package/dist/components/containers/dialogue/types.d.ts +1 -1
  55. package/dist/components/containers/divider/theme.d.ts +1 -1
  56. package/dist/components/containers/divider/theme.js +1 -1
  57. package/dist/components/containers/link-preview/theme.d.ts +1 -1
  58. package/dist/components/containers/link-preview/theme.js +2 -2
  59. package/dist/components/containers/list/List.svelte +8 -2
  60. package/dist/components/containers/list/ListItem.mdx +6 -0
  61. package/dist/components/containers/list/ListItem.svelte +77 -2
  62. package/dist/components/containers/list/context.d.ts +14 -0
  63. package/dist/components/containers/list/theme.d.ts +7 -1
  64. package/dist/components/containers/list/theme.js +11 -3
  65. package/dist/components/containers/menu/Menu.svelte +1 -1
  66. package/dist/components/containers/menu/MenuSub.svelte +1 -1
  67. package/dist/components/containers/menu/theme.d.ts +1 -1
  68. package/dist/components/containers/menu/theme.js +1 -1
  69. package/dist/components/containers/pane/DraggablePane.svelte +25 -63
  70. package/dist/components/containers/pane/Pane.svelte +1 -1
  71. package/dist/components/containers/pane/theme.d.ts +1 -1
  72. package/dist/components/containers/pane/theme.js +35 -9
  73. package/dist/components/containers/popover/theme.d.ts +4 -4
  74. package/dist/components/containers/popover/theme.js +2 -2
  75. package/dist/components/containers/scroll-area/theme.js +1 -1
  76. package/dist/components/containers/side-sheet/theme.d.ts +1 -1
  77. package/dist/components/containers/side-sheet/theme.js +1 -1
  78. package/dist/components/date/theme.d.ts +7 -7
  79. package/dist/components/date/theme.js +1 -1
  80. package/dist/components/forms/checkbox/theme.d.ts +1 -1
  81. package/dist/components/forms/checkbox/theme.js +2 -2
  82. package/dist/components/forms/command/theme.d.ts +4 -4
  83. package/dist/components/forms/command/theme.js +1 -1
  84. package/dist/components/forms/pin/theme.d.ts +1 -1
  85. package/dist/components/forms/pin/theme.js +1 -1
  86. package/dist/components/forms/radio-group/theme.d.ts +1 -1
  87. package/dist/components/forms/radio-group/theme.js +1 -1
  88. package/dist/components/forms/search/theme.d.ts +1 -1
  89. package/dist/components/forms/search/theme.js +1 -1
  90. package/dist/components/forms/select/Select.svelte +2 -2
  91. package/dist/components/forms/select/theme.d.ts +1 -1
  92. package/dist/components/forms/select/theme.js +2 -2
  93. package/dist/components/forms/slider/theme.d.ts +1 -1
  94. package/dist/components/forms/slider/theme.js +1 -1
  95. package/dist/components/forms/switch/theme.d.ts +1 -1
  96. package/dist/components/forms/switch/theme.js +1 -1
  97. package/dist/components/forms/textfield/theme.d.ts +1 -1
  98. package/dist/components/forms/textfield/theme.js +1 -1
  99. package/dist/components/forms/toggle-group/theme.d.ts +1 -1
  100. package/dist/components/forms/toggle-group/theme.js +1 -1
  101. package/dist/components/forms/tooltip/theme.d.ts +1 -1
  102. package/dist/components/forms/tooltip/theme.js +2 -2
  103. package/dist/components/misc/Avatar.stories.svelte +56 -7
  104. package/dist/components/misc/Avatar.stories.svelte.d.ts +2 -17
  105. package/dist/components/misc/Avatar.svelte +82 -8
  106. package/dist/components/misc/Avatar.svelte.d.ts +5 -0
  107. package/dist/components/misc/ThemeSwitcher.svelte +1 -1
  108. package/dist/components/misc/theme.d.ts +6 -0
  109. package/dist/components/misc/theme.js +6 -4
  110. package/dist/components/misc/types.d.ts +17 -1
  111. package/dist/components/nav/appbar/theme.d.ts +1 -1
  112. package/dist/components/nav/appbar/theme.js +3 -2
  113. package/dist/components/nav/breadcrumb/theme.d.ts +1 -1
  114. package/dist/components/nav/breadcrumb/theme.js +1 -1
  115. package/dist/components/nav/currentRoute.d.ts +11 -0
  116. package/dist/components/nav/currentRoute.js +28 -0
  117. package/dist/components/nav/navbar/NavbarItem.svelte +3 -6
  118. package/dist/components/nav/navbar/theme.d.ts +3 -3
  119. package/dist/components/nav/navbar/theme.js +2 -2
  120. package/dist/components/nav/rail/Rail.mdx +65 -23
  121. package/dist/components/nav/rail/Rail.stories.svelte +24 -4
  122. package/dist/components/nav/rail/Rail.stories.svelte.d.ts +2 -17
  123. package/dist/components/nav/rail/Rail.svelte +165 -48
  124. package/dist/components/nav/rail/Rail.svelte.d.ts +6 -3
  125. package/dist/components/nav/rail/RailItem.svelte +122 -65
  126. package/dist/components/nav/rail/RailItem.svelte.d.ts +3 -2
  127. package/dist/components/nav/rail/theme.d.ts +70 -85
  128. package/dist/components/nav/rail/theme.js +80 -67
  129. package/dist/components/nav/rail/types.d.ts +4 -0
  130. package/dist/components/nav/tabs/Tabs.stories.svelte +2 -1
  131. package/dist/components/nav/tabs/theme.d.ts +1 -1
  132. package/dist/components/nav/tabs/theme.js +1 -1
  133. package/dist/components/pill/theme.d.ts +1 -1
  134. package/dist/components/pill/theme.js +1 -1
  135. package/dist/components/progress/LinearProgress.svelte +1 -1
  136. package/dist/components/snackbar/theme.d.ts +1 -1
  137. package/dist/components/snackbar/theme.js +2 -2
  138. package/dist/components/table/theme.d.ts +1 -1
  139. package/dist/components/table/theme.js +1 -1
  140. package/dist/components/time/TimeField.svelte +1 -1
  141. package/dist/components/time/theme.d.ts +1 -1
  142. package/dist/components/time/theme.js +1 -1
  143. package/dist/components/toolbar/theme.d.ts +1 -1
  144. package/dist/components/toolbar/theme.js +2 -2
  145. package/dist/components/typography/body/theme.d.ts +1 -1
  146. package/dist/components/typography/body/theme.js +1 -1
  147. package/dist/components/typography/display/theme.d.ts +1 -1
  148. package/dist/components/typography/display/theme.js +1 -1
  149. package/dist/components/typography/headline/theme.d.ts +1 -1
  150. package/dist/components/typography/headline/theme.js +1 -1
  151. package/dist/components/typography/kbd/theme.d.ts +1 -1
  152. package/dist/components/typography/kbd/theme.js +1 -1
  153. package/dist/components/typography/label/theme.js +1 -1
  154. package/dist/components/typography/title/theme.d.ts +1 -1
  155. package/dist/components/typography/title/theme.js +1 -1
  156. package/dist/index.css +3 -0
  157. package/dist/styles/components.css +50 -0
  158. package/dist/styles/icon.css +13 -0
  159. package/dist/styles/layers.css +33 -0
  160. package/dist/utils/icon/Icon.stories.svelte +97 -0
  161. package/dist/utils/icon/Icon.stories.svelte.d.ts +4 -0
  162. package/dist/utils/icon/Icon.svelte +101 -12
  163. package/dist/utils/icon/Icon.svelte.d.ts +9 -0
  164. package/dist/utils/icon/MaterialSymbolsProvider.svelte +11 -4
  165. package/dist/utils/icon/base-icons.d.ts +1 -1
  166. package/dist/utils/icon/base-icons.js +1 -1
  167. package/dist/utils/icon/icon-names.d.ts +2 -0
  168. package/dist/utils/icon/icon-names.js +2 -0
  169. package/dist/utils/icon/swap.d.ts +6 -0
  170. package/dist/utils/icon/swap.js +45 -0
  171. package/dist/utils/icon/theme.d.ts +1 -1
  172. package/dist/utils/icon/theme.js +1 -1
  173. package/dist/utils/icon/types.d.ts +59 -7
  174. package/dist/utils/tv.d.ts +5 -0
  175. package/dist/utils/tv.js +20 -0
  176. package/package.json +2 -1
@@ -1,4 +1,4 @@
1
- import { tv } from 'tailwind-variants';
1
+ import { tv } from '../../../utils/tv.js';
2
2
  export const toggleGroup = tv({
3
3
  slots: {
4
4
  root: 'inline-flex h-spacing-500 overflow-hidden rounded-full border border-md-sys-color-outline'
@@ -1,4 +1,4 @@
1
- import { type VariantProps } from 'tailwind-variants';
1
+ import { type VariantProps } from '../../../utils/tv.js';
2
2
  export type TooltipVariants = VariantProps<typeof tooltip>;
3
3
  export declare const tooltip: import("tailwind-variants").TVReturnType<{
4
4
  variant: {
@@ -1,7 +1,7 @@
1
- import { tv } from 'tailwind-variants';
1
+ import { tv } from '../../../utils/tv.js';
2
2
  export const tooltip = tv({
3
3
  slots: {
4
- base: 'min-w-60 py-spacing-150 px-spacing-200 shadow-elevation-2 rounded-md relative z-[100] pointer-events-auto',
4
+ base: 'min-w-60 py-spacing-150 px-spacing-200 shadow-elevation-2 rounded-md relative z-layer-popup pointer-events-auto',
5
5
  subhead: '',
6
6
  supportingText: '',
7
7
  textContainer: '',
@@ -1,6 +1,7 @@
1
1
  <script module lang="ts">
2
2
  import { defineMeta } from '@storybook/addon-svelte-csf';
3
3
  import Avatar from './Avatar.svelte';
4
+ import { shapeNames } from '../../animation/shapeMorph.svelte.js';
4
5
 
5
6
  const { Story } = defineMeta({
6
7
  title: 'Misc/Avatar',
@@ -9,18 +10,34 @@
9
10
  argTypes: {
10
11
  size: { control: 'select', options: ['sm', 'md', 'lg'] },
11
12
  seed: { control: 'text' },
12
- dicebearStyle: { control: 'text' }
13
+ dicebearStyle: { control: 'text' },
14
+ shape: { control: 'select', options: shapeNames },
15
+ spin: { control: 'inline-radio', options: ['none', 'clockwise', 'counterclockwise'] }
13
16
  },
14
17
  args: {
15
18
  size: 'lg',
16
- seed: 'ogonek'
19
+ seed: 'ogonek',
20
+ shape: 'circle',
21
+ spin: 'none'
17
22
  }
18
23
  });
19
24
  </script>
20
25
 
26
+ <script lang="ts">
27
+ const morphShapes = ['circle', 'sunny', 'fourSidedCookie', 'pentagon', 'flower'] as const;
28
+ let morphIndex = $state(0);
29
+ const morphShape = $derived(morphShapes[morphIndex % morphShapes.length]);
30
+ </script>
31
+
21
32
  <Story name="Playground">
22
33
  {#snippet template(args)}
23
- <Avatar size={args.size} seed={args.seed} dicebearStyle={args.dicebearStyle} />
34
+ <Avatar
35
+ size={args.size}
36
+ seed={args.seed}
37
+ dicebearStyle={args.dicebearStyle}
38
+ shape={args.shape}
39
+ spin={args.spin}
40
+ />
24
41
  {/snippet}
25
42
  </Story>
26
43
 
@@ -55,10 +72,42 @@
55
72
  <Avatar seed="clickable" onclick={() => console.log('Avatar clicked')} />
56
73
  </Story>
57
74
 
58
- <Story name="Animated" asChild>
75
+ <Story name="Styles" asChild>
76
+ <!-- gaze with the animation tag is the default; ['!animation'] forces the still variant. -->
59
77
  <div class="gap-spacing-200 flex items-end">
60
- <Avatar seed="alice" dicebearStyle="gaze" tags={['animation']} />
61
- <Avatar seed="bob" dicebearStyle="shapes" tags={['animation']} />
62
- <Avatar seed="carol" dicebearStyle="blobs" tags={['animation']} />
78
+ <Avatar seed="alice" />
79
+ <Avatar seed="alice" tags={['!animation']} />
80
+ <Avatar seed="bob" dicebearStyle="shapes" />
81
+ <Avatar seed="carol" dicebearStyle="blobs" />
82
+ </div>
83
+ </Story>
84
+
85
+ <Story name="Shapes" asChild>
86
+ <div class="gap-spacing-200 grid grid-cols-5 sm:grid-cols-7">
87
+ {#each shapeNames as name (name)}
88
+ <Avatar size="md" seed={name} shape={name} title={name} />
89
+ {/each}
90
+ </div>
91
+ </Story>
92
+
93
+ <Story name="Shape morph" asChild>
94
+ <div class="gap-spacing-200 flex items-center">
95
+ <Avatar seed="morph" shape={morphShape} onclick={() => morphIndex++} />
96
+ <span class="md-sys-typescale-body-medium text-md-sys-color-on-surface-variant">
97
+ Click the avatar: {morphShape}
98
+ </span>
99
+ </div>
100
+ </Story>
101
+
102
+ <Story name="Spin" asChild>
103
+ <div class="gap-spacing-200 flex items-end">
104
+ <Avatar seed="clockwise" shape="sunny" spin="clockwise" />
105
+ <Avatar seed="counter" shape="eightLeafClover" spin="counterclockwise" />
106
+ <Avatar
107
+ seed="burst"
108
+ shape="softBurst"
109
+ spin="clockwise"
110
+ src="https://picsum.photos/seed/ogonek-2/200"
111
+ />
63
112
  </div>
64
113
  </Story>
@@ -1,19 +1,4 @@
1
1
  import Avatar from './Avatar.svelte';
2
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
- $$bindings?: Bindings;
5
- } & Exports;
6
- (internal: unknown, props: {
7
- $$events?: Events;
8
- $$slots?: Slots;
9
- }): Exports & {
10
- $set?: any;
11
- $on?: any;
12
- };
13
- z_$$bindings?: Bindings;
14
- }
15
- declare const Avatar: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
- [evt: string]: CustomEvent<any>;
17
- }, {}, {}, string>;
18
- type Avatar = InstanceType<typeof Avatar>;
2
+ declare const Avatar: import("svelte").Component<Record<string, never>, {}, "">;
3
+ type Avatar = ReturnType<typeof Avatar>;
19
4
  export default Avatar;
@@ -2,11 +2,53 @@
2
2
  @component
3
3
  Avatars can be used to represent people or objects.
4
4
 
5
+ The avatar is cut to one of the M3 Expressive shapes (`shape`, a circle by default), morphing on
6
+ `fastSpatial` when the shape changes. `spin` turns the shape at a constant speed, clockwise or
7
+ counterclockwise, while the picture inside stays upright.
8
+
5
9
  @see https://m3.material.io/components/avatars/overview
10
+ @see https://m3.material.io/styles/shape
6
11
  -->
12
+ <script module lang="ts">
13
+ import { animate } from 'motion';
14
+ import { prefersReducedMotion } from '../../animation/reducedMotion.js';
15
+
16
+ /*
17
+ One full turn of a spinning shape. A constant, linear spin has no spring to take from the
18
+ motion tokens, like the loading indicator's global rotation (4.666s a turn); an avatar is
19
+ decoration beside content, not a progress signal, so it turns about three times slower.
20
+ */
21
+ const SPIN_TURN_S = 14;
22
+
23
+ /*
24
+ The shapes sit in a 48 × 48 box with the circle spanning 8..40, so this maps the circle onto
25
+ the avatar's bounding box (clip-path objectBoundingBox units are 0..1). Wider shapes
26
+ (clamshell, the bursts) run to the avatar's edges.
27
+ */
28
+ const FIT = 'scale(0.03125) translate(-8 -8)';
29
+
30
+ /** Turns the clip path around the shape's centre; never under reduced motion (decorative). */
31
+ const spinClip = (path: SVGPathElement, direction: 'clockwise' | 'counterclockwise') => {
32
+ if (prefersReducedMotion()) return;
33
+ const sign = direction === 'clockwise' ? 1 : -1;
34
+ const controls = animate(0, 360, {
35
+ duration: SPIN_TURN_S,
36
+ ease: 'linear',
37
+ repeat: Infinity,
38
+ onUpdate: (deg) => path.setAttribute('transform', `${FIT} rotate(${sign * deg} 24 24)`)
39
+ });
40
+ return () => {
41
+ controls.stop();
42
+ path.setAttribute('transform', FIT);
43
+ };
44
+ };
45
+ </script>
46
+
7
47
  <script lang="ts">
48
+ import { untrack } from 'svelte';
8
49
  import { Avatar as AvatarPrimitive } from 'bits-ui';
9
50
  import { Layer } from '../../utils/index.js';
51
+ import { animatableShapesSmall, shapeMorph } from '../../animation/shapeMorph.svelte.js';
10
52
  import { avatar } from './theme.js';
11
53
  import { DicebearAvatarBuilder } from './DicebearAvatarBuilder.js';
12
54
  import { getMaterialAvatarPalette } from './materialAvatarPalette.js';
@@ -16,9 +58,11 @@ Avatars can be used to represent people or objects.
16
58
  src,
17
59
  alt = 'User avatar',
18
60
  seed = 'ogonek',
19
- dicebearStyle = 'shapes',
61
+ dicebearStyle = 'gaze',
20
62
  backgroundColor,
21
- tags,
63
+ tags = ['animation'],
64
+ shape = 'circle',
65
+ spin = 'none',
22
66
  size = 'lg',
23
67
  loadingStatus = $bindable('loading'),
24
68
  class: className,
@@ -34,20 +78,25 @@ Avatars can be used to represent people or objects.
34
78
  lg: 96
35
79
  } as const;
36
80
 
81
+ const uid = $props.id();
82
+ const clipId = `m3-avatar-${uid}`;
83
+ // Rendered into the markup so SSR draws the shape; later changes morph from here.
84
+ const initialShape = untrack(() => animatableShapesSmall[shape]);
85
+
37
86
  let generatedAvatar = $state('');
38
87
 
39
88
  $effect(() => {
40
89
  const sizePx = sizeMap[size ?? 'lg'] ?? 96;
41
90
  const currentSeed = seed;
42
- const currentStyle = dicebearStyle ?? 'shapes';
91
+ const currentStyle = dicebearStyle ?? 'gaze';
43
92
  const palette = backgroundColor ?? getMaterialAvatarPalette();
44
93
  const currentTags = tags;
45
94
  let cancelled = false;
46
95
 
96
+ // Square: the clip path gives the avatar its shape.
47
97
  const builder = new DicebearAvatarBuilder(currentStyle)
48
98
  .seed(currentSeed)
49
99
  .size(sizePx)
50
- .borderRadius(50)
51
100
  .background(palette);
52
101
 
53
102
  if (currentTags) builder.tags(currentTags);
@@ -62,15 +111,33 @@ Avatars can be used to represent people or objects.
62
111
  };
63
112
  });
64
113
 
65
- const { root, image, fallback, button } = $derived(avatar({ size }));
114
+ const { root, image, fallback, button, clip } = $derived(avatar({ size }));
66
115
 
67
116
  const rootClasses = $derived(root({ class: className }));
68
117
  </script>
69
118
 
119
+ <svg class={clip()} aria-hidden="true">
120
+ <clipPath id={clipId} clipPathUnits="objectBoundingBox">
121
+ <path
122
+ d={initialShape}
123
+ transform={FIT}
124
+ {@attach shapeMorph(() => animatableShapesSmall[shape])}
125
+ {@attach (path: SVGPathElement) => (spin === 'none' ? undefined : spinClip(path, spin))}
126
+ />
127
+ </clipPath>
128
+ </svg>
129
+
70
130
  {#if onclick}
71
131
  <button class={button({ class: rootClasses })} {onclick} type="button" data-cy="m3-avatar">
72
- <Layer />
73
- <AvatarPrimitive.Root {...rest} bind:loadingStatus class="h-full w-full">
132
+ <!-- The state layer sits inside the clipped root, so hover and press take the shape too. -->
133
+ <AvatarPrimitive.Root
134
+ {...rest}
135
+ bind:loadingStatus
136
+ class="relative h-full w-full"
137
+ style="clip-path: url(#{clipId})"
138
+ data-shape={shape}
139
+ >
140
+ <Layer />
74
141
  {#if src}
75
142
  <AvatarPrimitive.Image {src} {alt} class={image()} />
76
143
  {/if}
@@ -82,7 +149,14 @@ Avatars can be used to represent people or objects.
82
149
  </AvatarPrimitive.Root>
83
150
  </button>
84
151
  {:else}
85
- <AvatarPrimitive.Root {...rest} bind:loadingStatus class={rootClasses} data-cy="m3-avatar">
152
+ <AvatarPrimitive.Root
153
+ {...rest}
154
+ bind:loadingStatus
155
+ class={rootClasses}
156
+ style="clip-path: url(#{clipId})"
157
+ data-cy="m3-avatar"
158
+ data-shape={shape}
159
+ >
86
160
  {#if src}
87
161
  <AvatarPrimitive.Image {src} {alt} class={image()} />
88
162
  {/if}
@@ -2,7 +2,12 @@ import type { AvatarProps } from './types.js';
2
2
  /**
3
3
  * Avatars can be used to represent people or objects.
4
4
  *
5
+ * The avatar is cut to one of the M3 Expressive shapes (`shape`, a circle by default), morphing on
6
+ * `fastSpatial` when the shape changes. `spin` turns the shape at a constant speed, clockwise or
7
+ * counterclockwise, while the picture inside stays upright.
8
+ *
5
9
  * @see https://m3.material.io/components/avatars/overview
10
+ * @see https://m3.material.io/styles/shape
6
11
  */
7
12
  declare const Avatar: import("svelte").Component<AvatarProps, {}, "loadingStatus">;
8
13
  type Avatar = ReturnType<typeof Avatar>;
@@ -6,7 +6,7 @@
6
6
  let open = $state(false);
7
7
  </script>
8
8
 
9
- <div class="right-spacing-200 bottom-spacing-200 fixed z-50 hidden md:block">
9
+ <div class="right-spacing-200 bottom-spacing-200 z-layer-fab fixed hidden md:block">
10
10
  <Popover bind:open title="Theme Settings" side="top" align="end" sideOffset={16}>
11
11
  {#snippet trigger(triggerProps)}
12
12
  <FAB iconProps={{ name: 'palette' }} {...triggerProps} />
@@ -21,6 +21,8 @@ export declare const avatar: import("tailwind-variants").TVReturnType<{
21
21
  image: string;
22
22
  fallback: string;
23
23
  button: string;
24
+ /** The zero-size SVG holding the shape's clip path. */
25
+ clip: string;
24
26
  }, undefined, {
25
27
  size: {
26
28
  xxs: {
@@ -44,6 +46,8 @@ export declare const avatar: import("tailwind-variants").TVReturnType<{
44
46
  image: string;
45
47
  fallback: string;
46
48
  button: string;
49
+ /** The zero-size SVG holding the shape's clip path. */
50
+ clip: string;
47
51
  }, import("tailwind-variants").TVReturnTypeLike<{
48
52
  size: {
49
53
  xxs: {
@@ -67,4 +71,6 @@ export declare const avatar: import("tailwind-variants").TVReturnType<{
67
71
  image: string;
68
72
  fallback: string;
69
73
  button: string;
74
+ /** The zero-size SVG holding the shape's clip path. */
75
+ clip: string;
70
76
  }>>;
@@ -1,10 +1,12 @@
1
- import { tv } from 'tailwind-variants';
1
+ import { tv } from '../../utils/tv.js';
2
2
  export const avatar = tv({
3
3
  slots: {
4
- root: 'relative flex aspect-square shrink-0 items-center justify-center overflow-hidden rounded-full',
4
+ root: 'relative flex aspect-square shrink-0 items-center justify-center overflow-hidden',
5
5
  image: 'h-full w-full object-cover',
6
- fallback: 'flex h-full w-full items-center justify-center rounded-full bg-md-sys-color-surface-container-highest',
7
- button: 'relative rounded-full focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-md-sys-color-primary'
6
+ fallback: 'flex h-full w-full items-center justify-center bg-md-sys-color-surface-container-highest',
7
+ button: 'relative rounded-full focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-md-sys-color-primary',
8
+ /** The zero-size SVG holding the shape's clip path. */
9
+ clip: 'pointer-events-none absolute size-spacing-0 overflow-hidden'
8
10
  },
9
11
  variants: {
10
12
  size: {
@@ -1,3 +1,4 @@
1
+ import type { ShapeName } from '../../animation/shapeMorph.svelte.js';
1
2
  import type { Avatar as AvatarPrimitive } from 'bits-ui';
2
3
  import type { VariantProps } from 'tailwind-variants';
3
4
  import type { DicebearStyleName } from './dicebearStyles.js';
@@ -15,7 +16,7 @@ export type AvatarProps = AvatarPrimitive.RootProps & {
15
16
  alt?: string;
16
17
  /** The seed for the generated avatar if no src is provided. */
17
18
  seed?: string;
18
- /** The Dicebear style used for the generated fallback avatar. @default 'shapes' */
19
+ /** The Dicebear style used for the generated fallback avatar. @default 'gaze' */
19
20
  dicebearStyle?: DicebearStyle;
20
21
  /**
21
22
  * Hex background colors (no `#`) for the generated fallback avatar. Defaults to the
@@ -29,8 +30,23 @@ export type AvatarProps = AvatarPrimitive.RootProps & {
29
30
  * supporting style (`gaze`, `shapes`, `blobs`, …) into its animated variants, or
30
31
  * `['!animation']` to force the static one. A no-op for styles without that tag.
31
32
  * @see https://www.dicebear.com/guides/filter-variants-with-tags
33
+ * @default ['animation']
32
34
  */
33
35
  tags?: string[];
36
+ /**
37
+ * The M3 Expressive shape the avatar is cut to (`shapeNames` lists all 35). Changing it morphs
38
+ * the outline on the `fastSpatial` spring; under reduced motion it swaps instantly.
39
+ * @see https://m3.material.io/styles/shape
40
+ * @default 'circle'
41
+ */
42
+ shape?: ShapeName;
43
+ /**
44
+ * Turns the shape at a constant speed, one turn every 14s. Only the outline turns; the
45
+ * picture stays upright. Decorative, so reduced motion keeps it still. A circle looks the
46
+ * same at every angle, so pair this with another `shape`.
47
+ * @default 'none'
48
+ */
49
+ spin?: 'none' | 'clockwise' | 'counterclockwise';
34
50
  /** The loading status of the avatar image. */
35
51
  loadingStatus?: 'loading' | 'loaded' | 'error';
36
52
  /** Callback for when the avatar is clicked. If provided, the avatar is wrapped in a button. */
@@ -1,4 +1,4 @@
1
- import { type VariantProps } from 'tailwind-variants';
1
+ import { type VariantProps } from '../../../utils/tv.js';
2
2
  import type { Breakpoint } from '../../containers/pane/theme.js';
3
3
  export type AppbarVariants = VariantProps<typeof appbar>;
4
4
  export type AppBarSize = 'small' | 'medium' | 'large';
@@ -1,4 +1,4 @@
1
- import { tv } from 'tailwind-variants';
1
+ import { tv } from '../../../utils/tv.js';
2
2
  /*
3
3
  M3 Expressive app bar (https://m3.material.io/components/app-bars/specs), laid out on one
4
4
  3-column grid (leading · text · trailing) so the size can change per breakpoint in CSS alone:
@@ -121,7 +121,8 @@ export function appbarSize(size, hasSubtitle) {
121
121
  }
122
122
  export const appbar = tv({
123
123
  slots: {
124
- base: 'fixed top-spacing-0 left-spacing-0 w-full flex flex-col z-30 transition-colors md-sys-motion-effects',
124
+ // Starts beside a viewport-anchored Rail (styles/components.css), which runs full height.
125
+ base: 'fixed top-spacing-0 left-(--md-rail-inset) right-spacing-0 flex flex-col z-layer-bar transition-colors md-sys-motion-effects',
125
126
  row: 'grid w-full items-center px-spacing-50',
126
127
  childrenRow: 'w-full px-spacing-100 pb-spacing-100',
127
128
  ghost: 'w-full shrink-0 pointer-events-none',
@@ -1,4 +1,4 @@
1
- import { type VariantProps } from 'tailwind-variants';
1
+ import { type VariantProps } from '../../../utils/tv.js';
2
2
  export type BreadcrumbVariants = VariantProps<typeof breadcrumb>;
3
3
  export declare const breadcrumb: import("tailwind-variants").TVReturnType<{
4
4
  color: {
@@ -1,4 +1,4 @@
1
- import { tv } from 'tailwind-variants';
1
+ import { tv } from '../../../utils/tv.js';
2
2
  export const breadcrumb = tv({
3
3
  slots: {
4
4
  base: 'flex',
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Whether a destination's `href` is the page at `url` or one of its descendants, for marking
3
+ * the current rail/navbar destination.
4
+ *
5
+ * The href resolves against the page, so a relative one works and one on another origin (a
6
+ * Storybook link on its own port, an external site) never matches, even when its path does.
7
+ * An in-page anchor (`#…`) is never a destination. A destination's own path matches its
8
+ * sub-pages too (`/guides` for `/guides/icons`), except the app's root (`/`, or SvelteKit's `base`
9
+ * path when the app is served from a sub-path), which would match everything.
10
+ */
11
+ export declare function isCurrentRoute(href: string, url: URL, root?: string): boolean;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Whether a destination's `href` is the page at `url` or one of its descendants, for marking
3
+ * the current rail/navbar destination.
4
+ *
5
+ * The href resolves against the page, so a relative one works and one on another origin (a
6
+ * Storybook link on its own port, an external site) never matches, even when its path does.
7
+ * An in-page anchor (`#…`) is never a destination. A destination's own path matches its
8
+ * sub-pages too (`/guides` for `/guides/icons`), except the app's root (`/`, or SvelteKit's `base`
9
+ * path when the app is served from a sub-path), which would match everything.
10
+ */
11
+ export function isCurrentRoute(href, url, root = '/') {
12
+ if (!href || href.startsWith('#'))
13
+ return false;
14
+ let target;
15
+ try {
16
+ target = new URL(href, url);
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ if (target.origin !== url.origin)
22
+ return false;
23
+ const trim = (path) => (path.length > 1 ? path.replace(/\/+$/, '') : path);
24
+ const path = trim(target.pathname);
25
+ const current = trim(url.pathname);
26
+ const isRoot = path === '/' || path === trim(root || '/');
27
+ return current === path || (!isRoot && current.startsWith(path + '/'));
28
+ }
@@ -6,12 +6,14 @@ A single destination within a navigation bar.
6
6
  -->
7
7
  <script lang="ts">
8
8
  import { page } from '$app/state';
9
+ import { base as appBase } from '$app/paths';
9
10
  import type { NavItemProps } from './types.js';
10
11
  import { navitem } from './theme.js';
11
12
  import { Icon, Layer } from '../../../utils/index.js';
12
13
  import Badge from '../../badge/Badge.svelte';
13
14
  import { NavigationMenu } from 'bits-ui';
14
15
  import clsx from 'clsx';
16
+ import { isCurrentRoute } from '../currentRoute.js';
15
17
 
16
18
  let {
17
19
  href = '/',
@@ -23,12 +25,7 @@ A single destination within a navigation bar.
23
25
  ...rest
24
26
  }: NavItemProps = $props();
25
27
 
26
- const isActive = $derived(
27
- selected ||
28
- page.url.pathname === href ||
29
- (href !== '/' && page.url.href.startsWith(href)) ||
30
- page.url.href == href
31
- );
28
+ const isActive = $derived(selected || isCurrentRoute(href, page.url, appBase));
32
29
 
33
30
  const {
34
31
  base,
@@ -2,18 +2,18 @@ export declare const navbar: import("tailwind-variants").TVReturnType<{
2
2
  [key: string]: {
3
3
  [key: string]: import("tailwind-variants").ClassValue | {
4
4
  base?: import("tailwind-variants").ClassValue;
5
+ fab?: import("tailwind-variants").ClassValue;
5
6
  items?: import("tailwind-variants").ClassValue;
6
7
  ghost?: import("tailwind-variants").ClassValue;
7
- fab?: import("tailwind-variants").ClassValue;
8
8
  };
9
9
  };
10
10
  } | {
11
11
  [x: string]: {
12
12
  [x: string]: import("tailwind-variants").ClassValue | {
13
13
  base?: import("tailwind-variants").ClassValue;
14
+ fab?: import("tailwind-variants").ClassValue;
14
15
  items?: import("tailwind-variants").ClassValue;
15
16
  ghost?: import("tailwind-variants").ClassValue;
16
- fab?: import("tailwind-variants").ClassValue;
17
17
  };
18
18
  };
19
19
  } | {}, {
@@ -25,9 +25,9 @@ export declare const navbar: import("tailwind-variants").TVReturnType<{
25
25
  [key: string]: {
26
26
  [key: string]: import("tailwind-variants").ClassValue | {
27
27
  base?: import("tailwind-variants").ClassValue;
28
+ fab?: import("tailwind-variants").ClassValue;
28
29
  items?: import("tailwind-variants").ClassValue;
29
30
  ghost?: import("tailwind-variants").ClassValue;
30
- fab?: import("tailwind-variants").ClassValue;
31
31
  };
32
32
  };
33
33
  } | {}, {
@@ -1,7 +1,7 @@
1
- import { tv } from 'tailwind-variants';
1
+ import { tv } from '../../../utils/tv.js';
2
2
  export const navbar = tv({
3
3
  slots: {
4
- base: 'shadow-elevation-2 bg-md-sys-color-surface-container relative z-20 flex h-20 fixed bottom-spacing-0 w-full left-spacing-0 justify-around py-spacing-150 md:hidden',
4
+ base: 'shadow-elevation-2 bg-md-sys-color-surface-container z-layer-bar flex h-20 fixed bottom-spacing-0 w-full left-spacing-0 justify-around py-spacing-150 md:hidden',
5
5
  items: 'flex justify-around w-full',
6
6
  fab: 'bottom-24 right-spacing-200 absolute',
7
7
  ghost: 'h-20 w-full shrink-0 md:hidden pointer-events-none'