@noxlovette/material 0.6.1 → 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.
- 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/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/theme.js +2 -1
- 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,72 +1,163 @@
|
|
|
1
1
|
import { tv } from '../../utils/tv.js';
|
|
2
|
+
/*
|
|
3
|
+
Values from the M3 chip tokens (md.comp.{assist,filter,input,suggestion}-chip.*):
|
|
4
|
+
https://m3.material.io/components/chips/specs
|
|
5
|
+
|
|
6
|
+
- 32dp tall, 8dp corners, label-large. Padding is 16dp on each side, 8dp on a side that holds an
|
|
7
|
+
icon, and 4dp before an avatar; 8dp between elements. Icons are 18dp, avatars a 24dp circle.
|
|
8
|
+
- The 1dp outline is a border, so it sits inside the 32dp and stays visible (and darkens) while
|
|
9
|
+
the separate focus indicator is showing.
|
|
10
|
+
- Disabled: flat chips lose their fill-less outline to on-surface/12; elevated and selected
|
|
11
|
+
containers turn on-surface/12 with no shadow; label and icons are on-surface/38.
|
|
12
|
+
|
|
13
|
+
Chip doesn't use md-component-button-base: its disabled fill would paint flat chips, which M3
|
|
14
|
+
leaves unfilled.
|
|
15
|
+
|
|
16
|
+
An input chip is a container holding two sibling buttons, the primary `action` and the remove
|
|
17
|
+
button, since one can't nest inside the other. The container keeps the look (and shows the focus
|
|
18
|
+
indicator while the action has focus); the action carries the padding, and its state layer covers the whole chip.
|
|
19
|
+
*/
|
|
2
20
|
export const chip = tv({
|
|
3
21
|
slots: {
|
|
4
|
-
|
|
5
|
-
|
|
22
|
+
// in-data-dragged: M3's dragged state, set by ChipGroup on the chip's slot: 8dp elevation and
|
|
23
|
+
// a 16% state layer.
|
|
24
|
+
base: 'in-data-dragged:shadow-elevation-4 in-data-dragged:before:bg-current/16 state-layer md-sys-state-focus-indicator relative inline-flex h-spacing-400 max-w-max shrink-0 items-center justify-center gap-spacing-100 rounded-sm border border-transparent px-spacing-200 md-sys-typescale-label-large select-none before:rounded-sm transition-[background-color,border-color,box-shadow] md-sys-motion-effects',
|
|
25
|
+
// 18dp is the chip spec's own icon size, between the 16 and 20 Icon presets.
|
|
26
|
+
icon: 'size-[18px] shrink-0 text-[18px]',
|
|
27
|
+
avatar: 'size-spacing-300 shrink-0 overflow-hidden rounded-full',
|
|
28
|
+
// Static, so its Layer (state layer and hit area) spans the whole chip; the remove button is
|
|
29
|
+
// positioned and later in the DOM, so it stays on top.
|
|
30
|
+
action: 'static flex h-full min-w-spacing-0 cursor-pointer items-center gap-spacing-100 self-stretch rounded-sm px-spacing-200 outline-none disabled:cursor-not-allowed',
|
|
6
31
|
label: '',
|
|
7
|
-
|
|
8
|
-
|
|
32
|
+
// 18dp glyph with a 48dp touch target around it (::after).
|
|
33
|
+
trailing: "relative inline-flex size-[18px] shrink-0 cursor-pointer items-center justify-center rounded-full after:absolute after:top-1/2 after:left-1/2 after:size-spacing-600 after:-translate-1/2 after:content-['']",
|
|
34
|
+
checkIcon: 'size-[18px] shrink-0'
|
|
9
35
|
},
|
|
10
36
|
variants: {
|
|
11
37
|
variant: {
|
|
12
|
-
assist: '',
|
|
13
|
-
filter: '',
|
|
14
|
-
input: '',
|
|
15
|
-
suggestion:
|
|
38
|
+
assist: { base: 'text-md-sys-color-on-surface', icon: 'text-md-sys-color-primary' },
|
|
39
|
+
filter: { base: 'text-md-sys-color-on-surface-variant' },
|
|
40
|
+
input: { base: 'text-md-sys-color-on-surface-variant' },
|
|
41
|
+
suggestion: {
|
|
42
|
+
base: 'text-md-sys-color-on-surface-variant',
|
|
43
|
+
icon: 'text-md-sys-color-primary'
|
|
44
|
+
}
|
|
16
45
|
},
|
|
17
46
|
elevated: {
|
|
18
|
-
true: '',
|
|
47
|
+
true: { base: 'shadow-elevation-1 hover:shadow-elevation-2' },
|
|
19
48
|
false: ''
|
|
20
49
|
},
|
|
21
50
|
selected: {
|
|
22
|
-
true:
|
|
51
|
+
true: {
|
|
52
|
+
base: 'bg-md-sys-color-secondary-container text-md-sys-color-on-secondary-container'
|
|
53
|
+
},
|
|
54
|
+
false: ''
|
|
55
|
+
},
|
|
56
|
+
leading: {
|
|
57
|
+
none: '',
|
|
58
|
+
icon: { base: 'pl-spacing-100', action: 'pl-spacing-100' },
|
|
59
|
+
avatar: { base: 'pl-spacing-50', action: 'pl-spacing-50' }
|
|
60
|
+
},
|
|
61
|
+
/** A trailing icon: the filter chip's `trailingIconProps`, or the input chip's remove button. */
|
|
62
|
+
trailing: {
|
|
63
|
+
true: { base: 'pr-spacing-100', action: 'pr-spacing-100' },
|
|
23
64
|
false: ''
|
|
24
65
|
},
|
|
25
66
|
disabled: {
|
|
26
|
-
true:
|
|
67
|
+
true: {
|
|
68
|
+
base: 'text-md-sys-color-on-surface/38 shadow-none hover:shadow-none',
|
|
69
|
+
icon: 'text-md-sys-color-on-surface/38',
|
|
70
|
+
avatar: 'opacity-38',
|
|
71
|
+
trailing: 'text-md-sys-color-on-surface/38',
|
|
72
|
+
checkIcon: 'text-md-sys-color-on-surface/38'
|
|
73
|
+
},
|
|
27
74
|
false: ''
|
|
28
75
|
}
|
|
29
76
|
},
|
|
30
77
|
compoundVariants: [
|
|
78
|
+
// Flat, unselected: no fill, outline-variant border that darkens on focus.
|
|
31
79
|
{
|
|
32
80
|
elevated: false,
|
|
33
81
|
selected: false,
|
|
82
|
+
// The spec keeps a dragged flat chip unfilled, but one floating over its neighbours needs an
|
|
83
|
+
// opaque container, so it takes the elevated chip's.
|
|
34
84
|
class: {
|
|
35
|
-
base: '
|
|
85
|
+
base: 'border-md-sys-color-outline-variant focus-visible:border-md-sys-color-on-surface-variant in-data-dragged:bg-md-sys-color-surface-container-low'
|
|
36
86
|
}
|
|
37
87
|
},
|
|
38
88
|
{
|
|
39
|
-
|
|
40
|
-
|
|
89
|
+
variant: 'assist',
|
|
90
|
+
elevated: false,
|
|
91
|
+
class: { base: 'focus-visible:border-md-sys-color-on-surface' }
|
|
92
|
+
},
|
|
93
|
+
{ elevated: true, selected: false, class: { base: 'bg-md-sys-color-surface-container-low' } },
|
|
94
|
+
// A flat selected filter chip lifts on hover.
|
|
95
|
+
{
|
|
96
|
+
variant: 'filter',
|
|
97
|
+
elevated: false,
|
|
98
|
+
selected: true,
|
|
99
|
+
class: { base: 'hover:shadow-elevation-1' }
|
|
100
|
+
},
|
|
101
|
+
// Unselected filter icons are primary; selected ones inherit on-secondary-container.
|
|
102
|
+
{ variant: 'filter', selected: false, class: { icon: 'text-md-sys-color-primary' } },
|
|
103
|
+
// Input chips are the reverse: on-surface-variant, primary once selected.
|
|
104
|
+
{ variant: 'input', selected: true, class: { icon: 'text-md-sys-color-primary' } },
|
|
105
|
+
// The input chip's container has no padding of its own before the action, and needs the
|
|
106
|
+
// focus indicator (and darker border) while the action is focused.
|
|
107
|
+
{
|
|
108
|
+
variant: 'input',
|
|
41
109
|
class: {
|
|
42
|
-
base: '
|
|
110
|
+
base: 'pl-spacing-0 has-[[data-chip-action]:focus-visible]:outline-3 has-[[data-chip-action]:focus-visible]:outline-offset-2 has-[[data-chip-action]:focus-visible]:outline-md-sys-color-secondary has-[[data-chip-action]:focus-visible]:outline-solid'
|
|
43
111
|
}
|
|
44
112
|
},
|
|
113
|
+
{ variant: 'input', trailing: false, class: { base: 'pr-spacing-0' } },
|
|
45
114
|
{
|
|
46
|
-
|
|
115
|
+
variant: 'input',
|
|
116
|
+
elevated: false,
|
|
117
|
+
selected: false,
|
|
47
118
|
class: {
|
|
48
|
-
base: '
|
|
119
|
+
base: 'has-[[data-chip-action]:focus-visible]:border-md-sys-color-on-surface-variant'
|
|
49
120
|
}
|
|
50
121
|
},
|
|
51
122
|
{
|
|
52
|
-
|
|
123
|
+
disabled: true,
|
|
124
|
+
elevated: false,
|
|
53
125
|
selected: false,
|
|
54
|
-
class: {
|
|
126
|
+
class: { base: 'border-md-sys-color-on-surface/12' }
|
|
55
127
|
},
|
|
56
128
|
{
|
|
57
129
|
disabled: true,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
130
|
+
elevated: true,
|
|
131
|
+
class: { base: 'bg-md-sys-color-on-surface/12' }
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
disabled: true,
|
|
135
|
+
selected: true,
|
|
136
|
+
class: { base: 'bg-md-sys-color-on-surface/12 text-md-sys-color-on-surface/38' }
|
|
64
137
|
}
|
|
65
138
|
],
|
|
66
139
|
defaultVariants: {
|
|
67
140
|
variant: 'assist',
|
|
68
141
|
elevated: false,
|
|
69
142
|
selected: false,
|
|
143
|
+
leading: 'none',
|
|
144
|
+
trailing: false,
|
|
70
145
|
disabled: false
|
|
71
146
|
}
|
|
72
147
|
});
|
|
148
|
+
/** Layout of a set of chips: 8dp apart, wrapping onto new lines. */
|
|
149
|
+
export const chipGroup = tv({
|
|
150
|
+
slots: {
|
|
151
|
+
base: 'relative flex flex-wrap gap-spacing-100',
|
|
152
|
+
// Its own stacking context only while dragged, so the chip floats over its neighbours.
|
|
153
|
+
item: 'relative flex touch-manipulation data-dragged:z-10 data-dragged:cursor-grabbing',
|
|
154
|
+
hint: 'sr-only',
|
|
155
|
+
live: 'sr-only'
|
|
156
|
+
},
|
|
157
|
+
variants: {
|
|
158
|
+
reorderable: {
|
|
159
|
+
true: { item: 'cursor-grab select-none [-webkit-touch-callout:none]' },
|
|
160
|
+
false: ''
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
@@ -26,18 +26,52 @@ export type ChipProps = ChipVariants & AnchorButtonAttributes & {
|
|
|
26
26
|
disabled?: boolean;
|
|
27
27
|
/** Configuration for the leading icon. */
|
|
28
28
|
iconProps?: IconProps;
|
|
29
|
+
/**
|
|
30
|
+
* Filter chips only: a trailing icon, e.g. `arrow_drop_down` on a chip that opens a menu.
|
|
31
|
+
*/
|
|
32
|
+
trailingIconProps?: IconProps;
|
|
29
33
|
/**
|
|
30
34
|
* Custom leading content (e.g. an avatar image) rendered instead of `iconProps`.
|
|
31
35
|
* Typically used by 'input' chips to represent a contact or entity.
|
|
32
36
|
*/
|
|
33
37
|
avatar?: Snippet;
|
|
34
38
|
/**
|
|
35
|
-
* Called when the trailing remove icon is activated
|
|
36
|
-
* Providing this renders the trailing remove affordance on 'input' chips.
|
|
39
|
+
* Called when the trailing remove icon is activated, or on Backspace/Delete while the chip
|
|
40
|
+
* has focus. Providing this renders the trailing remove affordance on 'input' chips. Move
|
|
41
|
+
* focus to a neighbouring chip (or the input) afterwards, or it falls back to the page.
|
|
37
42
|
*/
|
|
38
43
|
onRemove?: () => void;
|
|
44
|
+
/**
|
|
45
|
+
* Accessible name of the input chip's remove button. Name the item, e.g. `Remove Ada`.
|
|
46
|
+
* @default 'Remove'
|
|
47
|
+
*/
|
|
48
|
+
removeLabel?: string;
|
|
39
49
|
/** The label content of the chip. */
|
|
40
50
|
children: Snippet;
|
|
41
51
|
/** The `formaction` attribute for the chip when used in a form. */
|
|
42
52
|
formaction?: string;
|
|
43
53
|
};
|
|
54
|
+
export type ChipGroupProps<T> = {
|
|
55
|
+
/** The items, one chip each. With `reorderable`, dragging rewrites their order. Bindable. */
|
|
56
|
+
items: T[];
|
|
57
|
+
/** A stable, unique key per item. */
|
|
58
|
+
key: (item: T) => string;
|
|
59
|
+
/** Renders one item's `Chip`. */
|
|
60
|
+
chip: Snippet<[item: T, index: number]>;
|
|
61
|
+
/**
|
|
62
|
+
* Lets people reorder the chips: drag one (on touch, press and hold first), or focus one and
|
|
63
|
+
* press Alt+Arrow. The dragged chip takes M3's dragged state and its neighbours make room.
|
|
64
|
+
* @default false
|
|
65
|
+
*/
|
|
66
|
+
reorderable?: boolean;
|
|
67
|
+
/** Called with the new order after a drag or a keyboard move. */
|
|
68
|
+
onReorder?: (items: T[]) => void;
|
|
69
|
+
/**
|
|
70
|
+
* The item's name in the announcement after a move ("Ada, moved to 2 of 5"). Defaults to its
|
|
71
|
+
* `key`.
|
|
72
|
+
*/
|
|
73
|
+
itemLabel?: (item: T) => string;
|
|
74
|
+
/** Accessible name of the group. */
|
|
75
|
+
'aria-label'?: string;
|
|
76
|
+
class?: string;
|
|
77
|
+
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { type VariantProps } from '../../../utils/tv.js';
|
|
2
2
|
export type AppVariants = VariantProps<typeof app>;
|
|
3
|
-
export declare const app: import("tailwind-variants").TVReturnType<{} | {} | {}, undefined, "bg-md-sys-color-surface-container text-md-sys-color-on-background min-h-dvh caret-md-sys-color-primary selection:bg-md-sys-color-primary-container selection:text-md-sys-color-on-primary-container relative min-h-dvh pt-[env(safe-area-inset-top)] pr-[env(safe-area-inset-right)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] antialiased", {} | {}, undefined, import("tailwind-variants").TVReturnTypeLike<unknown, undefined>>;
|
|
3
|
+
export declare const app: import("tailwind-variants").TVReturnType<{} | {} | {}, undefined, "md-app bg-md-sys-color-surface-container text-md-sys-color-on-background min-h-dvh caret-md-sys-color-primary selection:bg-md-sys-color-primary-container selection:text-md-sys-color-on-primary-container relative min-h-dvh pt-[env(safe-area-inset-top)] pr-[env(safe-area-inset-right)] pb-[env(safe-area-inset-bottom)] pl-[calc(env(safe-area-inset-left)+var(--md-rail-inset,0px))] antialiased", {} | {}, undefined, import("tailwind-variants").TVReturnTypeLike<unknown, undefined>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { tv } from '../../../utils/tv.js';
|
|
2
2
|
export const app = tv({
|
|
3
|
-
base: 'bg-md-sys-color-surface-container text-md-sys-color-on-background min-h-dvh caret-md-sys-color-primary selection:bg-md-sys-color-primary-container selection:text-md-sys-color-on-primary-container relative min-h-dvh pt-[env(safe-area-inset-top)] pr-[env(safe-area-inset-right)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] antialiased'
|
|
3
|
+
base: 'md-app bg-md-sys-color-surface-container text-md-sys-color-on-background min-h-dvh caret-md-sys-color-primary selection:bg-md-sys-color-primary-container selection:text-md-sys-color-on-primary-container relative min-h-dvh pt-[env(safe-area-inset-top)] pr-[env(safe-area-inset-right)] pb-[env(safe-area-inset-bottom)] pl-[calc(env(safe-area-inset-left)+var(--md-rail-inset,0px))] antialiased'
|
|
4
4
|
});
|
|
@@ -6,13 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
let { variant = 'standard', class: className, children, ...restProps }: ListProps = $props();
|
|
8
8
|
|
|
9
|
+
let element = $state<HTMLElement>();
|
|
10
|
+
|
|
9
11
|
setListContext({
|
|
10
12
|
get variant() {
|
|
11
13
|
return variant;
|
|
12
|
-
}
|
|
14
|
+
},
|
|
15
|
+
get element() {
|
|
16
|
+
return element;
|
|
17
|
+
},
|
|
18
|
+
from: null
|
|
13
19
|
});
|
|
14
20
|
</script>
|
|
15
21
|
|
|
16
|
-
<ul class={list({ variant, class: clsx(className) })} {...restProps}>
|
|
22
|
+
<ul bind:this={element} class={list({ variant, class: clsx(className) })} {...restProps}>
|
|
17
23
|
{@render children?.()}
|
|
18
24
|
</ul>
|
|
@@ -91,6 +91,12 @@ chevron, `aria-expanded` and `aria-controls`. The nested items are `inert` while
|
|
|
91
91
|
content to 38%. Hover, focus and press tints come from the shared state layer. The keyboard focus
|
|
92
92
|
ring is drawn inside the item, so segments don't overlap.
|
|
93
93
|
|
|
94
|
+
When the selection moves inside one `List` (a nav list following the route, a single-select
|
|
95
|
+
list), the fill travels: it springs from where the old one was drawn to the newly selected item,
|
|
96
|
+
up or down, on the `spatial` spring. It passes over the items in between, and a selection that
|
|
97
|
+
moves again mid-flight picks up from where the fill is. When several items change at once, or
|
|
98
|
+
under reduced motion, the fill just appears on the selected item.
|
|
99
|
+
|
|
94
100
|
<Canvas of={ListStories.Disabled} />
|
|
95
101
|
|
|
96
102
|
## Where this differs from the tokens
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
import { Icon, Layer } from '../../../utils/index.js';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { listItem } from './theme.js';
|
|
5
|
-
import { getListContext } from './context.js';
|
|
5
|
+
import { getListContext, type SelectionRect } from './context.js';
|
|
6
|
+
import { untrack } from 'svelte';
|
|
7
|
+
import { animate, type AnimationPlaybackControlsWithThen } from 'motion';
|
|
8
|
+
import { springTokens, springTransition } from '../../../animation/spring.js';
|
|
6
9
|
import type { ListitemProps } from './types.js';
|
|
7
10
|
import Badge from '../../badge/Badge.svelte';
|
|
8
11
|
|
|
@@ -53,6 +56,71 @@
|
|
|
53
56
|
})
|
|
54
57
|
);
|
|
55
58
|
|
|
59
|
+
/*
|
|
60
|
+
The selected fill travels. Before an update, the item losing the selection leaves where its
|
|
61
|
+
fill is drawn (mid-flight too) in the List's context; after it, the item gaining the
|
|
62
|
+
selection springs its new fill over from there, on spatial: a sizeable move, and part of a
|
|
63
|
+
navigation, so no fastSpatial bounce. Without a List, or with several items selected or
|
|
64
|
+
deselected at once, the fill just appears where it belongs. Reduced motion skips the ride.
|
|
65
|
+
*/
|
|
66
|
+
const showFill = $derived(!!selected && !disabled);
|
|
67
|
+
let fill = $state<HTMLElement>();
|
|
68
|
+
let travelling = $state(false);
|
|
69
|
+
let wasShown = untrack(() => showFill);
|
|
70
|
+
let ride: AnimationPlaybackControlsWithThen | undefined;
|
|
71
|
+
|
|
72
|
+
const measure = (el: HTMLElement): SelectionRect | null => {
|
|
73
|
+
const box = ctx?.element?.getBoundingClientRect();
|
|
74
|
+
if (!box) return null;
|
|
75
|
+
const r = el.getBoundingClientRect();
|
|
76
|
+
return {
|
|
77
|
+
top: r.top - box.top,
|
|
78
|
+
bottom: r.bottom - box.top,
|
|
79
|
+
left: r.left - box.left,
|
|
80
|
+
right: r.right - box.left
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
$effect.pre(() => {
|
|
85
|
+
const shown = showFill;
|
|
86
|
+
untrack(() => {
|
|
87
|
+
if (!wasShown || shown || !ctx || !fill) return;
|
|
88
|
+
ctx.from = measure(fill);
|
|
89
|
+
ride?.stop();
|
|
90
|
+
travelling = false;
|
|
91
|
+
// Only for this update: a deselect with no select after it leaves nothing behind.
|
|
92
|
+
queueMicrotask(() => (ctx.from = null));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
$effect(() => {
|
|
97
|
+
const shown = showFill;
|
|
98
|
+
untrack(() => {
|
|
99
|
+
const gained = shown && !wasShown;
|
|
100
|
+
wasShown = shown;
|
|
101
|
+
const from = ctx?.from;
|
|
102
|
+
if (!gained || !from || !fill) return;
|
|
103
|
+
ctx!.from = null;
|
|
104
|
+
const to = measure(fill);
|
|
105
|
+
if (!to || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
106
|
+
// Above the items it passes over, which are the item's siblings.
|
|
107
|
+
travelling = true;
|
|
108
|
+
ride = animate(
|
|
109
|
+
fill,
|
|
110
|
+
{
|
|
111
|
+
top: [`${from.top - to.top}px`, '0px'],
|
|
112
|
+
bottom: [`${to.bottom - from.bottom}px`, '0px'],
|
|
113
|
+
left: [`${from.left - to.left}px`, '0px'],
|
|
114
|
+
right: [`${to.right - from.right}px`, '0px']
|
|
115
|
+
},
|
|
116
|
+
springTransition(springTokens.spatial)
|
|
117
|
+
);
|
|
118
|
+
ride.then(() => (travelling = false));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
$effect(() => () => ride?.stop());
|
|
123
|
+
|
|
56
124
|
const attrs = $derived.by(() => {
|
|
57
125
|
const { label: _, href, onclick, ...extra } = rest;
|
|
58
126
|
const a: Record<string, unknown> = { ...extra };
|
|
@@ -81,6 +149,9 @@
|
|
|
81
149
|
</script>
|
|
82
150
|
|
|
83
151
|
{#snippet content()}
|
|
152
|
+
{#if showFill}
|
|
153
|
+
<span bind:this={fill} class={cls.selection()} aria-hidden="true"></span>
|
|
154
|
+
{/if}
|
|
84
155
|
{#if interactive}
|
|
85
156
|
<Layer />
|
|
86
157
|
{/if}
|
|
@@ -121,7 +192,11 @@
|
|
|
121
192
|
{/snippet}
|
|
122
193
|
|
|
123
194
|
{#snippet item()}
|
|
124
|
-
<svelte:element
|
|
195
|
+
<svelte:element
|
|
196
|
+
this={tag}
|
|
197
|
+
class={cls.base({ class: clsx(className, travelling && 'z-10') })}
|
|
198
|
+
{...attrs}
|
|
199
|
+
>
|
|
125
200
|
{@render content()}
|
|
126
201
|
</svelte:element>
|
|
127
202
|
{#if children}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
/** Where the outgoing selection was drawn, relative to the list's box. */
|
|
2
|
+
export type SelectionRect = {
|
|
3
|
+
top: number;
|
|
4
|
+
bottom: number;
|
|
5
|
+
left: number;
|
|
6
|
+
right: number;
|
|
7
|
+
};
|
|
1
8
|
type ListContext = {
|
|
2
9
|
readonly variant: 'standard' | 'segmented';
|
|
10
|
+
/** The list's own element, which selection rects are measured against. */
|
|
11
|
+
readonly element: HTMLElement | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Handed from the item losing the selection to the one gaining it within one update, so the
|
|
14
|
+
* selected fill travels between them. Cleared once that update settles.
|
|
15
|
+
*/
|
|
16
|
+
from: SelectionRect | null;
|
|
3
17
|
};
|
|
4
18
|
export declare const setListContext: (ctx: ListContext) => ListContext;
|
|
5
19
|
export declare const getListContext: () => ListContext | undefined;
|
|
@@ -85,6 +85,7 @@ export declare const listItem: import("tailwind-variants").TVReturnType<{
|
|
|
85
85
|
};
|
|
86
86
|
}, {
|
|
87
87
|
base: string[];
|
|
88
|
+
selection: string;
|
|
88
89
|
leading: string;
|
|
89
90
|
body: string;
|
|
90
91
|
overline: string;
|
|
@@ -165,6 +166,7 @@ export declare const listItem: import("tailwind-variants").TVReturnType<{
|
|
|
165
166
|
};
|
|
166
167
|
}, {
|
|
167
168
|
base: string[];
|
|
169
|
+
selection: string;
|
|
168
170
|
leading: string;
|
|
169
171
|
body: string;
|
|
170
172
|
overline: string;
|
|
@@ -245,6 +247,7 @@ export declare const listItem: import("tailwind-variants").TVReturnType<{
|
|
|
245
247
|
};
|
|
246
248
|
}, {
|
|
247
249
|
base: string[];
|
|
250
|
+
selection: string;
|
|
248
251
|
leading: string;
|
|
249
252
|
body: string;
|
|
250
253
|
overline: string;
|
|
@@ -327,6 +330,7 @@ export declare const listiem: import("tailwind-variants").TVReturnType<{
|
|
|
327
330
|
};
|
|
328
331
|
}, {
|
|
329
332
|
base: string[];
|
|
333
|
+
selection: string;
|
|
330
334
|
leading: string;
|
|
331
335
|
body: string;
|
|
332
336
|
overline: string;
|
|
@@ -407,6 +411,7 @@ export declare const listiem: import("tailwind-variants").TVReturnType<{
|
|
|
407
411
|
};
|
|
408
412
|
}, {
|
|
409
413
|
base: string[];
|
|
414
|
+
selection: string;
|
|
410
415
|
leading: string;
|
|
411
416
|
body: string;
|
|
412
417
|
overline: string;
|
|
@@ -487,6 +492,7 @@ export declare const listiem: import("tailwind-variants").TVReturnType<{
|
|
|
487
492
|
};
|
|
488
493
|
}, {
|
|
489
494
|
base: string[];
|
|
495
|
+
selection: string;
|
|
490
496
|
leading: string;
|
|
491
497
|
body: string;
|
|
492
498
|
overline: string;
|
|
@@ -7,6 +7,11 @@ import { tv } from '../../../utils/tv.js';
|
|
|
7
7
|
`List` sets `--li-top`/`--li-bottom` to 16dp on its first/last child, so the list's outer
|
|
8
8
|
corners stay 16dp while the inner ones morph. Custom properties inherit, so this reaches an item
|
|
9
9
|
through its `<li>`; each `<ul>` resets both so nested lists start clean.
|
|
10
|
+
|
|
11
|
+
Selection: the selected fill is its own element rather than the item's background. When one
|
|
12
|
+
item loses the selection and another gains it in the same update (a nav list following the
|
|
13
|
+
route, a single-select list), the fill springs from where the old one was drawn to the new
|
|
14
|
+
item, like the tab indicator, instead of blinking out and in.
|
|
10
15
|
*/
|
|
11
16
|
export const list = tv({
|
|
12
17
|
base: 'flex w-full flex-col [--li-bottom:initial] [--li-top:initial]',
|
|
@@ -22,11 +27,14 @@ export const list = tv({
|
|
|
22
27
|
export const listItem = tv({
|
|
23
28
|
slots: {
|
|
24
29
|
base: [
|
|
25
|
-
'relative flex w-full min-w-spacing-0 gap-spacing-150 ps-spacing-200 pe-spacing-200 pt-spacing-125 pb-spacing-125 text-start',
|
|
30
|
+
'relative isolate flex w-full min-w-spacing-0 gap-spacing-150 ps-spacing-200 pe-spacing-200 pt-spacing-125 pb-spacing-125 text-start',
|
|
26
31
|
'[--li-shape:0.25rem] rounded-t-[var(--li-top,var(--li-shape))] rounded-b-[var(--li-bottom,var(--li-shape))]',
|
|
27
32
|
'transition-[border-radius,background-color,color] md-sys-motion-fast-spatial',
|
|
28
33
|
'text-md-sys-color-on-surface-variant'
|
|
29
34
|
],
|
|
35
|
+
/* The selected fill, under the item's content and state layer, in the item's shape. When the
|
|
36
|
+
selection moves within a List it springs over from the previous item (ListItem.svelte). */
|
|
37
|
+
selection: 'pointer-events-none absolute inset-spacing-0 -z-10 rounded-[inherit] bg-md-sys-color-secondary-container',
|
|
30
38
|
leading: 'flex shrink-0 items-center text-md-sys-color-on-surface-variant',
|
|
31
39
|
body: 'flex min-w-spacing-0 flex-1 flex-col justify-center',
|
|
32
40
|
overline: 'md-sys-typescale-label-small line-clamp-1 text-md-sys-color-on-surface-variant',
|
|
@@ -71,7 +79,7 @@ export const listItem = tv({
|
|
|
71
79
|
},
|
|
72
80
|
selected: {
|
|
73
81
|
true: {
|
|
74
|
-
base: '
|
|
82
|
+
base: 'text-md-sys-color-on-secondary-container [--li-shape:1rem] hover:[--li-shape:1rem]',
|
|
75
83
|
leading: 'text-md-sys-color-on-secondary-container',
|
|
76
84
|
overline: 'text-md-sys-color-on-secondary-container',
|
|
77
85
|
headline: 'text-md-sys-color-on-secondary-container',
|
|
@@ -31,7 +31,7 @@ the pane itself stays `position: fixed` and doesn't move with either.
|
|
|
31
31
|
import { springTokens, springTransition } from '../../../animation/spring.js';
|
|
32
32
|
import { Icon, Layer } from '../../../utils/index.js';
|
|
33
33
|
import { ButtonIcon } from '../../buttons/index.js';
|
|
34
|
-
import { clickOutside } from '../../../attachments/index.js';
|
|
34
|
+
import { clickOutside, drag, resist } from '../../../attachments/index.js';
|
|
35
35
|
import { draggablePane, type ResizeEdge } from './theme.js';
|
|
36
36
|
import { dragPositions } from './dragStore.svelte.js';
|
|
37
37
|
import type { DraggablePaneProps } from './types.js';
|
|
@@ -313,11 +313,6 @@ the pane itself stays `position: fixed` and doesn't move with either.
|
|
|
313
313
|
the same spring. Grabbing the pane mid-spring stops it where it is.
|
|
314
314
|
*/
|
|
315
315
|
const SETTLE_SPRING = springTransition(springTokens.fastSpatial);
|
|
316
|
-
/* Share of the overshoot still applied at the edge, shrinking the further it's pulled. The
|
|
317
|
-
UIScrollView rubber-band constant — M3 has no token for this; tune by eye. */
|
|
318
|
-
const RUBBER_BAND = 0.55;
|
|
319
|
-
/* Pointer samples older than this don't count towards the release velocity. */
|
|
320
|
-
const VELOCITY_WINDOW_MS = 100;
|
|
321
316
|
|
|
322
317
|
let settleX: AnimationPlaybackControls | undefined;
|
|
323
318
|
let settleY: AnimationPlaybackControls | undefined;
|
|
@@ -352,61 +347,31 @@ the pane itself stays `position: fixed` and doesn't move with either.
|
|
|
352
347
|
if (persistKey) dragPositions.set(persistKey, { x: next.x, y: next.y, width, height });
|
|
353
348
|
}
|
|
354
349
|
|
|
355
|
-
/** Resisted overshoot: the further past the edge, the less the pane follows. */
|
|
356
|
-
function rubberBand(overshoot: number, limit: number) {
|
|
357
|
-
return (1 - 1 / ((overshoot * RUBBER_BAND) / limit + 1)) * limit;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function resist(value: number, min: number, max: number, size: number) {
|
|
361
|
-
if (value < min) return min - rubberBand(min - value, size);
|
|
362
|
-
if (value > max) return max + rubberBand(value - max, size);
|
|
363
|
-
return value;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
let dragStartX = 0;
|
|
367
|
-
let dragStartY = 0;
|
|
368
350
|
let originX = 0;
|
|
369
351
|
let originY = 0;
|
|
370
|
-
let samples: { x: number; y: number; t: number }[] = [];
|
|
371
|
-
|
|
372
|
-
function startDrag(event: PointerEvent) {
|
|
373
|
-
if (disableDrag) return;
|
|
374
|
-
stopSettle();
|
|
375
|
-
dragging = true;
|
|
376
|
-
dragStartX = event.clientX;
|
|
377
|
-
dragStartY = event.clientY;
|
|
378
|
-
originX = x ?? initialX;
|
|
379
|
-
originY = y ?? initialY;
|
|
380
|
-
samples = [{ x: event.clientX, y: event.clientY, t: event.timeStamp }];
|
|
381
|
-
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
|
382
|
-
}
|
|
383
352
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (!dragging) return;
|
|
407
|
-
dragging = false;
|
|
408
|
-
settleTo(x ?? initialX, y ?? initialY, releaseVelocity());
|
|
409
|
-
}
|
|
353
|
+
// Pointer plumbing (capture, release velocity) and the rubber-band come from the shared drag
|
|
354
|
+
// attachment; this only positions the pane.
|
|
355
|
+
const dragPane = drag(() => ({
|
|
356
|
+
disabled: disableDrag,
|
|
357
|
+
onStart: () => {
|
|
358
|
+
stopSettle();
|
|
359
|
+
dragging = true;
|
|
360
|
+
originX = x ?? initialX;
|
|
361
|
+
originY = y ?? initialY;
|
|
362
|
+
},
|
|
363
|
+
onMove: (offset) => {
|
|
364
|
+
const rect = boundsRect();
|
|
365
|
+
const { x: minX, y: minY } = clamp(-Infinity, -Infinity);
|
|
366
|
+
const { x: maxX, y: maxY } = clamp(Infinity, Infinity);
|
|
367
|
+
x = resist(originX + offset.x, minX, maxX, rect.right - rect.left);
|
|
368
|
+
y = resist(originY + offset.y, minY, maxY, rect.bottom - rect.top);
|
|
369
|
+
},
|
|
370
|
+
onEnd: (velocity) => {
|
|
371
|
+
dragging = false;
|
|
372
|
+
settleTo(x ?? initialX, y ?? initialY, velocity);
|
|
373
|
+
}
|
|
374
|
+
}));
|
|
410
375
|
|
|
411
376
|
const NUDGE = 8;
|
|
412
377
|
|
|
@@ -453,7 +418,7 @@ the pane itself stays `position: fixed` and doesn't move with either.
|
|
|
453
418
|
// every move rather than accumulated via `event.movementX`/`movementY` —
|
|
454
419
|
// the latter isn't reliably populated across every input path (some
|
|
455
420
|
// synthetic/automated drag sequences leave it 0), which would silently
|
|
456
|
-
// drop the resize.
|
|
421
|
+
// drop the resize. The drag attachment works from a fixed baseline the same way.
|
|
457
422
|
function moveResize(event: PointerEvent) {
|
|
458
423
|
if (!resizeDir) return;
|
|
459
424
|
applyResize(
|
|
@@ -534,10 +499,7 @@ the pane itself stays `position: fixed` and doesn't move with either.
|
|
|
534
499
|
tabindex={disableDrag ? undefined : 0}
|
|
535
500
|
aria-label={disableDrag ? undefined : title ? `Drag to move ${title}` : 'Drag to move'}
|
|
536
501
|
aria-keyshortcuts={disableDrag ? undefined : 'ArrowUp ArrowDown ArrowLeft ArrowRight'}
|
|
537
|
-
|
|
538
|
-
onpointermove={disableDrag ? undefined : moveDrag}
|
|
539
|
-
onpointerup={disableDrag ? undefined : endDrag}
|
|
540
|
-
onpointercancel={disableDrag ? undefined : endDrag}
|
|
502
|
+
{@attach dragPane}
|
|
541
503
|
onkeydown={disableDrag ? undefined : onKeydown}
|
|
542
504
|
>
|
|
543
505
|
{#if !disableDrag}
|
|
@@ -76,7 +76,7 @@ the document root yourself to get the same behavior.
|
|
|
76
76
|
|
|
77
77
|
const style = $derived(
|
|
78
78
|
clsx(
|
|
79
|
-
!flexible && `width: ${shown}px;
|
|
79
|
+
!flexible && `width: ${shown}px; --pane-basis: ${shown}px;`,
|
|
80
80
|
sticky && `--pane-sticky-top: ${stickyTop}px;`
|
|
81
81
|
)
|
|
82
82
|
);
|