@stll/ui 0.2.0 → 0.4.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/dist/calendar/index.d.ts +4 -0
- package/dist/calendar/index.js +4 -0
- package/dist/calendar/primitives.d.ts +10 -0
- package/dist/calendar/primitives.js +36 -0
- package/dist/calendar/resource-calendar.d.ts +35 -0
- package/dist/calendar/resource-calendar.js +184 -0
- package/dist/calendar/resource-calendar.logic.d.ts +28 -0
- package/dist/calendar/resource-calendar.logic.js +88 -0
- package/dist/components/button-variants.d.ts +1 -1
- package/dist/components/outline-rail.d.ts +5 -1
- package/dist/components/outline-rail.js +20 -9
- package/dist/data-table/index.d.ts +2 -0
- package/dist/data-table/index.js +2 -0
- package/dist/data-table/schema.d.ts +70 -0
- package/dist/data-table/schema.js +40 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +11 -1
- package/dist/kanban/card-properties.d.ts +27 -0
- package/dist/kanban/card-properties.js +19 -0
- package/dist/kanban/card-shell.d.ts +32 -0
- package/dist/kanban/card-shell.js +46 -0
- package/dist/kanban/column-header.d.ts +23 -0
- package/dist/kanban/column-header.js +24 -0
- package/dist/kanban/drag-interactions.d.ts +35 -0
- package/dist/kanban/drag-interactions.js +53 -0
- package/dist/kanban/grouping.d.ts +84 -0
- package/dist/kanban/grouping.js +76 -0
- package/dist/kanban/index.d.ts +6 -0
- package/dist/kanban/index.js +6 -0
- package/dist/lib/option-color.d.ts +28 -0
- package/dist/lib/option-color.js +59 -0
- package/package.json +21 -1
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { CalendarCell, CalendarEntryButton, CalendarEntrySurface, CalendarGrid, CalendarHeaderCell, CalendarHeaderRow } from "./primitives.js";
|
|
2
|
+
import { CalendarDateRange, ResourceCalendarLaneLayout, ResourceCalendarLanePlacement, ResourceCalendarPlacement, assertConsecutiveCalendarDates, getResourceCalendarPlacement, layoutResourceCalendarEntries, nextCalendarDate } from "./resource-calendar.logic.js";
|
|
3
|
+
import { ResourceCalendar, ResourceCalendarColumn, ResourceCalendarEntry, ResourceCalendarEntryTone, ResourceCalendarResource } from "./resource-calendar.js";
|
|
4
|
+
export { CalendarCell, type CalendarDateRange, CalendarEntryButton, CalendarEntrySurface, CalendarGrid, CalendarHeaderCell, CalendarHeaderRow, ResourceCalendar, type ResourceCalendarColumn, type ResourceCalendarEntry, type ResourceCalendarEntryTone, type ResourceCalendarLaneLayout, type ResourceCalendarLanePlacement, type ResourceCalendarPlacement, type ResourceCalendarResource, assertConsecutiveCalendarDates, getResourceCalendarPlacement, layoutResourceCalendarEntries, nextCalendarDate };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { CalendarCell, CalendarEntryButton, CalendarEntrySurface, CalendarGrid, CalendarHeaderCell, CalendarHeaderRow } from "./primitives.js";
|
|
2
|
+
import { assertConsecutiveCalendarDates, getResourceCalendarPlacement, layoutResourceCalendarEntries, nextCalendarDate } from "./resource-calendar.logic.js";
|
|
3
|
+
import { ResourceCalendar } from "./resource-calendar.js";
|
|
4
|
+
export { CalendarCell, CalendarEntryButton, CalendarEntrySurface, CalendarGrid, CalendarHeaderCell, CalendarHeaderRow, ResourceCalendar, assertConsecutiveCalendarDates, getResourceCalendarPlacement, layoutResourceCalendarEntries, nextCalendarDate };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React$1 from "react";
|
|
2
|
+
//#region src/calendar/primitives.d.ts
|
|
3
|
+
declare const CalendarGrid: ({ className, ...props }: React$1.ComponentProps<"div">) => React$1.JSX.Element;
|
|
4
|
+
declare const CalendarHeaderRow: ({ className, ...props }: React$1.ComponentProps<"div">) => React$1.JSX.Element;
|
|
5
|
+
declare const CalendarHeaderCell: ({ className, ...props }: React$1.ComponentProps<"div">) => React$1.JSX.Element;
|
|
6
|
+
declare const CalendarCell: ({ className, ...props }: React$1.ComponentProps<"div">) => React$1.JSX.Element;
|
|
7
|
+
declare const CalendarEntryButton: ({ className, ...props }: React$1.ComponentProps<"button">) => React$1.JSX.Element;
|
|
8
|
+
declare const CalendarEntrySurface: ({ className, ...props }: React$1.ComponentProps<"article">) => React$1.JSX.Element;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { CalendarCell, CalendarEntryButton, CalendarEntrySurface, CalendarGrid, CalendarHeaderCell, CalendarHeaderRow };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { cn } from "../lib/utils.js";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
//#region src/calendar/primitives.tsx
|
|
4
|
+
const CalendarGrid = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
|
|
5
|
+
className: cn("grid", className),
|
|
6
|
+
"data-slot": "calendar-grid",
|
|
7
|
+
...props
|
|
8
|
+
});
|
|
9
|
+
const CalendarHeaderRow = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
|
|
10
|
+
className: cn("grid border-b", className),
|
|
11
|
+
"data-slot": "calendar-header-row",
|
|
12
|
+
...props
|
|
13
|
+
});
|
|
14
|
+
const CalendarHeaderCell = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
|
|
15
|
+
className: cn("text-muted-foreground border-e px-2 py-2 text-center text-xs font-medium last:border-e-0", className),
|
|
16
|
+
"data-slot": "calendar-header-cell",
|
|
17
|
+
...props
|
|
18
|
+
});
|
|
19
|
+
const CalendarCell = ({ className, ...props }) => /* @__PURE__ */ jsx("div", {
|
|
20
|
+
className: cn("border-e border-b", className),
|
|
21
|
+
"data-slot": "calendar-cell",
|
|
22
|
+
...props
|
|
23
|
+
});
|
|
24
|
+
const CalendarEntryButton = ({ className, ...props }) => /* @__PURE__ */ jsx("button", {
|
|
25
|
+
className: cn("focus-visible:ring-ring min-w-0 rounded-md text-start text-xs outline-none focus-visible:ring-2 focus-visible:ring-offset-1", className),
|
|
26
|
+
"data-slot": "calendar-entry",
|
|
27
|
+
type: "button",
|
|
28
|
+
...props
|
|
29
|
+
});
|
|
30
|
+
const CalendarEntrySurface = ({ className, ...props }) => /* @__PURE__ */ jsx("article", {
|
|
31
|
+
className: cn("min-w-0 rounded-md text-start text-xs", className),
|
|
32
|
+
"data-slot": "calendar-entry",
|
|
33
|
+
...props
|
|
34
|
+
});
|
|
35
|
+
//#endregion
|
|
36
|
+
export { CalendarCell, CalendarEntryButton, CalendarEntrySurface, CalendarGrid, CalendarHeaderCell, CalendarHeaderRow };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ReactElement, ReactNode } from "react";
|
|
2
|
+
//#region src/calendar/resource-calendar.d.ts
|
|
3
|
+
type ResourceCalendarColumn = {
|
|
4
|
+
date: string;
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
meta?: ReactNode;
|
|
7
|
+
};
|
|
8
|
+
type ResourceCalendarResource = {
|
|
9
|
+
id: string;
|
|
10
|
+
label: ReactNode;
|
|
11
|
+
meta?: ReactNode;
|
|
12
|
+
};
|
|
13
|
+
type ResourceCalendarEntryTone = "accent" | "neutral" | "warning" | "destructive";
|
|
14
|
+
type ResourceCalendarEntry = {
|
|
15
|
+
accessibleLabel: string;
|
|
16
|
+
endDateExclusive: string;
|
|
17
|
+
id: string;
|
|
18
|
+
label: ReactNode;
|
|
19
|
+
meta?: ReactNode;
|
|
20
|
+
resourceId: string;
|
|
21
|
+
startDate: string;
|
|
22
|
+
tone?: ResourceCalendarEntryTone;
|
|
23
|
+
};
|
|
24
|
+
type ResourceCalendarProps = {
|
|
25
|
+
ariaLabel: string;
|
|
26
|
+
columns: readonly ResourceCalendarColumn[];
|
|
27
|
+
empty?: ReactNode;
|
|
28
|
+
entries: readonly ResourceCalendarEntry[];
|
|
29
|
+
onSelectEntry?: (entry: ResourceCalendarEntry) => void;
|
|
30
|
+
resourceHeader: ReactNode;
|
|
31
|
+
resources: readonly ResourceCalendarResource[];
|
|
32
|
+
};
|
|
33
|
+
declare const ResourceCalendar: ({ ariaLabel, columns, empty, entries, onSelectEntry, resourceHeader, resources }: ResourceCalendarProps) => ReactElement;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { ResourceCalendar, ResourceCalendarColumn, ResourceCalendarEntry, ResourceCalendarEntryTone, ResourceCalendarResource };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { cn } from "../lib/utils.js";
|
|
2
|
+
import { CalendarCell, CalendarEntryButton, CalendarEntrySurface, CalendarHeaderCell, CalendarHeaderRow } from "./primitives.js";
|
|
3
|
+
import { assertConsecutiveCalendarDates, layoutResourceCalendarEntries, nextCalendarDate } from "./resource-calendar.logic.js";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { useId } from "react";
|
|
6
|
+
//#region src/calendar/resource-calendar.tsx
|
|
7
|
+
const ResourceCalendar = ({ ariaLabel, columns, empty, entries, onSelectEntry, resourceHeader, resources }) => {
|
|
8
|
+
assertResourceCalendarContract({
|
|
9
|
+
columns,
|
|
10
|
+
entries,
|
|
11
|
+
resources
|
|
12
|
+
});
|
|
13
|
+
const calendarId = useId();
|
|
14
|
+
if (resources.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
15
|
+
"data-slot": "resource-calendar-empty",
|
|
16
|
+
children: empty
|
|
17
|
+
});
|
|
18
|
+
const gridStyle = { gridTemplateColumns: `14rem repeat(${String(columns.length)}, minmax(7rem, 1fr))` };
|
|
19
|
+
const calendarWidthStyle = { minWidth: `${String(14 + columns.length * 7)}rem` };
|
|
20
|
+
const visibleRange = {
|
|
21
|
+
endDateExclusive: nextCalendarDate(columns.at(-1)?.date ?? ""),
|
|
22
|
+
startDate: columns.at(0)?.date ?? ""
|
|
23
|
+
};
|
|
24
|
+
const entriesByResource = /* @__PURE__ */ new Map();
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const existing = entriesByResource.get(entry.resourceId);
|
|
27
|
+
if (existing === void 0) {
|
|
28
|
+
entriesByResource.set(entry.resourceId, [entry]);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
existing.push(entry);
|
|
32
|
+
}
|
|
33
|
+
const getColumnHeaderId = (index) => `${calendarId}-column-${String(index)}`;
|
|
34
|
+
return /* @__PURE__ */ jsx("section", {
|
|
35
|
+
"aria-colcount": columns.length + 1,
|
|
36
|
+
"aria-label": ariaLabel,
|
|
37
|
+
"aria-rowcount": resources.length + 1,
|
|
38
|
+
className: "border-border bg-card overflow-x-auto rounded-xl border",
|
|
39
|
+
"data-slot": "resource-calendar",
|
|
40
|
+
role: "table",
|
|
41
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
42
|
+
className: "w-full",
|
|
43
|
+
style: calendarWidthStyle,
|
|
44
|
+
children: [/* @__PURE__ */ jsxs(CalendarHeaderRow, {
|
|
45
|
+
role: "row",
|
|
46
|
+
style: gridStyle,
|
|
47
|
+
children: [/* @__PURE__ */ jsx(CalendarHeaderCell, {
|
|
48
|
+
"aria-colindex": 1,
|
|
49
|
+
className: "bg-card sticky start-0 z-30 text-start font-semibold tracking-wide uppercase",
|
|
50
|
+
role: "columnheader",
|
|
51
|
+
children: resourceHeader
|
|
52
|
+
}), columns.map((column, index) => /* @__PURE__ */ jsxs(CalendarHeaderCell, {
|
|
53
|
+
"aria-colindex": index + 2,
|
|
54
|
+
id: getColumnHeaderId(index),
|
|
55
|
+
role: "columnheader",
|
|
56
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
57
|
+
className: "block",
|
|
58
|
+
children: column.label
|
|
59
|
+
}), column.meta === void 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
60
|
+
className: "text-foreground mt-0.5 block font-semibold",
|
|
61
|
+
children: column.meta
|
|
62
|
+
})]
|
|
63
|
+
}, column.date))]
|
|
64
|
+
}), resources.map((resource, resourceIndex) => {
|
|
65
|
+
const resourceEntries = entriesByResource.get(resource.id) ?? [];
|
|
66
|
+
const layout = layoutResourceCalendarEntries(resourceEntries, visibleRange);
|
|
67
|
+
const resourceEntryById = new Map(resourceEntries.map((entry) => [entry.id, entry]));
|
|
68
|
+
const visibleEntriesByColumn = /* @__PURE__ */ new Map();
|
|
69
|
+
for (const placement of layout.placements) {
|
|
70
|
+
const entry = resourceEntryById.get(placement.entryId);
|
|
71
|
+
if (entry === void 0) throw new TypeError(`Resource calendar layout references unknown entry ${placement.entryId}`);
|
|
72
|
+
const existing = visibleEntriesByColumn.get(placement.columnStart);
|
|
73
|
+
if (existing === void 0) {
|
|
74
|
+
visibleEntriesByColumn.set(placement.columnStart, [{
|
|
75
|
+
entry,
|
|
76
|
+
placement
|
|
77
|
+
}]);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
existing.push({
|
|
81
|
+
entry,
|
|
82
|
+
placement
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const resourceHeaderId = `${calendarId}-resource-${String(resourceIndex)}`;
|
|
86
|
+
const resourceRowStyle = {
|
|
87
|
+
...gridStyle,
|
|
88
|
+
minHeight: `${String(Math.max(5, layout.rowCount * 3.75))}rem`
|
|
89
|
+
};
|
|
90
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
91
|
+
className: "border-border relative grid min-h-20 border-b last:border-b-0",
|
|
92
|
+
"data-slot": "resource-calendar-row",
|
|
93
|
+
role: "row",
|
|
94
|
+
style: resourceRowStyle,
|
|
95
|
+
children: [/* @__PURE__ */ jsxs(CalendarCell, {
|
|
96
|
+
"aria-colindex": 1,
|
|
97
|
+
"aria-rowindex": resourceIndex + 2,
|
|
98
|
+
className: "bg-card sticky start-0 z-20 border-b-0 px-4 py-3",
|
|
99
|
+
id: resourceHeaderId,
|
|
100
|
+
role: "rowheader",
|
|
101
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
102
|
+
className: "block truncate text-sm font-semibold",
|
|
103
|
+
children: resource.label
|
|
104
|
+
}), resource.meta === void 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
105
|
+
className: "text-muted-foreground mt-1 block truncate text-xs",
|
|
106
|
+
children: resource.meta
|
|
107
|
+
})]
|
|
108
|
+
}), columns.map((column, columnIndex) => {
|
|
109
|
+
const columnStart = columnIndex + 2;
|
|
110
|
+
const visibleEntries = visibleEntriesByColumn.get(columnStart) ?? [];
|
|
111
|
+
return /* @__PURE__ */ jsx(CalendarCell, {
|
|
112
|
+
"aria-colindex": columnStart,
|
|
113
|
+
"aria-labelledby": `${resourceHeaderId} ${getColumnHeaderId(columnIndex)}`,
|
|
114
|
+
"aria-rowindex": resourceIndex + 2,
|
|
115
|
+
className: "bg-background/40 relative border-b-0 last:border-e-0",
|
|
116
|
+
role: "gridcell",
|
|
117
|
+
children: visibleEntries.map(({ entry, placement }) => {
|
|
118
|
+
const labelledBy = [resourceHeaderId, ...Array.from({ length: placement.span }, (_, index) => getColumnHeaderId(columnIndex + index))].join(" ");
|
|
119
|
+
return /* @__PURE__ */ jsx(ResourceCalendarEntryView, {
|
|
120
|
+
entry,
|
|
121
|
+
labelledBy,
|
|
122
|
+
onSelectEntry,
|
|
123
|
+
placement,
|
|
124
|
+
rowCount: layout.rowCount
|
|
125
|
+
}, entry.id);
|
|
126
|
+
})
|
|
127
|
+
}, column.date);
|
|
128
|
+
})]
|
|
129
|
+
}, resource.id);
|
|
130
|
+
})]
|
|
131
|
+
})
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
const RESOURCE_CALENDAR_ENTRY_TONES = {
|
|
135
|
+
accent: "bg-primary text-primary-foreground",
|
|
136
|
+
destructive: "border-destructive/32 bg-destructive/12 text-foreground border",
|
|
137
|
+
neutral: "border-border bg-muted text-foreground border",
|
|
138
|
+
warning: "border-warning/30 bg-warning/15 text-foreground border"
|
|
139
|
+
};
|
|
140
|
+
const ResourceCalendarEntryView = ({ entry, labelledBy, onSelectEntry, placement, rowCount }) => {
|
|
141
|
+
const className = cn("h-full w-full px-3 py-2 font-medium shadow-sm", RESOURCE_CALENDAR_ENTRY_TONES[entry.tone ?? "accent"]);
|
|
142
|
+
const style = {
|
|
143
|
+
blockSize: `${String(100 / rowCount)}%`,
|
|
144
|
+
insetBlockStart: `${String((placement.rowStart - 1) * 100 / rowCount)}%`,
|
|
145
|
+
insetInlineStart: 0,
|
|
146
|
+
inlineSize: `calc(${String(placement.span)} * 100%)`
|
|
147
|
+
};
|
|
148
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
149
|
+
className: "block truncate",
|
|
150
|
+
children: entry.label
|
|
151
|
+
}), entry.meta === void 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
152
|
+
className: "block truncate opacity-75",
|
|
153
|
+
children: entry.meta
|
|
154
|
+
})] });
|
|
155
|
+
return /* @__PURE__ */ jsx("div", {
|
|
156
|
+
className: "absolute z-10 min-w-0 p-2",
|
|
157
|
+
style,
|
|
158
|
+
children: onSelectEntry === void 0 ? /* @__PURE__ */ jsx(CalendarEntrySurface, {
|
|
159
|
+
"aria-describedby": labelledBy,
|
|
160
|
+
"aria-label": entry.accessibleLabel,
|
|
161
|
+
className,
|
|
162
|
+
children: content
|
|
163
|
+
}) : /* @__PURE__ */ jsx(CalendarEntryButton, {
|
|
164
|
+
"aria-describedby": labelledBy,
|
|
165
|
+
"aria-label": entry.accessibleLabel,
|
|
166
|
+
className,
|
|
167
|
+
onClick: () => onSelectEntry(entry),
|
|
168
|
+
children: content
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
const assertUniqueIds = (ids, label) => {
|
|
173
|
+
if (new Set(ids).size !== ids.length) throw new TypeError(`${label} ids must be unique`);
|
|
174
|
+
};
|
|
175
|
+
const assertResourceCalendarContract = ({ columns, entries, resources }) => {
|
|
176
|
+
assertConsecutiveCalendarDates(columns.map(({ date }) => date));
|
|
177
|
+
assertUniqueIds(resources.map(({ id }) => id), "Resource calendar resource");
|
|
178
|
+
assertUniqueIds(entries.map(({ id }) => id), "Resource calendar entry");
|
|
179
|
+
const resourceIds = new Set(resources.map(({ id }) => id));
|
|
180
|
+
const orphan = entries.find(({ resourceId }) => !resourceIds.has(resourceId));
|
|
181
|
+
if (orphan !== void 0) throw new TypeError(`Resource calendar entry ${orphan.id} references an unknown resource`);
|
|
182
|
+
};
|
|
183
|
+
//#endregion
|
|
184
|
+
export { ResourceCalendar };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/calendar/resource-calendar.logic.d.ts
|
|
2
|
+
type CalendarDateRange = {
|
|
3
|
+
endDateExclusive: string;
|
|
4
|
+
startDate: string;
|
|
5
|
+
};
|
|
6
|
+
type ResourceCalendarPlacement = {
|
|
7
|
+
columnStart: number;
|
|
8
|
+
span: number;
|
|
9
|
+
};
|
|
10
|
+
type ResourceCalendarLanePlacement = ResourceCalendarPlacement & {
|
|
11
|
+
entryId: string;
|
|
12
|
+
rowStart: number;
|
|
13
|
+
};
|
|
14
|
+
type ResourceCalendarLaneLayout = {
|
|
15
|
+
placements: ResourceCalendarLanePlacement[];
|
|
16
|
+
rowCount: number;
|
|
17
|
+
};
|
|
18
|
+
declare const getResourceCalendarPlacement: ({ entry, visibleRange }: {
|
|
19
|
+
entry: CalendarDateRange;
|
|
20
|
+
visibleRange: CalendarDateRange;
|
|
21
|
+
}) => ResourceCalendarPlacement | null;
|
|
22
|
+
declare const assertConsecutiveCalendarDates: (dates: readonly string[]) => void;
|
|
23
|
+
declare const nextCalendarDate: (value: string) => string;
|
|
24
|
+
declare const layoutResourceCalendarEntries: (entries: readonly (CalendarDateRange & {
|
|
25
|
+
id: string;
|
|
26
|
+
})[], visibleRange: CalendarDateRange) => ResourceCalendarLaneLayout;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { CalendarDateRange, ResourceCalendarLaneLayout, ResourceCalendarLanePlacement, ResourceCalendarPlacement, assertConsecutiveCalendarDates, getResourceCalendarPlacement, layoutResourceCalendarEntries, nextCalendarDate };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
//#region src/calendar/resource-calendar.logic.ts
|
|
2
|
+
const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
|
|
3
|
+
const DAY_IN_MS = 864e5;
|
|
4
|
+
const toUTCDate = (value) => {
|
|
5
|
+
if (!ISO_DATE_PATTERN.test(value)) return null;
|
|
6
|
+
const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
7
|
+
return date.toISOString().slice(0, 10) === value ? date : null;
|
|
8
|
+
};
|
|
9
|
+
const differenceInCalendarDays = (later, earlier) => {
|
|
10
|
+
const laterDate = toUTCDate(later);
|
|
11
|
+
const earlierDate = toUTCDate(earlier);
|
|
12
|
+
if (laterDate === null || earlierDate === null) throw new RangeError("Calendar dates must use normalized YYYY-MM-DD values");
|
|
13
|
+
return (laterDate.getTime() - earlierDate.getTime()) / DAY_IN_MS;
|
|
14
|
+
};
|
|
15
|
+
const getResourceCalendarPlacement = ({ entry, visibleRange }) => {
|
|
16
|
+
const visibleDayCount = differenceInCalendarDays(visibleRange.endDateExclusive, visibleRange.startDate);
|
|
17
|
+
const entryDayCount = differenceInCalendarDays(entry.endDateExclusive, entry.startDate);
|
|
18
|
+
if (visibleDayCount <= 0 || entryDayCount <= 0) throw new RangeError("Calendar date ranges must be non-empty and half-open");
|
|
19
|
+
if (entry.startDate >= visibleRange.endDateExclusive || entry.endDateExclusive <= visibleRange.startDate) return null;
|
|
20
|
+
const clippedStart = entry.startDate < visibleRange.startDate ? visibleRange.startDate : entry.startDate;
|
|
21
|
+
const clippedEnd = entry.endDateExclusive > visibleRange.endDateExclusive ? visibleRange.endDateExclusive : entry.endDateExclusive;
|
|
22
|
+
return {
|
|
23
|
+
columnStart: differenceInCalendarDays(clippedStart, visibleRange.startDate) + 2,
|
|
24
|
+
span: differenceInCalendarDays(clippedEnd, clippedStart)
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
const assertConsecutiveCalendarDates = (dates) => {
|
|
28
|
+
if (dates.length === 0) throw new RangeError("A resource calendar needs at least one date column");
|
|
29
|
+
const first = dates.at(0);
|
|
30
|
+
if (first === void 0 || toUTCDate(first) === null) throw new RangeError("Resource calendar date columns must be consecutive normalized dates");
|
|
31
|
+
for (let index = 1; index < dates.length; index += 1) {
|
|
32
|
+
const previous = dates.at(index - 1);
|
|
33
|
+
const current = dates.at(index);
|
|
34
|
+
if (previous === void 0 || current === void 0 || differenceInCalendarDays(current, previous) !== 1) throw new RangeError("Resource calendar date columns must be consecutive normalized dates");
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const nextCalendarDate = (value) => {
|
|
38
|
+
const date = toUTCDate(value);
|
|
39
|
+
if (date === null) throw new RangeError("Calendar dates must use normalized YYYY-MM-DD values");
|
|
40
|
+
date.setUTCDate(date.getUTCDate() + 1);
|
|
41
|
+
const nextDate = date.toISOString().slice(0, 10);
|
|
42
|
+
if (toUTCDate(nextDate) === null) throw new RangeError("Calendar dates must have a following normalized YYYY-MM-DD value");
|
|
43
|
+
return nextDate;
|
|
44
|
+
};
|
|
45
|
+
const layoutResourceCalendarEntries = (entries, visibleRange) => {
|
|
46
|
+
const visibleEntries = [];
|
|
47
|
+
const entryIds = /* @__PURE__ */ new Set();
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (entryIds.has(entry.id)) throw new TypeError("Resource calendar entry ids must be unique");
|
|
50
|
+
entryIds.add(entry.id);
|
|
51
|
+
const placement = getResourceCalendarPlacement({
|
|
52
|
+
entry,
|
|
53
|
+
visibleRange
|
|
54
|
+
});
|
|
55
|
+
if (placement === null) continue;
|
|
56
|
+
visibleEntries.push({
|
|
57
|
+
columnStart: placement.columnStart,
|
|
58
|
+
entryId: entry.id,
|
|
59
|
+
span: placement.span
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
visibleEntries.sort((left, right) => {
|
|
63
|
+
const startDifference = left.columnStart - right.columnStart;
|
|
64
|
+
if (startDifference !== 0) return startDifference;
|
|
65
|
+
if (left.entryId < right.entryId) return -1;
|
|
66
|
+
if (left.entryId > right.entryId) return 1;
|
|
67
|
+
return 0;
|
|
68
|
+
});
|
|
69
|
+
const laneEnds = [];
|
|
70
|
+
const placements = [];
|
|
71
|
+
for (const entry of visibleEntries) {
|
|
72
|
+
const firstAvailableLane = laneEnds.findIndex((laneEnd) => laneEnd <= entry.columnStart);
|
|
73
|
+
const lane = firstAvailableLane === -1 ? laneEnds.length : firstAvailableLane;
|
|
74
|
+
laneEnds[lane] = entry.columnStart + entry.span;
|
|
75
|
+
placements.push({
|
|
76
|
+
columnStart: entry.columnStart,
|
|
77
|
+
entryId: entry.entryId,
|
|
78
|
+
rowStart: lane + 1,
|
|
79
|
+
span: entry.span
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
placements,
|
|
84
|
+
rowCount: Math.max(1, laneEnds.length)
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
export { assertConsecutiveCalendarDates, getResourceCalendarPlacement, layoutResourceCalendarEntries, nextCalendarDate };
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
declare const buttonAccessibleDisabledClass = "cursor-not-allowed opacity-64";
|
|
12
12
|
declare const buttonVariants: (props?: ({
|
|
13
|
-
size?: "
|
|
13
|
+
size?: "icon" | "sm" | "default" | "lg" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "xl" | "xs" | null | undefined;
|
|
14
14
|
variant?: "link" | "default" | "destructive" | "destructive-outline" | "ghost" | "outline" | "secondary" | null | undefined;
|
|
15
15
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
16
16
|
//#endregion
|
|
@@ -3,9 +3,13 @@ import { ReactNode, RefObject } from "react";
|
|
|
3
3
|
type OutlineItem = {
|
|
4
4
|
id: string;
|
|
5
5
|
label: string;
|
|
6
|
+
/** What the entry contains, after the label that names it. The label
|
|
7
|
+
* stays whole; the title is what truncates when the row is narrow. */
|
|
8
|
+
title?: string;
|
|
6
9
|
/** Nesting depth among included items; drives indent + tick taper. */
|
|
7
10
|
level: number;
|
|
8
|
-
/** Optional trailing annotation in the panel (e.g. a page number
|
|
11
|
+
/** Optional trailing annotation in the panel (e.g. a page number or a
|
|
12
|
+
* provision range); never truncated. */
|
|
9
13
|
meta?: string;
|
|
10
14
|
/** Optional CSS custom-property name colouring this entry's tick + chip
|
|
11
15
|
* (e.g. "--option-blue"). Defaults to the neutral foreground. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/utils.js";
|
|
3
3
|
import { Tooltip, TooltipContent as TooltipPopup, TooltipTrigger } from "./tooltip.js";
|
|
4
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
6
6
|
//#region src/components/outline-rail.tsx
|
|
7
7
|
/**
|
|
@@ -23,6 +23,8 @@ const TICK_BASE_WIDTH = 6;
|
|
|
23
23
|
const TICK_LEVEL_STEP = 2;
|
|
24
24
|
const TICK_MAX_LEVEL = 5;
|
|
25
25
|
const RAIL_MAX_TICKS = 40;
|
|
26
|
+
/** The entry as one line of text: label, then its title when it has one. */
|
|
27
|
+
const entryText = (item) => item.title === void 0 ? item.label : `${item.label} ${item.title}`;
|
|
26
28
|
const tickWidth = (level) => {
|
|
27
29
|
return TICK_BASE_WIDTH + (TICK_MAX_LEVEL - Math.min(Math.max(level, 0), TICK_MAX_LEVEL)) * TICK_LEVEL_STEP;
|
|
28
30
|
};
|
|
@@ -199,7 +201,8 @@ const OutlineRail = ({ items, scrollContainerRef, resolvePct, onJump, activeId,
|
|
|
199
201
|
}
|
|
200
202
|
onJumpRef.current(id, container);
|
|
201
203
|
}, [activeId, scrollContainerRef]);
|
|
202
|
-
const toggleCollapse = useCallback((id,
|
|
204
|
+
const toggleCollapse = useCallback((id, rowEl) => {
|
|
205
|
+
const rowTop = rowEl?.getBoundingClientRect().top;
|
|
203
206
|
setToggled((prev) => new Set(prev).add(id));
|
|
204
207
|
setCollapsed((prev) => {
|
|
205
208
|
const next = new Set(prev);
|
|
@@ -209,9 +212,8 @@ const OutlineRail = ({ items, scrollContainerRef, resolvePct, onJump, activeId,
|
|
|
209
212
|
});
|
|
210
213
|
requestAnimationFrame(() => {
|
|
211
214
|
const panel = panelRef.current;
|
|
212
|
-
if (!panel || !rowEl) return;
|
|
213
|
-
|
|
214
|
-
panel.scrollTop += rowEl.getBoundingClientRect().top - target;
|
|
215
|
+
if (!panel || !rowEl || rowTop === void 0) return;
|
|
216
|
+
panel.scrollTop += rowEl.getBoundingClientRect().top - rowTop;
|
|
215
217
|
});
|
|
216
218
|
}, []);
|
|
217
219
|
const openPanel = useCallback(() => {
|
|
@@ -261,7 +263,7 @@ const OutlineRail = ({ items, scrollContainerRef, resolvePct, onJump, activeId,
|
|
|
261
263
|
"aria-expanded": !isCollapsed,
|
|
262
264
|
"aria-label": isCollapsed ? "Expand" : "Collapse",
|
|
263
265
|
className: "text-muted-foreground hover:text-foreground flex size-5 shrink-0 items-center justify-center",
|
|
264
|
-
onClick: (event) => toggleCollapse(node.item.id,
|
|
266
|
+
onClick: (event) => toggleCollapse(node.item.id, event.currentTarget.parentElement),
|
|
265
267
|
style: { marginInlineStart: indent - 4 },
|
|
266
268
|
type: "button",
|
|
267
269
|
children: /* @__PURE__ */ jsx(Chevron, { open: !isCollapsed })
|
|
@@ -277,12 +279,21 @@ const OutlineRail = ({ items, scrollContainerRef, resolvePct, onJump, activeId,
|
|
|
277
279
|
}),
|
|
278
280
|
/* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
279
281
|
render: /* @__PURE__ */ jsx("button", {
|
|
280
|
-
className: cn("min-w-0 flex-1
|
|
282
|
+
className: cn("flex min-w-0 flex-1 items-baseline gap-1.5 py-1.5 text-start text-[13px] leading-snug", rowTextClass(isActive, hasChildren)),
|
|
281
283
|
onClick: () => jumpTo(node.item.id),
|
|
282
284
|
type: "button"
|
|
283
285
|
}),
|
|
284
|
-
children: node.item.
|
|
285
|
-
|
|
286
|
+
children: node.item.title === void 0 ? /* @__PURE__ */ jsx("span", {
|
|
287
|
+
className: "min-w-0 truncate",
|
|
288
|
+
children: node.item.label
|
|
289
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
290
|
+
className: "shrink-0 font-medium",
|
|
291
|
+
children: node.item.label
|
|
292
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
293
|
+
className: "min-w-0 truncate font-normal",
|
|
294
|
+
children: node.item.title
|
|
295
|
+
})] })
|
|
296
|
+
}), /* @__PURE__ */ jsx(TooltipPopup, { children: entryText(node.item) })] }),
|
|
286
297
|
node.item.meta !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
287
298
|
className: "text-foreground-placeholder shrink-0 ps-2 text-[11px] tabular-nums",
|
|
288
299
|
children: node.item.meta
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { TableColumnCapabilities, TableColumnDescriptor, TableSchema, duplicateColumnIds, findTableColumn, hideableColumnIds, sortableColumnIds, tableColumnIds, tableColumnSizing, visibleColumnIds } from "./schema.js";
|
|
2
|
+
export { type TableColumnCapabilities, type TableColumnDescriptor, type TableSchema, duplicateColumnIds, findTableColumn, hideableColumnIds, sortableColumnIds, tableColumnIds, tableColumnSizing, visibleColumnIds };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { duplicateColumnIds, findTableColumn, hideableColumnIds, sortableColumnIds, tableColumnIds, tableColumnSizing, visibleColumnIds } from "./schema.js";
|
|
2
|
+
export { duplicateColumnIds, findTableColumn, hideableColumnIds, sortableColumnIds, tableColumnIds, tableColumnSizing, visibleColumnIds };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//#region src/data-table/schema.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A table's schema: which columns exist, what each can do, and how wide it
|
|
4
|
+
* starts. Not how a cell draws — that is the caller's, and it is the reason
|
|
5
|
+
* this module holds no React.
|
|
6
|
+
*
|
|
7
|
+
* The split matters because "which columns does this view have, and which of
|
|
8
|
+
* them can be sorted, hidden, resized or pinned" is a question about data, and
|
|
9
|
+
* a question about data can be answered by a test. It used to be answerable
|
|
10
|
+
* only by rendering the table and asking TanStack.
|
|
11
|
+
*/
|
|
12
|
+
/** What a reader may do to a column. */
|
|
13
|
+
type TableColumnCapabilities = {
|
|
14
|
+
sort: boolean;
|
|
15
|
+
hide: boolean;
|
|
16
|
+
resize: boolean;
|
|
17
|
+
pin: boolean;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* One column.
|
|
21
|
+
*
|
|
22
|
+
* `render` is whatever the caller needs to draw the column: this module never
|
|
23
|
+
* looks inside it, which is what keeps the schema free of any idea about what
|
|
24
|
+
* a row holds.
|
|
25
|
+
*/
|
|
26
|
+
type TableColumnDescriptor<TRender> = {
|
|
27
|
+
/** Unique within the schema, and stable across renders. */
|
|
28
|
+
id: string;
|
|
29
|
+
/** Header text. Empty for a column whose header draws no text. */
|
|
30
|
+
label: string;
|
|
31
|
+
render: TRender;
|
|
32
|
+
/** Starting width, in pixels. */
|
|
33
|
+
size: number;
|
|
34
|
+
/** Narrowest this column may be resized to; the schema's default if absent. */
|
|
35
|
+
minSize?: number | undefined;
|
|
36
|
+
capabilities: TableColumnCapabilities;
|
|
37
|
+
/**
|
|
38
|
+
* Metadata columns read quieter than content ones; a utility column
|
|
39
|
+
* (a selection checkbox, an add-column affordance) draws no value at all.
|
|
40
|
+
*/
|
|
41
|
+
emphasis: "content" | "metadata" | "utility";
|
|
42
|
+
};
|
|
43
|
+
type TableSchema<TRender> = {
|
|
44
|
+
columns: readonly TableColumnDescriptor<TRender>[];
|
|
45
|
+
/** Narrowest any column may be resized to. */
|
|
46
|
+
defaultMinSize: number;
|
|
47
|
+
};
|
|
48
|
+
/** Every column a schema declares, in order. */
|
|
49
|
+
declare const tableColumnIds: <TRender>(schema: TableSchema<TRender>) => string[];
|
|
50
|
+
declare const sortableColumnIds: <TRender>(schema: TableSchema<TRender>) => string[];
|
|
51
|
+
declare const hideableColumnIds: <TRender>(schema: TableSchema<TRender>) => string[];
|
|
52
|
+
declare const findTableColumn: <TRender>(schema: TableSchema<TRender>, id: string) => TableColumnDescriptor<TRender> | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Which columns a view shows.
|
|
55
|
+
*
|
|
56
|
+
* A column that cannot be hidden stays visible whatever the stored hidden list
|
|
57
|
+
* says, so a stale list — a column that lost its hide capability while it was
|
|
58
|
+
* hidden — cannot strand the table without its select or name column.
|
|
59
|
+
*/
|
|
60
|
+
declare const visibleColumnIds: <TRender>(schema: TableSchema<TRender>, hiddenColumnIds: readonly string[]) => string[];
|
|
61
|
+
/** The starting width of every column, keyed by id. */
|
|
62
|
+
declare const tableColumnSizing: <TRender>(schema: TableSchema<TRender>) => Record<string, number>;
|
|
63
|
+
/**
|
|
64
|
+
* A duplicate column id silently drops a column: the table keys by id, so the
|
|
65
|
+
* second declaration wins and the first disappears with no error anywhere.
|
|
66
|
+
* Callers that build a schema from user data (a property list) check here.
|
|
67
|
+
*/
|
|
68
|
+
declare const duplicateColumnIds: <TRender>(schema: TableSchema<TRender>) => string[];
|
|
69
|
+
//#endregion
|
|
70
|
+
export { TableColumnCapabilities, TableColumnDescriptor, TableSchema, duplicateColumnIds, findTableColumn, hideableColumnIds, sortableColumnIds, tableColumnIds, tableColumnSizing, visibleColumnIds };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/data-table/schema.ts
|
|
2
|
+
/** Every column a schema declares, in order. */
|
|
3
|
+
const tableColumnIds = (schema) => schema.columns.map((column) => column.id);
|
|
4
|
+
const withCapability = (schema, capability) => schema.columns.filter((column) => column.capabilities[capability]).map((column) => column.id);
|
|
5
|
+
const sortableColumnIds = (schema) => withCapability(schema, "sort");
|
|
6
|
+
const hideableColumnIds = (schema) => withCapability(schema, "hide");
|
|
7
|
+
const findTableColumn = (schema, id) => schema.columns.find((column) => column.id === id);
|
|
8
|
+
/**
|
|
9
|
+
* Which columns a view shows.
|
|
10
|
+
*
|
|
11
|
+
* A column that cannot be hidden stays visible whatever the stored hidden list
|
|
12
|
+
* says, so a stale list — a column that lost its hide capability while it was
|
|
13
|
+
* hidden — cannot strand the table without its select or name column.
|
|
14
|
+
*/
|
|
15
|
+
const visibleColumnIds = (schema, hiddenColumnIds) => {
|
|
16
|
+
const hidden = new Set(hiddenColumnIds);
|
|
17
|
+
return schema.columns.filter((column) => !column.capabilities.hide || !hidden.has(column.id)).map((column) => column.id);
|
|
18
|
+
};
|
|
19
|
+
/** The starting width of every column, keyed by id. */
|
|
20
|
+
const tableColumnSizing = (schema) => {
|
|
21
|
+
const sizing = {};
|
|
22
|
+
for (const column of schema.columns) sizing[column.id] = column.size;
|
|
23
|
+
return sizing;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A duplicate column id silently drops a column: the table keys by id, so the
|
|
27
|
+
* second declaration wins and the first disappears with no error anywhere.
|
|
28
|
+
* Callers that build a schema from user data (a property list) check here.
|
|
29
|
+
*/
|
|
30
|
+
const duplicateColumnIds = (schema) => {
|
|
31
|
+
const seen = /* @__PURE__ */ new Set();
|
|
32
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
33
|
+
for (const column of schema.columns) {
|
|
34
|
+
if (seen.has(column.id)) duplicates.add(column.id);
|
|
35
|
+
seen.add(column.id);
|
|
36
|
+
}
|
|
37
|
+
return [...duplicates];
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { duplicateColumnIds, findTableColumn, hideableColumnIds, sortableColumnIds, tableColumnIds, tableColumnSizing, visibleColumnIds };
|