blixify-ui-web-mcp 0.1.19 → 0.1.20

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.
Files changed (37) hide show
  1. package/lib/components/action/iconButton/iconButton.stories.tsx +11 -2
  2. package/lib/components/chart/statsCard/README.md +24 -0
  3. package/lib/components/chart/statsCard/statsCard.stories.tsx +68 -0
  4. package/lib/components/chat/chatBase/chatBase.stories.tsx +1 -1
  5. package/lib/components/data/dataGraph/dataGraph.stories.tsx +5 -5
  6. package/lib/components/data/dataTemplate/README.md +112 -82
  7. package/lib/components/data/dataTemplate/dataTemplate.stories.tsx +307 -295
  8. package/lib/components/data/dataWrapper/README.md +109 -22
  9. package/lib/components/data/dataWrapper/dataWrapper.bareHooks.stories.tsx +951 -0
  10. package/lib/components/data/dataWrapper/dataWrapper.edit.stories.tsx +156 -5
  11. package/lib/components/data/dataWrapper/dataWrapper.view.stories.tsx +79 -5
  12. package/lib/components/display/calendar/calendar.stories.tsx +1 -1
  13. package/lib/components/display/detailList/README.md +11 -10
  14. package/lib/components/display/detailList/detailList.stories.tsx +16 -3
  15. package/lib/components/display/feeds/README.md +3 -3
  16. package/lib/components/display/feeds/feeds.stories.tsx +3 -3
  17. package/lib/components/display/filePreview/filePreview.stories.tsx +111 -0
  18. package/lib/components/display/modal/README.md +3 -0
  19. package/lib/components/display/modal/modal.stories.tsx +50 -1
  20. package/lib/components/display/newCalendar/newCalendar.stories.tsx +1 -1
  21. package/lib/components/display/newFeeds/newFeeds.stories.tsx +3 -3
  22. package/lib/components/display/newTable/newTable.stories.tsx +34 -9
  23. package/lib/components/input/markdown/markdown.stories.tsx +3 -1
  24. package/lib/components/navigation/leftNav/leftNav.stories.tsx +175 -0
  25. package/lib/components/navigation/tabs/tabs.stories.tsx +23 -0
  26. package/lib/components/template/calendarView/calendarView.stories.tsx +4 -4
  27. package/lib/components/template/chatDashboard/chatDashboard.stories.tsx +2 -2
  28. package/lib/mcp/tools/getBackendComponent.d.ts.map +1 -1
  29. package/lib/mcp/tools/getBackendComponent.js +7 -3
  30. package/lib/mcp/tools/getBackendComponent.js.map +1 -1
  31. package/lib/mcp/tools/suggestTemplate.js +3 -3
  32. package/lib/mcp/tools/suggestTemplate.js.map +1 -1
  33. package/package.json +1 -1
  34. package/lib/components/data/dataTemplate/dataModule.stories.tsx +0 -191
  35. package/lib/components/data/dataWrapper/dataWrapper.consumer.stories.tsx +0 -2125
  36. package/lib/components/data/dataWrapper/dataWrapper.utils.stories.tsx +0 -576
  37. package/lib/components/data/flexFilter/flexFilter.stories.tsx +0 -171
@@ -1,323 +1,335 @@
1
1
  import { Menu, Transition } from "@headlessui/react";
2
- import { TagIcon } from "@heroicons/react/24/outline";
3
2
  import axios from "axios";
3
+ import MockAdapter from "axios-mock-adapter";
4
4
  import Papa from "papaparse";
5
- import React, { useEffect, useRef, useState } from "react";
5
+ import React, { useState } from "react";
6
+ import { ButtonGroup } from "../../action/buttonGroup";
6
7
  import { DataTemplate as DataTemplateImpl } from ".";
7
- import { Button } from "../../action/button";
8
- import { Loading } from "../../display/loading";
9
- import { StorySection } from "../../storyUtils";
10
- import { Sort } from "../readQuery";
11
- import {
12
- dbAPIToken,
13
- defaultDBEndpoint,
14
- defaultImageEndpoint,
15
- defaultUploadEndpoint,
16
- getAllSpacesList,
17
- getSpaceData,
18
- getWorkspaceData,
19
- schemaUrl,
20
- storyRenderReferenceLabelOptions,
21
- storyRenderReferenceSpaceOptions,
22
- } from "../stories";
23
- import { blixifyDevCollection, blixifyDevWorkspace } from "../testData";
24
- import { parseSpaceToDataStructure, parseWorkspaceSettings } from "../utils";
25
8
  import { DataStructure } from "./model";
26
- import { AdditionalQuery, ExtendedTabs, Space } from "./spaceModel";
27
- import { Workspace } from "./workspaceModel";
9
+ import { ExtendedTabs } from "./spaceModel";
28
10
 
29
11
  const meta = { title: "Data/Data Template" };
30
12
  export default meta;
31
13
 
32
- export const DataTemplate = () => {
33
- const [selectedSpace, setSelectedSpace] = useState<Space | null>(null);
34
- const [allWorkspaceSpace, setAllWorkspaceSpace] = useState<Space[]>([]);
35
- const [selectedWorkspace, setSelectedWorkspace] = useState<Workspace | null>(
36
- null,
37
- );
38
- const [mount, setMount] = useState<boolean>(false);
39
- const [sort, setSort] = useState<Sort | undefined>({
40
- sortId: "baseUpdatedAt",
41
- type: "desc",
42
- });
43
- const [tabs, setTabs] = useState<ExtendedTabs[]>([]);
44
- const [queries, setQueries] = useState<AdditionalQuery[]>([]);
45
- const [collectionParam, setCollectionParams] = useState(blixifyDevCollection);
46
- const workspaceParam = blixifyDevWorkspace;
14
+ const DEMO_ENDPOINT = "https://dt-demo.local/api";
47
15
 
48
- const dataTemplateRef = useRef<any>(null);
16
+ const LAYOUT_TOGGLE = [
17
+ { id: "desktop", name: "Desktop" },
18
+ { id: "mobile", name: "Mobile" },
19
+ ];
20
+ const DEMO_COLLECTION = "demo-documents";
49
21
 
50
- const [viewParam, setViewParam] = useState("general");
51
- const [typeParam, setTypeParam] = useState("list");
52
- const [dataParam, setDataParam] = useState("");
22
+ // INFO: two file columns on purpose. A view unwinds ONE of them, chosen when
23
+ // the tab is made — which is why the column cannot be inferred and the Add Tab
24
+ // picker has to ask.
25
+ // INFO: a single image column holds the download TOKEN, not a url — the
26
+ // file's name lives on the column (imageFileName) and its folder on
27
+ // imageCollectionName + the row id. That is what a real datasheet stores, and
28
+ // it is the shape the file view had to learn to render.
29
+ const DEMO_SCHEMA: DataStructure[] = [
30
+ { id: "name", name: "Document", type: "string" },
31
+ { id: "owner", name: "Owner", type: "string" },
32
+ { id: "files", name: "Attachments", type: "multipleFile" },
33
+ { id: "scans", name: "Scans", type: "multipleFile" },
34
+ // INFO: a SINGLE file field stores only the token — its name comes from the
35
+ // schema (imageFileName), which is fixed config rather than the user's file.
36
+ // That is why the read view shows the field's LABEL here and the entry's own
37
+ // name for a multiple field.
38
+ {
39
+ id: "contract",
40
+ name: "Signed Contract",
41
+ type: "file",
42
+ imageFileName: "contract.pdf",
43
+ imageCollectionName: DEMO_COLLECTION,
44
+ },
45
+ {
46
+ id: "photo",
47
+ name: "Cover Photo",
48
+ type: "image",
49
+ imageFileName: "photo.png",
50
+ imageCollectionName: DEMO_COLLECTION,
51
+ },
52
+ ];
53
53
 
54
- const getLocalUrl = "/dashboard/spaceD";
55
- const title = selectedSpace?.name ?? "";
56
- const description = selectedSpace?.description ?? "List of sample data";
54
+ const renderDemoRecords = () => [
55
+ {
56
+ _id: "r1",
57
+ name: "Handbook",
58
+ owner: "alice",
59
+ files: [
60
+ {
61
+ fileName: "https://cdn.local/1757238400000_Onboarding.pdf",
62
+ token: "a",
63
+ },
64
+ { fileName: "https://cdn.local/1757238400001_Policies.pdf", token: "b" },
65
+ {
66
+ fileName:
67
+ "https://images.pexels.com/photos/2338407/pexels-photo-2338407.jpeg?auto=compress&cs=tinysrgb&w=600",
68
+ token: "c",
69
+ },
70
+ ],
71
+ scans: [
72
+ { fileName: "https://cdn.local/scan_front.png", token: "s1" },
73
+ // INFO: a multiple entry as storage names it — <uuid>-<name> so entries
74
+ // never collide. The tile must show "Site Plan.pdf", not the uuid.
75
+ {
76
+ fileName: "3f2b1c6a-1111-4222-8333-444455556666-Site Plan.pdf",
77
+ token: "s2",
78
+ },
79
+ ],
80
+ photo: "token-r1",
81
+ contract: "token-c1",
82
+ },
83
+ {
84
+ _id: "r2",
85
+ name: "Proposals",
86
+ owner: "bob",
87
+ files: [
88
+ {
89
+ fileName: "https://cdn.local/1757238400002_Q3%20Budget.xlsx",
90
+ token: "d",
91
+ },
92
+ {
93
+ fileName: "https://cdn.local/1757238400003_Walkthrough.mp4",
94
+ token: "e",
95
+ },
96
+ ],
97
+ scans: [],
98
+ photo: "token-r2",
99
+ contract: "token-c2",
100
+ },
101
+ // INFO: no attachments — contributes NO tiles. A blank tile per empty record
102
+ // would bury the ones that matter, which is why the unwind does not preserve
103
+ // empty arrays.
104
+ { _id: "r3", name: "Archive", owner: "carol", files: [], scans: [] },
105
+ ];
57
106
 
58
- useEffect(() => {
59
- setMount(false);
60
- handleRetrieveData();
61
- }, [collectionParam]);
62
-
63
- const handleRetrieveData = async () => {
64
- if (collectionParam) {
65
- const space: Space = await getSpaceData(collectionParam);
66
- if (space) {
67
- if (space.structure) {
68
- space.structure.forEach((field) => {
69
- if (field.type === "markdown") {
70
- field.enableImageUpload = true;
71
- field.enableVideoUpload = true;
72
- }
73
- });
74
- }
75
- setSelectedSpace(space);
76
- setTabs(space.extendedTabs ?? []);
77
- setQueries(space?.additionalQuery ?? []);
78
- if (space?.sortBy?.sortId !== "") {
79
- setSort(space.sortBy ?? undefined);
80
- }
81
- }
82
- }
83
- if (workspaceParam) {
84
- const workspace: any = await getWorkspaceData(workspaceParam);
85
- if (workspace) {
86
- setSelectedWorkspace(workspace);
87
- const allSpaces = await getAllSpacesList(workspaceParam);
88
- if (allSpaces) setAllWorkspaceSpace(allSpaces);
89
- }
107
+ /**
108
+ * INFO: the mock implements the SERVER's unwind rather than working around it —
109
+ * one row per element, and a count taken after the unwind and before paging. A
110
+ * mock that returned whole records would make the story look right while
111
+ * proving nothing about the thing on screen.
112
+ */
113
+ const handleInstallDemoMock = (store: any[]) => {
114
+ const mock = new MockAdapter(axios, {
115
+ onNoMatch: "passthrough",
116
+ delayResponse: 120,
117
+ });
118
+ const renderUrl = (type: string) =>
119
+ `${DEMO_ENDPOINT}/${DEMO_COLLECTION}/${type}`;
120
+ const handleParseBody = (config: any) => {
121
+ try {
122
+ return JSON.parse(config?.data ?? "{}");
123
+ } catch (err) {
124
+ return {};
90
125
  }
91
- setMount(true);
92
- };
93
-
94
- const handleSortData = (sortValue: Sort) => {
95
- setSort(sortValue.sortId !== "" ? sortValue : undefined);
96
126
  };
97
127
 
98
- const handleAddColumn = (structure: DataStructure[]) => {
99
- const clonedSpace = JSON.parse(JSON.stringify(selectedSpace));
100
- if (clonedSpace) {
101
- clonedSpace["structure"] = JSON.parse(
102
- JSON.stringify(
103
- structure.filter((eachStructure) => eachStructure.id !== "id"),
104
- ),
105
- );
128
+ mock.onPost(renderUrl("list")).reply((config) => {
129
+ const body = handleParseBody(config);
130
+ let rows: any[] = store;
131
+ if (body.unwind) {
132
+ // INFO: like the server's $unwind — an array becomes one row per
133
+ // element, a single value passes through as one row, and a record
134
+ // without the field contributes nothing
135
+ rows = store.flatMap((eachRecord: any) => {
136
+ const value = eachRecord[body.unwind];
137
+ const entries = Array.isArray(value) ? value : value ? [value] : [];
138
+ return entries.map((eachFile: any) => ({
139
+ ...eachRecord,
140
+ [body.unwind]: eachFile,
141
+ }));
142
+ });
106
143
  }
107
- setSelectedSpace(clonedSpace);
108
- dataTemplateRef.current?.handleRefresh?.();
109
- };
144
+ const cursor = typeof body.cursor === "number" ? body.cursor : 0;
145
+ const limit = typeof body.limit === "number" ? body.limit : 10;
146
+ return [
147
+ 200,
148
+ { data: rows.slice(cursor, cursor + limit), count: rows.length },
149
+ ];
150
+ });
110
151
 
111
- const handleDrag = (dragId: string, dropId: string) => {
112
- const selectedDS: DataStructure[] = JSON.parse(
113
- JSON.stringify(selectedSpace?.structure ?? []),
114
- );
115
- const selectedIndex = selectedDS.findIndex((item) => item.id === dragId);
116
- const [selectedItem] = selectedDS.splice(selectedIndex, 1);
117
- const placingIndex = selectedDS.findIndex((item) => item.id === dropId);
118
- selectedDS.splice(placingIndex, 0, selectedItem);
119
- const clonedSpace = JSON.parse(JSON.stringify(selectedSpace));
120
- if (clonedSpace) clonedSpace["structure"] = selectedDS;
121
- setSelectedSpace(clonedSpace);
122
- };
152
+ mock.onPost(renderUrl("get")).reply((config) => {
153
+ const body = handleParseBody(config);
154
+ return [200, { data: store.find((each) => each._id === body.id) ?? null }];
155
+ });
123
156
 
124
- const handleHide = (valueList: DataStructure[]) => {
125
- const clonedSpace: Space = JSON.parse(JSON.stringify(selectedSpace));
126
- clonedSpace.structure?.map((eachStructure) => {
127
- const selectedList = valueList.filter(
128
- (eachValue) => eachValue.id === eachStructure.id,
129
- );
130
- if (selectedList.length > 0) {
131
- eachStructure.hide = selectedList[0].hide ? true : false;
132
- }
133
- return null;
134
- });
135
- setSelectedSpace(clonedSpace);
136
- };
157
+ // INFO: the store is mutated in place so a create or an edit persists for the
158
+ // life of the story. Replying from a static array would make every write look
159
+ // like it silently did nothing.
160
+ // INFO: with a schemaEndpoint set, DataTemplate looks up sibling datasheets
161
+ // to offer as reference targets. Nothing in this demo references another
162
+ // space, so an empty list is the honest answer — but the call has to succeed
163
+ // or the tab bar never finishes rendering.
164
+ mock
165
+ .onPost(`${DEMO_ENDPOINT}/spaces/list`)
166
+ .reply(200, { data: [], count: 0 });
137
167
 
138
- const handleTab = (tabValue: ExtendedTabs[]) => {
139
- setTabs(tabValue);
140
- };
168
+ mock.onPost(renderUrl("create")).reply((config) => {
169
+ const body = handleParseBody(config);
170
+ const record = {
171
+ _id: `demo-${Date.now()}`,
172
+ files: [],
173
+ scans: [],
174
+ ...body.data,
175
+ };
176
+ store.push(record);
177
+ return [200, { data: record }];
178
+ });
179
+
180
+ mock.onPost(renderUrl("update")).reply((config) => {
181
+ const body = handleParseBody(config);
182
+ const index = store.findIndex((each) => each._id === body.id);
183
+ if (index >= 0) store[index] = { ...store[index], ...body.data };
184
+ return [200, { data: store[index] ?? null }];
185
+ });
186
+
187
+ mock.onPost(renderUrl("delete")).reply((config) => {
188
+ const body = handleParseBody(config);
189
+ const index = store.findIndex((each) => each._id === body.id);
190
+ if (index >= 0) store.splice(index, 1);
191
+ return [200, { data: true }];
192
+ });
193
+
194
+ return mock;
195
+ };
196
+
197
+ const demoDevSettings: any = {
198
+ server: "mongoServer",
199
+ dbEndpoint: DEMO_ENDPOINT,
200
+ dbAPIToken: "",
201
+ dbUserToken: "",
202
+ // INFO: schemaPresent is `devSettings.schemaEndpoint ? true : false`, and it
203
+ // gates the ENTIRE tab bar — an empty string here hides tabs and + New Tab
204
+ // with no other symptom. It points at the demo endpoint, which answers the
205
+ // spaces lookup below.
206
+ schemaEndpoint: DEMO_ENDPOINT,
207
+ schemaEndpointToken: "",
208
+ // INFO: a stored token is turned into <imageEndpoint><folder>%2F<row>%2F<name>
209
+ // ?alt=media&token=… — the same shape as our storage. A placeholder host
210
+ // that draws whatever path it is given stands in for the bucket, so the
211
+ // tile shows a real image rather than a broken one.
212
+ imageEndpoint: "https://placehold.co/600x400/e2e8f0/475569.png?text=",
213
+ uploadEndpoint: "",
214
+ };
141
215
 
142
- if (!mount) {
143
- return <Loading />;
144
- }
216
+ const DataTemplateDemo = () => {
217
+ const store = React.useMemo(() => renderDemoRecords(), []);
218
+ React.useMemo(() => handleInstallDemoMock(store), [store]);
145
219
 
146
- const devSettings = parseWorkspaceSettings(
220
+ // INFO: a real consumer keeps type and id in its own state and hands them
221
+ // back — that is what makes create and read views open at all. The story does
222
+ // the same rather than intercepting the navigation, which is why an alert
223
+ // stood in for a create form before.
224
+ const [typeParam, setTypeParam] = useState<"list" | "read" | "update">(
225
+ "list",
226
+ );
227
+ const [dataId, setDataId] = useState<string>("");
228
+ const [layout, setLayout] = useState<"desktop" | "mobile">("desktop");
229
+ const isMobile = layout === "mobile";
230
+ const [viewId, setViewId] = useState<string | undefined>(undefined);
231
+ const [tabs, setTabs] = useState<ExtendedTabs[]>([
147
232
  {
148
- dbEndpoint: defaultDBEndpoint,
149
- imageEndpoint: defaultImageEndpoint,
150
- uploadEndpoint: defaultUploadEndpoint,
233
+ name: "Documents",
234
+ type: "list",
235
+ viewType: "file",
236
+ defaultTab: true,
237
+ fileSettings: { columnId: "files" },
151
238
  },
152
- selectedWorkspace,
153
- );
239
+ {
240
+ name: "Scans",
241
+ type: "list",
242
+ viewType: "file",
243
+ fileSettings: { columnId: "scans" },
244
+ },
245
+ {
246
+ name: "Photos",
247
+ type: "list",
248
+ viewType: "file",
249
+ fileSettings: { columnId: "photo" },
250
+ },
251
+ ]);
154
252
 
155
253
  return (
156
- <>
157
- {/*
158
- * STORY CONTROLS
159
- * These buttons demonstrate imperative ref methods and collection switching.
160
- * In a real app these would be driven by routing / state management.
161
- */}
162
- <StorySection
163
- title="Data Template"
164
- description="Use DataTemplateImpl when you have a Space object from the Blixify backend — it drives the schema, sort, tabs, and queries from the database. Pass the Space via the space prop and derive the model using parseSpaceToDataStructure. For custom integrations without a Space object, use DataTemplateWrapper instead."
165
- >
166
- <div className="flex flex-wrap gap-3 mb-4">
167
- <Button
168
- text={typeParam === "list" ? "Switch → Update" : "Switch → List"}
169
- type="light"
170
- onClick={() =>
171
- setTypeParam((t) =>
172
- t === "update" || t === "read" ? "list" : "update",
173
- )
174
- }
175
- />
176
- <Button
177
- text="Prefill Form"
178
- type="light"
179
- onClick={() => {
180
- dataTemplateRef.current?.handleUpdateSelectedData([
181
- { key: "title", value: "Prefilled Value" },
182
- ]);
183
- }}
184
- />
185
- <Button
186
- text="Swap Collection"
187
- type="light"
188
- onClick={() => {
189
- setCollectionParams((c) =>
190
- c === "e40bcbbf-191f-4bac-83d0-322cb4cbabde"
191
- ? "602c90dd-ee39-4bf0-b44d-3d2c57f887cd"
192
- : "e40bcbbf-191f-4bac-83d0-322cb4cbabde",
193
- );
194
- }}
195
- />
196
- </div>
197
-
198
- {selectedSpace && selectedWorkspace && (
199
- <div className="w-full">
200
- <DataTemplateImpl
201
- ref={dataTemplateRef}
202
- title={title}
203
- description={description}
204
- type={typeParam}
205
- id={dataParam}
206
- collectionId={collectionParam}
207
- model={parseSpaceToDataStructure(selectedSpace)}
208
- localURLEndpoint={getLocalUrl}
209
- space={selectedSpace}
210
- limit={30}
211
- edit={{
212
- allEditable: true,
213
- listEditable: true,
214
- listAddable: true,
215
- deleteBtn: true,
216
- disabledUpdate: false,
217
- disableBatchUpdate: false,
218
- activityLog: {
219
- enableComments: true,
220
- commentCollectionId: collectionParam + "-comments",
221
- enableLogs: true,
222
- logsCollectionId: collectionParam + "-logs",
223
- userAPIEndpoint: "http://localhost:3000/api/data/users",
224
- currentUserId: "3P3il3L8uScSkASU2MdAoqGs1M82",
225
- },
226
- }}
227
- organise={{
228
- sort: sort ?? undefined,
229
- viewId: viewParam,
230
- extendedTabs: tabs,
231
- additionalQuery: queries,
232
- splitView: false,
233
- showGroupByFeature: true,
234
- showReadFloatingNav: true,
235
- disableMulti: false,
236
- exportSettings: {
237
- enableExportReadPDF: true,
238
- exportReadPDFDimensions: {
239
- forceDesktopView: true,
240
- captureWidth: 1440,
241
- captureHeight: 900,
242
- pageSize: "a4",
243
- scale: 2,
244
- },
245
- },
246
- filterComponentNewRow: true,
247
- addColumnEnabled: true,
248
- onChangeSort: handleSortData,
249
- onChangeTabs: handleTab,
250
- onChangeDrag: handleDrag,
251
- onAddColumns: handleAddColumn,
252
- onChangeHide: handleHide,
253
- handleNavigate: (type, selectedId, viewId) => {
254
- setTypeParam(type);
255
- if (selectedId) setDataParam(selectedId);
256
- if (viewId) setViewParam(viewId);
257
- },
258
- renderReferenceLabelOptions: storyRenderReferenceLabelOptions(
259
- allWorkspaceSpace,
260
- selectedSpace,
261
- ),
262
- renderReferenceSpaceOptions: storyRenderReferenceSpaceOptions(
263
- allWorkspaceSpace,
264
- dataParam,
265
- ),
266
- }}
267
- lib={{
268
- axios,
269
- Menu,
270
- Transition,
271
- navigate: null,
272
- papaParser: Papa,
273
- }}
274
- bareSettings={{
275
- // Custom action buttons shown on each list row
276
- bareListActionBtns: () => [
277
- <div
278
- key="tag"
279
- onClick={(e) => {
280
- e.stopPropagation();
281
- e.preventDefault();
282
- window.alert("Custom row action clicked!");
283
- }}
284
- >
285
- <TagIcon className="w-5 h-5 text-primary-500" />
286
- </div>,
287
- ],
288
- // Extra bulk-action buttons shown when rows are selected
289
- bareListMultipleSelectAddtionalActions: [
290
- {
291
- text: "Custom Bulk Action",
292
- onClick: (ids: string[]) =>
293
- window.alert(
294
- `Bulk action on ${ids.length} selected item(s)`,
295
- ),
296
- },
297
- ],
298
- }}
299
- devSettings={{
300
- ...devSettings,
301
- uploadEndpoint: defaultUploadEndpoint,
302
- imageEndpoint: defaultImageEndpoint,
303
- schemaEndpoint: schemaUrl,
304
- dbAPIToken: dbAPIToken,
305
- organisationMembers: [
306
- { key: "3P3il3L8uScSkASU2MdAoqGs1M82", label: "Crystal" },
307
- { key: "a2vG49kXjFg7hzzEYS5ZfJJJ43C3", label: "Chan Yi" },
308
- { key: "qFZVckKT3tMIkxvohlVS27IWuhn1", label: "Chris" },
309
- ],
310
- currentUser: {
311
- userEmail: "chanyi@blixify.co",
312
- userName: "Chan Yi",
313
- },
314
- functionEndpoint:
315
- "http://127.0.0.1:5001/blixify-space/asia-southeast1",
316
- }}
317
- />
318
- </div>
319
- )}
320
- </StorySection>
321
- </>
254
+ <div className="w-full">
255
+ <div className="mb-4">
256
+ <ButtonGroup
257
+ selection={LAYOUT_TOGGLE}
258
+ selectedId={layout}
259
+ variant="tag"
260
+ size="small"
261
+ onClick={(id) => setLayout(id as "desktop" | "mobile")}
262
+ />
263
+ </div>
264
+ {/* INFO: the PROPS force the mobile layout — NewTable's mobileView renders
265
+ cards regardless of viewport. The width only makes it read as a
266
+ phone; without the props, narrowing alone would show a squashed
267
+ desktop table, since Tailwind breakpoints key off the viewport. */}
268
+ <div className={isMobile ? "mx-auto max-w-sm" : "w-full"}>
269
+ <DataTemplateImpl
270
+ title="Documents"
271
+ description="Three records hold five attachments between them"
272
+ type={typeParam}
273
+ id={dataId}
274
+ collectionId={DEMO_COLLECTION}
275
+ model={DEMO_SCHEMA}
276
+ localURLEndpoint=""
277
+ devSettings={demoDevSettings}
278
+ lib={{ axios, Menu, Transition, papaParser: Papa }}
279
+ limit={4}
280
+ // INFO: allEditable is what gates isEditor, and isEditor is what puts
281
+ // "+ New Tab" and the create button on screen. A read-only demo hides
282
+ // the very controls it is meant to demonstrate.
283
+ edit={{ allEditable: true, deleteBtn: true, createBtn: true }}
284
+ organise={{
285
+ extendedTabs: tabs,
286
+ viewId,
287
+ onChangeTabs: (value: any) => setTabs(value),
288
+ handleNavigate: (type: string, id?: string, view?: string) => {
289
+ setTypeParam(type as any);
290
+ setDataId(id ?? "");
291
+ if (view) setViewId(view);
292
+ },
293
+ }}
294
+ // INFO: mirrors a typical work-order list — mobile gets the card list and
295
+ // the mobile form; desktop pins the first column so the record name
296
+ // stays visible while scrolling wide tables. A pinned column on a
297
+ // phone-width card list would pin nothing useful.
298
+ bareSettings={{
299
+ // INFO: mobileView only reaches NewTable, and NewTable only renders
300
+ // under bareNewDTW — on the legacy table bareListMobileView does
301
+ // nothing and "Mobile" would just squash the desktop table.
302
+ // Apps that use the mobile cards set this unconditionally for the same reason.
303
+ bareNewDTW: true,
304
+ bareListMobileView: isMobile,
305
+ bareUpdateMobileView: isMobile,
306
+ bareListTablePinColumns: isMobile
307
+ ? undefined
308
+ : { left: [DEMO_SCHEMA[0].id] },
309
+ }}
310
+ />
311
+ </div>
312
+ </div>
322
313
  );
323
314
  };
315
+
316
+ export const DataTemplate = () => (
317
+ // INFO: not wrapped in StorySection — that is w-[90vw], and a data table
318
+ // wants the width it is given.
319
+ <div className="w-full">
320
+ <div className="font-semibold mb-1 text-gray-900">
321
+ Data Template — self-contained demo
322
+ </div>
323
+ <div className="text-sm mb-4 text-gray-600">
324
+ Carries its own schema and data, so everything works with no backend:
325
+ create a record, edit inline, delete one, and add a tab through + New Tab.
326
+ Three records hold five attachments and the File view lists FIVE tiles —
327
+ the server unwinds the chosen column, so a row is a file rather than a
328
+ record, and the count and paging follow. The record with no attachments
329
+ contributes nothing. Switch to Scans for the same records unwound on a
330
+ different column, which is why + New Tab asks which column a File view
331
+ should list. Any tab can be set to open by default from that same modal.
332
+ </div>
333
+ <DataTemplateDemo />
334
+ </div>
335
+ );