@classic-homes/theme-svelte 1.22.3 → 1.24.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.
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { setContext } from 'svelte';
|
|
4
4
|
import { cn } from '../utils.js';
|
|
5
5
|
import type { Tab } from '../types/components.js';
|
|
6
|
+
import Select from './Select.svelte';
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
/** Array of tab definitions */
|
|
@@ -32,6 +33,12 @@
|
|
|
32
33
|
children?: Snippet;
|
|
33
34
|
/** Callback when active tab changes */
|
|
34
35
|
onValueChange?: (value: string) => void;
|
|
36
|
+
/**
|
|
37
|
+
* What to do when the tabs are wider than their container.
|
|
38
|
+
* - `auto` (default): show a full-width Select of the tabs instead
|
|
39
|
+
* - `never`: keep the tab row and wrap it onto more lines
|
|
40
|
+
*/
|
|
41
|
+
collapse?: 'auto' | 'never';
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
const uid = $props.id();
|
|
@@ -45,40 +52,41 @@
|
|
|
45
52
|
class: className,
|
|
46
53
|
children,
|
|
47
54
|
onValueChange,
|
|
55
|
+
collapse = 'auto',
|
|
48
56
|
}: Props = $props();
|
|
49
57
|
|
|
50
58
|
// Store references to tab button elements for direct focus management
|
|
51
59
|
let tabRefs: Record<string, HTMLButtonElement | undefined> = $state({});
|
|
52
60
|
|
|
53
|
-
//
|
|
61
|
+
// Collapse to a Select when the tab row is wider than the container. The
|
|
62
|
+
// row stays rendered (hidden) while collapsed, so it can still be measured
|
|
63
|
+
// and so TabPanel's aria-labelledby keeps pointing at a real tab element.
|
|
64
|
+
let root: HTMLDivElement | undefined = $state();
|
|
54
65
|
let tabList: HTMLDivElement | undefined = $state();
|
|
55
|
-
let
|
|
56
|
-
let canScrollRight = $state(false);
|
|
57
|
-
|
|
58
|
-
function updateScrollShadows() {
|
|
59
|
-
if (!tabList) return;
|
|
60
|
-
canScrollLeft = tabList.scrollLeft > 1;
|
|
61
|
-
canScrollRight = tabList.scrollLeft + tabList.clientWidth < tabList.scrollWidth - 1;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Scroll only the tab list (never the page) so deep-linked tabs are visible
|
|
65
|
-
function scrollTabIntoView(el: HTMLButtonElement) {
|
|
66
|
-
if (!tabList) return;
|
|
67
|
-
const listRect = tabList.getBoundingClientRect();
|
|
68
|
-
const rect = el.getBoundingClientRect();
|
|
69
|
-
if (rect.left < listRect.left) {
|
|
70
|
-
tabList.scrollLeft += rect.left - listRect.left - 8;
|
|
71
|
-
} else if (rect.right > listRect.right) {
|
|
72
|
-
tabList.scrollLeft += rect.right - listRect.right + 8;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
66
|
+
let collapsed = $state(false);
|
|
75
67
|
|
|
76
68
|
$effect(() => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
69
|
+
if (collapse !== 'auto' || !root || !tabList) {
|
|
70
|
+
collapsed = false;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const rootEl = root;
|
|
74
|
+
const listEl = tabList;
|
|
75
|
+
const measure = () => {
|
|
76
|
+
collapsed = listEl.scrollWidth > rootEl.clientWidth;
|
|
77
|
+
};
|
|
78
|
+
measure();
|
|
79
|
+
// Fires on container resize and when tab labels change the row's width
|
|
80
|
+
const observer = new ResizeObserver(measure);
|
|
81
|
+
observer.observe(rootEl);
|
|
82
|
+
observer.observe(listEl);
|
|
83
|
+
return () => observer.disconnect();
|
|
80
84
|
});
|
|
81
85
|
|
|
86
|
+
const selectOptions = $derived(
|
|
87
|
+
tabs.map((t) => ({ value: t.id, label: t.label, disabled: t.disabled }))
|
|
88
|
+
);
|
|
89
|
+
|
|
82
90
|
// Context for TabPanel children. Getters read the props directly, so the
|
|
83
91
|
// active panel is right on the first render and during SSR (an $effect
|
|
84
92
|
// would only fill it in after hydration).
|
|
@@ -133,27 +141,25 @@
|
|
|
133
141
|
}
|
|
134
142
|
</script>
|
|
135
143
|
|
|
136
|
-
<div class={cn('w-full', className)}>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
{
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
class="pointer-events-none absolute inset-y-0 right-0 z-10 w-6 rounded-r-md bg-gradient-to-l from-muted to-transparent"
|
|
148
|
-
aria-hidden="true"
|
|
149
|
-
></div>
|
|
150
|
-
{/if}
|
|
144
|
+
<div bind:this={root} class={cn('w-full min-w-0', className)}>
|
|
145
|
+
{#if collapsed}
|
|
146
|
+
<Select {value} options={selectOptions} onValueChange={selectTab} aria-label={label} />
|
|
147
|
+
{/if}
|
|
148
|
+
|
|
149
|
+
<!-- Tab List (kept for measuring while collapsed; zero height and inert) -->
|
|
150
|
+
<div
|
|
151
|
+
class={cn(collapsed && 'invisible h-0 overflow-hidden')}
|
|
152
|
+
inert={collapsed}
|
|
153
|
+
aria-hidden={collapsed ? 'true' : undefined}
|
|
154
|
+
>
|
|
151
155
|
<div
|
|
152
156
|
bind:this={tabList}
|
|
153
|
-
onscroll={updateScrollShadows}
|
|
154
157
|
role="tablist"
|
|
155
158
|
aria-label={label}
|
|
156
|
-
class=
|
|
159
|
+
class={cn(
|
|
160
|
+
'inline-flex items-center rounded-md bg-muted p-1 text-muted-foreground',
|
|
161
|
+
collapse === 'never' ? 'max-w-full flex-wrap' : 'h-10 w-max'
|
|
162
|
+
)}
|
|
157
163
|
>
|
|
158
164
|
{#each tabs as tab, index (tab.id)}
|
|
159
165
|
<button
|
|
@@ -28,6 +28,12 @@ interface Props {
|
|
|
28
28
|
children?: Snippet;
|
|
29
29
|
/** Callback when active tab changes */
|
|
30
30
|
onValueChange?: (value: string) => void;
|
|
31
|
+
/**
|
|
32
|
+
* What to do when the tabs are wider than their container.
|
|
33
|
+
* - `auto` (default): show a full-width Select of the tabs instead
|
|
34
|
+
* - `never`: keep the tab row and wrap it onto more lines
|
|
35
|
+
*/
|
|
36
|
+
collapse?: 'auto' | 'never';
|
|
31
37
|
}
|
|
32
38
|
declare const Tabs: import("svelte").Component<Props, {}, "value">;
|
|
33
39
|
type Tabs = ReturnType<typeof Tabs>;
|
|
@@ -444,9 +444,12 @@
|
|
|
444
444
|
<div
|
|
445
445
|
class={cn(
|
|
446
446
|
'group flex w-full items-center rounded-md',
|
|
447
|
-
|
|
447
|
+
// Inside a section panel rows sit on the panel's grey, so no rest fill
|
|
448
|
+
isLight && !item.active && (nested ? 'bg-transparent' : 'bg-background'),
|
|
449
|
+
isLight && !item.active && 'hover:bg-accent',
|
|
448
450
|
isLight && item.active && 'bg-primary/10',
|
|
449
|
-
!isLight && !item.active && 'bg-
|
|
451
|
+
!isLight && !item.active && (nested ? 'bg-transparent' : 'bg-sidebar'),
|
|
452
|
+
!isLight && !item.active && 'hover:bg-sidebar-accent',
|
|
450
453
|
!isLight && item.active && 'bg-sidebar-primary'
|
|
451
454
|
)}
|
|
452
455
|
>
|
|
@@ -145,32 +145,39 @@
|
|
|
145
145
|
{/each}
|
|
146
146
|
</ul>
|
|
147
147
|
{:else if isExpanded}
|
|
148
|
-
<!-- Collapsible and expanded:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
<!-- Collapsible and expanded: the same inset grey panel as an open submenu
|
|
149
|
+
(SidebarNavItem), so a section's pages read as inside it. Its items are
|
|
150
|
+
`nested`: no rest fill over the grey, and their own submenus get the rail -->
|
|
151
|
+
<div
|
|
152
|
+
class={cn(
|
|
153
|
+
'mx-2 overflow-hidden rounded-md p-1',
|
|
154
|
+
isLight ? 'bg-muted/50' : 'bg-sidebar-accent/30'
|
|
155
|
+
)}
|
|
152
156
|
transition:slide={{ duration: sidebarStore.animate ? 200 : 0 }}
|
|
153
157
|
>
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
158
|
+
<ul class="space-y-1" id={`sidebar-section-${section.id}`}>
|
|
159
|
+
{#each section.items as item}
|
|
160
|
+
<li>
|
|
161
|
+
<SidebarNavItem
|
|
162
|
+
{item}
|
|
163
|
+
{isLight}
|
|
164
|
+
{collapsed}
|
|
165
|
+
{isMobile}
|
|
166
|
+
{icon}
|
|
167
|
+
{searchQuery}
|
|
168
|
+
{enablePinning}
|
|
169
|
+
{pinIcon}
|
|
170
|
+
{isPinnedSection}
|
|
171
|
+
{onNavigate}
|
|
172
|
+
onMouseEnter={onItemMouseEnter}
|
|
173
|
+
onMouseLeave={onItemMouseLeave}
|
|
174
|
+
{autoCollapse}
|
|
175
|
+
siblingIds={expandableItemIds}
|
|
176
|
+
nested
|
|
177
|
+
/>
|
|
178
|
+
</li>
|
|
179
|
+
{/each}
|
|
180
|
+
</ul>
|
|
181
|
+
</div>
|
|
175
182
|
{/if}
|
|
176
183
|
</div>
|