@stll/workspace-ui 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @stll/workspace-ui
2
2
 
3
+ Reusable workspace presentation components and view helpers for Stella.
4
+
5
+ ## Saved views
6
+
7
+ `@stll/workspace-ui/view-switcher` exports a controlled saved-view strip with
8
+ keyboard selection, horizontal overflow, inline editing slots, and bidi-aware
9
+ Pragmatic Drag and Drop reordering. Hosts retain ownership of persistence,
10
+ permissions, translations, menus, and layout rendering.
11
+
3
12
  Reusable React presentation modules for Stella workspaces: field values,
4
13
  property icons, sorting chips, condition builders, table loading rows, and
5
14
  column calculations.
@@ -16,6 +25,7 @@ Use the explicit subpaths for focused modules:
16
25
  import { ConditionBuilder } from "@stll/workspace-ui/conditions";
17
26
  import { FieldValue } from "@stll/workspace-ui/field-value";
18
27
  import { SortChips } from "@stll/workspace-ui/sorts";
28
+ import { WorkspaceViewSwitcher } from "@stll/workspace-ui/view-switcher";
19
29
  ```
20
30
 
21
31
  The package also exposes a root entry for code that uses several workspace
@@ -31,9 +41,10 @@ bun run typecheck
31
41
  bun run lint
32
42
  ```
33
43
 
34
- Peer dependencies: `react`, `react-dom`, `@base-ui/react`, TanStack React Table,
35
- and Tailwind CSS v4. The package's components use Tailwind utility classes; the
36
- host owns the Tailwind entry point and token configuration.
44
+ Peer dependencies: `react`, `react-dom`, `@base-ui/react`, Pragmatic Drag and
45
+ Drop v3 with its hitbox package, TanStack React Table, and Tailwind CSS v4. The
46
+ package's components use Tailwind utility classes; the host owns the Tailwind
47
+ entry point and token configuration.
37
48
 
38
49
  Include both published component packages in that entry point so Tailwind scans
39
50
  their shipped class names:
package/dist/index.d.ts CHANGED
@@ -10,4 +10,6 @@ import { getClipFieldValueLabel } from "./field-value-logic.js";
10
10
  import { FieldValue, IntFieldValue, MoneyFieldValue, PersonFieldValue } from "./field-value.js";
11
11
  import { SortChips, SortChipsLabels, SortChipsProps, SortDescriptor, SortableProperty, sortDirectionHint } from "./sorts.js";
12
12
  import { TableSkeletonRows } from "./table-skeleton-rows.js";
13
- export { type ApplyCalculationSelectionParams, CONDITION_OPERATORS, type CalculationFormatters, CalculationKindPicker, type CalculationKindPickerProps, type CalculationLabels, CalculationPicker, type CalculationPickerProps, type CalculationProperty, type CalculationResult, type CalculationSelection, CalculationSummary, type CalculationSummaryProps, type ColorVariants, ColumnCalculation, ConditionBuilder, type ConditionBuilderLabels, type ConditionCapabilities, type ConditionOperator, FIELD_CONTENT_TYPES, type FieldContent, type FieldOption, type FieldOptionChoice, FieldValue, type FieldValueType, type FormatCalculationParams, type FormatMoneyCentsParams, type FormattedCalculation, type FormulaCellRenderCtx, type GenericProperty, IntFieldValue, MoneyFieldValue, type OptionColor, PersonFieldValue, PropertyIcon, type PropertyIconType, SortChips, type SortChipsLabels, type SortChipsProps, type SortDescriptor, type SortableProperty, TableSkeletonRows, type UseCalculationParams, type ValueEditorKind, type ValueEditorRenderCtx, type WorkspaceCalculationLabels, type WorkspaceFieldContent, appendChild, applyCalculationSelection, asGroup, buildLeaf, currencyMinorUnitDigits, emptyColor, fieldForNode, formatCalculationResult, formatMoneyCents, getClipFieldValueLabel, isConditionOperator, isMultiValue, leafFromField, leafOperand, leafOperator, leafValueList, leafValueString, operandsEqual, operatorsFor, optionColors, removeChild, replaceChild, resolveOptionColor, sortDirectionHint, useCalculation, valueEditorFor };
13
+ import { WorkspaceViewDirection, WorkspaceViewDropPosition, reorderWorkspaceViewIds, toWorkspaceViewDropPosition } from "./view-switcher.logic.js";
14
+ import { WorkspaceViewSwitcher, WorkspaceViewSwitcherEditing, WorkspaceViewSwitcherItem, WorkspaceViewSwitcherProps, WorkspaceViewSwitcherReorder } from "./view-switcher.js";
15
+ export { type ApplyCalculationSelectionParams, CONDITION_OPERATORS, type CalculationFormatters, CalculationKindPicker, type CalculationKindPickerProps, type CalculationLabels, CalculationPicker, type CalculationPickerProps, type CalculationProperty, type CalculationResult, type CalculationSelection, CalculationSummary, type CalculationSummaryProps, type ColorVariants, ColumnCalculation, ConditionBuilder, type ConditionBuilderLabels, type ConditionCapabilities, type ConditionOperator, FIELD_CONTENT_TYPES, type FieldContent, type FieldOption, type FieldOptionChoice, FieldValue, type FieldValueType, type FormatCalculationParams, type FormatMoneyCentsParams, type FormattedCalculation, type FormulaCellRenderCtx, type GenericProperty, IntFieldValue, MoneyFieldValue, type OptionColor, PersonFieldValue, PropertyIcon, type PropertyIconType, SortChips, type SortChipsLabels, type SortChipsProps, type SortDescriptor, type SortableProperty, TableSkeletonRows, type UseCalculationParams, type ValueEditorKind, type ValueEditorRenderCtx, type WorkspaceCalculationLabels, type WorkspaceFieldContent, type WorkspaceViewDirection, type WorkspaceViewDropPosition, WorkspaceViewSwitcher, type WorkspaceViewSwitcherEditing, type WorkspaceViewSwitcherItem, type WorkspaceViewSwitcherProps, type WorkspaceViewSwitcherReorder, appendChild, applyCalculationSelection, asGroup, buildLeaf, currencyMinorUnitDigits, emptyColor, fieldForNode, formatCalculationResult, formatMoneyCents, getClipFieldValueLabel, isConditionOperator, isMultiValue, leafFromField, leafOperand, leafOperator, leafValueList, leafValueString, operandsEqual, operatorsFor, optionColors, removeChild, reorderWorkspaceViewIds, replaceChild, resolveOptionColor, sortDirectionHint, toWorkspaceViewDropPosition, useCalculation, valueEditorFor };
package/dist/index.js CHANGED
@@ -10,4 +10,6 @@ import { PropertyIcon } from "./property-icon.js";
10
10
  import { SortChips, sortDirectionHint } from "./sorts.js";
11
11
  import { TableSkeletonRows } from "./table-skeleton-rows.js";
12
12
  import { FIELD_CONTENT_TYPES } from "./types.js";
13
- export { CONDITION_OPERATORS, CalculationKindPicker, CalculationPicker, CalculationSummary, ColumnCalculation, ConditionBuilder, FIELD_CONTENT_TYPES, FieldValue, IntFieldValue, MoneyFieldValue, PersonFieldValue, PropertyIcon, SortChips, TableSkeletonRows, appendChild, applyCalculationSelection, asGroup, buildLeaf, currencyMinorUnitDigits, emptyColor, fieldForNode, formatCalculationResult, formatMoneyCents, getClipFieldValueLabel, isConditionOperator, isMultiValue, leafFromField, leafOperand, leafOperator, leafValueList, leafValueString, operandsEqual, operatorsFor, optionColors, removeChild, replaceChild, resolveOptionColor, sortDirectionHint, useCalculation, valueEditorFor };
13
+ import { reorderWorkspaceViewIds, toWorkspaceViewDropPosition } from "./view-switcher.logic.js";
14
+ import { WorkspaceViewSwitcher } from "./view-switcher.js";
15
+ export { CONDITION_OPERATORS, CalculationKindPicker, CalculationPicker, CalculationSummary, ColumnCalculation, ConditionBuilder, FIELD_CONTENT_TYPES, FieldValue, IntFieldValue, MoneyFieldValue, PersonFieldValue, PropertyIcon, SortChips, TableSkeletonRows, WorkspaceViewSwitcher, appendChild, applyCalculationSelection, asGroup, buildLeaf, currencyMinorUnitDigits, emptyColor, fieldForNode, formatCalculationResult, formatMoneyCents, getClipFieldValueLabel, isConditionOperator, isMultiValue, leafFromField, leafOperand, leafOperator, leafValueList, leafValueString, operandsEqual, operatorsFor, optionColors, removeChild, reorderWorkspaceViewIds, replaceChild, resolveOptionColor, sortDirectionHint, toWorkspaceViewDropPosition, useCalculation, valueEditorFor };
@@ -0,0 +1,33 @@
1
+ import { WorkspaceViewDirection } from "./view-switcher.logic.js";
2
+ //#region src/view-switcher.d.ts
3
+ type WorkspaceViewSwitcherItem = {
4
+ id: string;
5
+ name: string;
6
+ };
7
+ type WorkspaceViewSwitcherEditing<View> = {
8
+ viewId: string;
9
+ renderLabel: (view: View) => React.ReactNode;
10
+ };
11
+ type WorkspaceViewSwitcherReorder<View> = {
12
+ getDragData?: (view: View) => Record<string | symbol, unknown>;
13
+ getDropData?: (view: View) => Record<string | symbol, unknown>;
14
+ isBlocked?: boolean;
15
+ onReorder: (viewIds: string[]) => void;
16
+ };
17
+ type WorkspaceViewSwitcherProps<View extends WorkspaceViewSwitcherItem> = {
18
+ activeViewId: string;
19
+ ariaLabel: string;
20
+ direction: WorkspaceViewDirection;
21
+ reorder: WorkspaceViewSwitcherReorder<View> | null;
22
+ views: readonly View[];
23
+ addControl?: React.ReactNode;
24
+ editing?: WorkspaceViewSwitcherEditing<View> | null;
25
+ onViewChange: (viewId: string) => void;
26
+ onViewContextMenu?: (view: View, event: React.MouseEvent<HTMLElement>) => void;
27
+ onViewDoubleClick?: (view: View, event: React.MouseEvent<HTMLElement>) => void;
28
+ renderActions?: (view: View) => React.ReactNode;
29
+ renderIcon: (view: View) => React.ReactNode;
30
+ };
31
+ declare const WorkspaceViewSwitcher: <View extends WorkspaceViewSwitcherItem>({ activeViewId, ariaLabel, direction, reorder, views, addControl, editing, onViewChange, onViewContextMenu, onViewDoubleClick, renderActions, renderIcon }: WorkspaceViewSwitcherProps<View>) => import("react").JSX.Element;
32
+ //#endregion
33
+ export { WorkspaceViewSwitcher, WorkspaceViewSwitcherEditing, WorkspaceViewSwitcherItem, WorkspaceViewSwitcherProps, WorkspaceViewSwitcherReorder };
@@ -0,0 +1,159 @@
1
+ "use client";
2
+ import { reorderWorkspaceViewIds, toWorkspaceViewDropPosition } from "./view-switcher.logic.js";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { cn } from "@stll/ui/utils";
5
+ import { useCallback, useEffect, useRef, useState } from "react";
6
+ import { attachClosestEdge, extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
7
+ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
8
+ import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
9
+ import { Tabs, TabsList, TabsTab } from "@stll/ui/tabs";
10
+ //#region src/view-switcher.tsx
11
+ const VIEW_DRAG_TYPE = "@stll/workspace-ui/view-switcher/drag-type";
12
+ const VIEW_DRAG_ID = "@stll/workspace-ui/view-switcher/view-id";
13
+ const VIEW_DRAG_INSTANCE = "@stll/workspace-ui/view-switcher/instance";
14
+ const useLatestCallback = (callback) => {
15
+ const latest = useRef(callback);
16
+ useEffect(() => {
17
+ latest.current = callback;
18
+ }, [callback]);
19
+ return useCallback((...args) => latest.current(...args), []);
20
+ };
21
+ const WorkspaceViewSwitcher = ({ activeViewId, ariaLabel, direction, reorder, views, addControl, editing, onViewChange, onViewContextMenu, onViewDoubleClick, renderActions, renderIcon }) => {
22
+ const canReorder = reorder !== null;
23
+ const [instanceId] = useState(Symbol);
24
+ const [stripContainer, setStripContainer] = useState(null);
25
+ useEffect(() => {
26
+ if (!stripContainer || !canReorder) return;
27
+ return dropTargetForElements({
28
+ element: stripContainer,
29
+ canDrop: ({ source }) => source.data[VIEW_DRAG_TYPE] === true && source.data[VIEW_DRAG_INSTANCE] === instanceId
30
+ });
31
+ }, [
32
+ canReorder,
33
+ instanceId,
34
+ stripContainer
35
+ ]);
36
+ const handleReorder = (draggedId, targetId, position) => {
37
+ const reordered = reorderWorkspaceViewIds({
38
+ ids: views.map((view) => view.id),
39
+ draggedId,
40
+ targetId,
41
+ position
42
+ });
43
+ if (reordered) reorder?.onReorder(reordered);
44
+ };
45
+ return /* @__PURE__ */ jsxs("div", {
46
+ className: "flex min-w-0 flex-1 items-center gap-1 px-2",
47
+ dir: direction,
48
+ children: [/* @__PURE__ */ jsx("div", {
49
+ className: "min-w-0 flex-1",
50
+ ref: setStripContainer,
51
+ children: /* @__PURE__ */ jsx(Tabs, {
52
+ onValueChange: (value) => {
53
+ if (typeof value === "string") onViewChange(value);
54
+ },
55
+ value: activeViewId,
56
+ children: /* @__PURE__ */ jsx(TabsList, {
57
+ "aria-label": ariaLabel,
58
+ variant: "underline",
59
+ children: views.map((view) => {
60
+ if (editing?.viewId === view.id) return /* @__PURE__ */ jsxs(TabsTab, {
61
+ nativeButton: false,
62
+ render: /* @__PURE__ */ jsx("div", {}),
63
+ value: view.id,
64
+ children: [renderIcon(view), editing.renderLabel(view)]
65
+ }, view.id);
66
+ return /* @__PURE__ */ jsx(WorkspaceViewTab, {
67
+ actions: renderActions?.(view),
68
+ canReorder,
69
+ direction,
70
+ getDragData: reorder?.getDragData,
71
+ getDropData: reorder?.getDropData,
72
+ isDragBlocked: reorder?.isBlocked ?? false,
73
+ instanceId,
74
+ onContextMenu: onViewContextMenu,
75
+ onDoubleClick: onViewDoubleClick,
76
+ onReorder: handleReorder,
77
+ renderIcon,
78
+ view
79
+ }, view.id);
80
+ })
81
+ })
82
+ })
83
+ }), addControl]
84
+ });
85
+ };
86
+ const WorkspaceViewTab = ({ actions, canReorder, direction, getDragData, getDropData, isDragBlocked, instanceId, onContextMenu, onDoubleClick, onReorder, renderIcon, view }) => {
87
+ const [tabContainer, setTabContainer] = useState(null);
88
+ const [dropPosition, setDropPosition] = useState(null);
89
+ const canDrag = useLatestCallback(() => !isDragBlocked);
90
+ const initialData = useLatestCallback(() => ({
91
+ ...getDragData?.(view),
92
+ [VIEW_DRAG_TYPE]: true,
93
+ [VIEW_DRAG_ID]: view.id,
94
+ [VIEW_DRAG_INSTANCE]: instanceId
95
+ }));
96
+ const dropData = useLatestCallback(() => ({
97
+ ...getDropData?.(view),
98
+ [VIEW_DRAG_ID]: view.id
99
+ }));
100
+ const reorder = useLatestCallback(onReorder);
101
+ useEffect(() => {
102
+ if (!tabContainer) return;
103
+ return combine(...canReorder ? [draggable({
104
+ element: tabContainer,
105
+ canDrag,
106
+ getInitialData: initialData
107
+ })] : [], dropTargetForElements({
108
+ element: tabContainer,
109
+ canDrop: ({ source }) => canReorder && source.data[VIEW_DRAG_TYPE] === true && source.data[VIEW_DRAG_INSTANCE] === instanceId && source.data[VIEW_DRAG_ID] !== view.id,
110
+ getData: ({ input, element }) => attachClosestEdge(dropData(), {
111
+ element,
112
+ input,
113
+ allowedEdges: ["left", "right"]
114
+ }),
115
+ getIsSticky: () => true,
116
+ onDrag: ({ self }) => setDropPosition(toWorkspaceViewDropPosition(extractClosestEdge(self.data), direction)),
117
+ onDragLeave: () => setDropPosition(null),
118
+ onDrop: ({ source, self }) => {
119
+ setDropPosition(null);
120
+ const draggedId = source.data[VIEW_DRAG_ID];
121
+ const position = toWorkspaceViewDropPosition(extractClosestEdge(self.data), direction);
122
+ if (typeof draggedId === "string" && position) reorder(draggedId, view.id, position);
123
+ }
124
+ }));
125
+ }, [
126
+ canReorder,
127
+ direction,
128
+ canDrag,
129
+ dropData,
130
+ initialData,
131
+ instanceId,
132
+ reorder,
133
+ tabContainer,
134
+ view.id
135
+ ]);
136
+ return /* @__PURE__ */ jsxs("div", {
137
+ className: "relative",
138
+ ref: setTabContainer,
139
+ children: [
140
+ dropPosition !== null && /* @__PURE__ */ jsx("span", {
141
+ "aria-hidden": "true",
142
+ className: cn("bg-primary pointer-events-none absolute inset-y-1 z-20 w-0.5 rounded-full", dropPosition === "before" ? "start-0" : "end-0")
143
+ }),
144
+ /* @__PURE__ */ jsxs(TabsTab, {
145
+ className: actions === null || actions === void 0 ? void 0 : "pe-6.5",
146
+ onContextMenu: (event) => onContextMenu?.(view, event),
147
+ onDoubleClick: (event) => onDoubleClick?.(view, event),
148
+ value: view.id,
149
+ children: [renderIcon(view), /* @__PURE__ */ jsx("span", {
150
+ className: "max-w-36 truncate",
151
+ children: view.name
152
+ })]
153
+ }),
154
+ actions
155
+ ]
156
+ });
157
+ };
158
+ //#endregion
159
+ export { WorkspaceViewSwitcher };
@@ -0,0 +1,14 @@
1
+ import { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
2
+ //#region src/view-switcher.logic.d.ts
3
+ type WorkspaceViewDirection = "ltr" | "rtl";
4
+ type WorkspaceViewDropPosition = "before" | "after";
5
+ declare const toWorkspaceViewDropPosition: (edge: Edge | null, direction: WorkspaceViewDirection) => WorkspaceViewDropPosition | null;
6
+ type ReorderWorkspaceViewIdsParams = {
7
+ ids: readonly string[];
8
+ draggedId: string;
9
+ targetId: string;
10
+ position: WorkspaceViewDropPosition;
11
+ };
12
+ declare const reorderWorkspaceViewIds: ({ ids, draggedId, targetId, position }: ReorderWorkspaceViewIdsParams) => string[] | null;
13
+ //#endregion
14
+ export { WorkspaceViewDirection, WorkspaceViewDropPosition, reorderWorkspaceViewIds, toWorkspaceViewDropPosition };
@@ -0,0 +1,14 @@
1
+ //#region src/view-switcher.logic.ts
2
+ const toWorkspaceViewDropPosition = (edge, direction) => {
3
+ if (edge !== "left" && edge !== "right") return null;
4
+ return (direction === "rtl" ? edge === "left" : edge === "right") ? "after" : "before";
5
+ };
6
+ const reorderWorkspaceViewIds = ({ ids, draggedId, targetId, position }) => {
7
+ if (draggedId === targetId || !ids.includes(draggedId) || !ids.includes(targetId)) return null;
8
+ const withoutDragged = ids.filter((id) => id !== draggedId);
9
+ const targetIndex = withoutDragged.indexOf(targetId);
10
+ const reordered = withoutDragged.toSpliced(position === "after" ? targetIndex + 1 : targetIndex, 0, draggedId);
11
+ return reordered.every((id, index) => id === ids[index]) ? null : reordered;
12
+ };
13
+ //#endregion
14
+ export { reorderWorkspaceViewIds, toWorkspaceViewDropPosition };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/workspace-ui",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Reusable workspace presentation components and view helpers for Stella.",
5
5
  "keywords": [
6
6
  "calculations",
@@ -74,6 +74,14 @@
74
74
  "./types": {
75
75
  "types": "./dist/types.d.ts",
76
76
  "import": "./dist/types.js"
77
+ },
78
+ "./view-switcher": {
79
+ "types": "./dist/view-switcher.d.ts",
80
+ "import": "./dist/view-switcher.js"
81
+ },
82
+ "./view-switcher-logic": {
83
+ "types": "./dist/view-switcher.logic.d.ts",
84
+ "import": "./dist/view-switcher.logic.js"
77
85
  }
78
86
  },
79
87
  "publishConfig": {
@@ -95,19 +103,21 @@
95
103
  "@stll/calculations": "^0.1.0",
96
104
  "@stll/conditions": "^0.2.0",
97
105
  "@stll/money": "^0.1.0",
98
- "@stll/ui": "^0.4.0",
106
+ "@stll/ui": "^0.5.0",
99
107
  "better-result": "3.0.0",
100
108
  "lucide-react": "1.30.0",
101
109
  "use-intl": "4.13.5"
102
110
  },
103
111
  "devDependencies": {
112
+ "@atlaskit/pragmatic-drag-and-drop": "^3.0.0",
113
+ "@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.0.1",
104
114
  "@base-ui/react": "1.7.0",
105
115
  "@stll/property-testing": "0.0.0",
106
116
  "@stll/typescript-config": "0.0.0",
107
117
  "@tanstack/react-table": "9.1.2",
108
- "@types/bun": "1.3.14",
109
118
  "@types/react": "^19.2.17",
110
119
  "@types/react-dom": "^19.2.3",
120
+ "bun-types": "1.4.0",
111
121
  "fast-check": "^4.9.0",
112
122
  "react": "^19.2.8",
113
123
  "react-dom": "^19.2.8",
@@ -115,6 +125,8 @@
115
125
  "tsdown": "0.22.14"
116
126
  },
117
127
  "peerDependencies": {
128
+ "@atlaskit/pragmatic-drag-and-drop": "^3.0.0",
129
+ "@atlaskit/pragmatic-drag-and-drop-hitbox": "^2.0.1",
118
130
  "@base-ui/react": "^1.7.0",
119
131
  "@tanstack/react-table": "^9.1.2",
120
132
  "react": ">=19",