@classic-homes/theme-svelte 1.22.4 → 1.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1278,6 +1278,18 @@
|
|
|
1278
1278
|
return 'animate-scale-in';
|
|
1279
1279
|
});
|
|
1280
1280
|
|
|
1281
|
+
// Resize handles sit over the content's right/bottom edge, where its
|
|
1282
|
+
// scrollbars live. Inset the scroll container so the scrollbars stay clickable.
|
|
1283
|
+
const resizeRight = $derived(
|
|
1284
|
+
!disableResize &&
|
|
1285
|
+
((variant === 'full' && (mode === 'detached' || edge === 'left')) ||
|
|
1286
|
+
(variant === 'card' && isExpanded && !isMobile))
|
|
1287
|
+
);
|
|
1288
|
+
const resizeBottom = $derived(
|
|
1289
|
+
!disableResize && variant === 'full' && (mode === 'detached' || edge === 'top')
|
|
1290
|
+
);
|
|
1291
|
+
const hasFooter = $derived(!!(footer || actions.length > 0 || footerActions));
|
|
1292
|
+
|
|
1281
1293
|
// Whether to show content (collapsed card only shows header)
|
|
1282
1294
|
const showContent = $derived(
|
|
1283
1295
|
variant !== 'card' ||
|
|
@@ -1535,7 +1547,9 @@
|
|
|
1535
1547
|
|
|
1536
1548
|
<!-- Content (hidden when card is collapsed) -->
|
|
1537
1549
|
{#if showContent}
|
|
1538
|
-
<DataPanelContent
|
|
1550
|
+
<DataPanelContent
|
|
1551
|
+
class={cn(resizeRight && 'mr-2.5', resizeBottom && !hasFooter && 'mb-2.5')}
|
|
1552
|
+
>
|
|
1539
1553
|
{@render children()}
|
|
1540
1554
|
</DataPanelContent>
|
|
1541
1555
|
|
|
@@ -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>;
|