@noxlovette/material 0.1.32 → 0.1.33

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.
@@ -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={`width: var(--splitpane-left-width); left: ${leftOffset};`}
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,11 +56,15 @@ 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);
32
68
  const supportingId = 'supporting-pane';
33
69
 
34
70
  const {
@@ -42,12 +78,14 @@ SupportingPane provides a layout with a main content area and a supporting side
42
78
  ? `--supporting-pane-width: ${supportingWidth}px; --supporting-pane-top: ${supportingTop}px;`
43
79
  : ''
44
80
  );
81
+
82
+ const inlineSupportingClass = $derived(mobileSheet ? 'hidden lg:block' : '');
45
83
  </script>
46
84
 
47
85
  <div class={base({ class: clsx(className) })} style={cssVars}>
48
86
  <div class={mainCls({ class: clsx('relative', mainClass) })}>
49
87
  {#if collapsible}
50
- <div class="top-3 right-3 z-10 hidden h-12 w-12 md:absolute md:block">
88
+ <div class="top-3 right-3 z-10 hidden h-12 w-12 lg:absolute lg:block">
51
89
  <ButtonIcon
52
90
  triggerClass=""
53
91
  type="button"
@@ -67,12 +105,24 @@ SupportingPane provides a layout with a main content area and a supporting side
67
105
  </div>
68
106
  {/if}
69
107
  {@render main()}
108
+ {#if mobileSheet}
109
+ <ButtonIcon
110
+ class={clsx('fixed right-4 bottom-6 z-30 lg:hidden', mobileTriggerClass)}
111
+ type="button"
112
+ variant="tonal"
113
+ iconProps={{ name: mobileSheetIcon }}
114
+ aria-label="Open supporting panel"
115
+ onclick={() => (mobileSheetOpen = true)}
116
+ />
117
+ {/if}
70
118
  </div>
119
+
71
120
  {#if supportingVisible}
72
121
  <aside
122
+ transition:slideWidth
73
123
  id={supportingId}
74
124
  class={supportingCls({
75
- class: clsx(supportingClass)
125
+ class: clsx(supportingClass, inlineSupportingClass)
76
126
  })}
77
127
  data-entered={entered}
78
128
  data-position={position}
@@ -81,3 +131,31 @@ SupportingPane provides a layout with a main content area and a supporting side
81
131
  </aside>
82
132
  {/if}
83
133
  </div>
134
+
135
+ {#if mobileSheet && mobileSheetOpen}
136
+ <!-- Scrim -->
137
+ <div
138
+ class="bg-md-sys-color-scrim/40 fixed inset-0 z-40 lg:hidden"
139
+ transition:fade={{ duration: 220 }}
140
+ onclick={() => (mobileSheetOpen = false)}
141
+ aria-hidden="true"
142
+ ></div>
143
+
144
+ <!-- Sheet -->
145
+ <div
146
+ 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"
147
+ in:sheetIn
148
+ out:sheetOut
149
+ role="dialog"
150
+ aria-modal="true"
151
+ >
152
+ <button
153
+ class="flex w-full cursor-pointer items-center justify-center pt-4 pb-2"
154
+ aria-label="Close supporting panel"
155
+ onclick={() => (mobileSheetOpen = false)}
156
+ >
157
+ <div class="bg-md-sys-color-on-surface-variant/40 h-1 w-8 rounded-full"></div>
158
+ </button>
159
+ {@render supporting()}
160
+ </div>
161
+ {/if}
@@ -312,16 +312,16 @@ export declare const supportingPane: import("tailwind-variants").TVReturnType<{
312
312
  };
313
313
  gap: {
314
314
  none: {
315
- base: string;
315
+ main: string;
316
316
  };
317
317
  sm: {
318
- base: string;
318
+ main: string;
319
319
  };
320
320
  md: {
321
- base: string;
321
+ main: string;
322
322
  };
323
323
  lg: {
324
- base: string;
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
- base: string;
374
+ main: string;
375
375
  };
376
376
  sm: {
377
- base: string;
377
+ main: string;
378
378
  };
379
379
  md: {
380
- base: string;
380
+ main: string;
381
381
  };
382
382
  lg: {
383
- base: string;
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
- base: string;
433
+ main: string;
434
434
  };
435
435
  sm: {
436
- base: string;
436
+ main: string;
437
437
  };
438
438
  md: {
439
- base: string;
439
+ main: string;
440
440
  };
441
441
  lg: {
442
- base: string;
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 md:flex-row',
95
- main: 'bg-md-sys-color-surface box-border grow order-2 md:order-1',
96
- supporting: 'text-md-sys-color-on-surface-container box-border order-1 md:order-2 min-w-min md:w-80 overflow-x-visible'
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: 'md:fixed md:top-[var(--supporting-pane-top,0px)] md:bottom-0 md:w-[var(--supporting-pane-width,320px)] md:overflow-y-auto scrollbar-none'
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: 'md:sticky md:top-4 overflow-y-scroll'
106
+ supporting: 'lg:sticky lg:top-4 overflow-y-scroll'
107
107
  }
108
108
  },
109
109
  position: {
110
110
  right: {
111
- base: 'md:flex-row',
111
+ base: 'lg:flex-row',
112
112
  supporting: ''
113
113
  },
114
114
  left: {
115
- base: 'md:flex-row-reverse',
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: { base: 'gap-0' },
129
- sm: { base: 'gap-4' },
130
- md: { base: 'gap-6' },
131
- lg: { base: 'gap-8' }
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: 'md:right-0',
152
- main: 'md:mr-[var(--supporting-pane-width,320px)]'
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: 'md:left-0',
160
- main: 'md:ml-[var(--supporting-pane-width,320px)]'
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 h-4 w-4 items-center justify-center text-[16px]',
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 - using group state
53
- iconChecked: 'group-data-[state=checked]:opacity-100',
54
- iconUnchecked: 'group-data-[state=unchecked]:opacity-100'
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
- <div class={`${scrim()} rail-scrim`} data-expanded={expanded}></div>
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
- {@render children?.()}
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 collapsed = $derived(collapsedProp ?? railCtx?.collapsed ?? true);
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,3 +1,4 @@
1
+ export * from './railStore.svelte.js';
1
2
  export * from './theme.js';
2
3
  export * from './types.js';
3
4
  export { default as Rail } from './Rail.svelte';
@@ -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
@@ -0,0 +1,3 @@
1
+ export declare const railStore: {
2
+ collapsed: boolean;
3
+ };
@@ -0,0 +1,9 @@
1
+ let _collapsed = $state(true);
2
+ export const railStore = {
3
+ get collapsed() {
4
+ return _collapsed;
5
+ },
6
+ set collapsed(v) {
7
+ _collapsed = v;
8
+ }
9
+ };
@@ -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: 'xl:hidden inset-0 -z-10 transition-opacity duration-[var(--md-sys-motion-duration-spatial)] ease-[var(--md-sys-motion-timing-function-emphasized)]'
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 xl:bg-md-sys-color-surface-container rounded-r-lg',
23
- ghost: 'w-24 xl:w-60',
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.32",
3
+ "version": "0.1.33",
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.59.1",
46
- "@sveltejs/package": "^2.5.7",
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.6.2",
52
- "eslint": "^10.3.0",
51
+ "@types/node": "^25.9.2",
52
+ "eslint": "^10.4.1",
53
53
  "eslint-config-prettier": "^10.1.8",
54
- "eslint-plugin-svelte": "^3.17.1",
54
+ "eslint-plugin-svelte": "^3.19.0",
55
55
  "globals": "^17.6.0",
56
- "lefthook": "^2.1.6",
56
+ "lefthook": "^2.1.9",
57
57
  "prettier": "^3.8.3",
58
58
  "prettier-plugin-organize-imports": "^4.3.0",
59
- "prettier-plugin-svelte": "^3.5.1",
59
+ "prettier-plugin-svelte": "^4.1.0",
60
60
  "prettier-plugin-tailwindcss": "^0.8.0",
61
- "publint": "^0.3.20",
62
- "svelte": "^5.55.5",
63
- "svelte-check": "^4.4.8",
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.59.2",
69
- "vite": "^8.0.12",
68
+ "typescript-eslint": "^8.60.1",
69
+ "vite": "^8.0.16",
70
70
  "vite-plugin-devtools-json": "^1.0.0",
71
- "vitest": "^4.1.5",
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.1",
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",