@stll/workspace-ui 0.0.1-placeholder.0 → 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/README.md +59 -0
- package/dist/calculation-format.d.ts +54 -0
- package/dist/calculation-format.js +72 -0
- package/dist/calculation-selection.d.ts +15 -0
- package/dist/calculation-selection.js +14 -0
- package/dist/calculations.d.ts +67 -0
- package/dist/calculations.js +108 -0
- package/dist/colors.d.ts +2 -0
- package/dist/colors.js +2 -0
- package/dist/conditions/builder.d.ts +98 -0
- package/dist/conditions/builder.js +447 -0
- package/dist/conditions/index.d.ts +3 -0
- package/dist/conditions/index.js +3 -0
- package/dist/conditions/logic.d.ts +70 -0
- package/dist/conditions/logic.js +248 -0
- package/dist/field-value-logic.d.ts +7 -0
- package/dist/field-value-logic.js +8 -0
- package/dist/field-value.d.ts +30 -0
- package/dist/field-value.js +308 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/property-icon.d.ts +12 -0
- package/dist/property-icon.js +56 -0
- package/dist/sorts.d.ts +41 -0
- package/dist/sorts.js +99 -0
- package/dist/table-skeleton-rows.d.ts +18 -0
- package/dist/table-skeleton-rows.js +34 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.js +17 -0
- package/dist/view-switcher.d.ts +33 -0
- package/dist/view-switcher.js +159 -0
- package/dist/view-switcher.logic.d.ts +14 -0
- package/dist/view-switcher.logic.js +14 -0
- package/package.json +135 -3
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AlertCircleIcon, BanknoteIcon, CalendarIcon, CircleDotIcon, FileIcon, FileQuestionIcon, HashIcon, LinkIcon, ListChecksIcon, TextIcon, UserIcon } from "lucide-react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "@stll/ui/utils";
|
|
4
|
+
//#region src/property-icon.tsx
|
|
5
|
+
const propertyMap = {
|
|
6
|
+
text: {
|
|
7
|
+
icon: TextIcon,
|
|
8
|
+
label: "Text"
|
|
9
|
+
},
|
|
10
|
+
money: {
|
|
11
|
+
icon: BanknoteIcon,
|
|
12
|
+
label: "Money"
|
|
13
|
+
},
|
|
14
|
+
person: {
|
|
15
|
+
icon: UserIcon,
|
|
16
|
+
label: "Person"
|
|
17
|
+
},
|
|
18
|
+
file: {
|
|
19
|
+
icon: FileIcon,
|
|
20
|
+
label: "File"
|
|
21
|
+
},
|
|
22
|
+
error: {
|
|
23
|
+
icon: AlertCircleIcon,
|
|
24
|
+
label: "Error"
|
|
25
|
+
},
|
|
26
|
+
"single-select": {
|
|
27
|
+
icon: CircleDotIcon,
|
|
28
|
+
label: "Single Select"
|
|
29
|
+
},
|
|
30
|
+
"multi-select": {
|
|
31
|
+
icon: ListChecksIcon,
|
|
32
|
+
label: "Multi Select"
|
|
33
|
+
},
|
|
34
|
+
unsupported: {
|
|
35
|
+
icon: FileQuestionIcon,
|
|
36
|
+
label: "Unsupported"
|
|
37
|
+
},
|
|
38
|
+
date: {
|
|
39
|
+
icon: CalendarIcon,
|
|
40
|
+
label: "Date"
|
|
41
|
+
},
|
|
42
|
+
int: {
|
|
43
|
+
icon: HashIcon,
|
|
44
|
+
label: "Number"
|
|
45
|
+
},
|
|
46
|
+
clip: {
|
|
47
|
+
icon: LinkIcon,
|
|
48
|
+
label: "Clip"
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const PropertyIcon = ({ type, className }) => {
|
|
52
|
+
const Icon = propertyMap[type].icon;
|
|
53
|
+
return /* @__PURE__ */ jsx(Icon, { className: cn("size-3.5 shrink-0", className) });
|
|
54
|
+
};
|
|
55
|
+
//#endregion
|
|
56
|
+
export { PropertyIcon };
|
package/dist/sorts.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { PropertyIconType } from "./property-icon.js";
|
|
2
|
+
//#region src/sorts.d.ts
|
|
3
|
+
/** One sort a view carries. */
|
|
4
|
+
type SortDescriptor = {
|
|
5
|
+
propertyId: string;
|
|
6
|
+
desc: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** A property a view may sort by. */
|
|
9
|
+
type SortableProperty = {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
type: PropertyIconType;
|
|
13
|
+
};
|
|
14
|
+
type SortChipsLabels = {
|
|
15
|
+
/** Accessible name of the "add a sort" trigger. */
|
|
16
|
+
add: string;
|
|
17
|
+
/** Accessible name of a chip's remove button. */
|
|
18
|
+
remove: string;
|
|
19
|
+
};
|
|
20
|
+
type SortChipsProps = {
|
|
21
|
+
sorts: readonly SortDescriptor[];
|
|
22
|
+
properties: readonly SortableProperty[];
|
|
23
|
+
onUpdate: (sorts: SortDescriptor[]) => void;
|
|
24
|
+
labels: SortChipsLabels;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The view's sorts as chips, plus the menu that adds one.
|
|
28
|
+
*
|
|
29
|
+
* A sort is a property id and a direction; which properties can be sorted is
|
|
30
|
+
* the caller's answer, so this reads nothing but the three fields a chip
|
|
31
|
+
* draws.
|
|
32
|
+
*/
|
|
33
|
+
declare const SortChips: ({ sorts, properties, onUpdate, labels }: SortChipsProps) => import("react").JSX.Element;
|
|
34
|
+
/**
|
|
35
|
+
* What ascending means depends on what is being sorted: A→Z for words, 1→9 for
|
|
36
|
+
* numbers, an arrow for dates. A type with no idiom falls back to the arrow
|
|
37
|
+
* icon, which is why this returns null rather than a default string.
|
|
38
|
+
*/
|
|
39
|
+
declare const sortDirectionHint: (propertyType: string | undefined, desc: boolean) => string | null;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { SortChips, SortChipsLabels, SortChipsProps, SortDescriptor, SortableProperty, sortDirectionHint };
|
package/dist/sorts.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { PropertyIcon } from "./property-icon.js";
|
|
2
|
+
import { ArrowDownIcon, ArrowUpDownIcon, ArrowUpIcon, XIcon } from "lucide-react";
|
|
3
|
+
import { Button } from "@stll/ui/button";
|
|
4
|
+
import { Menu, MenuItem, MenuPopup, MenuTrigger } from "@stll/ui/menu";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/sorts.tsx
|
|
7
|
+
/**
|
|
8
|
+
* The view's sorts as chips, plus the menu that adds one.
|
|
9
|
+
*
|
|
10
|
+
* A sort is a property id and a direction; which properties can be sorted is
|
|
11
|
+
* the caller's answer, so this reads nothing but the three fields a chip
|
|
12
|
+
* draws.
|
|
13
|
+
*/
|
|
14
|
+
const SortChips = ({ sorts, properties, onUpdate, labels }) => /* @__PURE__ */ jsxs(Fragment, { children: [sorts.map((sort) => {
|
|
15
|
+
const property = properties.find((candidate) => candidate.id === sort.propertyId);
|
|
16
|
+
if (!property) return null;
|
|
17
|
+
return /* @__PURE__ */ jsx(SortChip, {
|
|
18
|
+
desc: sort.desc,
|
|
19
|
+
labels,
|
|
20
|
+
onRemove: () => {
|
|
21
|
+
onUpdate(sorts.filter((candidate) => candidate.propertyId !== sort.propertyId));
|
|
22
|
+
},
|
|
23
|
+
onToggle: () => {
|
|
24
|
+
onUpdate(sorts.map((candidate) => candidate.propertyId === sort.propertyId ? {
|
|
25
|
+
...candidate,
|
|
26
|
+
desc: !candidate.desc
|
|
27
|
+
} : candidate));
|
|
28
|
+
},
|
|
29
|
+
propertyName: property.name,
|
|
30
|
+
propertyType: property.type
|
|
31
|
+
}, sort.propertyId);
|
|
32
|
+
}), /* @__PURE__ */ jsx(AddSortButton, {
|
|
33
|
+
labels,
|
|
34
|
+
onAdd: (sort) => onUpdate([...sorts, sort]),
|
|
35
|
+
properties,
|
|
36
|
+
sortedPropertyIds: new Set(sorts.map((sort) => sort.propertyId))
|
|
37
|
+
})] });
|
|
38
|
+
/**
|
|
39
|
+
* What ascending means depends on what is being sorted: A→Z for words, 1→9 for
|
|
40
|
+
* numbers, an arrow for dates. A type with no idiom falls back to the arrow
|
|
41
|
+
* icon, which is why this returns null rather than a default string.
|
|
42
|
+
*/
|
|
43
|
+
const sortDirectionHint = (propertyType, desc) => {
|
|
44
|
+
if (!propertyType) return null;
|
|
45
|
+
switch (propertyType) {
|
|
46
|
+
case "text":
|
|
47
|
+
case "single-select":
|
|
48
|
+
case "multi-select":
|
|
49
|
+
case "file":
|
|
50
|
+
case "person": return desc ? "Z→A" : "A→Z";
|
|
51
|
+
case "int":
|
|
52
|
+
case "money": return desc ? "9→1" : "1→9";
|
|
53
|
+
case "date": return desc ? "↓" : "↑";
|
|
54
|
+
default: return null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const SortChip = ({ propertyName, propertyType, desc, onToggle, onRemove, labels }) => {
|
|
58
|
+
const SortIcon = desc ? ArrowDownIcon : ArrowUpIcon;
|
|
59
|
+
const hint = sortDirectionHint(propertyType, desc);
|
|
60
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
61
|
+
className: "bg-muted/50 flex items-center rounded-md border",
|
|
62
|
+
children: [/* @__PURE__ */ jsxs(Button, {
|
|
63
|
+
onClick: onToggle,
|
|
64
|
+
size: "xs",
|
|
65
|
+
variant: "ghost",
|
|
66
|
+
children: [
|
|
67
|
+
!hint && /* @__PURE__ */ jsx(SortIcon, {}),
|
|
68
|
+
propertyName,
|
|
69
|
+
hint && /* @__PURE__ */ jsx("span", {
|
|
70
|
+
className: "text-muted-foreground",
|
|
71
|
+
children: hint
|
|
72
|
+
})
|
|
73
|
+
]
|
|
74
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
75
|
+
"aria-label": labels.remove,
|
|
76
|
+
onClick: onRemove,
|
|
77
|
+
size: "icon-xs",
|
|
78
|
+
variant: "ghost",
|
|
79
|
+
children: /* @__PURE__ */ jsx(XIcon, {})
|
|
80
|
+
})]
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
const AddSortButton = ({ properties, sortedPropertyIds, onAdd, labels }) => /* @__PURE__ */ jsxs(Menu, { children: [/* @__PURE__ */ jsx(MenuTrigger, {
|
|
84
|
+
"aria-label": labels.add,
|
|
85
|
+
render: /* @__PURE__ */ jsx(Button, {
|
|
86
|
+
size: "icon-xs",
|
|
87
|
+
variant: "ghost"
|
|
88
|
+
}),
|
|
89
|
+
children: /* @__PURE__ */ jsx(ArrowUpDownIcon, {})
|
|
90
|
+
}), /* @__PURE__ */ jsx(MenuPopup, { children: properties.map((property) => /* @__PURE__ */ jsxs(MenuItem, {
|
|
91
|
+
disabled: sortedPropertyIds.has(property.id),
|
|
92
|
+
onClick: () => onAdd({
|
|
93
|
+
propertyId: property.id,
|
|
94
|
+
desc: false
|
|
95
|
+
}),
|
|
96
|
+
children: [/* @__PURE__ */ jsx(PropertyIcon, { type: property.type }), property.name]
|
|
97
|
+
}, property.id)) })] });
|
|
98
|
+
//#endregion
|
|
99
|
+
export { SortChips, sortDirectionHint };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { Column, RowData, TableFeatures } from "@tanstack/react-table";
|
|
3
|
+
//#region src/table-skeleton-rows.d.ts
|
|
4
|
+
type TableSkeletonRowsProps<TFeatures extends TableFeatures, TData extends RowData> = {
|
|
5
|
+
columns: readonly Column<TFeatures, TData>[];
|
|
6
|
+
rowCount?: number;
|
|
7
|
+
renderCell?: (column: Column<TFeatures, TData>) => ReactNode;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Loading rows generated from the table's own column model, so the skeleton
|
|
11
|
+
* cannot drift from the real table: add, remove, or reorder a column and the
|
|
12
|
+
* placeholder gains, loses, or moves the matching cell automatically. Render it
|
|
13
|
+
* inside the same `<TableBody>` the real rows use, passing the table's leaf
|
|
14
|
+
* columns (e.g. `table.getAllLeafColumns()`).
|
|
15
|
+
*/
|
|
16
|
+
declare const TableSkeletonRows: <TFeatures extends TableFeatures, TData extends RowData>({ columns, rowCount, renderCell }: TableSkeletonRowsProps<TFeatures, TData>) => import("react").JSX.Element[];
|
|
17
|
+
//#endregion
|
|
18
|
+
export { TableSkeletonRows };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Skeleton } from "@stll/ui/skeleton";
|
|
3
|
+
import { TableCell, TableRow } from "@stll/ui/table";
|
|
4
|
+
//#region src/table-skeleton-rows.tsx
|
|
5
|
+
const SKELETON_ROW_KEYS = [
|
|
6
|
+
"a",
|
|
7
|
+
"b",
|
|
8
|
+
"c",
|
|
9
|
+
"d",
|
|
10
|
+
"e",
|
|
11
|
+
"f",
|
|
12
|
+
"g",
|
|
13
|
+
"h",
|
|
14
|
+
"i",
|
|
15
|
+
"j",
|
|
16
|
+
"k",
|
|
17
|
+
"l"
|
|
18
|
+
];
|
|
19
|
+
const DEFAULT_SKELETON_ROW_COUNT = 8;
|
|
20
|
+
/**
|
|
21
|
+
* Loading rows generated from the table's own column model, so the skeleton
|
|
22
|
+
* cannot drift from the real table: add, remove, or reorder a column and the
|
|
23
|
+
* placeholder gains, loses, or moves the matching cell automatically. Render it
|
|
24
|
+
* inside the same `<TableBody>` the real rows use, passing the table's leaf
|
|
25
|
+
* columns (e.g. `table.getAllLeafColumns()`).
|
|
26
|
+
*/
|
|
27
|
+
const TableSkeletonRows = ({ columns, rowCount = DEFAULT_SKELETON_ROW_COUNT, renderCell }) => {
|
|
28
|
+
return SKELETON_ROW_KEYS.slice(0, Math.min(rowCount, SKELETON_ROW_KEYS.length)).map((rowKey) => /* @__PURE__ */ jsx(TableRow, { children: columns.map((column) => {
|
|
29
|
+
const custom = renderCell?.(column);
|
|
30
|
+
return /* @__PURE__ */ jsx(TableCell, { children: custom === void 0 ? /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/5" }) : custom }, column.id);
|
|
31
|
+
}) }, rowKey));
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { TableSkeletonRows };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { OptionColor, OptionColor as OptionColor$1 } from "@stll/ui/option-color";
|
|
2
|
+
//#region src/types.d.ts
|
|
3
|
+
type FieldContent = {
|
|
4
|
+
type: "error";
|
|
5
|
+
version: 1;
|
|
6
|
+
} | {
|
|
7
|
+
type: "pending";
|
|
8
|
+
version: 1;
|
|
9
|
+
} | {
|
|
10
|
+
type: "unsupported";
|
|
11
|
+
version: 1;
|
|
12
|
+
} | {
|
|
13
|
+
type: "text";
|
|
14
|
+
version: 1;
|
|
15
|
+
value: string;
|
|
16
|
+
} | {
|
|
17
|
+
type: "single-select";
|
|
18
|
+
version: 1;
|
|
19
|
+
value: string | null;
|
|
20
|
+
} | {
|
|
21
|
+
type: "multi-select";
|
|
22
|
+
version: 1;
|
|
23
|
+
value: string[];
|
|
24
|
+
} | {
|
|
25
|
+
type: "file";
|
|
26
|
+
version: 1;
|
|
27
|
+
id: string;
|
|
28
|
+
fileName: string;
|
|
29
|
+
mimeType: string;
|
|
30
|
+
sizeBytes: number;
|
|
31
|
+
encrypted: boolean;
|
|
32
|
+
sha256Hex: string;
|
|
33
|
+
pdfFileId: string | null;
|
|
34
|
+
} | {
|
|
35
|
+
type: "date";
|
|
36
|
+
version: 1;
|
|
37
|
+
value: string | null;
|
|
38
|
+
} | {
|
|
39
|
+
type: "int";
|
|
40
|
+
version: 1;
|
|
41
|
+
value: number;
|
|
42
|
+
currency: string | null;
|
|
43
|
+
} | {
|
|
44
|
+
/**
|
|
45
|
+
* A monetary amount. Separate from `int` because it is stored in minor
|
|
46
|
+
* units and carries its own currency: an int's value is major units, so
|
|
47
|
+
* one type for both is a 100x bug waiting in every total.
|
|
48
|
+
*/
|
|
49
|
+
type: "money";
|
|
50
|
+
version: 1;
|
|
51
|
+
amountCents: number;
|
|
52
|
+
currency: string;
|
|
53
|
+
} | {
|
|
54
|
+
type: "person";
|
|
55
|
+
version: 1;
|
|
56
|
+
/** Null when the person is named but not a workspace member. */
|
|
57
|
+
userId: string | null;
|
|
58
|
+
name: string;
|
|
59
|
+
image: string | null;
|
|
60
|
+
} | {
|
|
61
|
+
type: "clip";
|
|
62
|
+
version: 1;
|
|
63
|
+
url: string;
|
|
64
|
+
snippet?: string;
|
|
65
|
+
citation?: string;
|
|
66
|
+
jurisdiction?: string;
|
|
67
|
+
sourceType?: string;
|
|
68
|
+
};
|
|
69
|
+
/** Runtime names for every content arm the field renderer understands. */
|
|
70
|
+
declare const FIELD_CONTENT_TYPES: readonly ["error", "pending", "unsupported", "text", "single-select", "multi-select", "file", "date", "int", "money", "person", "clip"];
|
|
71
|
+
type WorkspaceFieldContent = FieldContent;
|
|
72
|
+
/**
|
|
73
|
+
* Minimal structural bound the display layer needs from a property: the cell
|
|
74
|
+
* renderers only read `content.type` and, for selects, the `options` colors.
|
|
75
|
+
* Real callers (e.g. the app's `WorkspaceProperty`) satisfy this structurally.
|
|
76
|
+
*/
|
|
77
|
+
type GenericProperty = {
|
|
78
|
+
content: {
|
|
79
|
+
type: "file" | "text" | "date" | "int" | "money" | "person";
|
|
80
|
+
} | {
|
|
81
|
+
type: "single-select" | "multi-select";
|
|
82
|
+
options: {
|
|
83
|
+
value: string;
|
|
84
|
+
color: OptionColor$1;
|
|
85
|
+
}[];
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
//#endregion
|
|
89
|
+
export { FIELD_CONTENT_TYPES, FieldContent, GenericProperty, type OptionColor, WorkspaceFieldContent };
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Runtime names for every content arm the field renderer understands. */
|
|
2
|
+
const FIELD_CONTENT_TYPES = [
|
|
3
|
+
"error",
|
|
4
|
+
"pending",
|
|
5
|
+
"unsupported",
|
|
6
|
+
"text",
|
|
7
|
+
"single-select",
|
|
8
|
+
"multi-select",
|
|
9
|
+
"file",
|
|
10
|
+
"date",
|
|
11
|
+
"int",
|
|
12
|
+
"money",
|
|
13
|
+
"person",
|
|
14
|
+
"clip"
|
|
15
|
+
];
|
|
16
|
+
//#endregion
|
|
17
|
+
export { FIELD_CONTENT_TYPES };
|
|
@@ -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 };
|