@noxlovette/material 0.6.1 → 0.7.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.
- package/claude-skill/material-design/SKILL.md +5 -2
- package/claude-skill/material-design/references/accessibility-checklist.md +5 -0
- package/claude-skill/material-design/references/component-patterns.md +24 -3
- package/claude-skill/material-design/references/motion-guide.md +53 -11
- package/claude-skill/material-design/references/tokens-and-styles.md +4 -0
- package/dist/animation/TransitionPatterns.stories.svelte +50 -0
- package/dist/animation/containerTransform.js +10 -4
- package/dist/animation/reducedMotion.d.ts +2 -0
- package/dist/animation/reducedMotion.js +2 -0
- package/dist/animation/sharedAxisTransition.d.ts +20 -3
- package/dist/animation/sharedAxisTransition.js +67 -8
- package/dist/attachments/drag.d.ts +52 -0
- package/dist/attachments/drag.js +217 -0
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/components/buttons/ButtonIcon.svelte +6 -2
- package/dist/components/buttons/ButtonIcon.svelte.d.ts +1 -1
- package/dist/components/buttons/context.d.ts +10 -0
- package/dist/components/buttons/context.js +9 -0
- package/dist/components/chips/Chip.mdx +24 -11
- package/dist/components/chips/Chip.stories.svelte +10 -0
- package/dist/components/chips/Chip.svelte +58 -18
- package/dist/components/chips/Chip.svelte.d.ts +3 -1
- package/dist/components/chips/ChipGroup.stories.svelte +73 -0
- package/dist/components/chips/ChipGroup.stories.svelte.d.ts +4 -0
- package/dist/components/chips/ChipGroup.svelte +242 -0
- package/dist/components/chips/ChipGroup.svelte.d.ts +33 -0
- package/dist/components/chips/index.d.ts +1 -0
- package/dist/components/chips/index.js +1 -0
- package/dist/components/chips/theme.d.ts +182 -21
- package/dist/components/chips/theme.js +116 -25
- package/dist/components/chips/types.d.ts +36 -2
- package/dist/components/containers/app/theme.d.ts +1 -1
- package/dist/components/containers/app/theme.js +1 -1
- package/dist/components/containers/bottom-sheet/BottomSheet.stories.svelte +1 -1
- package/dist/components/containers/list/List.svelte +8 -2
- package/dist/components/containers/list/ListItem.mdx +6 -0
- package/dist/components/containers/list/ListItem.svelte +77 -2
- package/dist/components/containers/list/context.d.ts +14 -0
- package/dist/components/containers/list/theme.d.ts +6 -0
- package/dist/components/containers/list/theme.js +10 -2
- package/dist/components/containers/pane/DraggablePane.svelte +25 -63
- package/dist/components/containers/pane/Pane.svelte +1 -1
- package/dist/components/containers/pane/theme.js +32 -6
- package/dist/components/containers/popover/theme.d.ts +3 -3
- package/dist/components/date/theme.d.ts +6 -6
- package/dist/components/forms/checkbox/theme.js +1 -1
- package/dist/components/forms/command/theme.d.ts +3 -3
- package/dist/components/misc/Avatar.stories.svelte +56 -7
- package/dist/components/misc/Avatar.stories.svelte.d.ts +2 -17
- package/dist/components/misc/Avatar.svelte +82 -8
- package/dist/components/misc/Avatar.svelte.d.ts +5 -0
- package/dist/components/misc/theme.d.ts +6 -0
- package/dist/components/misc/theme.js +5 -3
- package/dist/components/misc/types.d.ts +17 -1
- package/dist/components/nav/appbar/AppBar.svelte +8 -0
- package/dist/components/nav/appbar/AppBar.svelte.d.ts +3 -0
- package/dist/components/nav/appbar/theme.js +14 -2
- package/dist/components/nav/currentRoute.d.ts +11 -0
- package/dist/components/nav/currentRoute.js +28 -0
- package/dist/components/nav/navbar/NavbarItem.svelte +3 -6
- package/dist/components/nav/navbar/theme.d.ts +3 -3
- package/dist/components/nav/rail/Rail.mdx +65 -23
- package/dist/components/nav/rail/Rail.stories.svelte +24 -4
- package/dist/components/nav/rail/Rail.stories.svelte.d.ts +2 -17
- package/dist/components/nav/rail/Rail.svelte +163 -48
- package/dist/components/nav/rail/Rail.svelte.d.ts +6 -3
- package/dist/components/nav/rail/RailItem.svelte +122 -65
- package/dist/components/nav/rail/RailItem.svelte.d.ts +3 -2
- package/dist/components/nav/rail/theme.d.ts +69 -84
- package/dist/components/nav/rail/theme.js +79 -66
- package/dist/components/nav/tabs/Tabs.stories.svelte +2 -1
- package/dist/index.css +2 -0
- package/dist/styles/components.css +50 -0
- package/dist/styles/icon.css +13 -0
- package/dist/utils/icon/Icon.stories.svelte +97 -0
- package/dist/utils/icon/Icon.stories.svelte.d.ts +4 -0
- package/dist/utils/icon/Icon.svelte +101 -12
- package/dist/utils/icon/Icon.svelte.d.ts +9 -0
- package/dist/utils/icon/MaterialSymbolsProvider.svelte +11 -4
- package/dist/utils/icon/base-icons.d.ts +1 -1
- package/dist/utils/icon/base-icons.js +1 -1
- package/dist/utils/icon/icon-names.d.ts +2 -0
- package/dist/utils/icon/icon-names.js +2 -0
- package/dist/utils/icon/swap.d.ts +6 -0
- package/dist/utils/icon/swap.js +45 -0
- package/dist/utils/icon/types.d.ts +59 -7
- package/dist/utils/tv.d.ts +3 -4
- package/dist/utils/tv.js +14 -6
- package/package.json +2 -1
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
import { tv } from '../../../utils/tv.js';
|
|
2
|
+
/*
|
|
3
|
+
M3 Expressive navigation rail (https://m3.material.io/components/navigation-rail/specs):
|
|
4
|
+
- Container: `surface-container`, the spec's optional container role, which is this library's
|
|
5
|
+
window colour (App), so the rail reads as part of the window beside `surface` panes rather
|
|
6
|
+
than as a strip. The modal rail's token is `surface-container` too.
|
|
7
|
+
- Collapsed: 96dp wide, no elevation or shape. Destinations sit 4dp apart.
|
|
8
|
+
- Expanded: hugs its widest destination between 220 and 360dp, destinations touching. Standard
|
|
9
|
+
(lg+, pushes content) keeps the collapsed container; modal (md, over a scrim) adds
|
|
10
|
+
elevation 3 and 16dp trailing corners.
|
|
11
|
+
- 44dp above the first element; at least 40dp between the menu/FAB header and the destinations.
|
|
12
|
+
- Header elements sit in a 56dp column 20dp from the leading edge, the same column as the
|
|
13
|
+
destinations' active indicators, so nothing shifts sideways when the rail expands.
|
|
14
|
+
Expanding is one spatial spring on a progress value, 0 collapsed to 1 expanded, which
|
|
15
|
+
Rail.svelte writes to `--rail-p` (after Compose's WideNavigationRail). The width, the gap
|
|
16
|
+
between destinations, the modal corners and each destination's geometry all follow it, so
|
|
17
|
+
nothing here transitions spatially on its own.
|
|
18
|
+
*/
|
|
2
19
|
export const rail = tv({
|
|
3
20
|
slots: {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
21
|
+
// Modal corners grow with the progress (0 → 16dp); the standard rail stays square.
|
|
22
|
+
base: 'rounded-r-[calc(var(--radius-lg)_*_var(--rail-p,0))] lg:rounded-r-none hidden md:flex flex-col z-layer-rail overflow-hidden pt-(--md-comp-nav-rail-collapsed-top-space) pb-spacing-250 bg-md-sys-color-surface-container transition-[box-shadow] md-sys-motion-effects',
|
|
23
|
+
// Always start-aligned: collapsed, the content box is exactly the 56dp column, so this is
|
|
24
|
+
// centred anyway. Centring it would fling the menu and FAB to the middle of the still-wide
|
|
25
|
+
// rail the moment a collapse starts.
|
|
26
|
+
header: 'flex flex-col items-start gap-spacing-300 px-spacing-250',
|
|
27
|
+
menu: 'flex w-spacing-700 justify-center',
|
|
28
|
+
nav: 'flex min-h-spacing-0 w-full flex-1 flex-col',
|
|
29
|
+
// 4dp between destinations collapsed, 0 expanded, following the progress.
|
|
30
|
+
items: 'flex w-full flex-col overflow-x-hidden overflow-y-auto [scrollbar-width:none] gap-[calc(var(--spacing-spacing-50)_*_(1_-_var(--rail-p,0)))]',
|
|
31
|
+
ghost: 'hidden md:block shrink-0 w-(--md-comp-nav-rail-collapsed-container-width) lg:w-(--rail-width)',
|
|
32
|
+
scrim: 'hidden md:block lg:hidden inset-spacing-0 z-layer-rail bg-md-sys-color-scrim/32 transition-opacity md-sys-motion-effects'
|
|
8
33
|
},
|
|
9
34
|
variants: {
|
|
10
35
|
anchor: {
|
|
@@ -19,15 +44,19 @@ export const rail = tv({
|
|
|
19
44
|
},
|
|
20
45
|
expanded: {
|
|
21
46
|
true: {
|
|
22
|
-
base: '
|
|
23
|
-
|
|
47
|
+
base: 'w-max min-w-(--md-comp-nav-rail-expanded-container-width-minimum) max-w-(--md-comp-nav-rail-expanded-container-width-maximum) pt-(--md-comp-nav-rail-expanded-top-space) shadow-elevation-3 lg:shadow-elevation-0',
|
|
48
|
+
scrim: 'opacity-100'
|
|
24
49
|
},
|
|
25
50
|
false: {
|
|
26
|
-
base: 'w-
|
|
27
|
-
|
|
28
|
-
ghost: 'w-24'
|
|
51
|
+
base: 'w-(--md-comp-nav-rail-collapsed-container-width)',
|
|
52
|
+
scrim: 'pointer-events-none opacity-0'
|
|
29
53
|
}
|
|
30
54
|
},
|
|
55
|
+
/* Header space: only when there is a header to keep the destinations away from. */
|
|
56
|
+
header: {
|
|
57
|
+
true: { nav: 'mt-spacing-500' },
|
|
58
|
+
false: {}
|
|
59
|
+
},
|
|
31
60
|
rounded: {
|
|
32
61
|
true: {
|
|
33
62
|
base: 'rounded-xl'
|
|
@@ -36,81 +65,65 @@ export const rail = tv({
|
|
|
36
65
|
}
|
|
37
66
|
},
|
|
38
67
|
defaultVariants: {
|
|
39
|
-
anchor: 'viewport'
|
|
68
|
+
anchor: 'viewport',
|
|
69
|
+
header: false
|
|
40
70
|
}
|
|
41
71
|
});
|
|
72
|
+
/*
|
|
73
|
+
One destination. The link spans the rail's full width (M3: the target area always does); the
|
|
74
|
+
active indicator inside it hugs its contents. Collapsed, the indicator is 56×32 around the icon
|
|
75
|
+
with the label 4dp below; expanded, it's a 56dp pill around icon and label, 16dp padding, 8dp
|
|
76
|
+
between them. State layers are `on-secondary-container` at 8% hover and 10% focus/press, drawn
|
|
77
|
+
on the indicator but triggered anywhere on the link. Selecting a destination grows its fill
|
|
78
|
+
out from the centre (M3's active indicator, after Compose's NavigationRailItem).
|
|
79
|
+
|
|
80
|
+
Geometry isn't here: it interpolates on the rail's expand progress, in RailItem.svelte's
|
|
81
|
+
<style>. `layout` is which side of the halfway point that progress is on, and decides only
|
|
82
|
+
what may switch while the label is invisible: its typescale and colour.
|
|
83
|
+
*/
|
|
42
84
|
export const railElement = tv({
|
|
43
85
|
slots: {
|
|
44
|
-
base: 'group relative
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
86
|
+
base: 'group rail-item relative flex w-full px-spacing-250 outline-none',
|
|
87
|
+
// As wide as the expanded pill, so an expanded rail (w-max) hugs its widest destination.
|
|
88
|
+
sizer: 'relative h-full shrink-0 pl-spacing-600 pr-spacing-200',
|
|
89
|
+
measure: 'invisible md-sys-typescale-label-large whitespace-nowrap',
|
|
90
|
+
indicator: 'rail-indicator absolute left-spacing-0 isolate rounded-full state-layer before:rounded-full',
|
|
91
|
+
// The selected fill, under the state layer. It grows from the indicator's centre to its
|
|
92
|
+
// edges on select and shrinks back on deselect, on RailItem's --rail-sel spring.
|
|
93
|
+
fill: 'rail-fill absolute -z-10 rounded-full bg-md-sys-color-secondary-container',
|
|
94
|
+
icon: 'rail-icon absolute left-spacing-200 inline-flex',
|
|
95
|
+
label: 'rail-label absolute whitespace-nowrap transition-colors md-sys-motion-fast-effects'
|
|
49
96
|
},
|
|
50
97
|
variants: {
|
|
51
98
|
active: {
|
|
52
99
|
true: {
|
|
53
|
-
|
|
54
|
-
label: 'font-bold',
|
|
55
|
-
iconContainer: 'group-hover:text-md-sys-color-secondary bg-md-sys-color-secondary-container'
|
|
100
|
+
icon: 'text-md-sys-color-on-secondary-container'
|
|
56
101
|
},
|
|
57
102
|
false: {
|
|
58
|
-
|
|
59
|
-
|
|
103
|
+
icon: 'text-md-sys-color-on-surface-variant',
|
|
104
|
+
label: 'text-md-sys-color-on-surface-variant'
|
|
60
105
|
}
|
|
61
106
|
},
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
content: 'p-spacing-200 gap-spacing-150',
|
|
66
|
-
iconContainer: '',
|
|
67
|
-
label: 'md-sys-typescale-label-large'
|
|
68
|
-
},
|
|
69
|
-
false: {
|
|
70
|
-
base: 'text-center justify-center',
|
|
71
|
-
content: 'flex-col',
|
|
72
|
-
iconContainer: 'py-spacing-75 px-spacing-200',
|
|
73
|
-
label: 'md-sys-typescale-label-medium'
|
|
74
|
-
}
|
|
107
|
+
layout: {
|
|
108
|
+
top: { label: 'md-sys-typescale-label-medium' },
|
|
109
|
+
start: { label: 'md-sys-typescale-label-large' }
|
|
75
110
|
},
|
|
76
111
|
disabled: {
|
|
77
112
|
true: {
|
|
78
113
|
base: 'cursor-not-allowed opacity-38'
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
mobile: {
|
|
82
|
-
true: {
|
|
83
|
-
base: 'flex-1 flex-col items-center justify-center h-full min-w-spacing-0 max-w-[168px]',
|
|
84
|
-
content: 'py-spacing-0 gap-spacing-50 items-center justify-center w-full',
|
|
85
|
-
iconContainer: 'h-spacing-400 w-spacing-800 py-spacing-0 px-spacing-0',
|
|
86
|
-
icon: 'size-spacing-300',
|
|
87
|
-
label: 'truncate w-full text-center'
|
|
88
114
|
},
|
|
89
|
-
false: {
|
|
115
|
+
false: {
|
|
116
|
+
// State layer and focus ring on the indicator, triggered by the full-width link.
|
|
117
|
+
indicator: 'group-hover:before:bg-md-sys-color-on-secondary-container/8 group-focus-visible:before:bg-md-sys-color-on-secondary-container/10 group-active:before:bg-md-sys-color-on-secondary-container/10 group-focus-visible:outline-3 group-focus-visible:outline-offset-2 group-focus-visible:outline-md-sys-color-secondary'
|
|
118
|
+
}
|
|
90
119
|
}
|
|
91
120
|
},
|
|
92
121
|
compoundVariants: [
|
|
93
|
-
{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
expanded: true,
|
|
103
|
-
active: false,
|
|
104
|
-
class: {
|
|
105
|
-
content: 'group-hover:bg-md-sys-color-surface-variant'
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
expanded: false,
|
|
110
|
-
active: false,
|
|
111
|
-
class: {
|
|
112
|
-
iconContainer: 'group-hover:bg-md-sys-color-surface-variant'
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
]
|
|
122
|
+
{ active: true, layout: 'top', class: { label: 'text-md-sys-color-secondary' } },
|
|
123
|
+
{ active: true, layout: 'start', class: { label: 'text-md-sys-color-on-secondary-container' } }
|
|
124
|
+
],
|
|
125
|
+
defaultVariants: {
|
|
126
|
+
layout: 'top',
|
|
127
|
+
disabled: false
|
|
128
|
+
}
|
|
116
129
|
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import TabHolder from './TabHolder.svelte';
|
|
4
4
|
import TabContent from './TabContent.svelte';
|
|
5
5
|
import Body from '../../typography/body/Body.svelte';
|
|
6
|
+
import type { TabProps } from './types.js';
|
|
6
7
|
|
|
7
8
|
const { Story } = defineMeta({
|
|
8
9
|
title: 'Navigation/Tabs',
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
{ value: 'flights', name: 'Flights', iconProps: { name: 'flight' } },
|
|
28
29
|
{ value: 'trips', name: 'Trips', iconProps: { name: 'luggage' } },
|
|
29
30
|
{ value: 'explore', name: 'Explore', iconProps: { name: 'explore' } }
|
|
30
|
-
];
|
|
31
|
+
] satisfies TabProps[];
|
|
31
32
|
</script>
|
|
32
33
|
|
|
33
34
|
<Story name="Playground">
|
package/dist/index.css
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* M3 component dimensions that aren't on the spacing scale (issue #30). Each property is named
|
|
3
|
+
* after its token in the M3 token database, md.comp.<component>.<…> with dots as dashes, and
|
|
4
|
+
* used as `w-(--md-comp-…)`. A dimension that lands on the spacing grid uses the spacing token
|
|
5
|
+
* instead (the rail's 4dp item gap is `gap-spacing-50`, not a property here).
|
|
6
|
+
*/
|
|
7
|
+
:root {
|
|
8
|
+
/* Navigation rail: https://m3.material.io/components/navigation-rail/specs */
|
|
9
|
+
--md-comp-nav-rail-collapsed-container-width: 6rem; /* 96dp */
|
|
10
|
+
--md-comp-nav-rail-collapsed-top-space: 2.75rem; /* 44dp */
|
|
11
|
+
--md-comp-nav-rail-expanded-top-space: 2.75rem; /* 44dp */
|
|
12
|
+
--md-comp-nav-rail-expanded-container-width-minimum: 13.75rem; /* 220dp */
|
|
13
|
+
--md-comp-nav-rail-expanded-container-width-maximum: 22.5rem; /* 360dp */
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* How far the page's content starts from the leading edge to clear a viewport-anchored Rail.
|
|
18
|
+
* The Rail writes --md-rail-width (its drawn width, every frame while it springs) on <html>;
|
|
19
|
+
* with no Rail mounted it's unset and the inset is 0. On medium windows the expanded rail is
|
|
20
|
+
* modal and overlays the content, so the inset stays at the collapsed width; from lg the
|
|
21
|
+
* standard rail pushes the content, so the inset follows its width. App's shell pads by it;
|
|
22
|
+
* fixed surfaces that span the window (AppBar) start at it. Custom layouts: `ps-(--md-rail-inset)`.
|
|
23
|
+
*/
|
|
24
|
+
:root {
|
|
25
|
+
--md-rail-inset: 0px;
|
|
26
|
+
}
|
|
27
|
+
@media (width >= 48rem) {
|
|
28
|
+
:root {
|
|
29
|
+
--md-rail-inset: var(--md-rail-rest, 0px);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
@media (width >= 64rem) {
|
|
33
|
+
:root {
|
|
34
|
+
--md-rail-inset: var(--md-rail-width, 0px);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
* A page built on App behaves as an app, not a document: scrolling stops at its edges instead of
|
|
40
|
+
* bouncing (macOS/iOS rubber-banding) or chaining into pull-to-refresh, which showed the blank
|
|
41
|
+
* canvas past App's shell. The canvas takes the window colour anyway, for the browsers that
|
|
42
|
+
* still overscroll and the safe areas around it. Scoped to pages with an App, and in the base
|
|
43
|
+
* layer, so a consumer can restore overscroll with one `html { overscroll-behavior: auto }`.
|
|
44
|
+
*/
|
|
45
|
+
@layer base {
|
|
46
|
+
:root:has(.md-app) {
|
|
47
|
+
overscroll-behavior: none;
|
|
48
|
+
background-color: var(--color-md-sys-color-surface-container);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Default grade for Icon's `grad="auto"`. Light icons on a dark background look heavier than dark
|
|
3
|
+
ones on a light background (visual bleed), so M3 drops the grade to -25 on dark UI.
|
|
4
|
+
https://m3.material.io/styles/icons/applying-icons#grade
|
|
5
|
+
*/
|
|
6
|
+
:root {
|
|
7
|
+
--md-icon-grade: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.dark,
|
|
11
|
+
[data-theme^='dark'] {
|
|
12
|
+
--md-icon-grade: -25;
|
|
13
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
3
|
+
import Body from '../../components/typography/body/Body.svelte';
|
|
4
|
+
import ButtonIcon from '../../components/buttons/ButtonIcon.svelte';
|
|
5
|
+
import Icon from './Icon.svelte';
|
|
6
|
+
|
|
7
|
+
const sizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
|
|
8
|
+
const variants = ['rounded', 'outlined', 'sharp'] as const;
|
|
9
|
+
|
|
10
|
+
const { Story } = defineMeta({
|
|
11
|
+
title: 'Utils/Icon',
|
|
12
|
+
component: Icon,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
argTypes: {
|
|
15
|
+
size: { control: 'select', options: ['inline', ...sizes] },
|
|
16
|
+
variant: { control: 'inline-radio', options: variants },
|
|
17
|
+
fill: { control: { type: 'range', min: 0, max: 1, step: 0.1 } },
|
|
18
|
+
wght: { control: { type: 'range', min: 100, max: 700, step: 100 } },
|
|
19
|
+
grad: { control: 'select', options: ['auto', -50, -25, 0, 100, 200] },
|
|
20
|
+
opsz: { control: { type: 'range', min: 20, max: 48, step: 1 } },
|
|
21
|
+
transition: { control: 'inline-radio', options: ['none', 'fade', 'rotate'] }
|
|
22
|
+
},
|
|
23
|
+
args: { name: 'favorite', size: 'xl', variant: 'rounded', fill: 0, wght: 400, grad: 'auto' }
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<script lang="ts">
|
|
28
|
+
let menuOpen = $state(false);
|
|
29
|
+
let adding = $state(false);
|
|
30
|
+
let playing = $state(false);
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<Story name="Playground" />
|
|
34
|
+
|
|
35
|
+
<!-- Toggle each button, and toggle again mid-flight: the spring reverses from where it is. -->
|
|
36
|
+
<Story name="Transitions" asChild>
|
|
37
|
+
<div class="gap-spacing-300 flex items-center">
|
|
38
|
+
<ButtonIcon
|
|
39
|
+
variant="standard"
|
|
40
|
+
tooltipContent="rotate: menu ↔ menu_open"
|
|
41
|
+
iconProps={{ name: menuOpen ? 'menu_open' : 'menu', transition: 'rotate' }}
|
|
42
|
+
onclick={() => (menuOpen = !menuOpen)}
|
|
43
|
+
/>
|
|
44
|
+
<ButtonIcon
|
|
45
|
+
variant="tonal"
|
|
46
|
+
tooltipContent="rotate: add ↔ close"
|
|
47
|
+
iconProps={{ name: adding ? 'close' : 'add', transition: 'rotate' }}
|
|
48
|
+
onclick={() => (adding = !adding)}
|
|
49
|
+
/>
|
|
50
|
+
<ButtonIcon
|
|
51
|
+
variant="filled"
|
|
52
|
+
tooltipContent="fade: play_arrow ↔ pause"
|
|
53
|
+
iconProps={{ name: playing ? 'pause' : 'play_arrow', transition: 'fade' }}
|
|
54
|
+
onclick={() => (playing = !playing)}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
</Story>
|
|
58
|
+
|
|
59
|
+
<Story name="Styles" asChild>
|
|
60
|
+
<div class="gap-spacing-300 flex">
|
|
61
|
+
{#each variants as variant (variant)}
|
|
62
|
+
<Icon name="home" size="lg" {variant} />
|
|
63
|
+
{/each}
|
|
64
|
+
</div>
|
|
65
|
+
</Story>
|
|
66
|
+
|
|
67
|
+
<Story name="Sizes" asChild>
|
|
68
|
+
<div class="gap-spacing-300 flex items-end">
|
|
69
|
+
{#each sizes as size (size)}
|
|
70
|
+
<Icon name="settings" {size} />
|
|
71
|
+
{/each}
|
|
72
|
+
</div>
|
|
73
|
+
</Story>
|
|
74
|
+
|
|
75
|
+
<Story name="Fill for state" asChild>
|
|
76
|
+
<div class="gap-spacing-300 flex">
|
|
77
|
+
<Icon name="favorite" size="lg" fill={0} />
|
|
78
|
+
<Icon name="favorite" size="lg" fill={1} />
|
|
79
|
+
</div>
|
|
80
|
+
</Story>
|
|
81
|
+
|
|
82
|
+
<Story name="Weights" asChild>
|
|
83
|
+
<div class="gap-spacing-200 flex">
|
|
84
|
+
{#each [200, 300, 400, 500, 600, 700] as wght (wght)}
|
|
85
|
+
<Icon name="search" size="lg" {wght} />
|
|
86
|
+
{/each}
|
|
87
|
+
</div>
|
|
88
|
+
</Story>
|
|
89
|
+
|
|
90
|
+
<Story name="Inline with text" asChild>
|
|
91
|
+
<Body size="large">
|
|
92
|
+
Saved to favorites <Icon name="favorite" size="inline" fill={1} />, shared <Icon
|
|
93
|
+
name="share"
|
|
94
|
+
size="inline"
|
|
95
|
+
/>
|
|
96
|
+
</Body>
|
|
97
|
+
</Story>
|
|
@@ -1,9 +1,29 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
A Material Symbol, rendered from the variable icon font with its four axes: fill, weight, grade and
|
|
4
|
+
optical size. https://m3.material.io/styles/icons/applying-icons
|
|
5
|
+
|
|
6
|
+
Decorative by default (`aria-hidden`), so a screen reader doesn't read out the ligature name.
|
|
7
|
+
Pass `aria-label` when the icon carries meaning with no visible text next to it.
|
|
8
|
+
|
|
9
|
+
With `transition`, a change of `name` crossfades on a spring instead of swapping the glyph.
|
|
10
|
+
-->
|
|
1
11
|
<script lang="ts">
|
|
2
12
|
import clsx from 'clsx';
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
13
|
+
import { untrack } from 'svelte';
|
|
14
|
+
import { presence } from '../../animation/presence.svelte.js';
|
|
15
|
+
import { twMerge } from '../tv.js';
|
|
16
|
+
import { atRest, iconSwap, iconSwapReduced } from './swap.js';
|
|
17
|
+
import type { IconProps, IconSize, MaterialSymbolName } from './types.js';
|
|
5
18
|
|
|
19
|
+
/*
|
|
20
|
+
The optical size tracks the rendered size, so a 40dp icon keeps its stroke weight instead of
|
|
21
|
+
being a scaled-up 24dp glyph (M3 designs 20, 24, 40 and 48). `inline` takes the size of the
|
|
22
|
+
surrounding text and drops the baseline by ~11.5% of it, as M3 asks for symbols set in a line
|
|
23
|
+
of text; body and label text sit nearest the 20 optical size.
|
|
24
|
+
*/
|
|
6
25
|
const sizeMap: Record<IconSize, { cls: string; opsz: number }> = {
|
|
26
|
+
inline: { cls: 'size-[1em] align-[-0.115em]', opsz: 20 },
|
|
7
27
|
xs: { cls: 'size-spacing-200 text-[16px]', opsz: 20 },
|
|
8
28
|
sm: { cls: 'size-spacing-250 text-[20px]', opsz: 20 },
|
|
9
29
|
md: { cls: 'size-spacing-300 text-[24px]', opsz: 24 },
|
|
@@ -17,21 +37,90 @@
|
|
|
17
37
|
variant = 'rounded',
|
|
18
38
|
fill = 0,
|
|
19
39
|
wght = 400,
|
|
20
|
-
grad =
|
|
40
|
+
grad = 'auto',
|
|
21
41
|
opsz,
|
|
22
|
-
|
|
42
|
+
transition = 'none',
|
|
43
|
+
class: className,
|
|
44
|
+
...restProps
|
|
23
45
|
}: IconProps = $props();
|
|
24
46
|
|
|
25
47
|
const resolved = $derived(sizeMap[size]);
|
|
26
48
|
const resolvedOpsz = $derived(opsz ?? resolved.opsz);
|
|
27
|
-
|
|
49
|
+
// 'auto' reads --md-icon-grade (styles/icon.css): 0 on light schemes, -25 on dark ones.
|
|
50
|
+
const resolvedGrad = $derived(grad === 'auto' ? 'var(--md-icon-grade, 0)' : grad);
|
|
51
|
+
const labelled = $derived(Boolean(restProps['aria-label'] || restProps['aria-labelledby']));
|
|
52
|
+
// With a transition the glyphs stack in one grid cell; without, the original single glyph.
|
|
53
|
+
const wrapperClass = $derived(
|
|
54
|
+
transition === 'none'
|
|
55
|
+
? 'inline-flex shrink-0 items-center justify-center leading-none'
|
|
56
|
+
: 'inline-grid shrink-0 place-items-center leading-none'
|
|
57
|
+
);
|
|
58
|
+
const glyphClass = $derived(
|
|
59
|
+
`material-symbols-${variant} md-sys-motion-fast-effects transition-[font-variation-settings] select-none`
|
|
60
|
+
);
|
|
61
|
+
const glyphStyle = $derived(
|
|
62
|
+
`font-size: 1em; font-variation-settings: 'FILL' ${fill}, 'wght' ${wght}, 'GRAD' ${resolvedGrad}, 'opsz' ${resolvedOpsz};`
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
With a transition, the glyphs are a stack: the current one plus any still leaving. A name
|
|
67
|
+
change marks the current glyph leaving and adds the new one; each drops out of the stack when
|
|
68
|
+
its exit spring finishes. Changing back to a name that's still leaving brings that glyph back,
|
|
69
|
+
retargeted mid-flight, instead of stacking a third.
|
|
70
|
+
*/
|
|
71
|
+
type Glyph = { id: number; name: MaterialSymbolName; leaving: boolean; initial: boolean };
|
|
72
|
+
let nextId = 1;
|
|
73
|
+
let glyphs = $state<Glyph[]>([
|
|
74
|
+
{ id: 0, name: untrack(() => name), leaving: false, initial: true }
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
$effect.pre(() => {
|
|
78
|
+
const next = name;
|
|
79
|
+
untrack(() => {
|
|
80
|
+
if (glyphs.find((g) => !g.leaving)?.name === next) return;
|
|
81
|
+
if (transition === 'none') {
|
|
82
|
+
glyphs = [{ id: nextId++, name: next, leaving: false, initial: true }];
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const g of glyphs) g.leaving = g.name !== next;
|
|
86
|
+
if (!glyphs.some((g) => g.name === next)) {
|
|
87
|
+
glyphs.push({ id: nextId++, name: next, leaving: false, initial: false });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const reducedMotion = () =>
|
|
93
|
+
typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
94
|
+
|
|
95
|
+
function swap(glyph: Glyph) {
|
|
96
|
+
const preset =
|
|
97
|
+
transition === 'none' || reducedMotion() ? iconSwapReduced : iconSwap[transition];
|
|
98
|
+
return presence(
|
|
99
|
+
() => !glyph.leaving,
|
|
100
|
+
glyph.initial ? atRest(preset) : preset,
|
|
101
|
+
() => (glyphs = glyphs.filter((g) => g.id !== glyph.id))
|
|
102
|
+
);
|
|
103
|
+
}
|
|
28
104
|
</script>
|
|
29
105
|
|
|
30
106
|
<!-- twMerge: a caller's size class replaces the preset's instead of racing it in the cascade. -->
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
107
|
+
<span
|
|
108
|
+
role={labelled ? 'img' : undefined}
|
|
109
|
+
aria-hidden={labelled ? undefined : 'true'}
|
|
110
|
+
{...restProps}
|
|
111
|
+
class={twMerge(resolved.cls, clsx(className), wrapperClass)}
|
|
112
|
+
>
|
|
113
|
+
<!-- translate="no": page translation would rename the ligature and break the glyph. -->
|
|
114
|
+
{#if transition === 'none'}
|
|
115
|
+
<span class={glyphClass} translate="no" style={glyphStyle}>{name}</span>
|
|
116
|
+
{:else}
|
|
117
|
+
{#each glyphs as glyph (glyph.id)}
|
|
118
|
+
<span
|
|
119
|
+
class="{glyphClass} [grid-area:1/1]"
|
|
120
|
+
translate="no"
|
|
121
|
+
style={glyphStyle}
|
|
122
|
+
{@attach swap(glyph)}>{glyph.name}</span
|
|
123
|
+
>
|
|
124
|
+
{/each}
|
|
125
|
+
{/if}
|
|
126
|
+
</span>
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { IconProps } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A Material Symbol, rendered from the variable icon font with its four axes: fill, weight, grade and
|
|
4
|
+
* optical size. https://m3.material.io/styles/icons/applying-icons
|
|
5
|
+
*
|
|
6
|
+
* Decorative by default (`aria-hidden`), so a screen reader doesn't read out the ligature name.
|
|
7
|
+
* Pass `aria-label` when the icon carries meaning with no visible text next to it.
|
|
8
|
+
*
|
|
9
|
+
* With `transition`, a change of `name` crossfades on a spring instead of swapping the glyph.
|
|
10
|
+
*/
|
|
2
11
|
declare const Icon: import("svelte").Component<IconProps, {}, "">;
|
|
3
12
|
type Icon = ReturnType<typeof Icon>;
|
|
4
13
|
export default Icon;
|
|
@@ -7,7 +7,12 @@ Use the App.svelte component instead
|
|
|
7
7
|
import { baseIcons } from './base-icons.js';
|
|
8
8
|
import type { MaterialSymbolsProviderProps } from './types.js';
|
|
9
9
|
|
|
10
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
extraIcons = [],
|
|
12
|
+
display = 'swap',
|
|
13
|
+
subset = true,
|
|
14
|
+
families = ['rounded', 'outlined', 'sharp']
|
|
15
|
+
}: MaterialSymbolsProviderProps = $props();
|
|
11
16
|
|
|
12
17
|
const iconList = $derived.by(() => {
|
|
13
18
|
const combined = [...baseIcons, ...extraIcons].filter(Boolean);
|
|
@@ -15,7 +20,7 @@ Use the App.svelte component instead
|
|
|
15
20
|
});
|
|
16
21
|
|
|
17
22
|
const iconNamesParam = $derived.by(() => {
|
|
18
|
-
if (!iconList.length) return '';
|
|
23
|
+
if (!subset || !iconList.length) return '';
|
|
19
24
|
return `&icon_names=${encodeURIComponent(iconList.join(','))}`;
|
|
20
25
|
});
|
|
21
26
|
|
|
@@ -25,9 +30,11 @@ Use the App.svelte component instead
|
|
|
25
30
|
});
|
|
26
31
|
|
|
27
32
|
const href = $derived.by(() => {
|
|
28
|
-
const families = ['Rounded', 'Outlined', 'Sharp'];
|
|
29
33
|
const familyParams = families
|
|
30
|
-
.map(
|
|
34
|
+
.map(
|
|
35
|
+
(f) =>
|
|
36
|
+
`family=Material+Symbols+${f[0].toUpperCase()}${f.slice(1)}:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200`
|
|
37
|
+
)
|
|
31
38
|
.join('&');
|
|
32
39
|
return `https://fonts.googleapis.com/css2?${familyParams}${iconNamesParam}${displayParam}`;
|
|
33
40
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const baseIcons: readonly ["arrow_drop_down", "check", "chevron_left", "chevron_right", "close", "palette", "
|
|
1
|
+
export declare const baseIcons: readonly ["arrow_drop_down", "check", "chevron_left", "chevron_right", "close", "palette", "upload", "home", "info", "search", "settings", "warning", "calendar_month", "schedule", "check_circle", "error", "delete", "right_panel_close", "right_panel_open", "edit", "add", "favorite", "share", "settings", "help", "menu", "menu_open"];
|