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.
Files changed (69) hide show
  1. package/ai/component-registry.json +18 -2
  2. package/ai/component-registry.md +1 -1
  3. package/dist/components/chart/Chart.svelte +3 -2
  4. package/dist/components/flow/Connectors.svelte +1 -1
  5. package/dist/components/flow/connectors.d.ts +2 -2
  6. package/dist/components/flow/connectors.js +17 -9
  7. package/dist/components/flow/flow.test.js +32 -23
  8. package/dist/components/sidebar/Sidebar.svelte +84 -39
  9. package/dist/components/sidebar/Sidebar.svelte.d.ts +1 -0
  10. package/dist/components/sidebar/SidebarCollapsible.svelte +21 -2
  11. package/dist/components/sidebar/SidebarCollapsible.svelte.d.ts +1 -0
  12. package/dist/components/sidebar/SidebarCollapsibleContent.svelte +24 -5
  13. package/dist/components/sidebar/SidebarCollapsibleTrigger.svelte +1 -1
  14. package/dist/components/sidebar/SidebarContent.svelte +3 -2
  15. package/dist/components/sidebar/SidebarGroupLabel.svelte +3 -2
  16. package/dist/components/sidebar/SidebarHeader.svelte +1 -1
  17. package/dist/components/sidebar/SidebarMenuButton.svelte +10 -8
  18. package/dist/components/sidebar/SidebarMenuButton.svelte.d.ts +1 -0
  19. package/dist/components/sidebar/SidebarMenuChevron.svelte +9 -1
  20. package/dist/components/sidebar/SidebarMenuSub.svelte +1 -1
  21. package/dist/components/sidebar/SidebarMenuSubButton.svelte +11 -3
  22. package/dist/components/sidebar/SidebarProvider.svelte +18 -4
  23. package/dist/components/sidebar/SidebarProvider.svelte.d.ts +1 -0
  24. package/dist/components/sidebar/SidebarSlidingViews.svelte +1 -1
  25. package/dist/components/sidebar/context.d.ts +5 -4
  26. package/dist/components/sidebar/context.js +4 -4
  27. package/dist/components/tabs/Tabs.svelte +21 -4
  28. package/dist/docs/demo-snippets/SidebarAutoScrollDemo.svelte +5 -0
  29. package/dist/docs/demo-snippets/SidebarAutoScrollDemo.svelte.d.ts +18 -0
  30. package/dist/docs/demo-snippets/SidebarDemoShell.svelte +87 -7
  31. package/dist/docs/demo-snippets/SidebarDemoShell.svelte.d.ts +1 -1
  32. package/dist/docs/demo-snippets/SidebarMobileDemo.svelte +5 -0
  33. package/dist/docs/demo-snippets/SidebarMobileDemo.svelte.d.ts +18 -0
  34. package/dist/docs/props-data/Collapsible.d.ts +1 -1
  35. package/dist/docs/props-data/Collapsible.js +46 -39
  36. package/dist/docs/props-data/Sidebar.d.ts +1 -1
  37. package/dist/docs/props-data/Sidebar.js +30 -24
  38. package/dist/docs/props-data/Sidebar__Provider.d.ts +1 -1
  39. package/dist/docs/props-data/Sidebar__Provider.js +94 -16
  40. package/dist/styles.css +1 -1
  41. package/package.json +1 -1
  42. package/src/lib/components/chart/Chart.svelte +3 -2
  43. package/src/lib/components/flow/Connectors.svelte +1 -1
  44. package/src/lib/components/flow/connectors.ts +19 -15
  45. package/src/lib/components/flow/flow.test.ts +37 -23
  46. package/src/lib/components/sidebar/Sidebar.svelte +84 -39
  47. package/src/lib/components/sidebar/SidebarCollapsible.svelte +21 -2
  48. package/src/lib/components/sidebar/SidebarCollapsibleContent.svelte +24 -5
  49. package/src/lib/components/sidebar/SidebarCollapsibleTrigger.svelte +1 -1
  50. package/src/lib/components/sidebar/SidebarContent.svelte +3 -2
  51. package/src/lib/components/sidebar/SidebarGroupLabel.svelte +3 -2
  52. package/src/lib/components/sidebar/SidebarHeader.svelte +1 -1
  53. package/src/lib/components/sidebar/SidebarMenuButton.svelte +10 -8
  54. package/src/lib/components/sidebar/SidebarMenuChevron.svelte +9 -1
  55. package/src/lib/components/sidebar/SidebarMenuSub.svelte +1 -1
  56. package/src/lib/components/sidebar/SidebarMenuSubButton.svelte +11 -3
  57. package/src/lib/components/sidebar/SidebarProvider.svelte +18 -4
  58. package/src/lib/components/sidebar/SidebarSlidingViews.svelte +1 -1
  59. package/src/lib/components/sidebar/context.ts +23 -13
  60. package/src/lib/components/tabs/Tabs.svelte +21 -4
  61. package/src/lib/docs/demo-snippets/SidebarAutoScrollDemo.svelte +5 -0
  62. package/src/lib/docs/demo-snippets/SidebarDemoShell.svelte +87 -7
  63. package/src/lib/docs/demo-snippets/SidebarMobileDemo.svelte +5 -0
  64. package/src/lib/docs/props-data/Collapsible.ts +48 -40
  65. package/src/lib/docs/props-data/Sidebar.ts +32 -25
  66. package/src/lib/docs/props-data/Sidebar__Provider.ts +99 -17
  67. package/src/lib/styles.css +40 -25
  68. package/src/routes/components/sidebar/+page.md +36 -3
  69. package/src/routes/llms.txt/+server.ts +265 -0
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.8",
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,
@@ -1,6 +1,6 @@
1
1
  # Kumo Svelte Component Registry
2
2
 
3
- Version: 0.1.8
3
+ Version: 0.2.0
4
4
 
5
5
  ## Autocomplete
6
6
 
@@ -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 theme = effectiveDarkMode ? { darkMode: true, color: CHART_DARK_COLORS } : { color: CHART_LIGHT_COLORS };
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 './context';
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 = 'vertical' } = {}) {
3
- const cornerRadius = Math.min(maxCornerRadius, Math.abs(orientation === 'horizontal' ? (y2 - y1) / 2 : (x2 - x1) / 2));
4
- if (orientation === 'horizontal') {
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 [`M ${x1} ${y1}`, isBottom ? bottomCurveCommands : topCurveCommands, `L ${pathEndX} ${y2}`].join(' ');
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 [`M ${x1} ${y1}`, isBottom ? bottomCurveCommands : topCurveCommands, `L ${x2} ${pathEndY}`].join(' ');
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 '@testing-library/svelte';
3
- import { beforeEach, describe, expect, it, vi } from 'vitest';
4
- import FlowPanningTestHost from './FlowPanningTestHost.svelte';
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, 'getBoundingClientRect').mockReturnValue(nextRect);
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('ResizeObserver', TestResizeObserver);
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('Flow canvas panning', () => {
43
- it('pans with the wheel and clamps to measured bounds', async () => {
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('flow-wrapper');
46
- const contents = screen.getByTestId('flow-contents');
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('translate(-500px, -300px)');
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('translate(0px, 0px)');
54
+ expect(contents.style.transform).toBe("translate(0px, 0px)");
54
55
  });
55
- it('drag-pans the canvas while ignoring drags that start on nodes', async () => {
56
+ it("drag-pans the canvas while ignoring drags that start on nodes", async () => {
56
57
  render(FlowPanningTestHost);
57
- const wrapper = screen.getByTestId('flow-wrapper');
58
- const contents = screen.getByTestId('flow-contents');
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('start'), {
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('translate(0px, 0px)');
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('grabbing');
81
- expect(document.body.style.userSelect).toBe('none');
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('translate(-60px, -90px)');
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
- sidebar.isMobile && 'fixed inset-y-0 max-w-[85vw] shadow-xl md:static md:max-w-none md:shadow-none',
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
- <DialogPrimitive.Root open={sidebar.openMobile} onOpenChange={(open) => sidebar.setOpenMobile(open)}>
47
- <DialogPrimitive.Portal>
48
- <DialogPrimitive.Overlay
49
- data-sidebar-backdrop
50
- data-kumo-component="Sidebar"
51
- data-kumo-part="mobile-backdrop"
52
- class="fixed inset-0 z-40 bg-black/50 transition-opacity duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0"
53
- />
54
- <DialogPrimitive.Content
55
- data-sidebar-popup
56
- aria-label="Sidebar"
57
- class={cn(
58
- 'fixed inset-y-0 z-50 flex w-(--sidebar-width) flex-col bg-kumo-base p-0 duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0',
59
- sidebar.side === 'left' && 'left-0 data-ending-style:-translate-x-full data-starting-style:-translate-x-full',
60
- sidebar.side === 'right' && 'right-0 data-ending-style:translate-x-full data-starting-style:translate-x-full'
61
- )}
62
- style="transition-property: transform, opacity; transition-timing-function: var(--default-transition-timing-function);"
63
- >
64
- <div
65
- data-state="expanded"
66
- data-side={sidebar.side}
67
- data-variant={sidebar.variant}
68
- data-collapsible={sidebar.collapsible}
69
- data-mobile="true"
70
- data-sidebar="sidebar"
71
- class={cn('group/sidebar flex h-full w-full flex-col bg-kumo-base text-kumo-default', className)}
72
- {...rest}
73
- >
74
- {@render sidebarContent()}
75
- </div>
76
- </DialogPrimitive.Content>
77
- </DialogPrimitive.Portal>
78
- </DialogPrimitive.Root>
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,6 +2,7 @@ import type { Snippet } from 'svelte';
2
2
  interface Props {
3
3
  children?: Snippet;
4
4
  class?: string;
5
+ contentClass?: string;
5
6
  [key: string]: unknown;
6
7
  }
7
8
  declare const Sidebar: import("svelte").Component<Props, {}, "">;
@@ -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 { children?: Snippet; class?: string; defaultOpen?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; [key: string]: unknown; }
6
- let { children, class: className, defaultOpen = false, open = $bindable(defaultOpen), onOpenChange, ...rest }: Props = $props();
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);
@@ -5,6 +5,7 @@ interface Props {
5
5
  defaultOpen?: boolean;
6
6
  open?: boolean;
7
7
  onOpenChange?: (open: boolean) => void;
8
+ autoScrollOnOpen?: boolean;
8
9
  [key: string]: unknown;
9
10
  }
10
11
  declare const SidebarCollapsible: import("svelte").Component<Props, {}, "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
- interface CollapsibleContext { get open(): boolean; get contentId(): string; toggle(): void; }
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={collapsible?.open ? '' : undefined}
33
+ data-open={isOpen ? '' : undefined}
15
34
  role="region"
16
- aria-hidden={collapsible?.open ? undefined : 'true'}
17
- inert={collapsible?.open ? undefined : true}
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
- collapsible?.open ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0',
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-[13px] transition-[padding] duration-(--sidebar-animation-duration) group-not-data-[state=collapsed]/sidebar:px-3.5 group-data-[state=collapsed]/sidebar:overflow-x-hidden!',
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 gap-2 group-data-[state=collapsed]/sidebar:min-w-0!">
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-4 mb-2 text-sm font-medium text-kumo-subtle [[data-sidebar=group]:first-child_&]:mt-2">
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.5', className)} {...rest}>
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,background-color,box-shadow,outline] duration-(--sidebar-animation-duration)',
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-kumo-tint',
35
- active && 'bg-kumo-tint',
36
- 'has-[[data-active]]:bg-transparent has-[[data-active]]:hover:bg-kumo-tint',
37
- 'focus:outline-none focus-visible:bg-kumo-tint focus-visible:text-kumo-strong',
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-50', size === 'base' ? 'size-4' : 'size-3.5')} />{/if}
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-50', size === 'base' ? 'size-4' : 'size-3.5')} />{/if}
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>
@@ -6,6 +6,7 @@ interface Props {
6
6
  active?: boolean;
7
7
  size?: 'base' | 'sm';
8
8
  href?: string;
9
+ target?: string;
9
10
  tooltip?: string;
10
11
  [key: string]: unknown;
11
12
  }