@stll/workspace-ui 0.7.2 → 0.8.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.
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
6
6
  import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
7
7
  import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
8
8
  import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
9
+ import { TOOLBAR_ROW_HEIGHT } from "@stll/ui/inspector";
9
10
  import { Tabs, TabsList, TabsTab } from "@stll/ui/tabs";
10
11
  //#region src/view-switcher.tsx
11
12
  const VIEW_DRAG_TYPE = "@stll/workspace-ui/view-switcher/drag-type";
@@ -43,7 +44,7 @@ const WorkspaceViewSwitcher = ({ activeViewId, ariaLabel, direction, reorder, vi
43
44
  if (reordered) reorder?.onReorder(reordered);
44
45
  };
45
46
  return /* @__PURE__ */ jsxs("div", {
46
- className: "flex min-w-0 flex-1 items-center gap-1 px-2",
47
+ className: cn("flex min-w-0 flex-1 items-center gap-1 px-2", TOOLBAR_ROW_HEIGHT),
47
48
  dir: direction,
48
49
  children: [/* @__PURE__ */ jsx("div", {
49
50
  className: "min-w-0 flex-1",
@@ -10,9 +10,35 @@ type WorkspaceFrameNavigationItem = {
10
10
  disabled?: boolean;
11
11
  onActivate: () => void;
12
12
  };
13
+ /**
14
+ * Renders the described navigation through the sidebar shell from
15
+ * `@stll/ui/sidebar` instead of the fixed application rail: the same
16
+ * collapsible sidebar Stella's own app mounts, so a host gets the header
17
+ * toggle, the labelled menu while expanded, and the tooltips while collapsed
18
+ * without composing the shell itself.
19
+ */
20
+ type WorkspaceFrameSidebarNavigation = {
21
+ /** Shown at the start of the header row while the sidebar is expanded,
22
+ * typically a wordmark. The collapse toggle sits at its end. */
23
+ brand?: ReactNode;
24
+ /** Accessible names for the header toggle in each state. */
25
+ toggleLabel: {
26
+ collapse: string;
27
+ expand: string;
28
+ };
29
+ /** Controlled open state, for a host that persists it. */
30
+ open?: boolean;
31
+ onOpenChange?: (open: boolean) => void;
32
+ defaultOpen?: boolean;
33
+ /** Keeps the sidebar collapsed regardless of the requested state, e.g.
34
+ * while an inspector pane needs the width. */
35
+ forceCollapsed?: boolean;
36
+ };
13
37
  type WorkspaceFrameNavigation = {
14
38
  label: string;
15
39
  items: readonly WorkspaceFrameNavigationItem[];
40
+ /** Rail-only: the application rail's header slot. The sidebar
41
+ * presentation owns its header (brand and toggle) and ignores this. */
16
42
  header?: ReactNode;
17
43
  footer?: ReactNode;
18
44
  compact: {
@@ -22,6 +48,7 @@ type WorkspaceFrameNavigation = {
22
48
  onOpenChange: (open: boolean) => void;
23
49
  trigger: ReactElement | null;
24
50
  };
51
+ sidebar?: WorkspaceFrameSidebarNavigation;
25
52
  };
26
53
  type WorkspaceFrameInspector = {
27
54
  pane: ReactNode;
@@ -67,7 +94,17 @@ type WorkspaceFrameTopBar = {
67
94
  * Stella's complete workspace frame. Hosts provide route descriptors and
68
95
  * actions; this component owns the shell, application rail, end rail, and
69
96
  * desktop inspector footprint.
97
+ *
98
+ * The two compositions mount different navigation primitives. `described`
99
+ * renders `navigation.items` through the narrower `ApplicationRail` from
100
+ * `@stll/ui/application-rail` — a fixed-width, always-collapsed rail with no
101
+ * expand/collapse state of its own — or, with `navigation.sidebar`, through
102
+ * the collapsible sidebar shell from `@stll/ui/sidebar`. `host-responsive`
103
+ * passes `navigation` straight through to `WorkspaceShell` and takes no
104
+ * position on what fills it; a host that composes its own sidebar content
105
+ * mounts the shell from `@stll/ui/sidebar` there, as this app's `AppSidebar`
106
+ * does.
70
107
  */
71
108
  declare const WorkspaceFrame: (props: WorkspaceFrameProps) => import("react").JSX.Element;
72
109
  //#endregion
73
- export { WorkspaceFrame, WorkspaceFrameEndRail, WorkspaceFrameInspector, WorkspaceFrameNavigation, WorkspaceFrameNavigationItem, WorkspaceFrameProps, WorkspaceFrameTopBar };
110
+ export { WorkspaceFrame, WorkspaceFrameEndRail, WorkspaceFrameInspector, WorkspaceFrameNavigation, WorkspaceFrameNavigationItem, WorkspaceFrameProps, WorkspaceFrameSidebarNavigation, WorkspaceFrameTopBar };
@@ -1,10 +1,15 @@
1
1
  "use client";
2
2
  import { resolveWorkspaceInspectorPresentation } from "./workspace-frame.logic.js";
3
+ import { PanelLeftIcon } from "lucide-react";
4
+ import { Button } from "@stll/ui/button";
5
+ import { Tooltip, TooltipPopup, TooltipTrigger } from "@stll/ui/tooltip";
3
6
  import { jsx, jsxs } from "react/jsx-runtime";
4
7
  import { cn } from "@stll/ui/utils";
8
+ import { InspectorDock, SIDE_RAIL_ICON_BUTTON_SIZE, TOOLBAR_ROW_HEIGHT } from "@stll/ui/inspector";
5
9
  import { ApplicationRail, ApplicationRailButton, ApplicationRailContent, ApplicationRailFooter, ApplicationRailHeader, ApplicationRailMenu } from "@stll/ui/application-rail";
6
- import { InspectorDock, TOOLBAR_ROW_HEIGHT } from "@stll/ui/inspector";
10
+ import { DirectionalIcon } from "@stll/ui/directional-icon";
7
11
  import { Sheet, SheetPopup, SheetTitle } from "@stll/ui/sheet";
12
+ import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, useSidebar } from "@stll/ui/sidebar";
8
13
  import { useIsMobile } from "@stll/ui/use-mobile";
9
14
  import { WorkspaceEndRail, WorkspaceShell } from "@stll/ui/workspace-shell";
10
15
  //#region src/workspace-frame.tsx
@@ -12,6 +17,16 @@ import { WorkspaceEndRail, WorkspaceShell } from "@stll/ui/workspace-shell";
12
17
  * Stella's complete workspace frame. Hosts provide route descriptors and
13
18
  * actions; this component owns the shell, application rail, end rail, and
14
19
  * desktop inspector footprint.
20
+ *
21
+ * The two compositions mount different navigation primitives. `described`
22
+ * renders `navigation.items` through the narrower `ApplicationRail` from
23
+ * `@stll/ui/application-rail` — a fixed-width, always-collapsed rail with no
24
+ * expand/collapse state of its own — or, with `navigation.sidebar`, through
25
+ * the collapsible sidebar shell from `@stll/ui/sidebar`. `host-responsive`
26
+ * passes `navigation` straight through to `WorkspaceShell` and takes no
27
+ * position on what fills it; a host that composes its own sidebar content
28
+ * mounts the shell from `@stll/ui/sidebar` there, as this app's `AppSidebar`
29
+ * does.
15
30
  */
16
31
  const WorkspaceFrame = (props) => {
17
32
  if (props.composition === "host-responsive") return /* @__PURE__ */ jsx(WorkspaceShell, {
@@ -28,22 +43,9 @@ const DescribedWorkspaceFrame = ({ children, endRail, inspector, navigation, top
28
43
  hasMobilePresentation: inspector?.mobile !== void 0,
29
44
  isCompact
30
45
  });
31
- const desktopNavigation = /* @__PURE__ */ jsxs(ApplicationRail, {
32
- "aria-label": navigation.label,
33
- children: [
34
- /* @__PURE__ */ jsx(ApplicationRailHeader, { children: navigation.header }),
35
- /* @__PURE__ */ jsx(ApplicationRailContent, { children: /* @__PURE__ */ jsx(ApplicationRailMenu, { children: navigation.items.map((item) => /* @__PURE__ */ jsx(ApplicationRailButton, {
36
- "aria-current": item.active ? "page" : void 0,
37
- "aria-label": item.label,
38
- "data-active": item.active || void 0,
39
- disabled: item.disabled,
40
- title: item.label,
41
- className: "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
42
- onClick: item.onActivate,
43
- children: item.icon
44
- }, item.id)) }) }),
45
- /* @__PURE__ */ jsx(ApplicationRailFooter, { children: navigation.footer })
46
- ]
46
+ const desktopNavigation = navigation.sidebar === void 0 ? /* @__PURE__ */ jsx(DescribedApplicationRail, { navigation }) : /* @__PURE__ */ jsx(DescribedSidebar, {
47
+ navigation,
48
+ sidebar: navigation.sidebar
47
49
  });
48
50
  const endDock = inspector && inspectorPresentation === "desktop" ? /* @__PURE__ */ jsx(InspectorDock, {
49
51
  rail: /* @__PURE__ */ jsx(WorkspaceEndRail, { ...endRail }),
@@ -54,7 +56,7 @@ const DescribedWorkspaceFrame = ({ children, endRail, inspector, navigation, top
54
56
  onResetWidth: inspector.onResetWidth,
55
57
  children: inspector.pane
56
58
  }) : /* @__PURE__ */ jsx(WorkspaceEndRail, { ...endRail });
57
- const frame = /* @__PURE__ */ jsx(WorkspaceShell, {
59
+ const shell = /* @__PURE__ */ jsx(WorkspaceShell, {
58
60
  endDock,
59
61
  navigation: {
60
62
  compact: navigation.compact,
@@ -81,6 +83,10 @@ const DescribedWorkspaceFrame = ({ children, endRail, inspector, navigation, top
81
83
  }),
82
84
  children
83
85
  });
86
+ const frame = navigation.sidebar === void 0 ? shell : /* @__PURE__ */ jsx(SidebarProvider, {
87
+ ...resolveSidebarProviderProps(navigation.sidebar),
88
+ children: shell
89
+ });
84
90
  if (inspector?.mobile === void 0) return frame;
85
91
  return /* @__PURE__ */ jsxs(Sheet, {
86
92
  open: inspectorPresentation === "mobile" && inspector.mobile.open,
@@ -98,5 +104,92 @@ const DescribedWorkspaceFrame = ({ children, endRail, inspector, navigation, top
98
104
  }) : null]
99
105
  });
100
106
  };
107
+ /**
108
+ * Only the sidebar options the host actually set reach the provider: an
109
+ * explicit `undefined` would otherwise turn a controlled `open` into an
110
+ * uncontrolled one, or clear the provider's own default.
111
+ */
112
+ const resolveSidebarProviderProps = ({ defaultOpen, forceCollapsed, open, onOpenChange }) => ({
113
+ ...defaultOpen === void 0 ? {} : { defaultOpen },
114
+ ...forceCollapsed === void 0 ? {} : { forceCollapsed },
115
+ ...open === void 0 ? {} : { open },
116
+ ...onOpenChange === void 0 ? {} : { onOpenChange }
117
+ });
118
+ const DescribedApplicationRail = ({ navigation }) => /* @__PURE__ */ jsxs(ApplicationRail, {
119
+ "aria-label": navigation.label,
120
+ children: [
121
+ /* @__PURE__ */ jsx(ApplicationRailHeader, { children: navigation.header }),
122
+ /* @__PURE__ */ jsx(ApplicationRailContent, { children: /* @__PURE__ */ jsx(ApplicationRailMenu, { children: navigation.items.map((item) => /* @__PURE__ */ jsx(ApplicationRailButton, {
123
+ "aria-current": item.active ? "page" : void 0,
124
+ "aria-label": item.label,
125
+ "data-active": item.active || void 0,
126
+ disabled: item.disabled,
127
+ title: item.label,
128
+ className: "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
129
+ onClick: item.onActivate,
130
+ children: item.icon
131
+ }, item.id)) }) }),
132
+ /* @__PURE__ */ jsx(ApplicationRailFooter, { children: navigation.footer })
133
+ ]
134
+ });
135
+ /**
136
+ * The described items as Stella's collapsible sidebar. The header is the
137
+ * same row Stella's app shows: brand at the start while expanded, the
138
+ * collapse toggle at the end, at toolbar-row height so it lines up with the
139
+ * top bar beside it. Each item is a sidebar menu button, which shows its
140
+ * label while expanded and a tooltip while collapsed.
141
+ */
142
+ const DescribedSidebar = ({ navigation, sidebar }) => {
143
+ const { isMobile, state, toggleSidebar } = useSidebar();
144
+ const collapsed = state === "collapsed" && !isMobile;
145
+ const toggleLabel = collapsed ? sidebar.toggleLabel.expand : sidebar.toggleLabel.collapse;
146
+ return /* @__PURE__ */ jsxs(Sidebar, {
147
+ "aria-label": navigation.label,
148
+ collapsible: "icon",
149
+ mobileTitle: navigation.label,
150
+ children: [
151
+ /* @__PURE__ */ jsx(SidebarHeader, {
152
+ className: cn("border-b p-0", TOOLBAR_ROW_HEIGHT),
153
+ children: /* @__PURE__ */ jsxs("div", {
154
+ className: cn("flex h-full items-center", collapsed ? "justify-center" : "justify-between ps-3 pe-2"),
155
+ children: [collapsed ? null : sidebar.brand, /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, { render: /* @__PURE__ */ jsx(Button, {
156
+ "aria-label": toggleLabel,
157
+ className: cn("text-muted-foreground", SIDE_RAIL_ICON_BUTTON_SIZE),
158
+ "data-slot": "workspace-frame-sidebar-toggle",
159
+ size: "icon",
160
+ variant: "ghost",
161
+ onClick: toggleSidebar,
162
+ children: /* @__PURE__ */ jsx(DirectionalIcon, {
163
+ className: "size-4",
164
+ icon: PanelLeftIcon
165
+ })
166
+ }) }), /* @__PURE__ */ jsx(TooltipPopup, {
167
+ side: "right",
168
+ children: toggleLabel
169
+ })] })]
170
+ })
171
+ }),
172
+ /* @__PURE__ */ jsx(SidebarContent, { children: /* @__PURE__ */ jsx("nav", {
173
+ "aria-label": navigation.label,
174
+ children: /* @__PURE__ */ jsx(SidebarGroup, {
175
+ className: "group-data-[collapsible=icon]:px-0.5",
176
+ children: /* @__PURE__ */ jsx(SidebarMenu, { children: navigation.items.map((item) => /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(SidebarMenuButton, {
177
+ "aria-current": item.active ? "page" : void 0,
178
+ disabled: item.disabled,
179
+ isActive: item.active ?? false,
180
+ size: "rail",
181
+ tooltip: item.label,
182
+ onClick: item.onActivate,
183
+ children: [item.icon, /* @__PURE__ */ jsx("span", {
184
+ className: "truncate",
185
+ children: item.label
186
+ })]
187
+ }) }, item.id)) })
188
+ })
189
+ }) }),
190
+ /* @__PURE__ */ jsx(SidebarFooter, { children: navigation.footer })
191
+ ]
192
+ });
193
+ };
101
194
  //#endregion
102
195
  export { WorkspaceFrame };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/workspace-ui",
3
- "version": "0.7.2",
3
+ "version": "0.8.1",
4
4
  "description": "Reusable workspace presentation components and view helpers for Stella.",
5
5
  "keywords": [
6
6
  "calculations",
@@ -119,7 +119,7 @@
119
119
  "@stll/calculations": "^0.1.0",
120
120
  "@stll/conditions": "^0.3.0",
121
121
  "@stll/money": "^0.1.0",
122
- "@stll/ui": "^0.19.0",
122
+ "@stll/ui": "^0.21.0",
123
123
  "better-result": "3.0.0",
124
124
  "lucide-react": "1.30.0",
125
125
  "use-intl": "4.13.5"
@@ -130,7 +130,7 @@
130
130
  "@base-ui/react": "1.7.0",
131
131
  "@stll/property-testing": "0.0.0",
132
132
  "@stll/typescript-config": "0.0.0",
133
- "@tanstack/react-table": "9.2.3",
133
+ "@tanstack/react-table": "9.2.4",
134
134
  "@types/react": "^19.2.17",
135
135
  "@types/react-dom": "^19.2.3",
136
136
  "bun-types": "1.4.0",