kumo-svelte 0.1.8 → 0.2.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/ai/component-registry.json +18 -2
- package/ai/component-registry.md +1 -1
- package/dist/components/chart/Chart.svelte +3 -2
- package/dist/components/flow/Connectors.svelte +1 -1
- package/dist/components/flow/connectors.d.ts +2 -2
- package/dist/components/flow/connectors.js +17 -9
- package/dist/components/flow/flow.test.js +32 -23
- package/dist/components/sidebar/Sidebar.svelte +84 -39
- package/dist/components/sidebar/Sidebar.svelte.d.ts +1 -0
- package/dist/components/sidebar/SidebarCollapsible.svelte +21 -2
- package/dist/components/sidebar/SidebarCollapsible.svelte.d.ts +1 -0
- package/dist/components/sidebar/SidebarCollapsibleContent.svelte +24 -5
- package/dist/components/sidebar/SidebarCollapsibleTrigger.svelte +1 -1
- package/dist/components/sidebar/SidebarContent.svelte +3 -2
- package/dist/components/sidebar/SidebarGroupLabel.svelte +3 -2
- package/dist/components/sidebar/SidebarHeader.svelte +1 -1
- package/dist/components/sidebar/SidebarMenuButton.svelte +10 -8
- package/dist/components/sidebar/SidebarMenuButton.svelte.d.ts +1 -0
- package/dist/components/sidebar/SidebarMenuChevron.svelte +9 -1
- package/dist/components/sidebar/SidebarMenuSub.svelte +1 -1
- package/dist/components/sidebar/SidebarMenuSubButton.svelte +11 -3
- package/dist/components/sidebar/SidebarProvider.svelte +18 -4
- package/dist/components/sidebar/SidebarProvider.svelte.d.ts +1 -0
- package/dist/components/sidebar/SidebarSlidingViews.svelte +1 -1
- package/dist/components/sidebar/context.d.ts +5 -4
- package/dist/components/sidebar/context.js +4 -4
- package/dist/components/tabs/Tabs.svelte +21 -4
- package/dist/docs/demo-snippets/SidebarAutoScrollDemo.svelte +5 -0
- package/dist/docs/demo-snippets/SidebarAutoScrollDemo.svelte.d.ts +18 -0
- package/dist/docs/demo-snippets/SidebarDemoShell.svelte +87 -7
- package/dist/docs/demo-snippets/SidebarDemoShell.svelte.d.ts +1 -1
- package/dist/docs/demo-snippets/SidebarMobileDemo.svelte +5 -0
- package/dist/docs/demo-snippets/SidebarMobileDemo.svelte.d.ts +18 -0
- package/dist/docs/props-data/Collapsible.d.ts +1 -1
- package/dist/docs/props-data/Collapsible.js +46 -39
- package/dist/docs/props-data/Sidebar.d.ts +1 -1
- package/dist/docs/props-data/Sidebar.js +30 -24
- package/dist/docs/props-data/Sidebar__Provider.d.ts +1 -1
- package/dist/docs/props-data/Sidebar__Provider.js +94 -16
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/lib/components/chart/Chart.svelte +3 -2
- package/src/lib/components/flow/Connectors.svelte +1 -1
- package/src/lib/components/flow/connectors.ts +19 -15
- package/src/lib/components/flow/flow.test.ts +37 -23
- package/src/lib/components/sidebar/Sidebar.svelte +84 -39
- package/src/lib/components/sidebar/SidebarCollapsible.svelte +21 -2
- package/src/lib/components/sidebar/SidebarCollapsibleContent.svelte +24 -5
- package/src/lib/components/sidebar/SidebarCollapsibleTrigger.svelte +1 -1
- package/src/lib/components/sidebar/SidebarContent.svelte +3 -2
- package/src/lib/components/sidebar/SidebarGroupLabel.svelte +3 -2
- package/src/lib/components/sidebar/SidebarHeader.svelte +1 -1
- package/src/lib/components/sidebar/SidebarMenuButton.svelte +10 -8
- package/src/lib/components/sidebar/SidebarMenuChevron.svelte +9 -1
- package/src/lib/components/sidebar/SidebarMenuSub.svelte +1 -1
- package/src/lib/components/sidebar/SidebarMenuSubButton.svelte +11 -3
- package/src/lib/components/sidebar/SidebarProvider.svelte +18 -4
- package/src/lib/components/sidebar/SidebarSlidingViews.svelte +1 -1
- package/src/lib/components/sidebar/context.ts +23 -13
- package/src/lib/components/tabs/Tabs.svelte +21 -4
- package/src/lib/docs/demo-snippets/SidebarAutoScrollDemo.svelte +5 -0
- package/src/lib/docs/demo-snippets/SidebarDemoShell.svelte +87 -7
- package/src/lib/docs/demo-snippets/SidebarMobileDemo.svelte +5 -0
- package/src/lib/docs/props-data/Collapsible.ts +48 -40
- package/src/lib/docs/props-data/Sidebar.ts +32 -25
- package/src/lib/docs/props-data/Sidebar__Provider.ts +99 -17
- package/src/lib/styles.css +40 -25
- package/src/routes/components/sidebar/+page.md +36 -3
- package/src/routes/llms.txt/+server.ts +265 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.2.0",
|
|
3
3
|
"components": {
|
|
4
4
|
"Autocomplete": {
|
|
5
5
|
"name": "Autocomplete",
|
|
@@ -1206,6 +1206,12 @@
|
|
|
1206
1206
|
"optional": true,
|
|
1207
1207
|
"description": "Called when open state changes."
|
|
1208
1208
|
},
|
|
1209
|
+
"autoScrollOnOpen": {
|
|
1210
|
+
"type": "boolean",
|
|
1211
|
+
"optional": true,
|
|
1212
|
+
"default": "false",
|
|
1213
|
+
"description": "Scrolls newly expanded sidebar content into view after opening."
|
|
1214
|
+
},
|
|
1209
1215
|
"onOpenChangeComplete": {
|
|
1210
1216
|
"type": "(open: boolean) => void",
|
|
1211
1217
|
"optional": true,
|
|
@@ -4077,6 +4083,11 @@
|
|
|
4077
4083
|
"required": true,
|
|
4078
4084
|
"description": "Sidebar content — Header, Content, Footer, groups, menus, and related slots."
|
|
4079
4085
|
},
|
|
4086
|
+
"contentClass": {
|
|
4087
|
+
"type": "string",
|
|
4088
|
+
"optional": true,
|
|
4089
|
+
"description": "Additional classes for the desktop content container."
|
|
4090
|
+
},
|
|
4080
4091
|
"class": {
|
|
4081
4092
|
"type": "string",
|
|
4082
4093
|
"optional": true,
|
|
@@ -4084,7 +4095,6 @@
|
|
|
4084
4095
|
}
|
|
4085
4096
|
},
|
|
4086
4097
|
"colors": [
|
|
4087
|
-
"bg-kumo-base",
|
|
4088
4098
|
"bg-kumo-brand",
|
|
4089
4099
|
"bg-kumo-hairline",
|
|
4090
4100
|
"bg-kumo-line",
|
|
@@ -4293,6 +4303,12 @@
|
|
|
4293
4303
|
"default": "250",
|
|
4294
4304
|
"description": "Transition duration in milliseconds for width and peek animations."
|
|
4295
4305
|
},
|
|
4306
|
+
"mobileBreakpoint": {
|
|
4307
|
+
"type": "number",
|
|
4308
|
+
"optional": true,
|
|
4309
|
+
"default": "768",
|
|
4310
|
+
"description": "Viewport width below which the sidebar renders as a mobile navigation drawer."
|
|
4311
|
+
},
|
|
4296
4312
|
"children": {
|
|
4297
4313
|
"type": "Snippet",
|
|
4298
4314
|
"optional": false,
|
package/ai/component-registry.md
CHANGED
|
@@ -73,6 +73,8 @@
|
|
|
73
73
|
|
|
74
74
|
const prepareChartOptions = (rawOptions: KumoChartOption, suppressAnimation = false): EChartsOption => {
|
|
75
75
|
const optionsWithTheme = {
|
|
76
|
+
backgroundColor: 'transparent',
|
|
77
|
+
color: effectiveDarkMode ? CHART_DARK_COLORS : CHART_LIGHT_COLORS,
|
|
76
78
|
...rawOptions,
|
|
77
79
|
...(suppressAnimation ? { animation: false } : {}),
|
|
78
80
|
textStyle: {
|
|
@@ -123,8 +125,7 @@
|
|
|
123
125
|
const initChart = () => {
|
|
124
126
|
if (!el) return;
|
|
125
127
|
chart?.dispose();
|
|
126
|
-
const
|
|
127
|
-
const nextChart = echarts.init(el, theme, { renderer: 'canvas' });
|
|
128
|
+
const nextChart = echarts.init(el, effectiveDarkMode ? 'dark' : undefined, { renderer: 'canvas' });
|
|
128
129
|
chart = nextChart;
|
|
129
130
|
updateOptions();
|
|
130
131
|
};
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
});
|
|
23
23
|
</script>
|
|
24
24
|
|
|
25
|
-
<svg width="100%" height="100%" aria-hidden="true" class="text-kumo-inactive overflow-visible">
|
|
25
|
+
<svg width="100%" height="100%" overflow="visible" aria-hidden="true" class="text-kumo-inactive overflow-visible">
|
|
26
26
|
<defs>
|
|
27
27
|
{#if markerId}
|
|
28
28
|
<marker
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FlowOrientation } from
|
|
1
|
+
import type { FlowOrientation } from "./context";
|
|
2
2
|
export interface Connector {
|
|
3
3
|
x1: number;
|
|
4
4
|
y1: number;
|
|
@@ -23,4 +23,4 @@ export declare function createRoundedPath({ x1, y1, x2, y2 }: {
|
|
|
23
23
|
y1: number;
|
|
24
24
|
x2: number;
|
|
25
25
|
y2: number;
|
|
26
|
-
}, { cornerRadius: maxCornerRadius, midOffset, arrowheadOffset, isBottom, single, orientation }?: PathProps): string;
|
|
26
|
+
}, { cornerRadius: maxCornerRadius, midOffset, arrowheadOffset, isBottom, single, orientation, }?: PathProps): string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const FLAT_THRESHOLD = 2;
|
|
2
|
-
export function createRoundedPath({ x1, y1, x2, y2 }, { cornerRadius: maxCornerRadius = 8, midOffset = 32, arrowheadOffset = 8, isBottom = false, single = false, orientation =
|
|
3
|
-
const cornerRadius = Math.min(maxCornerRadius, Math.abs(orientation ===
|
|
4
|
-
if (orientation ===
|
|
2
|
+
export function createRoundedPath({ x1, y1, x2, y2 }, { cornerRadius: maxCornerRadius = 8, midOffset = 32, arrowheadOffset = 8, isBottom = false, single = false, orientation = "vertical", } = {}) {
|
|
3
|
+
const cornerRadius = Math.min(maxCornerRadius, Math.abs(orientation === "horizontal" ? (y2 - y1) / 2 : (x2 - x1) / 2));
|
|
4
|
+
if (orientation === "horizontal") {
|
|
5
5
|
if (Math.abs(y2 - y1) <= FLAT_THRESHOLD) {
|
|
6
6
|
return `M ${x1} ${y1} L ${x2 - arrowheadOffset} ${y2}`;
|
|
7
7
|
}
|
|
@@ -18,16 +18,20 @@ export function createRoundedPath({ x1, y1, x2, y2 }, { cornerRadius: maxCornerR
|
|
|
18
18
|
`Q ${verticalX} ${y1} ${verticalX} ${verticalStart}`,
|
|
19
19
|
single
|
|
20
20
|
? `L ${verticalX} ${verticalEnd} Q ${verticalX} ${y2} ${secondHorizontalStart} ${y2}`
|
|
21
|
-
: `L ${verticalX} ${y2}
|
|
21
|
+
: `L ${verticalX} ${y2}`,
|
|
22
22
|
];
|
|
23
23
|
const topCurveCommands = [
|
|
24
24
|
single
|
|
25
25
|
? `L ${firstHorizontalEnd} ${y1} Q ${verticalX} ${y1} ${verticalX} ${verticalStart}`
|
|
26
26
|
: `L ${verticalX} ${y1}`,
|
|
27
27
|
`L ${verticalX} ${verticalEnd}`,
|
|
28
|
-
`Q ${verticalX} ${y2} ${secondHorizontalStart} ${y2}
|
|
28
|
+
`Q ${verticalX} ${y2} ${secondHorizontalStart} ${y2}`,
|
|
29
29
|
];
|
|
30
|
-
return [
|
|
30
|
+
return [
|
|
31
|
+
`M ${x1} ${y1}`,
|
|
32
|
+
...(isBottom ? bottomCurveCommands : topCurveCommands),
|
|
33
|
+
`L ${pathEndX} ${y2}`,
|
|
34
|
+
].join(" ");
|
|
31
35
|
}
|
|
32
36
|
if (Math.abs(x2 - x1) <= FLAT_THRESHOLD) {
|
|
33
37
|
return `M ${x1} ${y1} L ${x2} ${y2 - arrowheadOffset}`;
|
|
@@ -43,12 +47,16 @@ export function createRoundedPath({ x1, y1, x2, y2 }, { cornerRadius: maxCornerR
|
|
|
43
47
|
const bottomCurveCommands = [
|
|
44
48
|
`L ${x1} ${firstVerticalEnd}`,
|
|
45
49
|
`Q ${x1} ${horizontalY} ${horizontalStart} ${horizontalY}`,
|
|
46
|
-
`L ${x2} ${horizontalY}
|
|
50
|
+
`L ${x2} ${horizontalY}`,
|
|
47
51
|
];
|
|
48
52
|
const topCurveCommands = [
|
|
49
53
|
`L ${x1} ${horizontalY}`,
|
|
50
54
|
`L ${horizontalEnd} ${horizontalY}`,
|
|
51
|
-
`Q ${x2} ${horizontalY} ${x2} ${secondVerticalStart}
|
|
55
|
+
`Q ${x2} ${horizontalY} ${x2} ${secondVerticalStart}`,
|
|
52
56
|
];
|
|
53
|
-
return [
|
|
57
|
+
return [
|
|
58
|
+
`M ${x1} ${y1}`,
|
|
59
|
+
...(isBottom ? bottomCurveCommands : topCurveCommands),
|
|
60
|
+
`L ${x2} ${pathEndY}`,
|
|
61
|
+
].join(" ");
|
|
54
62
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
|
-
import { fireEvent, render, screen } from
|
|
3
|
-
import { beforeEach, describe, expect, it, vi } from
|
|
4
|
-
import
|
|
2
|
+
import { fireEvent, render, screen } from "@testing-library/svelte";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { createRoundedPath } from "./connectors";
|
|
5
|
+
import FlowPanningTestHost from "./FlowPanningTestHost.svelte";
|
|
5
6
|
const resizeObservers = [];
|
|
6
7
|
class TestResizeObserver {
|
|
7
8
|
callback;
|
|
@@ -25,7 +26,7 @@ function setRect(element, rect) {
|
|
|
25
26
|
height: 0,
|
|
26
27
|
...rect,
|
|
27
28
|
};
|
|
28
|
-
vi.spyOn(element,
|
|
29
|
+
vi.spyOn(element, "getBoundingClientRect").mockReturnValue(nextRect);
|
|
29
30
|
}
|
|
30
31
|
async function measure() {
|
|
31
32
|
for (const observer of resizeObservers) {
|
|
@@ -35,31 +36,31 @@ async function measure() {
|
|
|
35
36
|
}
|
|
36
37
|
beforeEach(() => {
|
|
37
38
|
resizeObservers.length = 0;
|
|
38
|
-
vi.stubGlobal(
|
|
39
|
-
document.body.style.cursor =
|
|
40
|
-
document.body.style.userSelect =
|
|
39
|
+
vi.stubGlobal("ResizeObserver", TestResizeObserver);
|
|
40
|
+
document.body.style.cursor = "";
|
|
41
|
+
document.body.style.userSelect = "";
|
|
41
42
|
});
|
|
42
|
-
describe(
|
|
43
|
-
it(
|
|
43
|
+
describe("Flow canvas panning", () => {
|
|
44
|
+
it("pans with the wheel and clamps to measured bounds", async () => {
|
|
44
45
|
render(FlowPanningTestHost);
|
|
45
|
-
const wrapper = screen.getByTestId(
|
|
46
|
-
const contents = screen.getByTestId(
|
|
46
|
+
const wrapper = screen.getByTestId("flow-wrapper");
|
|
47
|
+
const contents = screen.getByTestId("flow-contents");
|
|
47
48
|
setRect(wrapper, { width: 300, height: 200 });
|
|
48
49
|
setRect(contents, { width: 800, height: 500 });
|
|
49
50
|
await measure();
|
|
50
51
|
await fireEvent.wheel(wrapper, { deltaX: 700, deltaY: 400 });
|
|
51
|
-
expect(contents.style.transform).toBe(
|
|
52
|
+
expect(contents.style.transform).toBe("translate(-500px, -300px)");
|
|
52
53
|
await fireEvent.wheel(wrapper, { deltaX: -1000, deltaY: -1000 });
|
|
53
|
-
expect(contents.style.transform).toBe(
|
|
54
|
+
expect(contents.style.transform).toBe("translate(0px, 0px)");
|
|
54
55
|
});
|
|
55
|
-
it(
|
|
56
|
+
it("drag-pans the canvas while ignoring drags that start on nodes", async () => {
|
|
56
57
|
render(FlowPanningTestHost);
|
|
57
|
-
const wrapper = screen.getByTestId(
|
|
58
|
-
const contents = screen.getByTestId(
|
|
58
|
+
const wrapper = screen.getByTestId("flow-wrapper");
|
|
59
|
+
const contents = screen.getByTestId("flow-contents");
|
|
59
60
|
setRect(wrapper, { width: 300, height: 200 });
|
|
60
61
|
setRect(contents, { width: 800, height: 500 });
|
|
61
62
|
await measure();
|
|
62
|
-
await fireEvent.pointerDown(screen.getByTestId(
|
|
63
|
+
await fireEvent.pointerDown(screen.getByTestId("start"), {
|
|
63
64
|
button: 0,
|
|
64
65
|
clientX: 100,
|
|
65
66
|
clientY: 100,
|
|
@@ -70,23 +71,31 @@ describe('Flow canvas panning', () => {
|
|
|
70
71
|
clientY: 50,
|
|
71
72
|
pointerId: 1,
|
|
72
73
|
});
|
|
73
|
-
expect(contents.style.transform).toBe(
|
|
74
|
+
expect(contents.style.transform).toBe("translate(0px, 0px)");
|
|
74
75
|
await fireEvent.pointerDown(wrapper, {
|
|
75
76
|
button: 0,
|
|
76
77
|
clientX: 100,
|
|
77
78
|
clientY: 100,
|
|
78
79
|
pointerId: 2,
|
|
79
80
|
});
|
|
80
|
-
expect(document.body.style.cursor).toBe(
|
|
81
|
-
expect(document.body.style.userSelect).toBe(
|
|
81
|
+
expect(document.body.style.cursor).toBe("grabbing");
|
|
82
|
+
expect(document.body.style.userSelect).toBe("none");
|
|
82
83
|
await fireEvent.pointerMove(wrapper, {
|
|
83
84
|
clientX: 40,
|
|
84
85
|
clientY: 10,
|
|
85
86
|
pointerId: 2,
|
|
86
87
|
});
|
|
87
|
-
expect(contents.style.transform).toBe(
|
|
88
|
+
expect(contents.style.transform).toBe("translate(-60px, -90px)");
|
|
88
89
|
await fireEvent.pointerUp(wrapper, { pointerId: 2 });
|
|
89
|
-
expect(document.body.style.cursor).toBe(
|
|
90
|
-
expect(document.body.style.userSelect).toBe(
|
|
90
|
+
expect(document.body.style.cursor).toBe("");
|
|
91
|
+
expect(document.body.style.userSelect).toBe("");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe("createRoundedPath", () => {
|
|
95
|
+
it("serializes connector command arrays without comma separators", () => {
|
|
96
|
+
const verticalPath = createRoundedPath({ x1: 0, y1: 0, x2: 80, y2: 120 });
|
|
97
|
+
const horizontalPath = createRoundedPath({ x1: 0, y1: 0, x2: 120, y2: 80 }, { orientation: "horizontal" });
|
|
98
|
+
expect(verticalPath).not.toContain(",");
|
|
99
|
+
expect(horizontalPath).not.toContain(",");
|
|
91
100
|
});
|
|
92
101
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { Dialog as DialogPrimitive } from 'bits-ui';
|
|
3
2
|
import type { Snippet } from 'svelte';
|
|
4
3
|
import { cn } from '../../utils/cn';
|
|
5
4
|
import { TooltipProvider } from '../tooltip';
|
|
@@ -8,11 +7,15 @@
|
|
|
8
7
|
interface Props {
|
|
9
8
|
children?: Snippet;
|
|
10
9
|
class?: string;
|
|
10
|
+
contentClass?: string;
|
|
11
11
|
[key: string]: unknown;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
let { children, class: className, ...rest }: Props = $props();
|
|
14
|
+
let { children, class: className, contentClass, ...rest }: Props = $props();
|
|
15
15
|
const sidebar = getSidebarContext('Sidebar');
|
|
16
|
+
let mobileNode: HTMLElement | null = $state(null);
|
|
17
|
+
let mobileTrigger: HTMLElement | null = null;
|
|
18
|
+
let shouldRestoreFocus = false;
|
|
16
19
|
const isAlwaysExpanded = $derived(sidebar.collapsible === 'none');
|
|
17
20
|
const isVisible = $derived(sidebar.open || sidebar.isPeeking || isAlwaysExpanded);
|
|
18
21
|
const railWidth = $derived(isAlwaysExpanded || sidebar.open ? 'var(--sidebar-width)' : sidebar.collapsible === 'icon' ? 'var(--sidebar-width-icon)' : '0px');
|
|
@@ -28,12 +31,45 @@
|
|
|
28
31
|
!sidebar.open && sidebar.side === 'left' && 'left-0',
|
|
29
32
|
!sidebar.open && sidebar.side === 'right' && 'right-0',
|
|
30
33
|
sidebar.open && 'relative',
|
|
31
|
-
|
|
32
|
-
sidebar.isMobile && !sidebar.open && 'hidden md:flex',
|
|
33
|
-
sidebar.isMobile && sidebar.side === 'left' && 'left-0',
|
|
34
|
-
sidebar.isMobile && sidebar.side === 'right' && 'right-0'
|
|
34
|
+
contentClass
|
|
35
35
|
)
|
|
36
36
|
);
|
|
37
|
+
|
|
38
|
+
function closeMobile(restoreFocus: boolean) {
|
|
39
|
+
shouldRestoreFocus = restoreFocus;
|
|
40
|
+
sidebar.setOpenMobile(false);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function handleMobileKeydown(event: KeyboardEvent) {
|
|
44
|
+
if (event.key === 'Escape') closeMobile(true);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function handleMobileFocusout(event: FocusEvent) {
|
|
48
|
+
if (mobileNode && !mobileNode.contains(event.relatedTarget as Node | null)) {
|
|
49
|
+
closeMobile(false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
$effect(() => {
|
|
54
|
+
if (!sidebar.isMobile || !sidebar.openMobile) return;
|
|
55
|
+
|
|
56
|
+
mobileTrigger = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
57
|
+
shouldRestoreFocus = false;
|
|
58
|
+
|
|
59
|
+
requestAnimationFrame(() => {
|
|
60
|
+
const firstFocusable = mobileNode?.querySelector<HTMLElement>(
|
|
61
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
62
|
+
);
|
|
63
|
+
(firstFocusable ?? mobileNode)?.focus();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
$effect(() => {
|
|
68
|
+
if (sidebar.openMobile || !shouldRestoreFocus || !mobileTrigger) return;
|
|
69
|
+
mobileTrigger.focus();
|
|
70
|
+
mobileTrigger = null;
|
|
71
|
+
shouldRestoreFocus = false;
|
|
72
|
+
});
|
|
37
73
|
</script>
|
|
38
74
|
|
|
39
75
|
{#snippet sidebarContent()}
|
|
@@ -43,39 +79,48 @@
|
|
|
43
79
|
{/snippet}
|
|
44
80
|
|
|
45
81
|
{#if sidebar.isMobile}
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
<div
|
|
83
|
+
data-sidebar-backdrop
|
|
84
|
+
data-kumo-component="Sidebar"
|
|
85
|
+
data-kumo-part="mobile-backdrop"
|
|
86
|
+
class={cn(
|
|
87
|
+
sidebar.contained ? 'absolute inset-0 z-40 bg-kumo-recessed' : 'fixed inset-0 z-40 bg-kumo-recessed',
|
|
88
|
+
'transition-opacity duration-(--sidebar-animation-duration) ease-(--sidebar-easing) motion-reduce:transition-none',
|
|
89
|
+
sidebar.openMobile ? 'opacity-80' : 'pointer-events-none opacity-0'
|
|
90
|
+
)}
|
|
91
|
+
onclick={() => closeMobile(true)}
|
|
92
|
+
aria-hidden="true"
|
|
93
|
+
></div>
|
|
94
|
+
|
|
95
|
+
<nav
|
|
96
|
+
bind:this={mobileNode}
|
|
97
|
+
tabindex="-1"
|
|
98
|
+
aria-label="Navigation"
|
|
99
|
+
aria-hidden={sidebar.openMobile ? undefined : 'true'}
|
|
100
|
+
inert={sidebar.openMobile ? undefined : true}
|
|
101
|
+
data-state={sidebar.openMobile ? 'expanded' : 'collapsed'}
|
|
102
|
+
data-side={sidebar.side}
|
|
103
|
+
data-variant={sidebar.variant}
|
|
104
|
+
data-collapsible={sidebar.collapsible}
|
|
105
|
+
data-mobile="true"
|
|
106
|
+
data-sidebar="sidebar"
|
|
107
|
+
class={cn(
|
|
108
|
+
sidebar.contained
|
|
109
|
+
? 'group/sidebar absolute inset-y-0 z-50 flex w-(--sidebar-width) flex-col overflow-hidden'
|
|
110
|
+
: 'group/sidebar fixed inset-y-0 z-50 flex w-(--sidebar-width) flex-col overflow-hidden',
|
|
111
|
+
'border-r border-kumo-line bg-(--sidebar-bg) text-kumo-default transition-transform duration-(--sidebar-animation-duration) ease-(--sidebar-easing) motion-reduce:transition-none',
|
|
112
|
+
sidebar.side === 'left' && 'left-0',
|
|
113
|
+
sidebar.side === 'right' && 'right-0',
|
|
114
|
+
sidebar.side === 'left' && (sidebar.openMobile ? 'translate-x-0' : '-translate-x-full'),
|
|
115
|
+
sidebar.side === 'right' && (sidebar.openMobile ? 'translate-x-0' : 'translate-x-full'),
|
|
116
|
+
className
|
|
117
|
+
)}
|
|
118
|
+
onkeydown={handleMobileKeydown}
|
|
119
|
+
onfocusout={handleMobileFocusout}
|
|
120
|
+
{...rest}
|
|
121
|
+
>
|
|
122
|
+
{@render sidebarContent()}
|
|
123
|
+
</nav>
|
|
79
124
|
{:else}
|
|
80
125
|
<aside
|
|
81
126
|
data-state={sidebar.state}
|
|
@@ -2,8 +2,24 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { setContext } from 'svelte';
|
|
4
4
|
import { cn } from '../../utils/cn';
|
|
5
|
-
interface Props {
|
|
6
|
-
|
|
5
|
+
interface Props {
|
|
6
|
+
children?: Snippet;
|
|
7
|
+
class?: string;
|
|
8
|
+
defaultOpen?: boolean;
|
|
9
|
+
open?: boolean;
|
|
10
|
+
onOpenChange?: (open: boolean) => void;
|
|
11
|
+
autoScrollOnOpen?: boolean;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
let {
|
|
15
|
+
children,
|
|
16
|
+
class: className,
|
|
17
|
+
defaultOpen = false,
|
|
18
|
+
open = $bindable(defaultOpen),
|
|
19
|
+
onOpenChange,
|
|
20
|
+
autoScrollOnOpen = false,
|
|
21
|
+
...rest
|
|
22
|
+
}: Props = $props();
|
|
7
23
|
const contentId = `kumo-sidebar-collapsible-${Math.random().toString(36).slice(2)}`;
|
|
8
24
|
const context = {
|
|
9
25
|
get open() {
|
|
@@ -12,6 +28,9 @@
|
|
|
12
28
|
get contentId() {
|
|
13
29
|
return contentId;
|
|
14
30
|
},
|
|
31
|
+
get autoScrollOnOpen() {
|
|
32
|
+
return autoScrollOnOpen;
|
|
33
|
+
},
|
|
15
34
|
toggle() {
|
|
16
35
|
open = !open;
|
|
17
36
|
onOpenChange?.(open);
|
|
@@ -2,22 +2,41 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { getContext } from 'svelte';
|
|
4
4
|
import { cn } from '../../utils/cn';
|
|
5
|
-
|
|
5
|
+
import { getSidebarContext } from './context';
|
|
6
|
+
interface CollapsibleContext { get open(): boolean; get contentId(): string; get autoScrollOnOpen(): boolean; toggle(): void; }
|
|
6
7
|
interface Props { children?: Snippet; class?: string; [key: string]: unknown; }
|
|
7
8
|
let { children, class: className, ...rest }: Props = $props();
|
|
8
9
|
const collapsible = getContext<CollapsibleContext>('kumo-sidebar-collapsible');
|
|
10
|
+
const sidebar = getSidebarContext('Sidebar.CollapsibleContent');
|
|
11
|
+
let contentEl: HTMLDivElement | null = $state(null);
|
|
12
|
+
const isOpen = $derived(Boolean(collapsible?.open && sidebar.state !== 'collapsed'));
|
|
13
|
+
|
|
14
|
+
$effect(() => {
|
|
15
|
+
if (!isOpen || !collapsible?.autoScrollOnOpen || !contentEl) return;
|
|
16
|
+
|
|
17
|
+
const timeout = window.setTimeout(() => {
|
|
18
|
+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
19
|
+
contentEl?.scrollIntoView({
|
|
20
|
+
block: 'nearest',
|
|
21
|
+
behavior: prefersReducedMotion ? 'auto' : 'smooth'
|
|
22
|
+
});
|
|
23
|
+
}, sidebar.animationDuration);
|
|
24
|
+
|
|
25
|
+
return () => window.clearTimeout(timeout);
|
|
26
|
+
});
|
|
9
27
|
</script>
|
|
10
28
|
|
|
11
29
|
<div
|
|
30
|
+
bind:this={contentEl}
|
|
12
31
|
id={collapsible?.contentId}
|
|
13
32
|
data-sidebar="collapsible-content"
|
|
14
|
-
data-open={
|
|
33
|
+
data-open={isOpen ? '' : undefined}
|
|
15
34
|
role="region"
|
|
16
|
-
aria-hidden={
|
|
17
|
-
inert={
|
|
35
|
+
aria-hidden={isOpen ? undefined : 'true'}
|
|
36
|
+
inert={isOpen ? undefined : true}
|
|
18
37
|
class={cn(
|
|
19
38
|
'grid overflow-hidden transition-[grid-template-rows,opacity] duration-(--sidebar-animation-duration) motion-reduce:transition-none',
|
|
20
|
-
|
|
39
|
+
isOpen ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0',
|
|
21
40
|
className
|
|
22
41
|
)}
|
|
23
42
|
{...rest}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { getContext } from 'svelte';
|
|
4
4
|
import { cn } from '../../utils/cn';
|
|
5
|
-
interface CollapsibleContext { get open(): boolean; get contentId(): string; toggle(): void; }
|
|
5
|
+
interface CollapsibleContext { get open(): boolean; get contentId(): string; get autoScrollOnOpen(): boolean; toggle(): void; }
|
|
6
6
|
interface Props { children?: Snippet; class?: string; [key: string]: unknown; }
|
|
7
7
|
let { children, class: className, ...rest }: Props = $props();
|
|
8
8
|
const collapsible = getContext<CollapsibleContext>('kumo-sidebar-collapsible');
|
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
<div
|
|
9
9
|
data-sidebar="content"
|
|
10
10
|
class={cn(
|
|
11
|
-
'relative min-w-0 flex-1 overflow-y-auto overflow-x-hidden px-[11px] py-
|
|
11
|
+
'relative min-w-0 flex-1 overflow-y-auto overflow-x-hidden px-[11px] py-3 transition-[padding] duration-(--sidebar-animation-duration) group-not-data-[state=collapsed]/sidebar:px-3.5 group-data-[state=collapsed]/sidebar:overflow-x-hidden!',
|
|
12
|
+
'group-data-[mobile=true]/sidebar:px-3.5',
|
|
12
13
|
className
|
|
13
14
|
)}
|
|
14
15
|
role="presentation"
|
|
15
16
|
{...rest}
|
|
16
17
|
>
|
|
17
|
-
<div class="flex min-w-0 flex-col
|
|
18
|
+
<div class="flex min-w-0! flex-col">
|
|
18
19
|
{@render children?.()}
|
|
19
20
|
</div>
|
|
20
21
|
</div>
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
data-sidebar="group-label"
|
|
10
10
|
class={cn(
|
|
11
11
|
'grid grid-rows-[0fr] overflow-hidden border-b border-kumo-line my-3 transition-[grid-template-rows,margin,border-color] duration-(--sidebar-animation-duration) ease-(--sidebar-easing) [[data-sidebar=group]:first-child_&]:my-0 [[data-sidebar=group]:first-child_&]:border-transparent group-not-data-[state=collapsed]/sidebar:grid-rows-[1fr] group-not-data-[state=collapsed]/sidebar:my-0 group-not-data-[state=collapsed]/sidebar:border-transparent',
|
|
12
|
+
'group-data-[mobile=true]/sidebar:my-0 group-data-[mobile=true]/sidebar:grid-rows-[1fr] group-data-[mobile=true]/sidebar:border-transparent group-data-[mobile=true]/sidebar:transition-none',
|
|
12
13
|
className
|
|
13
14
|
)}
|
|
14
15
|
{...rest}
|
|
15
16
|
>
|
|
16
|
-
<div class="min-h-0">
|
|
17
|
-
<div class="truncate px-3 mt-
|
|
17
|
+
<div class="min-h-0 min-w-0">
|
|
18
|
+
<div class="truncate px-3 mt-6 mb-2 text-sm font-medium text-kumo-subtle [[data-sidebar=group]:first-child_&]:mt-2">
|
|
18
19
|
{@render children?.()}
|
|
19
20
|
</div>
|
|
20
21
|
</div>
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
let { children, class: className, ...rest }: Props = $props();
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
|
-
<div data-sidebar="header" class={cn('flex h-[58px] items-center gap-1 overflow-hidden border-b border-kumo-line px-3
|
|
8
|
+
<div data-sidebar="header" class={cn('flex h-[58px] shrink-0 items-center gap-1 overflow-hidden border-b border-kumo-line px-3', className)} {...rest}>
|
|
9
9
|
{@render children?.()}
|
|
10
10
|
</div>
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
active?: boolean;
|
|
12
12
|
size?: 'base' | 'sm';
|
|
13
13
|
href?: string;
|
|
14
|
+
target?: string;
|
|
14
15
|
tooltip?: string;
|
|
15
16
|
[key: string]: unknown;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
let { children, class: className, icon: Icon, active = false, size = 'base', href, tooltip, ...rest }: Props = $props();
|
|
19
|
+
let { children, class: className, icon: Icon, active = false, size = 'base', href, target, tooltip, ...rest }: Props = $props();
|
|
19
20
|
const menuItem = getSidebarMenuItemContext();
|
|
20
21
|
let sidebar = $state<ReturnType<typeof getSidebarContext> | undefined>();
|
|
21
22
|
|
|
@@ -27,14 +28,14 @@
|
|
|
27
28
|
|
|
28
29
|
const classes = $derived(
|
|
29
30
|
cn(
|
|
30
|
-
'group/menu-button relative flex w-full min-w-0 cursor-pointer items-center gap-2.5 rounded-lg text-kumo-default outline-none transition-[color,
|
|
31
|
+
'group/menu-button relative flex w-full min-w-0 cursor-pointer items-center gap-2.5 rounded-lg text-kumo-default outline-none transition-[color,box-shadow,outline] duration-(--sidebar-animation-duration)',
|
|
31
32
|
'before:absolute before:inset-x-0 before:-inset-y-px',
|
|
32
33
|
size === 'base' && 'min-h-8.5 px-3 py-0 text-sm font-medium',
|
|
33
34
|
size === 'sm' && 'min-h-7 px-2 py-0 text-sm',
|
|
34
|
-
!active && 'hover:bg-
|
|
35
|
-
active && 'bg-
|
|
36
|
-
'has-[[data-active]]:bg-transparent has-[[data-active]]:hover:bg-
|
|
37
|
-
'focus:outline-none focus-visible:bg-
|
|
35
|
+
!active && 'hover:bg-(--sidebar-active-bg)',
|
|
36
|
+
active && 'bg-(--sidebar-active-bg)',
|
|
37
|
+
'has-[[data-active]]:bg-transparent has-[[data-active]]:hover:bg-(--sidebar-active-bg)',
|
|
38
|
+
'focus:outline-none focus-visible:bg-(--sidebar-active-bg) focus-visible:text-kumo-strong',
|
|
38
39
|
className
|
|
39
40
|
)
|
|
40
41
|
);
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
<a
|
|
47
48
|
class={cn(classes, 'no-underline!')}
|
|
48
49
|
{href}
|
|
50
|
+
{target}
|
|
49
51
|
data-active={active || undefined}
|
|
50
52
|
data-sidebar="menu-button"
|
|
51
53
|
data-kumo-component="Sidebar"
|
|
@@ -55,7 +57,7 @@
|
|
|
55
57
|
{...rest}
|
|
56
58
|
>
|
|
57
59
|
<span class="flex min-w-0 flex-1 items-center gap-3 translate-x-[-3px] transition-transform duration-(--sidebar-animation-duration) group-not-data-[state=collapsed]/sidebar:translate-x-0">
|
|
58
|
-
{#if Icon}<Icon class={cn('shrink-0 opacity-
|
|
60
|
+
{#if Icon}<Icon class={cn('shrink-0 opacity-40', size === 'base' ? 'size-4' : 'size-3.5')} />{/if}
|
|
59
61
|
<span class="flex min-w-0 flex-1 items-center gap-2 overflow-hidden text-left">
|
|
60
62
|
{@render children?.()}
|
|
61
63
|
</span>
|
|
@@ -74,7 +76,7 @@
|
|
|
74
76
|
{...rest}
|
|
75
77
|
>
|
|
76
78
|
<span class="flex min-w-0 flex-1 items-center gap-3 translate-x-[-3px] transition-transform duration-(--sidebar-animation-duration) group-not-data-[state=collapsed]/sidebar:translate-x-0">
|
|
77
|
-
{#if Icon}<Icon class={cn('shrink-0 opacity-
|
|
79
|
+
{#if Icon}<Icon class={cn('shrink-0 opacity-40', size === 'base' ? 'size-4' : 'size-3.5')} />{/if}
|
|
78
80
|
<span class="flex min-w-0 flex-1 items-center gap-2 overflow-hidden text-left">
|
|
79
81
|
{@render children?.()}
|
|
80
82
|
</span>
|