@noxlovette/material 0.1.32 → 0.1.34
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/dist/components/badge/Badge.svelte +2 -2
- package/dist/components/containers/list/ListItem.svelte +13 -3
- package/dist/components/containers/list/types.d.ts +6 -0
- package/dist/components/containers/panes/SplitPane.svelte +11 -3
- package/dist/components/containers/panes/SupportingPane.svelte +99 -4
- package/dist/components/containers/panes/theme.d.ts +12 -12
- package/dist/components/containers/panes/theme.js +15 -15
- package/dist/components/containers/panes/types.d.ts +12 -1
- package/dist/components/forms/switch/Switch.svelte +2 -2
- package/dist/components/forms/switch/theme.js +11 -5
- package/dist/components/nav/rail/Rail.svelte +60 -2
- package/dist/components/nav/rail/Rail.svelte.d.ts +1 -0
- package/dist/components/nav/rail/RailItem.svelte +15 -5
- package/dist/components/nav/rail/RailItem.svelte.d.ts +1 -0
- package/dist/components/nav/rail/RailNavContext.svelte +14 -0
- package/dist/components/nav/rail/RailNavContext.svelte.d.ts +8 -0
- package/dist/components/nav/rail/index.d.ts +1 -0
- package/dist/components/nav/rail/index.js +1 -0
- package/dist/components/nav/rail/railStore.svelte.d.ts +3 -0
- package/dist/components/nav/rail/railStore.svelte.js +9 -0
- package/dist/components/nav/rail/theme.d.ts +30 -3
- package/dist/components/nav/rail/theme.js +13 -4
- package/dist/components/nav/rail/types.d.ts +6 -0
- package/package.json +15 -15
|
@@ -17,9 +17,9 @@ Large badges are used for specific counts.
|
|
|
17
17
|
const styling = $derived(badge({ size }));
|
|
18
18
|
</script>
|
|
19
19
|
|
|
20
|
-
{#if number}
|
|
20
|
+
{#if size === 'sm' || number}
|
|
21
21
|
<div class={clsx(styling, className)} {...restProps}>
|
|
22
|
-
{#if size
|
|
22
|
+
{#if size !== 'sm' && number}
|
|
23
23
|
{number > 99 ? '99+' : number}
|
|
24
24
|
{/if}
|
|
25
25
|
</div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { listiem } from './theme.js';
|
|
5
5
|
import type { ListitemProps } from './types.js';
|
|
6
|
+
import Badge from '../../badge/Badge.svelte';
|
|
6
7
|
|
|
7
8
|
let {
|
|
8
9
|
leading,
|
|
@@ -10,6 +11,7 @@
|
|
|
10
11
|
headline = '',
|
|
11
12
|
supporting = '',
|
|
12
13
|
trailing,
|
|
14
|
+
badge,
|
|
13
15
|
selected,
|
|
14
16
|
class: className,
|
|
15
17
|
asChild = false,
|
|
@@ -36,9 +38,17 @@
|
|
|
36
38
|
<p class={cls.supporting()}>{supporting}</p>
|
|
37
39
|
{/if}
|
|
38
40
|
</div>
|
|
39
|
-
{#if trailing}
|
|
40
|
-
<div class={cls.trailing()}>
|
|
41
|
-
{
|
|
41
|
+
{#if trailing || badge !== undefined}
|
|
42
|
+
<div class={clsx(cls.trailing(), 'relative overflow-visible')}>
|
|
43
|
+
{#if trailing}
|
|
44
|
+
{@render trailing()}
|
|
45
|
+
{/if}
|
|
46
|
+
{#if badge !== undefined}
|
|
47
|
+
<Badge
|
|
48
|
+
size={badge === true ? 'sm' : 'lg'}
|
|
49
|
+
number={badge === true ? undefined : (badge as number)}
|
|
50
|
+
/>
|
|
51
|
+
{/if}
|
|
42
52
|
</div>
|
|
43
53
|
{/if}
|
|
44
54
|
{/snippet}
|
|
@@ -35,6 +35,12 @@ export type ListitemProps = {
|
|
|
35
35
|
* Optional trailing slot, typically used for an icon, metadata, or switch.
|
|
36
36
|
*/
|
|
37
37
|
trailing?: Snippet;
|
|
38
|
+
/**
|
|
39
|
+
* Badge to display in the trailing area.
|
|
40
|
+
* - `true`: renders a small dot badge (notification indicator).
|
|
41
|
+
* - `number`: renders a large badge with that count (capped at 99+).
|
|
42
|
+
*/
|
|
43
|
+
badge?: number | boolean;
|
|
38
44
|
/**
|
|
39
45
|
* The number of text lines to reserve space for.
|
|
40
46
|
* If not provided, it's calculated based on presence of overline and supporting text.
|
|
@@ -22,6 +22,7 @@ SplitPane provides a resizable two-column layout.
|
|
|
22
22
|
persist = true,
|
|
23
23
|
onPaddingChange,
|
|
24
24
|
anchor = 'viewport',
|
|
25
|
+
mobilePane = 'right',
|
|
25
26
|
class: className,
|
|
26
27
|
leftClass,
|
|
27
28
|
rightClass
|
|
@@ -35,6 +36,11 @@ SplitPane provides a resizable two-column layout.
|
|
|
35
36
|
handleGrip
|
|
36
37
|
} = $derived(splitPane({ full, anchor, rounded }));
|
|
37
38
|
|
|
39
|
+
const leftMobileClass = $derived(
|
|
40
|
+
mobilePane === 'left' ? 'block w-full md:w-[var(--splitpane-left-width)]' : ''
|
|
41
|
+
);
|
|
42
|
+
const rightMobileClass = $derived(mobilePane === 'left' ? 'hidden md:flex' : '');
|
|
43
|
+
|
|
38
44
|
const leftOffset = $derived(anchor === 'viewport' ? 'var(--splitpane-offset, 0px)' : '0px');
|
|
39
45
|
const handleOffset = $derived(
|
|
40
46
|
anchor === 'viewport'
|
|
@@ -98,8 +104,10 @@ SplitPane provides a resizable two-column layout.
|
|
|
98
104
|
<div class={base({ class: clsx(className) })} style={`--splitpane-left-width: ${leftWidth}px;`}>
|
|
99
105
|
<!-- LEFT PANE -->
|
|
100
106
|
<div
|
|
101
|
-
class={lCls({ class: leftClass })}
|
|
102
|
-
style={
|
|
107
|
+
class={lCls({ class: clsx(leftClass, leftMobileClass) })}
|
|
108
|
+
style={mobilePane === 'left'
|
|
109
|
+
? `left: ${leftOffset};`
|
|
110
|
+
: `width: var(--splitpane-left-width); left: ${leftOffset};`}
|
|
103
111
|
>
|
|
104
112
|
{@render left()}
|
|
105
113
|
</div>
|
|
@@ -126,7 +134,7 @@ SplitPane provides a resizable two-column layout.
|
|
|
126
134
|
</div>
|
|
127
135
|
|
|
128
136
|
<!-- RIGHT PANE -->
|
|
129
|
-
<div class={rCls({ class: rightClass })}>
|
|
137
|
+
<div class={rCls({ class: clsx(rightClass, rightMobileClass) })}>
|
|
130
138
|
{@render right()}
|
|
131
139
|
</div>
|
|
132
140
|
</div>
|
|
@@ -9,6 +9,38 @@ SupportingPane provides a layout with a main content area and a supporting side
|
|
|
9
9
|
import type { SupportingPaneProps } from './types.js';
|
|
10
10
|
import { supportingPane } from './theme.js';
|
|
11
11
|
import ButtonIcon from '../../buttons/ButtonIcon.svelte';
|
|
12
|
+
import {
|
|
13
|
+
easeEmphasized,
|
|
14
|
+
easeEmphasizedAccel,
|
|
15
|
+
easeEmphasizedDecel
|
|
16
|
+
} from '../../../animation/easing.js';
|
|
17
|
+
import { fade } from 'svelte/transition';
|
|
18
|
+
import type { TransitionConfig } from 'svelte/transition';
|
|
19
|
+
|
|
20
|
+
function slideWidth(node: HTMLElement): TransitionConfig {
|
|
21
|
+
const width = node.offsetWidth;
|
|
22
|
+
return {
|
|
23
|
+
duration: 500,
|
|
24
|
+
easing: easeEmphasized,
|
|
25
|
+
css: (t) => `width: ${t * width}px; min-width: 0; overflow: hidden;`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sheetIn(_node: HTMLElement): TransitionConfig {
|
|
30
|
+
return {
|
|
31
|
+
duration: 500,
|
|
32
|
+
easing: easeEmphasizedDecel,
|
|
33
|
+
css: (t) => `transform: translateY(${(1 - t) * 100}%);`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sheetOut(_node: HTMLElement): TransitionConfig {
|
|
38
|
+
return {
|
|
39
|
+
duration: 280,
|
|
40
|
+
easing: easeEmphasizedAccel,
|
|
41
|
+
css: (t) => `transform: translateY(${(1 - t) * 100}%);`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
12
44
|
|
|
13
45
|
const {
|
|
14
46
|
main,
|
|
@@ -24,13 +56,28 @@ SupportingPane provides a layout with a main content area and a supporting side
|
|
|
24
56
|
supportingClass,
|
|
25
57
|
collapsible,
|
|
26
58
|
supportingWidth = 320,
|
|
27
|
-
supportingTop = 0
|
|
59
|
+
supportingTop = 0,
|
|
60
|
+
mobileSheet = false,
|
|
61
|
+
mobileSheetIcon = 'info',
|
|
62
|
+
mobileTriggerClass
|
|
28
63
|
}: SupportingPaneProps = $props();
|
|
29
64
|
|
|
30
65
|
let entered = $state(false);
|
|
31
66
|
let supportingVisible = $state(true);
|
|
67
|
+
let mobileSheetOpen = $state(false);
|
|
68
|
+
let isLargeScreen = $state(false);
|
|
32
69
|
const supportingId = 'supporting-pane';
|
|
33
70
|
|
|
71
|
+
$effect(() => {
|
|
72
|
+
const mq = window.matchMedia('(min-width: 1024px)');
|
|
73
|
+
isLargeScreen = mq.matches;
|
|
74
|
+
const handler = (e: MediaQueryListEvent) => {
|
|
75
|
+
isLargeScreen = e.matches;
|
|
76
|
+
};
|
|
77
|
+
mq.addEventListener('change', handler);
|
|
78
|
+
return () => mq.removeEventListener('change', handler);
|
|
79
|
+
});
|
|
80
|
+
|
|
34
81
|
const {
|
|
35
82
|
base,
|
|
36
83
|
main: mainCls,
|
|
@@ -42,12 +89,20 @@ SupportingPane provides a layout with a main content area and a supporting side
|
|
|
42
89
|
? `--supporting-pane-width: ${supportingWidth}px; --supporting-pane-top: ${supportingTop}px;`
|
|
43
90
|
: ''
|
|
44
91
|
);
|
|
92
|
+
|
|
93
|
+
const mainViewportStyle = $derived(
|
|
94
|
+
anchor === 'viewport' && isLargeScreen
|
|
95
|
+
? `transition: margin-${position === 'right' ? 'right' : 'left'} ${supportingVisible ? '500ms' : '280ms'} cubic-bezier(0.2, 0, 0, 1); margin-${position === 'right' ? 'right' : 'left'}: ${supportingVisible ? supportingWidth : 0}px;`
|
|
96
|
+
: ''
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const inlineSupportingClass = $derived(mobileSheet ? 'hidden lg:block' : '');
|
|
45
100
|
</script>
|
|
46
101
|
|
|
47
102
|
<div class={base({ class: clsx(className) })} style={cssVars}>
|
|
48
|
-
<div class={mainCls({ class: clsx('relative', mainClass) })}>
|
|
103
|
+
<div class={mainCls({ class: clsx('relative', mainClass) })} style={mainViewportStyle}>
|
|
49
104
|
{#if collapsible}
|
|
50
|
-
<div class="top-3 right-3 z-10 hidden h-12 w-12
|
|
105
|
+
<div class="top-3 right-3 z-10 hidden h-12 w-12 lg:absolute lg:block">
|
|
51
106
|
<ButtonIcon
|
|
52
107
|
triggerClass=""
|
|
53
108
|
type="button"
|
|
@@ -67,12 +122,24 @@ SupportingPane provides a layout with a main content area and a supporting side
|
|
|
67
122
|
</div>
|
|
68
123
|
{/if}
|
|
69
124
|
{@render main()}
|
|
125
|
+
{#if mobileSheet}
|
|
126
|
+
<ButtonIcon
|
|
127
|
+
class={clsx('fixed right-4 bottom-6 z-30 lg:hidden', mobileTriggerClass)}
|
|
128
|
+
type="button"
|
|
129
|
+
variant="tonal"
|
|
130
|
+
iconProps={{ name: mobileSheetIcon }}
|
|
131
|
+
aria-label="Open supporting panel"
|
|
132
|
+
onclick={() => (mobileSheetOpen = true)}
|
|
133
|
+
/>
|
|
134
|
+
{/if}
|
|
70
135
|
</div>
|
|
136
|
+
|
|
71
137
|
{#if supportingVisible}
|
|
72
138
|
<aside
|
|
139
|
+
transition:slideWidth
|
|
73
140
|
id={supportingId}
|
|
74
141
|
class={supportingCls({
|
|
75
|
-
class: clsx(supportingClass)
|
|
142
|
+
class: clsx(supportingClass, inlineSupportingClass)
|
|
76
143
|
})}
|
|
77
144
|
data-entered={entered}
|
|
78
145
|
data-position={position}
|
|
@@ -81,3 +148,31 @@ SupportingPane provides a layout with a main content area and a supporting side
|
|
|
81
148
|
</aside>
|
|
82
149
|
{/if}
|
|
83
150
|
</div>
|
|
151
|
+
|
|
152
|
+
{#if mobileSheet && mobileSheetOpen}
|
|
153
|
+
<!-- Scrim -->
|
|
154
|
+
<div
|
|
155
|
+
class="bg-md-sys-color-scrim/40 fixed inset-0 z-40 lg:hidden"
|
|
156
|
+
transition:fade={{ duration: 220 }}
|
|
157
|
+
onclick={() => (mobileSheetOpen = false)}
|
|
158
|
+
aria-hidden="true"
|
|
159
|
+
></div>
|
|
160
|
+
|
|
161
|
+
<!-- Sheet -->
|
|
162
|
+
<div
|
|
163
|
+
class="bg-md-sys-color-surface-container-low fixed inset-x-0 bottom-0 z-50 max-h-[85dvh] scrollbar-none overflow-y-auto rounded-t-[28px] pb-[env(safe-area-inset-bottom)] lg:hidden"
|
|
164
|
+
in:sheetIn
|
|
165
|
+
out:sheetOut
|
|
166
|
+
role="dialog"
|
|
167
|
+
aria-modal="true"
|
|
168
|
+
>
|
|
169
|
+
<button
|
|
170
|
+
class="flex w-full cursor-pointer items-center justify-center pt-4 pb-2"
|
|
171
|
+
aria-label="Close supporting panel"
|
|
172
|
+
onclick={() => (mobileSheetOpen = false)}
|
|
173
|
+
>
|
|
174
|
+
<div class="bg-md-sys-color-on-surface-variant/40 h-1 w-8 rounded-full"></div>
|
|
175
|
+
</button>
|
|
176
|
+
{@render supporting()}
|
|
177
|
+
</div>
|
|
178
|
+
{/if}
|
|
@@ -312,16 +312,16 @@ export declare const supportingPane: import("tailwind-variants").TVReturnType<{
|
|
|
312
312
|
};
|
|
313
313
|
gap: {
|
|
314
314
|
none: {
|
|
315
|
-
|
|
315
|
+
main: string;
|
|
316
316
|
};
|
|
317
317
|
sm: {
|
|
318
|
-
|
|
318
|
+
main: string;
|
|
319
319
|
};
|
|
320
320
|
md: {
|
|
321
|
-
|
|
321
|
+
main: string;
|
|
322
322
|
};
|
|
323
323
|
lg: {
|
|
324
|
-
|
|
324
|
+
main: string;
|
|
325
325
|
};
|
|
326
326
|
};
|
|
327
327
|
full: {
|
|
@@ -371,16 +371,16 @@ export declare const supportingPane: import("tailwind-variants").TVReturnType<{
|
|
|
371
371
|
};
|
|
372
372
|
gap: {
|
|
373
373
|
none: {
|
|
374
|
-
|
|
374
|
+
main: string;
|
|
375
375
|
};
|
|
376
376
|
sm: {
|
|
377
|
-
|
|
377
|
+
main: string;
|
|
378
378
|
};
|
|
379
379
|
md: {
|
|
380
|
-
|
|
380
|
+
main: string;
|
|
381
381
|
};
|
|
382
382
|
lg: {
|
|
383
|
-
|
|
383
|
+
main: string;
|
|
384
384
|
};
|
|
385
385
|
};
|
|
386
386
|
full: {
|
|
@@ -430,16 +430,16 @@ export declare const supportingPane: import("tailwind-variants").TVReturnType<{
|
|
|
430
430
|
};
|
|
431
431
|
gap: {
|
|
432
432
|
none: {
|
|
433
|
-
|
|
433
|
+
main: string;
|
|
434
434
|
};
|
|
435
435
|
sm: {
|
|
436
|
-
|
|
436
|
+
main: string;
|
|
437
437
|
};
|
|
438
438
|
md: {
|
|
439
|
-
|
|
439
|
+
main: string;
|
|
440
440
|
};
|
|
441
441
|
lg: {
|
|
442
|
-
|
|
442
|
+
main: string;
|
|
443
443
|
};
|
|
444
444
|
};
|
|
445
445
|
full: {
|
|
@@ -91,28 +91,28 @@ export const singlePane = tv({
|
|
|
91
91
|
});
|
|
92
92
|
export const supportingPane = tv({
|
|
93
93
|
slots: {
|
|
94
|
-
base: 'flex w-full flex-col
|
|
95
|
-
main: 'bg-md-sys-color-surface box-border grow order-2
|
|
96
|
-
supporting: 'text-md-sys-color-on-surface-container box-border order-1
|
|
94
|
+
base: 'flex w-full flex-col lg:flex-row',
|
|
95
|
+
main: 'bg-md-sys-color-surface box-border grow order-2 lg:order-1',
|
|
96
|
+
supporting: 'text-md-sys-color-on-surface-container box-border order-1 lg:order-2 min-w-0 lg:w-80'
|
|
97
97
|
},
|
|
98
98
|
variants: {
|
|
99
99
|
anchor: {
|
|
100
100
|
// supporting scrolls independently; main scrolls the page
|
|
101
101
|
viewport: {
|
|
102
|
-
supporting: '
|
|
102
|
+
supporting: 'lg:fixed lg:top-[var(--supporting-pane-top,0px)] lg:bottom-0 lg:w-[var(--supporting-pane-width,320px)] lg:overflow-y-auto scrollbar-none'
|
|
103
103
|
},
|
|
104
104
|
// supporting sticks as user scrolls the page (classic sticky)
|
|
105
105
|
parent: {
|
|
106
|
-
supporting: '
|
|
106
|
+
supporting: 'lg:sticky lg:top-4 overflow-y-scroll'
|
|
107
107
|
}
|
|
108
108
|
},
|
|
109
109
|
position: {
|
|
110
110
|
right: {
|
|
111
|
-
base: '
|
|
111
|
+
base: 'lg:flex-row',
|
|
112
112
|
supporting: ''
|
|
113
113
|
},
|
|
114
114
|
left: {
|
|
115
|
-
base: '
|
|
115
|
+
base: 'lg:flex-row-reverse',
|
|
116
116
|
supporting: ''
|
|
117
117
|
}
|
|
118
118
|
},
|
|
@@ -125,10 +125,10 @@ export const supportingPane = tv({
|
|
|
125
125
|
}
|
|
126
126
|
},
|
|
127
127
|
gap: {
|
|
128
|
-
none: {
|
|
129
|
-
sm: {
|
|
130
|
-
md: {
|
|
131
|
-
lg: {
|
|
128
|
+
none: { main: 'gap-0' },
|
|
129
|
+
sm: { main: 'gap-4' },
|
|
130
|
+
md: { main: 'gap-6' },
|
|
131
|
+
lg: { main: 'gap-8' }
|
|
132
132
|
},
|
|
133
133
|
full: {
|
|
134
134
|
true: { base: 'min-h-dvh' },
|
|
@@ -148,16 +148,16 @@ export const supportingPane = tv({
|
|
|
148
148
|
anchor: 'viewport',
|
|
149
149
|
position: 'right',
|
|
150
150
|
class: {
|
|
151
|
-
supporting: '
|
|
152
|
-
main: '
|
|
151
|
+
supporting: 'lg:right-0',
|
|
152
|
+
main: 'lg:mr-[var(--supporting-pane-width,320px)]'
|
|
153
153
|
}
|
|
154
154
|
},
|
|
155
155
|
{
|
|
156
156
|
anchor: 'viewport',
|
|
157
157
|
position: 'left',
|
|
158
158
|
class: {
|
|
159
|
-
supporting: '
|
|
160
|
-
main: '
|
|
159
|
+
supporting: 'lg:left-0',
|
|
160
|
+
main: 'lg:ml-[var(--supporting-pane-width,320px)]'
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
],
|
|
@@ -38,6 +38,11 @@ export type SplitPaneProps = SplitPaneVariants & DivAttrs & {
|
|
|
38
38
|
onPaddingChange?: (padding: number) => void;
|
|
39
39
|
/** The anchor mode for the panes. */
|
|
40
40
|
anchor?: SplitPaneVariants['anchor'];
|
|
41
|
+
/**
|
|
42
|
+
* Which pane to show on mobile. On desktop both panes are always visible.
|
|
43
|
+
* @default 'right'
|
|
44
|
+
*/
|
|
45
|
+
mobilePane?: 'left' | 'right';
|
|
41
46
|
/** Optional class for the left pane. */
|
|
42
47
|
leftClass?: string;
|
|
43
48
|
/** Optional class for the right pane. */
|
|
@@ -55,8 +60,14 @@ export type SupportingPaneProps = SupportingPaneVariants & DivAttrs & {
|
|
|
55
60
|
mainClass?: string;
|
|
56
61
|
/** Optional class for the supporting pane. */
|
|
57
62
|
supportingClass?: string;
|
|
58
|
-
/** Whether the supporting pane can be collapsed. */
|
|
63
|
+
/** Whether the supporting pane can be collapsed (desktop). */
|
|
59
64
|
collapsible?: boolean;
|
|
65
|
+
/** Show a bottom sheet on mobile instead of inline stacking. @default false */
|
|
66
|
+
mobileSheet?: boolean;
|
|
67
|
+
/** Material symbol name for the mobile sheet trigger button. @default 'info' */
|
|
68
|
+
mobileSheetIcon?: string;
|
|
69
|
+
/** Extra class for the mobile trigger button (use to position above navbars). */
|
|
70
|
+
mobileTriggerClass?: string;
|
|
60
71
|
/**
|
|
61
72
|
* Width of the supporting pane in px (viewport anchor mode).
|
|
62
73
|
* @default 320
|
|
@@ -59,10 +59,10 @@ Switches toggle the state of a single setting on or off.
|
|
|
59
59
|
<div class={String(handle())}>
|
|
60
60
|
{#if icons !== 'none'}
|
|
61
61
|
<!-- checked icon -->
|
|
62
|
-
<Icon class={clsx(icon(), iconChecked())} {...checkedIconProps} />
|
|
62
|
+
<Icon size="xs" class={clsx(icon(), iconChecked())} {...checkedIconProps} />
|
|
63
63
|
{#if icons === 'both'}
|
|
64
64
|
<!-- unchecked icon -->
|
|
65
|
-
<Icon class={clsx(icon(), iconUnchecked())} {...uncheckedIconProps} />
|
|
65
|
+
<Icon size="xs" class={clsx(icon(), iconUnchecked())} {...uncheckedIconProps} />
|
|
66
66
|
{/if}
|
|
67
67
|
{/if}
|
|
68
68
|
</div>
|
|
@@ -44,14 +44,20 @@ export const toggle = tv({
|
|
|
44
44
|
'group-hover:bg-md-sys-color-on-surface/8',
|
|
45
45
|
'group-data-[state=checked]:group-hover:bg-md-sys-color-primary/8'
|
|
46
46
|
].join(' '),
|
|
47
|
-
// Icon inside thumb - shared base
|
|
47
|
+
// Icon inside thumb - shared base (sized via size="xs" prop on Icon, not here)
|
|
48
48
|
icon: [
|
|
49
|
-
'absolute inset-0 m-auto pointer-events-none flex
|
|
49
|
+
'absolute inset-0 m-auto pointer-events-none flex items-center justify-center',
|
|
50
50
|
'opacity-0 transition-opacity duration-150 shrink-0'
|
|
51
51
|
].join(' '),
|
|
52
|
-
// Specific icon states -
|
|
53
|
-
iconChecked:
|
|
54
|
-
|
|
52
|
+
// Specific icon states - colour matches M3 spec for each thumb background
|
|
53
|
+
iconChecked: [
|
|
54
|
+
'group-data-[state=checked]:opacity-100',
|
|
55
|
+
'text-md-sys-color-on-primary-container'
|
|
56
|
+
].join(' '),
|
|
57
|
+
iconUnchecked: [
|
|
58
|
+
'group-data-[state=unchecked]:opacity-100',
|
|
59
|
+
'text-md-sys-color-on-surface-variant'
|
|
60
|
+
].join(' ')
|
|
55
61
|
},
|
|
56
62
|
variants: {
|
|
57
63
|
icons: {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Material 3 Navigation Rail.
|
|
4
4
|
|
|
5
5
|
Navigation rails provide access to primary destinations in apps when using tablet and desktop screens.
|
|
6
|
+
On mobile (< md) it automatically renders as a bottom navigation bar.
|
|
6
7
|
|
|
7
8
|
@see https://m3.material.io/components/navigation-rail/overview
|
|
8
9
|
-->
|
|
@@ -14,6 +15,8 @@ Navigation rails provide access to primary destinations in apps when using table
|
|
|
14
15
|
import ButtonIcon from '../../buttons/ButtonIcon.svelte';
|
|
15
16
|
import { rail } from './theme';
|
|
16
17
|
import { NavigationMenu } from 'bits-ui';
|
|
18
|
+
import { railStore } from './railStore.svelte.js';
|
|
19
|
+
import RailNavContext from './RailNavContext.svelte';
|
|
17
20
|
|
|
18
21
|
let {
|
|
19
22
|
children,
|
|
@@ -24,6 +27,9 @@ Navigation rails provide access to primary destinations in apps when using table
|
|
|
24
27
|
collapsed = $bindable(true),
|
|
25
28
|
anchor = 'viewport',
|
|
26
29
|
railTop = 0,
|
|
30
|
+
mobileNav = true,
|
|
31
|
+
showMobileFab = true,
|
|
32
|
+
showMobileFooter = false,
|
|
27
33
|
class: className
|
|
28
34
|
}: RailProps = $props();
|
|
29
35
|
|
|
@@ -34,15 +40,29 @@ Navigation rails provide access to primary destinations in apps when using table
|
|
|
34
40
|
}
|
|
35
41
|
};
|
|
36
42
|
setContext('rail', railCtx);
|
|
43
|
+
|
|
44
|
+
$effect(() => {
|
|
45
|
+
railStore.collapsed = collapsed;
|
|
46
|
+
});
|
|
47
|
+
|
|
37
48
|
const { base, items, ghost, scrim } = $derived(rail({ expanded, anchor, rounded }));
|
|
38
49
|
const railBaseClass = $derived(`${base({ class: clsx(className) })} rail-base`);
|
|
39
50
|
const toggleClass = $derived(collapsed ? 'cursor-e-resize' : 'cursor-w-resize');
|
|
40
51
|
const cssVars = $derived(`--rail-top: ${railTop}px;`);
|
|
41
52
|
</script>
|
|
42
53
|
|
|
54
|
+
<!-- Push ghost: hidden on mobile, becomes flex item on md+ to push content -->
|
|
43
55
|
<div class={ghost()}></div>
|
|
44
56
|
|
|
45
|
-
|
|
57
|
+
<!-- Tablet scrim (md:block lg:hidden) — overlays content on expand -->
|
|
58
|
+
<div
|
|
59
|
+
class={`${scrim()} rail-scrim`}
|
|
60
|
+
data-expanded={expanded}
|
|
61
|
+
onclick={() => (collapsed = true)}
|
|
62
|
+
role="presentation"
|
|
63
|
+
></div>
|
|
64
|
+
|
|
65
|
+
<!-- Desktop/tablet sidebar rail -->
|
|
46
66
|
<div
|
|
47
67
|
class={railBaseClass}
|
|
48
68
|
style={cssVars}
|
|
@@ -65,7 +85,9 @@ Navigation rails provide access to primary destinations in apps when using table
|
|
|
65
85
|
|
|
66
86
|
<NavigationMenu.Root orientation="vertical" class="w-full">
|
|
67
87
|
<NavigationMenu.List class={`${items()} rail-items`}>
|
|
68
|
-
{
|
|
88
|
+
<RailNavContext mobile={false}>
|
|
89
|
+
{@render children?.()}
|
|
90
|
+
</RailNavContext>
|
|
69
91
|
</NavigationMenu.List>
|
|
70
92
|
</NavigationMenu.Root>
|
|
71
93
|
|
|
@@ -76,14 +98,50 @@ Navigation rails provide access to primary destinations in apps when using table
|
|
|
76
98
|
{/if}
|
|
77
99
|
</div>
|
|
78
100
|
|
|
101
|
+
<!-- Mobile bottom navbar (md:hidden) -->
|
|
102
|
+
{#if mobileNav}
|
|
103
|
+
{#if showMobileFab && fab}
|
|
104
|
+
<div class="fixed right-4 bottom-24 z-30 md:hidden">
|
|
105
|
+
{@render fab?.()}
|
|
106
|
+
</div>
|
|
107
|
+
{/if}
|
|
108
|
+
|
|
109
|
+
<nav
|
|
110
|
+
class="bg-md-sys-color-surface-container shadow-elevation-2 fixed right-0 bottom-0 left-0 z-20 flex h-20 items-center py-3 md:hidden"
|
|
111
|
+
aria-label="Mobile navigation"
|
|
112
|
+
>
|
|
113
|
+
<NavigationMenu.Root orientation="horizontal" class="flex w-full">
|
|
114
|
+
<NavigationMenu.List class="flex w-full justify-around">
|
|
115
|
+
<RailNavContext mobile={true}>
|
|
116
|
+
{@render children?.()}
|
|
117
|
+
</RailNavContext>
|
|
118
|
+
</NavigationMenu.List>
|
|
119
|
+
</NavigationMenu.Root>
|
|
120
|
+
|
|
121
|
+
{#if showMobileFooter && railFooter}
|
|
122
|
+
<div class="shrink-0 px-2">
|
|
123
|
+
{@render railFooter()}
|
|
124
|
+
</div>
|
|
125
|
+
{/if}
|
|
126
|
+
</nav>
|
|
127
|
+
|
|
128
|
+
<!-- Spacer so page content clears the fixed mobile navbar -->
|
|
129
|
+
<div class="h-20 w-full shrink-0 md:hidden" aria-hidden="true"></div>
|
|
130
|
+
{/if}
|
|
131
|
+
|
|
79
132
|
<style>
|
|
80
133
|
.rail-scrim {
|
|
81
134
|
opacity: 0;
|
|
82
135
|
pointer-events: none;
|
|
136
|
+
background-color: transparent;
|
|
137
|
+
z-index: -1;
|
|
138
|
+
transition: opacity 200ms ease;
|
|
83
139
|
}
|
|
84
140
|
|
|
85
141
|
.rail-scrim[data-expanded='true'] {
|
|
86
142
|
opacity: 1;
|
|
87
143
|
pointer-events: auto;
|
|
144
|
+
background-color: rgb(0 0 0 / 0.4);
|
|
145
|
+
z-index: 30;
|
|
88
146
|
}
|
|
89
147
|
</style>
|
|
@@ -3,6 +3,7 @@ import type { RailProps } from './types';
|
|
|
3
3
|
* Material 3 Navigation Rail.
|
|
4
4
|
*
|
|
5
5
|
* Navigation rails provide access to primary destinations in apps when using tablet and desktop screens.
|
|
6
|
+
* On mobile (< md) it automatically renders as a bottom navigation bar.
|
|
6
7
|
*
|
|
7
8
|
* @see https://m3.material.io/components/navigation-rail/overview
|
|
8
9
|
*/
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Material 3 Navigation Rail Item.
|
|
4
4
|
|
|
5
5
|
A single destination within a navigation rail.
|
|
6
|
+
In mobile (navbar) mode the same item renders as a bottom-bar tab.
|
|
6
7
|
-->
|
|
7
8
|
<script lang="ts">
|
|
8
9
|
import { page } from '$app/state';
|
|
@@ -27,7 +28,11 @@ A single destination within a navigation rail.
|
|
|
27
28
|
}: RailItemProps = $props();
|
|
28
29
|
|
|
29
30
|
const railCtx = getContext<{ collapsed: boolean } | undefined>('rail');
|
|
30
|
-
const
|
|
31
|
+
const railModeCtx = getContext<{ mobile: boolean } | undefined>('railMode');
|
|
32
|
+
const isMobile = $derived(railModeCtx?.mobile ?? false);
|
|
33
|
+
|
|
34
|
+
// Mobile items always render in collapsed (icon+label stacked) layout
|
|
35
|
+
const collapsed = $derived(isMobile ? true : (collapsedProp ?? railCtx?.collapsed ?? true));
|
|
31
36
|
|
|
32
37
|
const isDisabled = $derived(!!disabled);
|
|
33
38
|
const target = $derived(!isDisabled && external ? '_blank' : undefined);
|
|
@@ -57,11 +62,18 @@ A single destination within a navigation rail.
|
|
|
57
62
|
railElement({
|
|
58
63
|
active: isActive,
|
|
59
64
|
expanded: !collapsed,
|
|
60
|
-
disabled: isDisabled
|
|
65
|
+
disabled: isDisabled,
|
|
66
|
+
mobile: isMobile
|
|
61
67
|
})
|
|
62
68
|
);
|
|
63
69
|
const rootClass = $derived([base(), className].filter(Boolean).join(' '));
|
|
64
70
|
|
|
71
|
+
const itemClass = $derived(
|
|
72
|
+
isMobile
|
|
73
|
+
? 'flex h-full flex-1 justify-center'
|
|
74
|
+
: ['flex w-full items-center', collapsed && 'justify-center'].filter(Boolean).join(' ')
|
|
75
|
+
);
|
|
76
|
+
|
|
65
77
|
function handleClick(event: MouseEvent) {
|
|
66
78
|
if (isDisabled) {
|
|
67
79
|
event.preventDefault();
|
|
@@ -70,9 +82,7 @@ A single destination within a navigation rail.
|
|
|
70
82
|
}
|
|
71
83
|
</script>
|
|
72
84
|
|
|
73
|
-
<NavigationMenu.Item
|
|
74
|
-
class={['flex w-full items-center', collapsed && 'justify-center'].filter(Boolean).join(' ')}
|
|
75
|
-
>
|
|
85
|
+
<NavigationMenu.Item class={itemClass}>
|
|
76
86
|
<NavigationMenu.Link active={isActive} href={hrefValue}>
|
|
77
87
|
{#snippet child({ props })}
|
|
78
88
|
<a
|
|
@@ -3,6 +3,7 @@ import type { RailItemProps } from './types.js';
|
|
|
3
3
|
* Material 3 Navigation Rail Item.
|
|
4
4
|
*
|
|
5
5
|
* A single destination within a navigation rail.
|
|
6
|
+
* In mobile (navbar) mode the same item renders as a bottom-bar tab.
|
|
6
7
|
*/
|
|
7
8
|
declare const RailItem: import("svelte").Component<RailItemProps, {}, "">;
|
|
8
9
|
type RailItem = ReturnType<typeof RailItem>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { setContext } from 'svelte';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
|
|
5
|
+
let { children, mobile = false }: { children: Snippet; mobile?: boolean } = $props();
|
|
6
|
+
|
|
7
|
+
setContext('railMode', {
|
|
8
|
+
get mobile() {
|
|
9
|
+
return mobile;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
{@render children()}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
children: Snippet;
|
|
4
|
+
mobile?: boolean;
|
|
5
|
+
};
|
|
6
|
+
declare const RailNavContext: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type RailNavContext = ReturnType<typeof RailNavContext>;
|
|
8
|
+
export default RailNavContext;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// THIS FILE IS AUTO-GENERATED BY scripts/generate-components-index.ts - DO NOT EDIT MANUALLY
|
|
2
2
|
// re-export TS/JS modules from this folder
|
|
3
|
+
export * from './railStore.svelte.js';
|
|
3
4
|
export * from './theme.js';
|
|
4
5
|
export * from './types.js';
|
|
5
6
|
// default exports for components in this folder
|
|
@@ -16,7 +16,6 @@ export declare const rail: import("tailwind-variants").TVReturnType<{
|
|
|
16
16
|
true: {
|
|
17
17
|
base: string;
|
|
18
18
|
ghost: string;
|
|
19
|
-
scrim: string;
|
|
20
19
|
};
|
|
21
20
|
false: {
|
|
22
21
|
base: string;
|
|
@@ -50,7 +49,6 @@ export declare const rail: import("tailwind-variants").TVReturnType<{
|
|
|
50
49
|
true: {
|
|
51
50
|
base: string;
|
|
52
51
|
ghost: string;
|
|
53
|
-
scrim: string;
|
|
54
52
|
};
|
|
55
53
|
false: {
|
|
56
54
|
base: string;
|
|
@@ -84,7 +82,6 @@ export declare const rail: import("tailwind-variants").TVReturnType<{
|
|
|
84
82
|
true: {
|
|
85
83
|
base: string;
|
|
86
84
|
ghost: string;
|
|
87
|
-
scrim: string;
|
|
88
85
|
};
|
|
89
86
|
false: {
|
|
90
87
|
base: string;
|
|
@@ -135,6 +132,16 @@ export declare const railElement: import("tailwind-variants").TVReturnType<{
|
|
|
135
132
|
base: string;
|
|
136
133
|
};
|
|
137
134
|
};
|
|
135
|
+
mobile: {
|
|
136
|
+
true: {
|
|
137
|
+
base: string;
|
|
138
|
+
content: string;
|
|
139
|
+
iconContainer: string;
|
|
140
|
+
icon: string;
|
|
141
|
+
label: string;
|
|
142
|
+
};
|
|
143
|
+
false: {};
|
|
144
|
+
};
|
|
138
145
|
}, {
|
|
139
146
|
base: string;
|
|
140
147
|
content: string;
|
|
@@ -172,6 +179,16 @@ export declare const railElement: import("tailwind-variants").TVReturnType<{
|
|
|
172
179
|
base: string;
|
|
173
180
|
};
|
|
174
181
|
};
|
|
182
|
+
mobile: {
|
|
183
|
+
true: {
|
|
184
|
+
base: string;
|
|
185
|
+
content: string;
|
|
186
|
+
iconContainer: string;
|
|
187
|
+
icon: string;
|
|
188
|
+
label: string;
|
|
189
|
+
};
|
|
190
|
+
false: {};
|
|
191
|
+
};
|
|
175
192
|
}, {
|
|
176
193
|
base: string;
|
|
177
194
|
content: string;
|
|
@@ -209,6 +226,16 @@ export declare const railElement: import("tailwind-variants").TVReturnType<{
|
|
|
209
226
|
base: string;
|
|
210
227
|
};
|
|
211
228
|
};
|
|
229
|
+
mobile: {
|
|
230
|
+
true: {
|
|
231
|
+
base: string;
|
|
232
|
+
content: string;
|
|
233
|
+
iconContainer: string;
|
|
234
|
+
icon: string;
|
|
235
|
+
label: string;
|
|
236
|
+
};
|
|
237
|
+
false: {};
|
|
238
|
+
};
|
|
212
239
|
}, {
|
|
213
240
|
base: string;
|
|
214
241
|
content: string;
|
|
@@ -4,7 +4,7 @@ export const rail = tv({
|
|
|
4
4
|
base: 'py-12 hidden bg-md-sys-color-surface-container z-40 flex-col md:flex gap-6 transition-[width,padding,background-color] duration-[var(--md-sys-motion-duration-spatial)] ease-[var(--md-sys-motion-timing-function-emphasized)]',
|
|
5
5
|
items: 'flex flex-col gap-3 pt-6 scrollbar-none w-full overflow-x-hidden',
|
|
6
6
|
ghost: 'hidden md:block shrink-0 transition-[width] duration-[var(--md-sys-motion-duration-spatial)] ease-[var(--md-sys-motion-timing-function-emphasized)]',
|
|
7
|
-
scrim: '
|
|
7
|
+
scrim: 'hidden md:block lg:hidden inset-0'
|
|
8
8
|
},
|
|
9
9
|
variants: {
|
|
10
10
|
anchor: {
|
|
@@ -19,9 +19,8 @@ export const rail = tv({
|
|
|
19
19
|
},
|
|
20
20
|
expanded: {
|
|
21
21
|
true: {
|
|
22
|
-
base: 'pl-9 w-60 items-start bg-md-sys-color-surface-container-highest
|
|
23
|
-
ghost: 'w-24
|
|
24
|
-
scrim: 'z-20 bg-black/40'
|
|
22
|
+
base: 'pl-9 w-60 items-start bg-md-sys-color-surface-container-highest lg:bg-md-sys-color-surface-container rounded-r-lg',
|
|
23
|
+
ghost: 'w-24 lg:w-60'
|
|
25
24
|
},
|
|
26
25
|
false: {
|
|
27
26
|
base: 'w-24 items-center',
|
|
@@ -78,6 +77,16 @@ export const railElement = tv({
|
|
|
78
77
|
true: {
|
|
79
78
|
base: 'cursor-not-allowed opacity-38'
|
|
80
79
|
}
|
|
80
|
+
},
|
|
81
|
+
mobile: {
|
|
82
|
+
true: {
|
|
83
|
+
base: 'flex-1 flex-col items-center justify-center h-full min-w-0 max-w-[168px]',
|
|
84
|
+
content: 'py-0 gap-1 items-center justify-center w-full',
|
|
85
|
+
iconContainer: 'h-8 w-16 py-0 px-0',
|
|
86
|
+
icon: 'size-6',
|
|
87
|
+
label: 'truncate w-full text-center'
|
|
88
|
+
},
|
|
89
|
+
false: {}
|
|
81
90
|
}
|
|
82
91
|
},
|
|
83
92
|
compoundVariants: [
|
|
@@ -43,4 +43,10 @@ export type RailProps = RailVariants & HTMLAttributes<HTMLDivElement> & {
|
|
|
43
43
|
anchor?: RailVariants['anchor'];
|
|
44
44
|
/** Top offset in px for viewport/parent anchor (e.g. height of a top bar). Default 0. */
|
|
45
45
|
railTop?: number;
|
|
46
|
+
/** Render a bottom navbar on mobile (< md). Default true. */
|
|
47
|
+
mobileNav?: boolean;
|
|
48
|
+
/** Show the fab slot above the mobile navbar. Default true. */
|
|
49
|
+
showMobileFab?: boolean;
|
|
50
|
+
/** Show the railFooter slot inside the mobile navbar. Default false. */
|
|
51
|
+
showMobileFooter?: boolean;
|
|
46
52
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noxlovette/material",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -42,38 +42,38 @@
|
|
|
42
42
|
"@eslint/compat": "^2.1.0",
|
|
43
43
|
"@eslint/js": "^10.0.1",
|
|
44
44
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
45
|
-
"@sveltejs/kit": "^2.
|
|
46
|
-
"@sveltejs/package": "^2.5.
|
|
45
|
+
"@sveltejs/kit": "^2.63.0",
|
|
46
|
+
"@sveltejs/package": "^2.5.8",
|
|
47
47
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
48
48
|
"@tailwindcss/cli": "^4.3.0",
|
|
49
49
|
"@tailwindcss/typography": "^0.5.19",
|
|
50
50
|
"@tailwindcss/vite": "^4.3.0",
|
|
51
|
-
"@types/node": "^25.
|
|
52
|
-
"eslint": "^10.
|
|
51
|
+
"@types/node": "^25.9.2",
|
|
52
|
+
"eslint": "^10.4.1",
|
|
53
53
|
"eslint-config-prettier": "^10.1.8",
|
|
54
|
-
"eslint-plugin-svelte": "^3.
|
|
54
|
+
"eslint-plugin-svelte": "^3.19.0",
|
|
55
55
|
"globals": "^17.6.0",
|
|
56
|
-
"lefthook": "^2.1.
|
|
56
|
+
"lefthook": "^2.1.9",
|
|
57
57
|
"prettier": "^3.8.3",
|
|
58
58
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
59
|
-
"prettier-plugin-svelte": "^
|
|
59
|
+
"prettier-plugin-svelte": "^4.1.0",
|
|
60
60
|
"prettier-plugin-tailwindcss": "^0.8.0",
|
|
61
|
-
"publint": "^0.3.
|
|
62
|
-
"svelte": "^5.
|
|
63
|
-
"svelte-check": "^4.
|
|
61
|
+
"publint": "^0.3.21",
|
|
62
|
+
"svelte": "^5.56.2",
|
|
63
|
+
"svelte-check": "^4.6.0",
|
|
64
64
|
"sveltekit-superforms": "^2.30.1",
|
|
65
65
|
"tailwind-scrollbar": "^4.0.2",
|
|
66
66
|
"tailwindcss": "^4.3.0",
|
|
67
67
|
"typescript": "^6.0.3",
|
|
68
|
-
"typescript-eslint": "^8.
|
|
69
|
-
"vite": "^8.0.
|
|
68
|
+
"typescript-eslint": "^8.60.1",
|
|
69
|
+
"vite": "^8.0.16",
|
|
70
70
|
"vite-plugin-devtools-json": "^1.0.0",
|
|
71
|
-
"vitest": "^4.1.
|
|
71
|
+
"vitest": "^4.1.8",
|
|
72
72
|
"zod": "^4.4.3"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
75
|
"@floating-ui/dom": "^1.7.6",
|
|
76
|
-
"@internationalized/date": "^3.12.
|
|
76
|
+
"@internationalized/date": "^3.12.2",
|
|
77
77
|
"@ktibow/material-color-utilities-nightly": "^0.4.1772748028000",
|
|
78
78
|
"bits-ui": "^2.18.1",
|
|
79
79
|
"clsx": "^2.1.1",
|