sid-ui 0.0.0-stage → 0.1.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 +54 -2
- package/package.json +51 -4
- package/src/brand/BrandFooter.tsx +45 -0
- package/src/brand/BrandHeading.tsx +24 -0
- package/src/brand/BrandLockup.tsx +47 -0
- package/src/brand/JoinedLogo.tsx +67 -0
- package/src/brand/JoinedMark.tsx +76 -0
- package/src/brand/geometry.ts +26 -0
- package/src/brand/index.ts +11 -0
- package/src/brand/name.ts +5 -0
- package/src/brand/paint.tsx +56 -0
- package/src/components/Action.tsx +44 -0
- package/src/components/AddressSelector.tsx +99 -0
- package/src/components/BarChart.tsx +132 -0
- package/src/components/Calendar.tsx +284 -0
- package/src/components/CalendarAgenda.tsx +87 -0
- package/src/components/CalendarMonth.tsx +223 -0
- package/src/components/CalendarSchedule.tsx +210 -0
- package/src/components/Card.tsx +80 -0
- package/src/components/Charts.tsx +64 -0
- package/src/components/Chat.tsx +35 -0
- package/src/components/CitySelector.tsx +81 -0
- package/src/components/Clock.tsx +196 -0
- package/src/components/CodeInput.tsx +183 -0
- package/src/components/Container.tsx +20 -0
- package/src/components/Content.tsx +50 -0
- package/src/components/Data.tsx +43 -0
- package/src/components/DataInput.tsx +107 -0
- package/src/components/DateField.tsx +162 -0
- package/src/components/DonutChart.tsx +123 -0
- package/src/components/Drawer.tsx +110 -0
- package/src/components/Feedback.tsx +37 -0
- package/src/components/FileUploader.tsx +377 -0
- package/src/components/FunnelChart.tsx +83 -0
- package/src/components/Glyph.tsx +330 -0
- package/src/components/GoogleSignIn.tsx +67 -0
- package/src/components/GridSystem.tsx +131 -0
- package/src/components/HeatmapCalendar.tsx +167 -0
- package/src/components/KanbanBoard.tsx +421 -0
- package/src/components/Kpi.tsx +61 -0
- package/src/components/LayoutPrimitives.tsx +86 -0
- package/src/components/Lists.tsx +19 -0
- package/src/components/LocationSelector.tsx +49 -0
- package/src/components/MessageList.tsx +80 -0
- package/src/components/Nav.tsx +69 -0
- package/src/components/Navigation.tsx +52 -0
- package/src/components/Notification.tsx +115 -0
- package/src/components/NotificationTrigger.tsx +248 -0
- package/src/components/Overlay.tsx +50 -0
- package/src/components/Page.tsx +143 -0
- package/src/components/PageTabs.tsx +28 -0
- package/src/components/PickerShell.tsx +138 -0
- package/src/components/PillNav.tsx +194 -0
- package/src/components/Primitives.tsx +30 -0
- package/src/components/Rating.tsx +78 -0
- package/src/components/Responsive.tsx +128 -0
- package/src/components/ResponsiveFrame.tsx +122 -0
- package/src/components/Segment.tsx +190 -0
- package/src/components/Sparkline.tsx +52 -0
- package/src/components/StateSelector.tsx +39 -0
- package/src/components/Sticky.tsx +95 -0
- package/src/components/Table.tsx +344 -0
- package/src/components/ThemeToggle.tsx +48 -0
- package/src/components/Tile.tsx +23 -0
- package/src/components/TimeColumns.tsx +155 -0
- package/src/components/TimeDial.tsx +260 -0
- package/src/components/TimeField.tsx +196 -0
- package/src/components/TimeInput.tsx +213 -0
- package/src/components/TimeSlots.tsx +83 -0
- package/src/components/Timeline.tsx +111 -0
- package/src/components/TokenDemo.tsx +378 -0
- package/src/components/Tree.tsx +283 -0
- package/src/components/TrendChart.tsx +211 -0
- package/src/components/breakpoints.ts +42 -0
- package/src/components/calendarTypes.ts +26 -0
- package/src/components/chartMath.ts +125 -0
- package/src/components/chartParts.tsx +116 -0
- package/src/components/codeInputRules.ts +58 -0
- package/src/components/date.ts +87 -0
- package/src/components/hooks.ts +53 -0
- package/src/components/index.ts +195 -0
- package/src/components/kanban.ts +78 -0
- package/src/components/motion.ts +78 -0
- package/src/components/places-search.tsx +73 -0
- package/src/components/places.ts +209 -0
- package/src/components/size.ts +3 -0
- package/src/components/time.ts +83 -0
- package/src/components/useFileDrop.ts +104 -0
- package/src/places/geoapify.ts +129 -0
- package/src/styles/astryx.css +5 -0
- package/src/styles/components/brand.css +98 -0
- package/src/styles/components/calendar.css +680 -0
- package/src/styles/components/charts.css +659 -0
- package/src/styles/components/code-input.css +233 -0
- package/src/styles/components/google.css +11 -0
- package/src/styles/components/message-list.css +153 -0
- package/src/styles/components/notification.css +350 -0
- package/src/styles/components/picker.css +114 -0
- package/src/styles/components/pill-nav.css +179 -0
- package/src/styles/components/responsive.css +419 -0
- package/src/styles/components/sticky.css +23 -0
- package/src/styles/components/table.css +267 -0
- package/src/styles/components/time.css +526 -0
- package/src/styles/components/timeline.css +426 -0
- package/src/styles/components/tree.css +269 -0
- package/src/styles/components.css +423 -0
- package/src/styles/joined.css +4 -0
- package/src/styles/theme.css +79 -0
- package/src/styles/tokens.css +338 -0
- package/src/theme/JoinedProvider.tsx +50 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/joined.d.ts +11 -0
- package/src/theme/joined.js +569 -0
- package/src/theme/joined.theme.ts +74 -0
- package/src/theme/joined.variants.d.ts +16 -0
- package/src/theme/theme.css +786 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type DragEvent,
|
|
8
|
+
type KeyboardEvent,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
|
|
12
|
+
import { Badge } from "./Feedback";
|
|
13
|
+
import { icons } from "./Glyph";
|
|
14
|
+
import {
|
|
15
|
+
cellItems,
|
|
16
|
+
moveKanbanItem,
|
|
17
|
+
slotOf,
|
|
18
|
+
type KanbanColumn,
|
|
19
|
+
type KanbanItemBase,
|
|
20
|
+
type KanbanLane,
|
|
21
|
+
type KanbanMove,
|
|
22
|
+
type KanbanSlot,
|
|
23
|
+
} from "./kanban";
|
|
24
|
+
import { ScrollableArea } from "./LayoutPrimitives";
|
|
25
|
+
import { VisuallyHidden } from "./Lists";
|
|
26
|
+
import { Card, HStack, Heading, Icon, Stack, Text } from "./Primitives";
|
|
27
|
+
|
|
28
|
+
export interface KanbanItemState {
|
|
29
|
+
/** Being dragged with the pointer. */
|
|
30
|
+
isDragging: boolean;
|
|
31
|
+
/** Picked up with the keyboard (Space) and moving with the arrow keys. */
|
|
32
|
+
isGrabbed: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface KanbanBoardProps<T extends KanbanItemBase> {
|
|
36
|
+
/** Names the board for assistive tech. */
|
|
37
|
+
label: string;
|
|
38
|
+
columns: KanbanColumn[];
|
|
39
|
+
/** Every item on the board; order within a column is array order. */
|
|
40
|
+
items: T[];
|
|
41
|
+
onItemsChange: (items: T[], move: KanbanMove) => void;
|
|
42
|
+
renderItem: (item: T, state: KanbanItemState) => ReactNode;
|
|
43
|
+
/** Short name for announcements, e.g. the ticket key and title. */
|
|
44
|
+
getItemLabel: (item: T) => string;
|
|
45
|
+
/** Rows across the board — by assignee, epic, or priority. */
|
|
46
|
+
lanes?: KanbanLane[];
|
|
47
|
+
/** Veto a drop, e.g. only QA can move to Done. */
|
|
48
|
+
canDrop?: (item: T, to: Omit<KanbanSlot, "index">) => boolean;
|
|
49
|
+
/** Extra actions in each column header. */
|
|
50
|
+
renderColumnActions?: (column: KanbanColumn) => ReactNode;
|
|
51
|
+
/** Content under each cell's cards — an "Add card" button. */
|
|
52
|
+
renderCellFooter?: (column: KanbanColumn, laneId?: string) => ReactNode;
|
|
53
|
+
/** Shown in an empty cell. */
|
|
54
|
+
emptyText?: string;
|
|
55
|
+
columnWidth?: number;
|
|
56
|
+
/** Caps each cell's height and scrolls its cards. */
|
|
57
|
+
maxCellHeight?: number;
|
|
58
|
+
isDisabled?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type Target = { columnId: string; laneId?: string; index: number; allowed: boolean };
|
|
62
|
+
|
|
63
|
+
const COLUMN_WIDTH = 280;
|
|
64
|
+
const MIN_CELL_HEIGHT = 72;
|
|
65
|
+
const DRAGGING_OPACITY = 0.4;
|
|
66
|
+
const KEY_HINT =
|
|
67
|
+
"Press Space to pick up. Arrow keys move — Left and Right between columns, Up and Down within one, Shift with Up and Down between lanes. Space drops, Escape cancels.";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A Jira-style board. Drag cards between columns and lanes with the pointer,
|
|
71
|
+
* or pick one up with Space and move it with the arrow keys. Columns show
|
|
72
|
+
* counts and WIP limits; lanes group rows by anything you like.
|
|
73
|
+
*/
|
|
74
|
+
export function KanbanBoard<T extends KanbanItemBase>({
|
|
75
|
+
label,
|
|
76
|
+
columns,
|
|
77
|
+
items,
|
|
78
|
+
onItemsChange,
|
|
79
|
+
renderItem,
|
|
80
|
+
getItemLabel,
|
|
81
|
+
lanes,
|
|
82
|
+
canDrop,
|
|
83
|
+
renderColumnActions,
|
|
84
|
+
renderCellFooter,
|
|
85
|
+
emptyText = "Drop cards here",
|
|
86
|
+
columnWidth = COLUMN_WIDTH,
|
|
87
|
+
maxCellHeight,
|
|
88
|
+
isDisabled = false,
|
|
89
|
+
}: KanbanBoardProps<T>) {
|
|
90
|
+
const [dragId, setDragId] = useState<string | null>(null);
|
|
91
|
+
const [dragHeight, setDragHeight] = useState(0);
|
|
92
|
+
const [target, setTarget] = useState<Target | null>(null);
|
|
93
|
+
const [grabbed, setGrabbed] = useState<{ id: string; origin: KanbanSlot } | null>(null);
|
|
94
|
+
const [announcement, setAnnouncement] = useState("");
|
|
95
|
+
const nodes = useRef(new Map<string, HTMLElement>());
|
|
96
|
+
// Read synchronously in dragover/drop, which can fire before React re-renders.
|
|
97
|
+
const dragging = useRef<string | null>(null);
|
|
98
|
+
const refocus = useRef<string | null>(null);
|
|
99
|
+
const hintId = `${label.replace(/\s+/g, "-").toLowerCase()}-kanban-hint`;
|
|
100
|
+
|
|
101
|
+
const laneIds: (string | undefined)[] = lanes?.length ? lanes.map((l) => l.id) : [undefined];
|
|
102
|
+
const columnById = new Map(columns.map((c) => [c.id, c]));
|
|
103
|
+
const titleOf = (columnId: string, laneId?: string) =>
|
|
104
|
+
[columnById.get(columnId)?.title, lanes?.find((l) => l.id === laneId)?.title]
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.join(", ");
|
|
107
|
+
|
|
108
|
+
// Keep keyboard focus on a card after it moves to another column (it remounts).
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!refocus.current) return;
|
|
111
|
+
nodes.current.get(refocus.current)?.focus();
|
|
112
|
+
refocus.current = null;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const allowed = (item: T, columnId: string, laneId?: string) =>
|
|
116
|
+
!columnById.get(columnId)?.isLocked && (canDrop ? canDrop(item, { columnId, laneId }) : true);
|
|
117
|
+
|
|
118
|
+
const commit = (itemId: string, to: KanbanSlot, focus = false) => {
|
|
119
|
+
const from = slotOf(items, itemId);
|
|
120
|
+
if (!from) return;
|
|
121
|
+
const next = moveKanbanItem(items, itemId, to);
|
|
122
|
+
const landed = slotOf(next, itemId) ?? to;
|
|
123
|
+
if (focus) refocus.current = itemId;
|
|
124
|
+
onItemsChange(next, { itemId, from, to: landed });
|
|
125
|
+
return landed;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const reset = () => {
|
|
129
|
+
dragging.current = null;
|
|
130
|
+
setDragId(null);
|
|
131
|
+
setTarget(null);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// ---- pointer drag and drop ----
|
|
135
|
+
const onCardDragStart = (event: DragEvent<HTMLElement>, item: T) => {
|
|
136
|
+
if (isDisabled) return;
|
|
137
|
+
event.dataTransfer.setData("text/plain", item.id);
|
|
138
|
+
event.dataTransfer.effectAllowed = "move";
|
|
139
|
+
setDragHeight(event.currentTarget.offsetHeight);
|
|
140
|
+
dragging.current = item.id;
|
|
141
|
+
setDragId(item.id);
|
|
142
|
+
setAnnouncement(`Picked up ${getItemLabel(item)}.`);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const onCellDragOver = (event: DragEvent<HTMLElement>, columnId: string, laneId?: string) => {
|
|
146
|
+
const id = dragging.current;
|
|
147
|
+
if (!id) return;
|
|
148
|
+
const item = items.find((i) => i.id === id);
|
|
149
|
+
if (!item) return;
|
|
150
|
+
const ok = allowed(item, columnId, laneId);
|
|
151
|
+
if (ok) {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
event.dataTransfer.dropEffect = "move";
|
|
154
|
+
}
|
|
155
|
+
const cards: HTMLElement[] = Array.from(
|
|
156
|
+
event.currentTarget.querySelectorAll<HTMLElement>(
|
|
157
|
+
"[data-kanban-card]",
|
|
158
|
+
) as ArrayLike<HTMLElement>,
|
|
159
|
+
).filter((el) => el.dataset.kanbanCard !== id);
|
|
160
|
+
const index = cards.findIndex((el) => {
|
|
161
|
+
const box = el.getBoundingClientRect();
|
|
162
|
+
return event.clientY < box.top + box.height / 2;
|
|
163
|
+
});
|
|
164
|
+
const next = { columnId, laneId, index: index === -1 ? cards.length : index, allowed: ok };
|
|
165
|
+
if (
|
|
166
|
+
!target ||
|
|
167
|
+
target.columnId !== next.columnId ||
|
|
168
|
+
target.laneId !== next.laneId ||
|
|
169
|
+
target.index !== next.index ||
|
|
170
|
+
target.allowed !== next.allowed
|
|
171
|
+
)
|
|
172
|
+
setTarget(next);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const onCellDrop = (event: DragEvent<HTMLElement>, columnId: string, laneId?: string) => {
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
const id = dragging.current;
|
|
178
|
+
const item = id ? items.find((i) => i.id === id) : undefined;
|
|
179
|
+
// Fall back to the end of the cell if no dragover landed a target yet.
|
|
180
|
+
const slot =
|
|
181
|
+
target && target.columnId === columnId && target.laneId === laneId
|
|
182
|
+
? target
|
|
183
|
+
: {
|
|
184
|
+
columnId,
|
|
185
|
+
laneId,
|
|
186
|
+
index: cellItems(items, columnId, laneId).length,
|
|
187
|
+
allowed: item ? allowed(item, columnId, laneId) : false,
|
|
188
|
+
};
|
|
189
|
+
if (id && item && slot.allowed) {
|
|
190
|
+
const landed = commit(id, slot);
|
|
191
|
+
if (item && landed)
|
|
192
|
+
setAnnouncement(
|
|
193
|
+
`Moved ${getItemLabel(item)} to ${titleOf(landed.columnId, landed.laneId)}, position ${landed.index + 1}.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
reset();
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// ---- keyboard move ----
|
|
200
|
+
const onCardKeyDown = (event: KeyboardEvent<HTMLElement>, item: T) => {
|
|
201
|
+
if (isDisabled || event.target !== event.currentTarget) return;
|
|
202
|
+
const name = getItemLabel(item);
|
|
203
|
+
const here = slotOf(items, item.id);
|
|
204
|
+
if (!here) return;
|
|
205
|
+
|
|
206
|
+
if (event.key === " " || event.key === "Enter") {
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
if (grabbed?.id === item.id) {
|
|
209
|
+
setGrabbed(null);
|
|
210
|
+
setAnnouncement(
|
|
211
|
+
`Dropped ${name} in ${titleOf(here.columnId, here.laneId)}, position ${here.index + 1}.`,
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
setGrabbed({ id: item.id, origin: here });
|
|
215
|
+
setAnnouncement(`Picked up ${name}. ${KEY_HINT}`);
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (grabbed?.id !== item.id) return;
|
|
220
|
+
|
|
221
|
+
if (event.key === "Escape") {
|
|
222
|
+
event.preventDefault();
|
|
223
|
+
commit(item.id, grabbed.origin, true);
|
|
224
|
+
setGrabbed(null);
|
|
225
|
+
setAnnouncement(
|
|
226
|
+
`Cancelled. ${name} is back in ${titleOf(grabbed.origin.columnId, grabbed.origin.laneId)}.`,
|
|
227
|
+
);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const colIndex = columns.findIndex((c) => c.id === here.columnId);
|
|
232
|
+
const laneIndex = laneIds.indexOf(here.laneId);
|
|
233
|
+
let to: KanbanSlot | null = null;
|
|
234
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
235
|
+
const step = event.key === "ArrowLeft" ? -1 : 1;
|
|
236
|
+
for (let c = colIndex + step; c >= 0 && c < columns.length; c += step) {
|
|
237
|
+
if (allowed(item, columns[c].id, here.laneId)) {
|
|
238
|
+
to = { columnId: columns[c].id, laneId: here.laneId, index: here.index };
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} else if (
|
|
243
|
+
(event.key === "ArrowUp" || event.key === "ArrowDown") &&
|
|
244
|
+
event.shiftKey &&
|
|
245
|
+
lanes?.length
|
|
246
|
+
) {
|
|
247
|
+
const nextLane = laneIds[laneIndex + (event.key === "ArrowUp" ? -1 : 1)];
|
|
248
|
+
if (nextLane !== undefined && allowed(item, here.columnId, nextLane))
|
|
249
|
+
to = { columnId: here.columnId, laneId: nextLane, index: here.index };
|
|
250
|
+
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
251
|
+
const size = cellItems(items, here.columnId, here.laneId).length;
|
|
252
|
+
const index = here.index + (event.key === "ArrowUp" ? -1 : 1);
|
|
253
|
+
if (index >= 0 && index < size) to = { ...here, index };
|
|
254
|
+
}
|
|
255
|
+
if (!to) return;
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
const landed = commit(item.id, to, true);
|
|
258
|
+
if (landed)
|
|
259
|
+
setAnnouncement(
|
|
260
|
+
`${name}: ${titleOf(landed.columnId, landed.laneId)}, position ${landed.index + 1}.`,
|
|
261
|
+
);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// ---- rendering ----
|
|
265
|
+
const renderCell = (column: KanbanColumn, laneId?: string) => {
|
|
266
|
+
const cards = cellItems(items, column.id, laneId);
|
|
267
|
+
const isTarget = dragId !== null && target?.columnId === column.id && target?.laneId === laneId;
|
|
268
|
+
const blocked = isTarget && !target?.allowed;
|
|
269
|
+
const placeholder = (
|
|
270
|
+
<Card key="kanban-placeholder" variant="blue" height={dragHeight} padding={0} />
|
|
271
|
+
);
|
|
272
|
+
const visible = cards.filter((c) => c.id !== dragId);
|
|
273
|
+
|
|
274
|
+
let list: ReactNode[] = cards.map((item) => {
|
|
275
|
+
const isDragging = item.id === dragId;
|
|
276
|
+
const node = (
|
|
277
|
+
<div
|
|
278
|
+
key={item.id}
|
|
279
|
+
ref={(el) => {
|
|
280
|
+
if (el) nodes.current.set(item.id, el);
|
|
281
|
+
else nodes.current.delete(item.id);
|
|
282
|
+
}}
|
|
283
|
+
data-kanban-card={item.id}
|
|
284
|
+
draggable={!isDisabled}
|
|
285
|
+
tabIndex={isDisabled ? undefined : 0}
|
|
286
|
+
role="listitem"
|
|
287
|
+
aria-roledescription="draggable card"
|
|
288
|
+
aria-describedby={hintId}
|
|
289
|
+
aria-label={getItemLabel(item)}
|
|
290
|
+
onDragStart={(e) => onCardDragStart(e, item)}
|
|
291
|
+
onDragEnd={reset}
|
|
292
|
+
onKeyDown={(e) => onCardKeyDown(e, item)}
|
|
293
|
+
onBlur={() => grabbed?.id === item.id && refocus.current !== item.id && setGrabbed(null)}
|
|
294
|
+
style={isDragging ? { opacity: DRAGGING_OPACITY } : undefined}
|
|
295
|
+
>
|
|
296
|
+
{renderItem(item, { isDragging, isGrabbed: grabbed?.id === item.id })}
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
return node;
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
if (isTarget && target?.allowed) {
|
|
303
|
+
const anchor = visible[target.index];
|
|
304
|
+
const at = anchor
|
|
305
|
+
? list.findIndex((n) => (n as { key?: string }).key === anchor.id)
|
|
306
|
+
: list.length;
|
|
307
|
+
list = [...list.slice(0, at), placeholder, ...list.slice(at)];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<Card
|
|
312
|
+
key={`${column.id}:${laneId ?? ""}`}
|
|
313
|
+
className="os-kanban-cell"
|
|
314
|
+
data-kanban-column={column.id}
|
|
315
|
+
variant={blocked ? "red" : "muted"}
|
|
316
|
+
padding={2}
|
|
317
|
+
width={columnWidth}
|
|
318
|
+
minHeight={MIN_CELL_HEIGHT}
|
|
319
|
+
>
|
|
320
|
+
<div
|
|
321
|
+
role="list"
|
|
322
|
+
aria-label={titleOf(column.id, laneId)}
|
|
323
|
+
onDragOver={(e) => onCellDragOver(e, column.id, laneId)}
|
|
324
|
+
onDragLeave={(e) => {
|
|
325
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setTarget(null);
|
|
326
|
+
}}
|
|
327
|
+
onDrop={(e) => onCellDrop(e, column.id, laneId)}
|
|
328
|
+
>
|
|
329
|
+
<Stack
|
|
330
|
+
gap={2}
|
|
331
|
+
minHeight={MIN_CELL_HEIGHT - 16}
|
|
332
|
+
isScrollable={maxCellHeight !== undefined}
|
|
333
|
+
height={maxCellHeight}
|
|
334
|
+
>
|
|
335
|
+
{list}
|
|
336
|
+
{cards.length === 0 && !isTarget && (
|
|
337
|
+
<Stack hAlign="center" gap={1} paddingBlock={3}>
|
|
338
|
+
<Text type="supporting" color="secondary">
|
|
339
|
+
{column.isLocked ? "Locked" : emptyText}
|
|
340
|
+
</Text>
|
|
341
|
+
</Stack>
|
|
342
|
+
)}
|
|
343
|
+
{blocked && (
|
|
344
|
+
<HStack gap={1} vAlign="center" hAlign="center">
|
|
345
|
+
<Icon icon={icons.lock} size="sm" color="error" />
|
|
346
|
+
<Text type="supporting" color="secondary">
|
|
347
|
+
Can’t move here
|
|
348
|
+
</Text>
|
|
349
|
+
</HStack>
|
|
350
|
+
)}
|
|
351
|
+
{renderCellFooter?.(column, laneId)}
|
|
352
|
+
</Stack>
|
|
353
|
+
</div>
|
|
354
|
+
</Card>
|
|
355
|
+
);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const header = (
|
|
359
|
+
<HStack gap={3} wrap="nowrap">
|
|
360
|
+
{columns.map((column) => {
|
|
361
|
+
const count = items.filter((i) => i.columnId === column.id).length;
|
|
362
|
+
const over = column.limit !== undefined && count > column.limit;
|
|
363
|
+
return (
|
|
364
|
+
<HStack
|
|
365
|
+
key={column.id}
|
|
366
|
+
width={columnWidth}
|
|
367
|
+
gap={2}
|
|
368
|
+
vAlign="center"
|
|
369
|
+
hAlign="between"
|
|
370
|
+
paddingInline={2}
|
|
371
|
+
>
|
|
372
|
+
<HStack gap={2} vAlign="center">
|
|
373
|
+
{column.isLocked && <Icon icon={icons.lock} size="sm" color="secondary" />}
|
|
374
|
+
<Text weight="semibold">{column.title}</Text>
|
|
375
|
+
<Badge
|
|
376
|
+
label={column.limit !== undefined ? `${count}/${column.limit}` : count}
|
|
377
|
+
variant={over ? "warning" : "neutral"}
|
|
378
|
+
/>
|
|
379
|
+
</HStack>
|
|
380
|
+
{renderColumnActions?.(column)}
|
|
381
|
+
</HStack>
|
|
382
|
+
);
|
|
383
|
+
})}
|
|
384
|
+
</HStack>
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
return (
|
|
388
|
+
<ScrollableArea label={label} axis="inline">
|
|
389
|
+
<VisuallyHidden>
|
|
390
|
+
<span id={hintId}>{KEY_HINT}</span>
|
|
391
|
+
<span aria-live="polite">{announcement}</span>
|
|
392
|
+
</VisuallyHidden>
|
|
393
|
+
<Stack gap={3} paddingBlockEnd={2}>
|
|
394
|
+
{header}
|
|
395
|
+
{laneIds.map((laneId) => {
|
|
396
|
+
const lane = lanes?.find((l) => l.id === laneId);
|
|
397
|
+
const count =
|
|
398
|
+
laneId === undefined ? items.length : items.filter((i) => i.laneId === laneId).length;
|
|
399
|
+
return (
|
|
400
|
+
<Stack key={laneId ?? "board"} gap={2}>
|
|
401
|
+
{lane && (
|
|
402
|
+
<HStack gap={2} vAlign="center" paddingInline={2}>
|
|
403
|
+
<Heading level={5}>{lane.title}</Heading>
|
|
404
|
+
{lane.subtitle && (
|
|
405
|
+
<Text type="supporting" color="secondary">
|
|
406
|
+
{lane.subtitle}
|
|
407
|
+
</Text>
|
|
408
|
+
)}
|
|
409
|
+
<Badge label={count} />
|
|
410
|
+
</HStack>
|
|
411
|
+
)}
|
|
412
|
+
<HStack gap={3} wrap="nowrap" vAlign="stretch" className="os-kanban-row">
|
|
413
|
+
{columns.map((column) => renderCell(column, laneId))}
|
|
414
|
+
</HStack>
|
|
415
|
+
</Stack>
|
|
416
|
+
);
|
|
417
|
+
})}
|
|
418
|
+
</Stack>
|
|
419
|
+
</ScrollableArea>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Badge } from "@astryxdesign/core/Badge";
|
|
2
|
+
import { Card } from "@astryxdesign/core/Card";
|
|
3
|
+
import { HStack, Stack } from "@astryxdesign/core/Stack";
|
|
4
|
+
import { Heading, Text } from "@astryxdesign/core/Text";
|
|
5
|
+
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
|
|
8
|
+
export type KpiDelta = {
|
|
9
|
+
/** Already formatted, e.g. "+12%". */
|
|
10
|
+
value: string;
|
|
11
|
+
/** Colours the change: up is good, down is bad, flat is quiet. */
|
|
12
|
+
direction: "up" | "down" | "flat";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DELTA_VARIANT = { up: "success", down: "error", flat: "neutral" } as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A dashboard number with room for what makes it readable at a glance: a quiet label with an
|
|
19
|
+
* optional action, a change since last period, a hint, and one visual (a SegmentBar).
|
|
20
|
+
*/
|
|
21
|
+
export function KpiWidget({
|
|
22
|
+
label,
|
|
23
|
+
value,
|
|
24
|
+
delta,
|
|
25
|
+
hint,
|
|
26
|
+
action,
|
|
27
|
+
children,
|
|
28
|
+
}: {
|
|
29
|
+
label: string;
|
|
30
|
+
value: string;
|
|
31
|
+
delta?: KpiDelta;
|
|
32
|
+
hint?: string;
|
|
33
|
+
action?: ReactNode;
|
|
34
|
+
/** The visual under the number. */
|
|
35
|
+
children?: ReactNode;
|
|
36
|
+
}) {
|
|
37
|
+
return (
|
|
38
|
+
<Card padding={5}>
|
|
39
|
+
<Stack gap={3}>
|
|
40
|
+
<HStack hAlign="between" vAlign="center" gap={2}>
|
|
41
|
+
<Text type="supporting" color="secondary" display="block">
|
|
42
|
+
{label}
|
|
43
|
+
</Text>
|
|
44
|
+
{action}
|
|
45
|
+
</HStack>
|
|
46
|
+
<HStack gap={2} vAlign="end" wrap="wrap">
|
|
47
|
+
<Heading level={2} type="display-3">
|
|
48
|
+
{value}
|
|
49
|
+
</Heading>
|
|
50
|
+
{delta ? <Badge label={delta.value} variant={DELTA_VARIANT[delta.direction]} /> : null}
|
|
51
|
+
</HStack>
|
|
52
|
+
{children}
|
|
53
|
+
{hint ? (
|
|
54
|
+
<Text type="supporting" color="secondary" display="block">
|
|
55
|
+
{hint}
|
|
56
|
+
</Text>
|
|
57
|
+
) : null}
|
|
58
|
+
</Stack>
|
|
59
|
+
</Card>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout and navigation frames are original Astryx. Joined adds the
|
|
3
|
+
* responsive layer on top (GridSystem, ResponsiveStack, Show/Hide, ResponsiveFrame).
|
|
4
|
+
*/
|
|
5
|
+
export { AppShell, useAppShellMobile } from "@astryxdesign/core/AppShell";
|
|
6
|
+
export type {
|
|
7
|
+
AppShellProps,
|
|
8
|
+
AppShellVariant,
|
|
9
|
+
AppShellBreakpoint,
|
|
10
|
+
MobileNavConfig,
|
|
11
|
+
} from "@astryxdesign/core/AppShell";
|
|
12
|
+
|
|
13
|
+
export { AspectRatio } from "@astryxdesign/core/AspectRatio";
|
|
14
|
+
export type {
|
|
15
|
+
AspectRatioProps,
|
|
16
|
+
AspectRatioShape,
|
|
17
|
+
AspectRatioFit,
|
|
18
|
+
} from "@astryxdesign/core/AspectRatio";
|
|
19
|
+
|
|
20
|
+
export { Center } from "@astryxdesign/core/Center";
|
|
21
|
+
export type { CenterProps, CenterAxis } from "@astryxdesign/core/Center";
|
|
22
|
+
|
|
23
|
+
export { Divider } from "@astryxdesign/core/Divider";
|
|
24
|
+
export type { DividerProps, DividerVariant } from "@astryxdesign/core/Divider";
|
|
25
|
+
|
|
26
|
+
export { FormLayout } from "@astryxdesign/core/FormLayout";
|
|
27
|
+
export type {
|
|
28
|
+
FormLayoutProps,
|
|
29
|
+
FormLayoutDirection,
|
|
30
|
+
FormOptionality,
|
|
31
|
+
} from "@astryxdesign/core/FormLayout";
|
|
32
|
+
|
|
33
|
+
export { Grid, GridSpan } from "@astryxdesign/core/Grid";
|
|
34
|
+
export type { GridProps, GridColumns, GridAlignment, GridSpanProps } from "@astryxdesign/core/Grid";
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
Layout,
|
|
38
|
+
LayoutHeader,
|
|
39
|
+
LayoutContent,
|
|
40
|
+
LayoutFooter,
|
|
41
|
+
LayoutPanel,
|
|
42
|
+
} from "@astryxdesign/core/Layout";
|
|
43
|
+
export type {
|
|
44
|
+
LayoutProps,
|
|
45
|
+
LayoutHeight,
|
|
46
|
+
LayoutHeaderProps,
|
|
47
|
+
LayoutContentProps,
|
|
48
|
+
LayoutFooterProps,
|
|
49
|
+
LayoutPanelProps,
|
|
50
|
+
} from "@astryxdesign/core/Layout";
|
|
51
|
+
|
|
52
|
+
export { Section } from "@astryxdesign/core/Section";
|
|
53
|
+
export type { SectionProps, SectionVariant } from "@astryxdesign/core/Section";
|
|
54
|
+
|
|
55
|
+
export { ScrollableArea } from "@astryxdesign/core/ScrollableArea";
|
|
56
|
+
export type { ScrollableAreaProps } from "@astryxdesign/core/ScrollableArea";
|
|
57
|
+
|
|
58
|
+
export { ResizeHandle, useResizable, percent } from "@astryxdesign/core/Resizable";
|
|
59
|
+
export type {
|
|
60
|
+
ResizeHandleProps,
|
|
61
|
+
ResizableRegion,
|
|
62
|
+
ResizableProps,
|
|
63
|
+
} from "@astryxdesign/core/Resizable";
|
|
64
|
+
|
|
65
|
+
export { TopNav, TopNavHeading, TopNavItem, TopNavMenu } from "@astryxdesign/core/TopNav";
|
|
66
|
+
export type {
|
|
67
|
+
TopNavProps,
|
|
68
|
+
TopNavHeadingProps,
|
|
69
|
+
TopNavItemProps,
|
|
70
|
+
TopNavMenuProps,
|
|
71
|
+
} from "@astryxdesign/core/TopNav";
|
|
72
|
+
|
|
73
|
+
export {
|
|
74
|
+
SideNav,
|
|
75
|
+
SideNavHeading,
|
|
76
|
+
SideNavItem,
|
|
77
|
+
SideNavSection,
|
|
78
|
+
SideNavCollapseButton,
|
|
79
|
+
} from "@astryxdesign/core/SideNav";
|
|
80
|
+
export type {
|
|
81
|
+
SideNavProps,
|
|
82
|
+
SideNavItemProps,
|
|
83
|
+
SideNavSectionProps,
|
|
84
|
+
} from "@astryxdesign/core/SideNav";
|
|
85
|
+
|
|
86
|
+
export { useMediaQuery } from "@astryxdesign/core/hooks";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lists are original Astryx — Joined only themes them. Table, Timeline, and
|
|
3
|
+
* Tree are Joined composites that sit beside these.
|
|
4
|
+
*/
|
|
5
|
+
export { List, ListItem } from "@astryxdesign/core/List";
|
|
6
|
+
export type { ListProps, ListItemProps, ListDensity } from "@astryxdesign/core/List";
|
|
7
|
+
|
|
8
|
+
export { MetadataList, MetadataListItem } from "@astryxdesign/core/MetadataList";
|
|
9
|
+
export type {
|
|
10
|
+
MetadataListProps,
|
|
11
|
+
MetadataListItemProps,
|
|
12
|
+
MetadataListColumns,
|
|
13
|
+
} from "@astryxdesign/core/MetadataList";
|
|
14
|
+
|
|
15
|
+
export { OverflowList } from "@astryxdesign/core/OverflowList";
|
|
16
|
+
export type { OverflowListProps, OverflowItem } from "@astryxdesign/core/OverflowList";
|
|
17
|
+
|
|
18
|
+
export { VisuallyHidden } from "@astryxdesign/core/VisuallyHidden";
|
|
19
|
+
export type { VisuallyHiddenProps } from "@astryxdesign/core/VisuallyHidden";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
|
|
5
|
+
import { Typeahead } from "./DataInput";
|
|
6
|
+
import {
|
|
7
|
+
PLACES_DEBOUNCE_MS,
|
|
8
|
+
PLACES_MIN_QUERY,
|
|
9
|
+
PlacesCredit,
|
|
10
|
+
placeItem,
|
|
11
|
+
placeSearch,
|
|
12
|
+
} from "./places-search";
|
|
13
|
+
import { Stack } from "./Primitives";
|
|
14
|
+
|
|
15
|
+
/** Search a city, a state, or a country. The saved value is the chosen label. */
|
|
16
|
+
export function LocationSelector({
|
|
17
|
+
label,
|
|
18
|
+
value,
|
|
19
|
+
onChange,
|
|
20
|
+
isLabelHidden,
|
|
21
|
+
isDisabled,
|
|
22
|
+
placeholder = "City, state, or country",
|
|
23
|
+
}: {
|
|
24
|
+
label: string;
|
|
25
|
+
value: string;
|
|
26
|
+
onChange: (location: string) => void;
|
|
27
|
+
isLabelHidden?: boolean;
|
|
28
|
+
isDisabled?: boolean;
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
}) {
|
|
31
|
+
const source = useMemo(() => placeSearch("location"), []);
|
|
32
|
+
return (
|
|
33
|
+
<Stack gap={1}>
|
|
34
|
+
<Typeahead
|
|
35
|
+
label={label}
|
|
36
|
+
isLabelHidden={isLabelHidden}
|
|
37
|
+
isDisabled={isDisabled}
|
|
38
|
+
searchSource={source}
|
|
39
|
+
value={placeItem(value)}
|
|
40
|
+
onChange={(place) => onChange(place?.label ?? "")}
|
|
41
|
+
placeholder={placeholder}
|
|
42
|
+
minQueryLength={PLACES_MIN_QUERY}
|
|
43
|
+
debounceMs={PLACES_DEBOUNCE_MS}
|
|
44
|
+
emptySearchResultsText="No places match."
|
|
45
|
+
/>
|
|
46
|
+
<PlacesCredit />
|
|
47
|
+
</Stack>
|
|
48
|
+
);
|
|
49
|
+
}
|