blixify-ui-web-mcp 0.1.49 → 0.1.52
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/lib/components/data/dataTemplate/README.md +5 -4
- package/lib/components/data/dataTemplate/activityModal.stories.tsx +70 -10
- package/lib/components/data/dataTemplate/dataTemplate.stories.tsx +242 -0
- package/lib/components/display/newTable/README.md +28 -0
- package/lib/components/display/newTable/newTable.stories.tsx +73 -2
- package/lib/components/navigation/leftNav/README.md +33 -0
- package/lib/components/navigation/leftNav/leftNav.stories.tsx +133 -1
- package/package.json +1 -1
|
@@ -427,7 +427,7 @@ see the Write permission matrix below.
|
|
|
427
427
|
edit={{
|
|
428
428
|
allEditable: true,
|
|
429
429
|
listEditable: true, // inline cell editing in table view
|
|
430
|
-
listAddable: true, //
|
|
430
|
+
listAddable: true, // an empty row at the bottom of the table view to type new records into
|
|
431
431
|
deleteBtn: true,
|
|
432
432
|
}}
|
|
433
433
|
organise={{ handleNavigate }}
|
|
@@ -1114,7 +1114,7 @@ Which field types can be edited from the list table, and how. Types not listed u
|
|
|
1114
1114
|
| `enableComments` | `boolean` | Enables create/update/delete comments |
|
|
1115
1115
|
| `commentCollectionId` | `string` | Collection ID for comments |
|
|
1116
1116
|
| `commentQuery` | `Query[]` | Additional queries to filter comments |
|
|
1117
|
-
| `enableLogs` | `boolean` | Displays create/update/delete logs in list and read views |
|
|
1117
|
+
| `enableLogs` | `boolean` | Displays create/update/delete logs in list and read views as one row per save (time, author, then the field label + type icon from the DTW schema with before › after, or "Updated N fields" with the field names when one save changed several). Clicking a row opens a popover with every before › after in full; a delete is one "Deleted" row whose popover lists the deleted values. Person and listPerson values show the member's name |
|
|
1118
1118
|
| `logsCollectionId` | `string` | Collection ID for logs |
|
|
1119
1119
|
| `logQuery` | `Query[]` | Additional queries to filter logs |
|
|
1120
1120
|
| `userAPIEndpoint` | `string` | URL returning `{ data: { name: string, ...rest } }` — used to resolve user names in comments/logs |
|
|
@@ -1156,14 +1156,15 @@ Which field types can be edited from the list table, and how. Types not listed u
|
|
|
1156
1156
|
| `addColumnEnabled` | `boolean` | Shows an add-column button in the table header |
|
|
1157
1157
|
| `persistViewState` | `"persistent" \| "session"` | Remembers the list's sort, filters, hidden columns, page, rows per page, search and reading position per collection, so a Back navigation lands where the user left. `"persistent"` uses `localStorage`; `"session"` writes `sessionStorage` and reads it first, falling back to `localStorage`. The reading position is restored on mount and on returning from a record, not on a tab switch; with several lists on one page, only the list a record was last opened from restores (tracked in `blixify-dtw-persist-last-drill`). Covers table, list, grid, kanban and feeds; not map. List view only |
|
|
1158
1158
|
| `persistKey` | `string` | Keeps a separate saved view state for this instance: the storage key becomes `blixify-dtw-persist-<collectionId>-<persistKey>`. Saved state is read once, on mount, so give the component a React `key` that changes with `persistKey` |
|
|
1159
|
-
| `savedViews` | `boolean` | Shows a row of view tags under the tab bar: save the current filters, flex filters, sort, rows per page and columns under a name, reapply it, update it or delete it. Views belong to the tab they were saved on and are kept in `localStorage` under `blixify-dtw-views-<collectionId>[-<persistKey>][-<currentUser.userEmail>]`. Sort is held by the template while it is on, as with `persistViewState`, and applying a view calls `onChangeSort`, `onChangePagination(0)` and `bareListHandleFilter` when those change. Each write re-reads storage first, so views saved in another browser tab are kept. With `onChangeHide` / `onChangeDrag`, a view's hides and column order stay on screen only: a toggle or reorder after applying one writes back the schema's columns plus that single change |
|
|
1159
|
+
| `savedViews` | `boolean` | Shows a row of view tags under the tab bar: save the current filters, flex filters, sort, rows per page, grouping and columns under a name, reapply it, update it or delete it. Views belong to the tab they were saved on and are kept in `localStorage` under `blixify-dtw-views-<collectionId>[-<persistKey>][-<currentUser.userEmail>]`. Sort is held by the template while it is on, as with `persistViewState`, and applying a view calls `onChangeSort`, `onChangeGroupBy`, `onChangePagination(0)` and `bareListHandleFilter` when those change. Each write re-reads storage first, so views saved in another browser tab are kept. With `onChangeHide` / `onChangeDrag`, a view's hides and column order stay on screen only: a toggle or reorder after applying one writes back the schema's columns plus that single change |
|
|
1160
1160
|
| `defaultSavedViews` | `DefaultSavedView[]` | Views provided by the app (`{ id?, name, tabId?, state }`), listed before the user's own and never deleted. Without `tabId` a view is listed on every tab. A field left out of `state` is reset to its default when the view is applied. `id` must be unique; without one the view gets `default-<index>`, its position in the whole array |
|
|
1161
1161
|
| `sharedViews` | `SharedSavedView[]` | Views the app stores for everyone (`{ id, name, tabId, state }`), e.g. on the record the list belongs to. Given, it replaces this browser's storage: the list shows only these, saving goes to `onChangeSharedViews`, and nothing is read from or written to `localStorage` |
|
|
1162
1162
|
| `onChangeSharedViews` | `(views: SharedSavedView[]) => void` | Called with the whole list after a view is saved, updated or deleted, for the app to store and pass back as `sharedViews`; a second change before it comes back builds on the list just sent. The whole list is replaced each time, so two editors saving at once is last-writer-wins. Without it the app's views are read-only and “+ Save view” is hidden |
|
|
1163
1163
|
| `defaultSavedViewId` | `string` | The view everyone opens on, marked ★, stored by the app. The list opens on it when no `initialSavedViewId` is given — also when it arrives after mount — unless `persistViewState` restored a state or a view was already picked. A view that no longer exists leaves the list on All. Opening on it does not call `onChangeSort` |
|
|
1164
1164
|
| `onChangeDefaultSavedViewId` | `(id: string \| null) => void` | Gives each view a ☆ that makes it the default; the ★ on the default takes it off (`null`, the list opens on All). Deleting the default view also sends `null` |
|
|
1165
1165
|
| `initialSavedViewId` | `string` | A view the list opens on, once, in place of the ★ default — e.g. a page embedding a datasheet opens it on a chosen view. A view that no longer exists, or is on another tab, leaves the list on All. Opening on it does not call `onChangeSort` |
|
|
1166
|
-
| `groupBy` | `string` | Column ID to group list results by |
|
|
1166
|
+
| `groupBy` | `string` | Column ID to group list results by. With `bareNewDTW` the groups are bands in one table. Grouped by a `list` column, every option plus "Unknown/No Value" is a band with its real total that reads its own rows under the list's filters, search and sort: in scroll mode each band loads more as it scrolls, in page mode each band has its own pager and the list-wide pager is hidden. Grouped by any other column, page mode groups the rows of the current page and scroll mode keeps the normal scroll. A search, filter, sort or regroup starts every band over. Firebase and `bareListMobileView` keep the normal pagination. Saved views store and restore it |
|
|
1167
|
+
| `onChangeGroupBy` | `(groupBy: string) => void` | Called when the user picks another grouping in the group-by selector or by applying a saved view (`""` for none), so the app can store it with the tab. Not called when the list opens on a view |
|
|
1167
1168
|
| `groupIdCollapsible` | `{ [key: string]: boolean }` | Initial collapsed/expanded state per group ID |
|
|
1168
1169
|
| `sortGroupByAlphabetically` | `boolean` | Sorts group-by values alphabetically |
|
|
1169
1170
|
| `unionCollections` | `any[]` | Additional collections to union-query alongside the main collection |
|
|
@@ -5,6 +5,7 @@ import { ActivityModal } from "./activityModal";
|
|
|
5
5
|
import { DataStructure, DevSettings } from "./model";
|
|
6
6
|
import { DataTemplateWrapper } from "../dataWrapper";
|
|
7
7
|
import { ActivityLog } from "./dataStateModel";
|
|
8
|
+
import { Modal } from "../../display/modal";
|
|
8
9
|
import { handleApplyStoryQueries } from "../dataWrapper/storyBackend.fixtures";
|
|
9
10
|
|
|
10
11
|
const meta = { title: "Data/Activity Panel" };
|
|
@@ -35,15 +36,34 @@ const taskSchema: DataStructure[] = [
|
|
|
35
36
|
},
|
|
36
37
|
{ id: "owner", name: "Owner", type: "person" },
|
|
37
38
|
];
|
|
39
|
+
// INFO: the standalone panel gets the full schema so every log row resolves a
|
|
40
|
+
// label and type icon; the DTW story keeps a narrow table and shows the raw-id fallback
|
|
41
|
+
const activitySchema: DataStructure[] = [
|
|
42
|
+
...taskSchema,
|
|
43
|
+
{
|
|
44
|
+
id: "priority",
|
|
45
|
+
name: "Priority",
|
|
46
|
+
type: "list",
|
|
47
|
+
listSelection: ["Normal", "High"],
|
|
48
|
+
listSelectionColour: ["blue", "orange"],
|
|
49
|
+
},
|
|
50
|
+
{ id: "notes", name: "Notes", type: "textArea" },
|
|
51
|
+
{ id: "dueDate", name: "Due Date", type: "date" },
|
|
52
|
+
{ id: "contactEmail", name: "Contact Email", type: "email" },
|
|
53
|
+
{ id: "contactPhone", name: "Contact Phone", type: "contact" },
|
|
54
|
+
{ id: "driveLink", name: "Project Drive", type: "string" },
|
|
55
|
+
];
|
|
38
56
|
|
|
39
57
|
const ActivityDemo = ({
|
|
40
58
|
darkMode = false,
|
|
41
59
|
logsOnly = false,
|
|
42
60
|
integrated = false,
|
|
61
|
+
inModal = false,
|
|
43
62
|
}: {
|
|
44
63
|
darkMode?: boolean;
|
|
45
64
|
logsOnly?: boolean;
|
|
46
65
|
integrated?: boolean;
|
|
66
|
+
inModal?: boolean;
|
|
47
67
|
}) => {
|
|
48
68
|
const [ready, setReady] = useState(false);
|
|
49
69
|
const [failPings, setFailPings] = useState(false);
|
|
@@ -123,6 +143,26 @@ const ActivityDemo = ({
|
|
|
123
143
|
},
|
|
124
144
|
],
|
|
125
145
|
},
|
|
146
|
+
{
|
|
147
|
+
_id: "log-3",
|
|
148
|
+
collectionId: "tasks",
|
|
149
|
+
itemId: "task-1",
|
|
150
|
+
userId: "sam",
|
|
151
|
+
type: "update",
|
|
152
|
+
createdAt: "2026-09-28T08:15:00Z",
|
|
153
|
+
values: [
|
|
154
|
+
{
|
|
155
|
+
columnId: "contactEmail",
|
|
156
|
+
oldValue: "",
|
|
157
|
+
newValue: "sam.lee@example.com",
|
|
158
|
+
},
|
|
159
|
+
{ columnId: "contactPhone", newValue: "0123456789" },
|
|
160
|
+
{
|
|
161
|
+
columnId: "driveLink",
|
|
162
|
+
newValue: "https://drive.example.com/folders/review-activity",
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
126
166
|
{
|
|
127
167
|
_id: "log-2",
|
|
128
168
|
collectionId: "tasks",
|
|
@@ -188,6 +228,20 @@ const ActivityDemo = ({
|
|
|
188
228
|
setRecipients(event.mentionedUserIds.join(", "));
|
|
189
229
|
},
|
|
190
230
|
};
|
|
231
|
+
|
|
232
|
+
const renderPanel = () => (
|
|
233
|
+
<ActivityModal
|
|
234
|
+
title="Tasks"
|
|
235
|
+
collectionId="tasks"
|
|
236
|
+
dataId="task-1"
|
|
237
|
+
devSettings={settings}
|
|
238
|
+
darkMode={darkMode}
|
|
239
|
+
activityLog={activityLog}
|
|
240
|
+
model={activitySchema}
|
|
241
|
+
initialTab={inModal ? "logs" : undefined}
|
|
242
|
+
/>
|
|
243
|
+
);
|
|
244
|
+
|
|
191
245
|
return (
|
|
192
246
|
<div
|
|
193
247
|
className={`mx-auto max-w-3xl rounded-2xl p-3 sm:p-6 ${darkMode ? "bg-gray-900 text-gray-100" : "bg-gray-50 text-gray-800"}`}
|
|
@@ -200,9 +254,10 @@ const ActivityDemo = ({
|
|
|
200
254
|
{integrated && (
|
|
201
255
|
<p className="mb-4 text-sm text-gray-500">
|
|
202
256
|
Click the chat icon beside a task for its comments and audit history,
|
|
203
|
-
or open the task and click its chat bubble. The list clock opens logs
|
|
204
|
-
|
|
205
|
-
Audit entries are seeded
|
|
257
|
+
or open the task and click its chat bubble. The list clock opens logs
|
|
258
|
+
first. Tasks have separate conversations. All data and notifications
|
|
259
|
+
are simulated; refreshing resets the demo. Audit entries are seeded
|
|
260
|
+
examples.
|
|
206
261
|
</p>
|
|
207
262
|
)}
|
|
208
263
|
{!logsOnly && (
|
|
@@ -247,15 +302,19 @@ const ActivityDemo = ({
|
|
|
247
302
|
setSelectedId(id ?? "");
|
|
248
303
|
}}
|
|
249
304
|
/>
|
|
250
|
-
) : (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
305
|
+
) : inModal ? (
|
|
306
|
+
// INFO: mirrors how DTW mounts the panel, so row popovers are checked
|
|
307
|
+
// against the Modal's clipping and outside-click behaviour
|
|
308
|
+
<Modal
|
|
309
|
+
open
|
|
310
|
+
title="Activity Log"
|
|
311
|
+
maxWidth="4xl"
|
|
256
312
|
darkMode={darkMode}
|
|
257
|
-
|
|
313
|
+
onClose={() => {}}
|
|
314
|
+
renderContent={renderPanel}
|
|
258
315
|
/>
|
|
316
|
+
) : (
|
|
317
|
+
renderPanel()
|
|
259
318
|
))}
|
|
260
319
|
</div>
|
|
261
320
|
);
|
|
@@ -265,3 +324,4 @@ export const CommentsAndLogs = () => <ActivityDemo />;
|
|
|
265
324
|
export const DarkMode = () => <ActivityDemo darkMode />;
|
|
266
325
|
export const LogsOnly = () => <ActivityDemo logsOnly />;
|
|
267
326
|
export const WithDataTemplateWrapper = () => <ActivityDemo integrated />;
|
|
327
|
+
export const InModal = () => <ActivityDemo inModal />;
|
|
@@ -1024,3 +1024,245 @@ export const RelatedRecords = () => (
|
|
|
1024
1024
|
<RelatedRecordsDemo />
|
|
1025
1025
|
</div>
|
|
1026
1026
|
);
|
|
1027
|
+
|
|
1028
|
+
// INFO: Monday-board per-group scroll pagination demo. A single collection with
|
|
1029
|
+
// thousands of rows spread across a handful of owners — grouping by owner then
|
|
1030
|
+
// pages EACH band independently as you scroll inside it, instead of bulk
|
|
1031
|
+
// loading every grouped row at once. All local mock data, no backend.
|
|
1032
|
+
const GROUPBY_COLLECTION = "demo-groupby-items";
|
|
1033
|
+
|
|
1034
|
+
// INFO: six owners with matching Tag-palette colours so each band is tinted;
|
|
1035
|
+
// a status list column is offered as a second groupable column.
|
|
1036
|
+
const GROUPBY_OWNERS = ["Alice", "Bob", "Carol", "Dan", "Eve", "Frank"];
|
|
1037
|
+
const GROUPBY_OWNER_COLOURS = [
|
|
1038
|
+
"red",
|
|
1039
|
+
"blue",
|
|
1040
|
+
"green",
|
|
1041
|
+
"yellow",
|
|
1042
|
+
"indigo",
|
|
1043
|
+
"purple",
|
|
1044
|
+
];
|
|
1045
|
+
const GROUPBY_STATUSES = ["Draft", "In Review", "Approved"];
|
|
1046
|
+
|
|
1047
|
+
const GROUPBY_SCHEMA: DataStructure[] = [
|
|
1048
|
+
{ id: "name", name: "Item", type: "string" },
|
|
1049
|
+
{
|
|
1050
|
+
id: "owner",
|
|
1051
|
+
name: "Owner",
|
|
1052
|
+
type: "list",
|
|
1053
|
+
listSelection: GROUPBY_OWNERS,
|
|
1054
|
+
listSelectionColour: GROUPBY_OWNER_COLOURS,
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
id: "status",
|
|
1058
|
+
name: "Status",
|
|
1059
|
+
type: "list",
|
|
1060
|
+
listSelection: GROUPBY_STATUSES,
|
|
1061
|
+
},
|
|
1062
|
+
{ id: "createdAt", name: "Created At", type: "date" },
|
|
1063
|
+
];
|
|
1064
|
+
|
|
1065
|
+
// INFO: 2400 rows in owner blocks (~400 each), so the first page holds only
|
|
1066
|
+
// Alice — every other band has to load on its own. Every 40th row has no owner
|
|
1067
|
+
// and lands in "Unknown/No Value".
|
|
1068
|
+
const renderGroupByRecords = () => {
|
|
1069
|
+
const rows: any[] = [];
|
|
1070
|
+
const total = 2400;
|
|
1071
|
+
for (let index = 0; index < total; index++) {
|
|
1072
|
+
const owner =
|
|
1073
|
+
index % 40 === 39
|
|
1074
|
+
? ""
|
|
1075
|
+
: GROUPBY_OWNERS[Math.floor(index / (total / GROUPBY_OWNERS.length))];
|
|
1076
|
+
const status = GROUPBY_STATUSES[index % GROUPBY_STATUSES.length];
|
|
1077
|
+
rows.push({
|
|
1078
|
+
_id: `gb-${index}`,
|
|
1079
|
+
name: `${owner || "No owner"} item #${index}`,
|
|
1080
|
+
owner,
|
|
1081
|
+
status,
|
|
1082
|
+
createdAt: new Date(2026, 0, 1 + (index % 180)),
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
return rows;
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* INFO: the mock honours cursor/limit AND the active group filter so each group
|
|
1090
|
+
* pages independently. The list request body carries a
|
|
1091
|
+
* { type:"=", queryId:"owner"/"status", value } entry when a group is expanded —
|
|
1092
|
+
* the mock filters the store to that value, then slices by cursor/limit, so
|
|
1093
|
+
* `count` is that group's total and the band knows when it is exhausted. With no
|
|
1094
|
+
* group filter present it falls back to the full store (the initial grouped
|
|
1095
|
+
* read that seeds the first page of every band). delayResponse makes the
|
|
1096
|
+
* per-group spinner watchable as rows stream in.
|
|
1097
|
+
*/
|
|
1098
|
+
const handleInstallGroupByMock = (store: any[]) => {
|
|
1099
|
+
const mock = new MockAdapter(axios, {
|
|
1100
|
+
onNoMatch: "passthrough",
|
|
1101
|
+
delayResponse: 500,
|
|
1102
|
+
});
|
|
1103
|
+
const renderUrl = (type: string) =>
|
|
1104
|
+
`${DEMO_ENDPOINT}/${GROUPBY_COLLECTION}/${type}`;
|
|
1105
|
+
const handleParseBody = (config: any) => {
|
|
1106
|
+
try {
|
|
1107
|
+
return JSON.parse(config?.data ?? "{}");
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
return {};
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
// INFO: applies every "=" and "!in" condition — a band's own query and
|
|
1114
|
+
// any filter the list carries — so a filter narrows each band's total
|
|
1115
|
+
const handleMatchQuery = (row: any, query: any[]) =>
|
|
1116
|
+
(Array.isArray(query) ? query : []).every((eachQuery) => {
|
|
1117
|
+
if (!eachQuery?.queryId) return true;
|
|
1118
|
+
const value = row[eachQuery.queryId];
|
|
1119
|
+
if (eachQuery.type === "=") return value === eachQuery.value;
|
|
1120
|
+
if (eachQuery.type === "!in") return !eachQuery.value.includes(value);
|
|
1121
|
+
return true;
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
mock.onPost(renderUrl("list")).reply((config) => {
|
|
1125
|
+
const body = handleParseBody(config);
|
|
1126
|
+
// INFO: e2e hook — a band read for this value fails, counted per attempt
|
|
1127
|
+
const failValue = (window as any).__groupByFailValue;
|
|
1128
|
+
if (
|
|
1129
|
+
failValue &&
|
|
1130
|
+
(body.query ?? []).some(
|
|
1131
|
+
(eachQuery: any) =>
|
|
1132
|
+
eachQuery?.type === "=" && eachQuery.value === failValue,
|
|
1133
|
+
)
|
|
1134
|
+
) {
|
|
1135
|
+
(window as any).__groupByFailCount =
|
|
1136
|
+
((window as any).__groupByFailCount ?? 0) + 1;
|
|
1137
|
+
return [500, { message: "failed" }];
|
|
1138
|
+
}
|
|
1139
|
+
const cursor = typeof body.cursor === "number" ? body.cursor : 0;
|
|
1140
|
+
// INFO: no limit means every row, as the list endpoint does for a
|
|
1141
|
+
// grouped page-mode read
|
|
1142
|
+
const limit =
|
|
1143
|
+
typeof body.limit === "number" ? body.limit : Number.MAX_SAFE_INTEGER;
|
|
1144
|
+
const rows = store.filter((eachRow) =>
|
|
1145
|
+
handleMatchQuery(eachRow, body.query),
|
|
1146
|
+
);
|
|
1147
|
+
return [
|
|
1148
|
+
200,
|
|
1149
|
+
{ data: rows.slice(cursor, cursor + limit), count: rows.length },
|
|
1150
|
+
];
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
mock.onPost(renderUrl("get")).reply((config) => {
|
|
1154
|
+
const body = handleParseBody(config);
|
|
1155
|
+
return [200, { data: store.find((each) => each._id === body.id) ?? null }];
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
mock
|
|
1159
|
+
.onPost(`${DEMO_ENDPOINT}/spaces/list`)
|
|
1160
|
+
.reply(200, { data: [], count: 0 });
|
|
1161
|
+
|
|
1162
|
+
return mock;
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
const GroupByScrollPaginationDemo = (props: {
|
|
1166
|
+
paginationType: "scroll" | "page";
|
|
1167
|
+
}) => {
|
|
1168
|
+
const store = React.useMemo(() => renderGroupByRecords(), []);
|
|
1169
|
+
React.useMemo(() => handleInstallGroupByMock(store), [store]);
|
|
1170
|
+
|
|
1171
|
+
const [typeParam, setTypeParam] = useState<"list" | "read" | "update">(
|
|
1172
|
+
"list",
|
|
1173
|
+
);
|
|
1174
|
+
const [dataId, setDataId] = useState<string>("");
|
|
1175
|
+
const [viewId, setViewId] = useState<string | undefined>(undefined);
|
|
1176
|
+
|
|
1177
|
+
return (
|
|
1178
|
+
<div className="w-full">
|
|
1179
|
+
<DataTemplateImpl
|
|
1180
|
+
title="Group by scroll pagination"
|
|
1181
|
+
description="2,400 items across six owners — each group pages on its own as you scroll"
|
|
1182
|
+
type={typeParam}
|
|
1183
|
+
id={dataId}
|
|
1184
|
+
collectionId={GROUPBY_COLLECTION}
|
|
1185
|
+
model={GROUPBY_SCHEMA}
|
|
1186
|
+
localURLEndpoint=""
|
|
1187
|
+
devSettings={demoDevSettings}
|
|
1188
|
+
lib={{ axios, Menu, Transition, papaParser: Papa }}
|
|
1189
|
+
limit={25}
|
|
1190
|
+
bareSettings={{ bareNewDTW: true, bareFlexFilters: true }}
|
|
1191
|
+
organise={{
|
|
1192
|
+
viewId,
|
|
1193
|
+
showGroupByFeature: true,
|
|
1194
|
+
groupBy: "owner",
|
|
1195
|
+
// INFO: a saved view regroups by status; All goes back to owner
|
|
1196
|
+
savedViews: true,
|
|
1197
|
+
defaultSavedViews: [
|
|
1198
|
+
{
|
|
1199
|
+
id: "by-status",
|
|
1200
|
+
name: "By status",
|
|
1201
|
+
state: { groupBy: "status" },
|
|
1202
|
+
},
|
|
1203
|
+
{
|
|
1204
|
+
id: "draft-only",
|
|
1205
|
+
name: "Draft only",
|
|
1206
|
+
state: {
|
|
1207
|
+
flexFilters: [
|
|
1208
|
+
{
|
|
1209
|
+
id: "f1",
|
|
1210
|
+
columnId: "status",
|
|
1211
|
+
operator: "=",
|
|
1212
|
+
value: "Draft",
|
|
1213
|
+
},
|
|
1214
|
+
],
|
|
1215
|
+
},
|
|
1216
|
+
},
|
|
1217
|
+
],
|
|
1218
|
+
paginationType: props.paginationType,
|
|
1219
|
+
// INFO: the group Select consumes Option[] (key/label) — offer the two
|
|
1220
|
+
// list columns that make sense to group a board on.
|
|
1221
|
+
renderGroupByOptions: () => [
|
|
1222
|
+
{ key: "owner", label: "Owner" },
|
|
1223
|
+
{ key: "status", label: "Status" },
|
|
1224
|
+
],
|
|
1225
|
+
handleNavigate: (type: string, id?: string, view?: string) => {
|
|
1226
|
+
setTypeParam(type as any);
|
|
1227
|
+
setDataId(id ?? "");
|
|
1228
|
+
if (view) setViewId(view);
|
|
1229
|
+
},
|
|
1230
|
+
}}
|
|
1231
|
+
/>
|
|
1232
|
+
</div>
|
|
1233
|
+
);
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
export const GroupByScrollPagination = () => (
|
|
1237
|
+
<div className="w-full">
|
|
1238
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
1239
|
+
Data Template — Monday-board per-group scroll pagination
|
|
1240
|
+
</div>
|
|
1241
|
+
<div className="text-sm mb-4 text-gray-600">
|
|
1242
|
+
2,400 items in owner blocks (~400 each, every 40th with no owner), all
|
|
1243
|
+
from local mock data. The board opens grouped by <code>Owner</code>; the
|
|
1244
|
+
first page holds only Alice, yet every owner and Unknown/No Value is a
|
|
1245
|
+
band from the start and loads its own rows. To demo: keep the{" "}
|
|
1246
|
+
<strong>Group</strong> picker on Owner (or switch it to Status), then
|
|
1247
|
+
scroll down inside a band — that group fetches its next page on its own,
|
|
1248
|
+
with a short spinner, while the other groups stay where they are. Each
|
|
1249
|
+
group pages independently against the mock, Monday-board style, rather
|
|
1250
|
+
than bulk-loading every row.
|
|
1251
|
+
</div>
|
|
1252
|
+
<GroupByScrollPaginationDemo paginationType="scroll" />
|
|
1253
|
+
</div>
|
|
1254
|
+
);
|
|
1255
|
+
|
|
1256
|
+
export const GroupByPagePagination = () => (
|
|
1257
|
+
<div className="w-full">
|
|
1258
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
1259
|
+
Data Template — group by with page pagination
|
|
1260
|
+
</div>
|
|
1261
|
+
<div className="text-sm mb-4 text-gray-600">
|
|
1262
|
+
The same 2,400 items in page mode. Each page reads 25 rows and groups only
|
|
1263
|
+
those rows, so the pager moves through the list without skipping or
|
|
1264
|
+
repeating rows.
|
|
1265
|
+
</div>
|
|
1266
|
+
<GroupByScrollPaginationDemo paginationType="page" />
|
|
1267
|
+
</div>
|
|
1268
|
+
);
|
|
@@ -173,3 +173,31 @@ unconditionally hands every viewer a delete button.
|
|
|
173
173
|
The older `organise.addColumnEnabled` path is unaffected: it still opens the
|
|
174
174
|
formula builder and gets no bin, since those tables never gained a plain-column
|
|
175
175
|
`+` and so have no way to add a deleted column back.
|
|
176
|
+
|
|
177
|
+
## Adding rows
|
|
178
|
+
|
|
179
|
+
With `addable={{ active: true, onAddData }}` the table keeps an empty row at the bottom, as a spreadsheet does:
|
|
180
|
+
|
|
181
|
+
- Clicking any of its cells starts a new row with that cell focused.
|
|
182
|
+
- Enter saves the row through `onAddData` and opens the next one, focused on its first cell. A multi-line field keeps Enter for a new line.
|
|
183
|
+
- Escape drops the row. Clicking another row of the table saves it.
|
|
184
|
+
- A required field left empty keeps the row open with the field marked. If the click went to another row, the row closes and what was typed comes back the next time it is opened.
|
|
185
|
+
- Text, number, phone, email, long text, yes/no, date, single and multi select, person, people, role, Gantt and canvas columns are filled in the row (`supportedNewTableAddList`). Any other column, and a disabled one, stays empty in the new row and never stops a row being added — it is filled in afterwards. A required column of such a type is not checked when the row is saved, so the record starts without it.
|
|
186
|
+
- If `onAddData` rejects, the typed values come back in the row, unless the next row is already being typed in. DataTemplate rejects when a create fails, after its own error message.
|
|
187
|
+
- The row is hidden while rows are selected and in the mobile view.
|
|
188
|
+
|
|
189
|
+
## Editing cells
|
|
190
|
+
|
|
191
|
+
With `editable.active` most columns edit in the cell (`supportedInlineEditList`). Image, images, address, file and files open the same form field in a modal (`supportedModalEditList`) and save through `onUpdateData` like any cell. Markdown, signature, address list and password are edited on the record, not in the table. A disabled column stays read-only.
|
|
192
|
+
|
|
193
|
+
## Groups
|
|
194
|
+
|
|
195
|
+
`groups` draws rows as coloured, collapsible bands inside the one table. DataTemplate builds them from `organise.groupBy`; a consumer using NewTable on its own passes them directly.
|
|
196
|
+
|
|
197
|
+
| Prop | Type | Behavior |
|
|
198
|
+
| -------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
199
|
+
| `groups` | `{ id, title, colour?, rowIds, count? }[]` | One band per entry; `rowIds` reference row ids. `count` is the band's real total, shown in its header instead of the rows it holds |
|
|
200
|
+
| `onGroupLoadMore` | `(groupId: string) => void` | Scroll paging: fired when the bottom of a band scrolls into view, so that band loads its next rows |
|
|
201
|
+
| `onGroupPageChange` | `(groupId: string, page: number) => void` | Page paging: each band gets the list footer (numbered pages, chevrons, go-to-page) under its rows, scoped to that band. 0-based page |
|
|
202
|
+
| `groupLoadingStatus` | `{ [groupId]: { loadingMore?, exhausted?, page?, size?, fetched? } }` | Per band: a spinner while a page loads; `exhausted` removes the scroll sentinel; `page` and `size` drive the band pager once `fetched` |
|
|
203
|
+
|
|
@@ -155,6 +155,70 @@ const matrixColumns: NewTableColumn[] = [
|
|
|
155
155
|
{ key: "run", title: "" },
|
|
156
156
|
];
|
|
157
157
|
|
|
158
|
+
// INFO: rows added like a spreadsheet — the empty bottom row takes typing in
|
|
159
|
+
// any cell, Enter saves and opens the next row. Photo and Contract have no
|
|
160
|
+
// inline editor: they stay empty in a new row without blocking it, and an
|
|
161
|
+
// existing row's cell opens its editor in a modal
|
|
162
|
+
const spreadsheetColumns: NewTableColumn[] = [
|
|
163
|
+
{ key: "name", title: "Name", type: "string" },
|
|
164
|
+
{ key: "qty", title: "Qty", type: "number", optional: true },
|
|
165
|
+
{
|
|
166
|
+
key: "stage",
|
|
167
|
+
title: "Stage",
|
|
168
|
+
type: "list",
|
|
169
|
+
listSelection: ["Lead", "Quoted", "Won"],
|
|
170
|
+
optional: true,
|
|
171
|
+
},
|
|
172
|
+
{ key: "dueDate", title: "Due", type: "date", optional: true },
|
|
173
|
+
{ key: "photo", title: "Photo", type: "image" },
|
|
174
|
+
{ key: "contract", title: "Contract", type: "file" },
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const SpreadsheetRows = () => {
|
|
178
|
+
const [rows, setRows] = useState<NewTableRow[]>([
|
|
179
|
+
{ id: "row-1", name: "Rooftop survey", qty: 2, stage: "Lead" },
|
|
180
|
+
]);
|
|
181
|
+
const [saved, setSaved] = useState<any[]>([]);
|
|
182
|
+
return (
|
|
183
|
+
<>
|
|
184
|
+
<NewTableImpl
|
|
185
|
+
columns={spreadsheetColumns}
|
|
186
|
+
data={rows}
|
|
187
|
+
paginationType="page"
|
|
188
|
+
size={rows.length}
|
|
189
|
+
limit={20}
|
|
190
|
+
pageIndex={0}
|
|
191
|
+
handleUpdatePageIndex={() => {}}
|
|
192
|
+
editable={{
|
|
193
|
+
active: true,
|
|
194
|
+
onUpdateData: async (rowId, columnId, value) => {
|
|
195
|
+
setRows((previous) =>
|
|
196
|
+
previous.map((eachRow) =>
|
|
197
|
+
eachRow.id === rowId
|
|
198
|
+
? { ...eachRow, [columnId]: value?.[columnId] ?? value }
|
|
199
|
+
: eachRow,
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
}}
|
|
204
|
+
addable={{
|
|
205
|
+
active: true,
|
|
206
|
+
onAddData: (added) => {
|
|
207
|
+
setSaved((previous) => [...previous, added]);
|
|
208
|
+
setRows((previous) => [
|
|
209
|
+
...previous,
|
|
210
|
+
{ id: `row-${previous.length + 1}`, ...added },
|
|
211
|
+
]);
|
|
212
|
+
},
|
|
213
|
+
}}
|
|
214
|
+
/>
|
|
215
|
+
<pre data-testid="spreadsheet-saved" className="mt-3 text-xs">
|
|
216
|
+
{JSON.stringify(saved)}
|
|
217
|
+
</pre>
|
|
218
|
+
</>
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
|
|
158
222
|
// INFO: inline edit and add-column together — the + appends to the same columns
|
|
159
223
|
// the cells are editing, so a new field is immediately fillable
|
|
160
224
|
const EditableWithAddColumn = () => {
|
|
@@ -386,7 +450,7 @@ const ElementCellsMatrix = () => {
|
|
|
386
450
|
setRows((prev) => prev.filter((row) => row.id !== rowId)),
|
|
387
451
|
}}
|
|
388
452
|
enableCopy={false}
|
|
389
|
-
emptyTitle="No rows yet
|
|
453
|
+
emptyTitle="No rows yet"
|
|
390
454
|
/>
|
|
391
455
|
</div>
|
|
392
456
|
);
|
|
@@ -611,6 +675,13 @@ export const NewTable = () => {
|
|
|
611
675
|
<EditableWithAddColumn />
|
|
612
676
|
</StorySection>
|
|
613
677
|
|
|
678
|
+
<StorySection
|
|
679
|
+
title="Adding rows like a spreadsheet"
|
|
680
|
+
description="addable.active=true keeps an empty row at the bottom. Click any of its cells and type; Enter saves the row and opens the next one, Escape drops it, and clicking another row saves it. A column the row cannot fill in (Photo, Contract) stays empty without blocking it; on a saved row its cell opens the editor in a modal."
|
|
681
|
+
>
|
|
682
|
+
<SpreadsheetRows />
|
|
683
|
+
</StorySection>
|
|
684
|
+
|
|
614
685
|
<StorySection
|
|
615
686
|
title="Editable with disabled columns"
|
|
616
687
|
description="A column marked disabled in the schema stays read-only in an editable table, the same as in the form. Hover Value or Notes to edit; Unit, Source and the coloured Status column never open an editor, and a row click still reaches the row."
|
|
@@ -620,7 +691,7 @@ export const NewTable = () => {
|
|
|
620
691
|
|
|
621
692
|
<StorySection
|
|
622
693
|
title="Element cells with row actions (config matrix)"
|
|
623
|
-
description="Cells can be React elements: kit inputs with cellInput={{ active: true }} (the same compact mode the DTW inline edit matrix uses) and IconButtons per row, while the parent owns the row state. Typed columns are what make the
|
|
694
|
+
description="Cells can be React elements: kit inputs with cellInput={{ active: true }} (the same compact mode the DTW inline edit matrix uses) and IconButtons per row, while the parent owns the row state. Typed columns are what make the empty add row render inputs — element-only columns render nothing there. Do NOT combine with editable.active: edit mode wraps element cells in its own editors (from 1.2.66 a React element cell passes through, but its inputs are ~286px wide). Wrap in overflow-x-auto when the columns outgrow the pane."
|
|
624
695
|
>
|
|
625
696
|
<ElementCellsMatrix />
|
|
626
697
|
</StorySection>
|
|
@@ -97,6 +97,39 @@ const navigation = [
|
|
|
97
97
|
/>;
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
##### Icon rail
|
|
101
|
+
|
|
102
|
+
A narrow column of icons left of the sidebar — the two-level layout of tools like monday.com. The rail switches between whole areas of the app (a workspace tree, chat, agents, notifications); the sidebar beside it navigates inside the area that is open. Without `railItems` nothing changes.
|
|
103
|
+
|
|
104
|
+
```jsx
|
|
105
|
+
import { BellIcon, ChatBubbleLeftRightIcon, Cog6ToothIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
|
106
|
+
|
|
107
|
+
const [railId, setRailId] = useState("workspace");
|
|
108
|
+
|
|
109
|
+
<LeftNav
|
|
110
|
+
railItems={[
|
|
111
|
+
{ id: "workspace", name: "Workspace", icon: Squares2X2Icon },
|
|
112
|
+
{ id: "chat", name: "Chat", icon: ChatBubbleLeftRightIcon, href: "/chat" },
|
|
113
|
+
{ id: "notifications", name: "Alerts", icon: BellIcon, badge: 9 },
|
|
114
|
+
]}
|
|
115
|
+
railFooterItems={[{ id: "settings", name: "Settings", icon: Cog6ToothIcon }]}
|
|
116
|
+
selectedRailId={railId}
|
|
117
|
+
handleRailOnClick={(id) => setRailId(id)}
|
|
118
|
+
{...otherProps}
|
|
119
|
+
/>;
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
| Name | Type | Description |
|
|
123
|
+
| ------------------ | ---------------------- | ------------------------------------------------------------------------------------------------ |
|
|
124
|
+
| `railItems` | `RailItem[]` | Icons at the top of the rail. Turns the rail on. |
|
|
125
|
+
| `railFooterItems` | `RailItem[]` | Icons pinned to the bottom of the rail, for settings-type entries. |
|
|
126
|
+
| `selectedRailId` | `string` | The `id` of the open area, highlighted. |
|
|
127
|
+
| `handleRailOnClick`| `(id: string) => void` | Called with the item `id` on every click, link or not. |
|
|
128
|
+
|
|
129
|
+
`RailItem` is `{ id, name, icon, href?, badge? }`. With `href` the item is a link through the same `linkType` / `custom` as the sidebar; without it the item only reports `handleRailOnClick`. `badge` shows a count on the icon — `0`, empty or absent hides it, and a number over 99 reads `99+`.
|
|
130
|
+
|
|
131
|
+
Below `lg` the rail moves into the mobile drawer as a row of icons above the sidebar's list.
|
|
132
|
+
|
|
100
133
|
##### Required props
|
|
101
134
|
|
|
102
135
|
| Name | Type | Description |
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
+
BellIcon,
|
|
3
|
+
ChatBubbleLeftRightIcon,
|
|
4
|
+
Cog6ToothIcon,
|
|
5
|
+
CpuChipIcon,
|
|
2
6
|
FolderIcon,
|
|
3
7
|
FunnelIcon,
|
|
4
8
|
HomeIcon,
|
|
9
|
+
Squares2X2Icon,
|
|
5
10
|
UsersIcon,
|
|
6
11
|
} from "@heroicons/react/24/outline";
|
|
7
12
|
import React, { useState } from "react";
|
|
8
|
-
import { LeftNav as LeftNavImpl, Navigation } from ".";
|
|
13
|
+
import { LeftNav as LeftNavImpl, Navigation, RailItem } from ".";
|
|
9
14
|
import { BarMove } from "../bars";
|
|
10
15
|
import { StorySection } from "../../storyUtils";
|
|
11
16
|
import Logo from "../../../assets/logo.png";
|
|
@@ -221,6 +226,74 @@ const workspaceNavigation = [
|
|
|
221
226
|
},
|
|
222
227
|
];
|
|
223
228
|
|
|
229
|
+
// INFO: the rail switches between whole areas; the sidebar beside it lists the
|
|
230
|
+
// pages inside the area that is open
|
|
231
|
+
const railItems: RailItem[] = [
|
|
232
|
+
{ id: "workspace", name: "Workspace", icon: Squares2X2Icon },
|
|
233
|
+
{ id: "chat", name: "Chat", icon: ChatBubbleLeftRightIcon },
|
|
234
|
+
{ id: "agents", name: "Agents", icon: CpuChipIcon },
|
|
235
|
+
{ id: "notifications", name: "Alerts", icon: BellIcon, badge: 9 },
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
// INFO: each area brings its own sidebar list
|
|
239
|
+
const railNavigation: { [railId: string]: Navigation[] } = {
|
|
240
|
+
chat: [
|
|
241
|
+
{
|
|
242
|
+
id: "chat-new",
|
|
243
|
+
name: "New chat",
|
|
244
|
+
href: "/chat",
|
|
245
|
+
icon: ChatBubbleLeftRightIcon,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "chat-1",
|
|
249
|
+
name: "Quarterly sales recap",
|
|
250
|
+
href: "/chat/1",
|
|
251
|
+
icon: ChatBubbleLeftRightIcon,
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
agents: [
|
|
255
|
+
{
|
|
256
|
+
id: "agent-1",
|
|
257
|
+
name: "Lead qualifier",
|
|
258
|
+
href: "/agents/1",
|
|
259
|
+
icon: CpuChipIcon,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: "agent-2",
|
|
263
|
+
name: "Support triage",
|
|
264
|
+
href: "/agents/2",
|
|
265
|
+
icon: CpuChipIcon,
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
notifications: [
|
|
269
|
+
{ id: "note-all", name: "All", href: "/alerts", icon: BellIcon },
|
|
270
|
+
{
|
|
271
|
+
id: "note-unread",
|
|
272
|
+
name: "Unread",
|
|
273
|
+
href: "/alerts/unread",
|
|
274
|
+
icon: BellIcon,
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
settings: [
|
|
278
|
+
{
|
|
279
|
+
id: "set-members",
|
|
280
|
+
name: "Members",
|
|
281
|
+
href: "/settings/members",
|
|
282
|
+
icon: UsersIcon,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: "set-workspace",
|
|
286
|
+
name: "Workspace",
|
|
287
|
+
href: "/settings/workspace",
|
|
288
|
+
icon: Cog6ToothIcon,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const railFooterItems: RailItem[] = [
|
|
294
|
+
{ id: "settings", name: "Settings", icon: Cog6ToothIcon },
|
|
295
|
+
];
|
|
296
|
+
|
|
224
297
|
const renderLogo = (collapse?: boolean) => (
|
|
225
298
|
<div className="flex items-center flex-shrink-0 px-6 cursor-pointer">
|
|
226
299
|
<img
|
|
@@ -285,6 +358,8 @@ export const LeftNav = () => {
|
|
|
285
358
|
// button, so each story needs its own open state for that button to work on
|
|
286
359
|
// a phone
|
|
287
360
|
const [mobileStoryId, setMobileStoryId] = useState("");
|
|
361
|
+
const [railId, setRailId] = useState("workspace");
|
|
362
|
+
const [darkRailId, setDarkRailId] = useState("workspace");
|
|
288
363
|
|
|
289
364
|
const handleMobileMenu = (storyId: string) => ({
|
|
290
365
|
leftNavMobileMenu: mobileStoryId === storyId,
|
|
@@ -639,6 +714,63 @@ export const LeftNav = () => {
|
|
|
639
714
|
</div>
|
|
640
715
|
</StorySection>
|
|
641
716
|
|
|
717
|
+
<StorySection
|
|
718
|
+
title="Icon Rail"
|
|
719
|
+
description="railItems adds a narrow column of icons left of the sidebar, the two-level layout of tools like monday.com: the rail switches between whole areas (workspace, chat, agents, alerts), the sidebar beside it navigates inside the open one. selectedRailId highlights the open area, badge shows a count (over 99 reads 99+), and railFooterItems sit at the bottom for settings-type entries. An item with href is a link; without one it only reports handleRailOnClick. Without railItems nothing changes. Below lg the rail moves into the drawer as a row of icons above the list — open the menu at phone width to see it."
|
|
720
|
+
>
|
|
721
|
+
<div className="w-full h-96 relative overflow-hidden border rounded-lg">
|
|
722
|
+
<LeftNavImpl
|
|
723
|
+
selectedId={workspaceSelectedId}
|
|
724
|
+
navigation={
|
|
725
|
+
railId === "workspace"
|
|
726
|
+
? workspaceNavigation
|
|
727
|
+
: (railNavigation[railId] ?? [])
|
|
728
|
+
}
|
|
729
|
+
{...handleMobileMenu("story-rail")}
|
|
730
|
+
compactMode={true}
|
|
731
|
+
railItems={railItems}
|
|
732
|
+
railFooterItems={railFooterItems}
|
|
733
|
+
selectedRailId={railId}
|
|
734
|
+
handleRailOnClick={(id) => setRailId(id)}
|
|
735
|
+
handleBarsOnClick={(id) => setWorkspaceSelectedId(id)}
|
|
736
|
+
renderDashboardLogo={() => renderLogo()}
|
|
737
|
+
renderDashboardContent={() => (
|
|
738
|
+
<div className="p-6 text-sm text-gray-500">
|
|
739
|
+
{`Open area: ${railId}`}
|
|
740
|
+
</div>
|
|
741
|
+
)}
|
|
742
|
+
renderDashboardHeaderContent={renderHeader}
|
|
743
|
+
/>
|
|
744
|
+
</div>
|
|
745
|
+
</StorySection>
|
|
746
|
+
|
|
747
|
+
<StorySection
|
|
748
|
+
title="Icon Rail — Dark Mode"
|
|
749
|
+
description="The rail follows darkMode and navbarBgColor like the sidebar, so the two read as one surface."
|
|
750
|
+
dark
|
|
751
|
+
>
|
|
752
|
+
<div className="w-full h-96 relative overflow-hidden border border-gray-700 rounded-lg">
|
|
753
|
+
<LeftNavImpl
|
|
754
|
+
selectedId={selectedId}
|
|
755
|
+
navigation={navigation}
|
|
756
|
+
{...handleMobileMenu("story-rail-dark")}
|
|
757
|
+
darkMode={true}
|
|
758
|
+
railItems={railItems}
|
|
759
|
+
railFooterItems={railFooterItems}
|
|
760
|
+
selectedRailId={darkRailId}
|
|
761
|
+
handleRailOnClick={(id) => setDarkRailId(id)}
|
|
762
|
+
handleBarsOnClick={(id) => setSelectedId(id)}
|
|
763
|
+
renderDashboardLogo={() => renderLogo()}
|
|
764
|
+
renderDashboardContent={() => (
|
|
765
|
+
<div className="p-6 text-gray-400 text-sm">
|
|
766
|
+
{`Open area: ${darkRailId}`}
|
|
767
|
+
</div>
|
|
768
|
+
)}
|
|
769
|
+
renderDashboardHeaderContent={renderHeader}
|
|
770
|
+
/>
|
|
771
|
+
</div>
|
|
772
|
+
</StorySection>
|
|
773
|
+
|
|
642
774
|
<StorySection
|
|
643
775
|
title="Transparent"
|
|
644
776
|
description="transparent makes the TOP HEADER BAR see-through, dropping its background, shadow and bottom border so a page can run its own hero or image up under it — the same pair Header carries. mobileOpaque keeps that bar solid below lg, where it holds the hamburger and is the only way back to the nav. The sidebar itself is unaffected. Shown over a gradient page so the effect is visible."
|