blixify-ui-web-mcp 0.1.39 → 0.1.41

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.
@@ -80,7 +80,7 @@ return (
80
80
 
81
81
  | Name | Type | Default | Description |
82
82
  | ---------------- | ----------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------- |
83
- | `positionOffset` | `string` | `"bottom-6 right-6"` | Tailwind classes that control the fixed position of the anchor. Ignored on mobile when open. |
83
+ | `positionOffset` | `string` | `"bottom-28 right-4 md:bottom-6 md:right-6"` | Tailwind classes that control the fixed position of the anchor. On phones the default starts above a bottom bar such as DataTemplate's Quick find. Ignored on mobile when open. |
84
84
  | `greetingCard` | `boolean` | `false` | When `true`, shows a greeting bubble next to the anchor button while the chat is closed. |
85
85
  | `greetingText` | `string` | `"Welcome! How can I help you today?"` | Text displayed inside the greeting card bubble. |
86
86
  | `openIcon` | `React.ReactNode` \| `string` | `ChatBubbleOvalLeftIcon` | Icon shown when the chat is closed. A string is treated as an image URL (56×56 rounded circle). |
@@ -47,9 +47,9 @@ Three schema-level settings shape how a record is laid out. They nest in one fix
47
47
  ```
48
48
  Tab (DataStructure.tabId) ← tab bar across the top of the read + update views
49
49
  └─ Section (DataStructure.groupId) ← titled card inside the tab; collapsible in the form
50
- └─ fields ← an object / listObject field renders as a nested
51
- block or table INSIDE its section — it is a field,
52
- not a further structural level
50
+ └─ fields ← an object, or a listObject with
51
+ listObjectApplyGroup: true, renders inside
52
+ its section as a block or table
53
53
  ```
54
54
 
55
55
  - Columns with a `tabId` but no `groupId` sit directly under the tab; columns with
@@ -60,6 +60,45 @@ Tab (DataStructure.tabId) ← tab bar across the top of the read
60
60
  - `organise.groupBy` is different again: it splits the **list** into one table per value
61
61
  of a column ("Group By X"), and has nothing to do with `groupId`.
62
62
 
63
+ **List-object section grouping** is opt-in on each schema field:
64
+
65
+ | Attribute | Type | Behavior |
66
+ | --- | --- | --- |
67
+ | `listObjectApplyGroup` | `boolean` (optional) | `true` includes the list object in its `groupId` section in read and update/create views. False or omitted keeps it outside named groups. |
68
+
69
+ ```tsx
70
+ {
71
+ id: "items",
72
+ name: "Items",
73
+ type: "listObject",
74
+ tabId: "Details",
75
+ groupId: "Shipping",
76
+ listObjectApplyGroup: true,
77
+ listObjectTableView: true,
78
+ objectData: [
79
+ { id: "name", name: "Name", type: "string" },
80
+ { id: "quantity", name: "Quantity", type: "number" },
81
+ ],
82
+ }
83
+ ```
84
+
85
+ Grouped list objects share the read section's card with ordinary fields. A
86
+ section containing only a list object still shows its group heading. In update
87
+ forms, list objects span all 12 grid columns regardless of list/table mode or
88
+ `rowColumns`; their existing add/edit modal and save behavior are retained.
89
+
90
+ In grouped read views, list objects with `listObjectApplyGroup` false or omitted
91
+ render once after **all tab sections**, even if they have a `tabId` and `groupId`.
92
+ In update forms, regular ungrouped lists render after the form; `listObjOrder`
93
+ and `listFlexTable` lists remain in the form's default area outside named groups.
94
+ To keep an existing list object inside a named section, explicitly set
95
+ `listObjectApplyGroup: true`. `parseSpaceToDataStructure` preserves the attribute
96
+ when deriving a model from a Space schema.
97
+
98
+ The **DTW Group Test** story in `dataWrapper.view.stories.tsx` includes a grouped
99
+ Operation Crew table, a grouped Sprayed Plots list, and an ungrouped Operation
100
+ Equipment table for comparison.
101
+
63
102
  **How a tabbed form is dressed** (`tabAlign: "left"`):
64
103
 
65
104
  - The rail is a numbered `Stepper` (`variant="checklist"`) — the active step is a filled primary block, and a
@@ -400,6 +400,44 @@ organise={{ tabAlign: "stepper" }} // renders tabs as a step-by-step wizard
400
400
 
401
401
  `tabId` and `groupId` can be combined — fields are first split by tab, then grouped within each tab.
402
402
 
403
+ #### List objects inside sections
404
+
405
+ Set `listObjectApplyGroup: true` on a `listObject` schema entry to include it in
406
+ its `groupId` section in read and update/create views:
407
+
408
+ ```tsx
409
+ {
410
+ id: "items",
411
+ name: "Items",
412
+ type: "listObject",
413
+ tabId: "Details",
414
+ groupId: "Shipping",
415
+ listObjectApplyGroup: true,
416
+ listObjectTableView: true,
417
+ objectData: [
418
+ { id: "name", name: "Name", type: "string" },
419
+ { id: "quantity", name: "Quantity", type: "number" },
420
+ ],
421
+ }
422
+ ```
423
+
424
+ `listObjectApplyGroup?: boolean` defaults to ungrouped when false or omitted.
425
+ It is independent of `listObjectTableView`: both lists and tables can join a
426
+ section. Grouped list objects share the read section's card and span all columns
427
+ of the update section, regardless of `rowColumns`. Adding and editing rows uses
428
+ the existing modal.
429
+
430
+ Ungrouped list objects appear after all tab sections in grouped read views,
431
+ even when they have a `tabId` and `groupId`. In update forms, regular ungrouped
432
+ lists appear after the form; ordered (`listObjOrder`) and FlexTable
433
+ (`listFlexTable`) lists remain in the default form area outside named groups.
434
+ Existing schemas must explicitly set `listObjectApplyGroup: true` to place a
435
+ list object inside a named section.
436
+
437
+ Open **Data / Data Wrapper / View / DTW Group Test** in Storybook to compare
438
+ Operation Crew (grouped table), Sprayed Plots (grouped list), and Operation
439
+ Equipment (ungrouped table), with sample rows provided in the story backend.
440
+
403
441
  ---
404
442
 
405
443
  ## Usage
@@ -19,6 +19,7 @@ import {
19
19
  CalendarSettings,
20
20
  FeedsSettings,
21
21
  KanbanSettings,
22
+ TimelineSettings,
22
23
  } from "../dataTemplate/spaceModel";
23
24
  import {
24
25
  DataWrapperTestSchema,
@@ -64,6 +65,10 @@ const handleBuildChecklistRecord = (index: number) => ({
64
65
  date: dayjs()
65
66
  .add(index - 15, "day")
66
67
  .toISOString(),
68
+ // INFO: a span a few days after the start so the timeline draws a readable bar
69
+ endDate: dayjs()
70
+ .add(index - 15 + (3 + (index % 5)), "day")
71
+ .toISOString(),
67
72
  installerName: STORY_NAMES[(index + 4) % STORY_NAMES.length],
68
73
  numberOfModules: 8 + (index % 12),
69
74
  typeOfInverter: index % 2 === 0 ? "String Inverter" : "Microinverter",
@@ -119,6 +124,7 @@ export const DataWrapper = () => {
119
124
  calendarSettings?: CalendarSettings;
120
125
  feedsSettings?: FeedsSettings;
121
126
  kanbanSettings?: KanbanSettings;
127
+ timelineSettings?: TimelineSettings;
122
128
  } = {};
123
129
 
124
130
  if (listLayout === "grid") {
@@ -146,6 +152,20 @@ export const DataWrapper = () => {
146
152
  viewSwitch: ["month", "week", "agenda"],
147
153
  };
148
154
  }
155
+ if (listLayout === "timeline") {
156
+ viewSettings.timelineSettings = {
157
+ title: "clientName",
158
+ startDate: "date",
159
+ endDate: "endDate",
160
+ // INFO: colour bars by inverter type, same grouping the calendar story
161
+ // uses so the two views read the same data the same way
162
+ groupId: "typeOfInverter",
163
+ groupColorMap: {
164
+ "String Inverter": "blue",
165
+ Microinverter: "indigo",
166
+ },
167
+ };
168
+ }
149
169
  return viewSettings;
150
170
  };
151
171
 
@@ -179,6 +199,7 @@ export const DataWrapper = () => {
179
199
  { id: "scrollList", name: "Scroll Pagination List", href: "#" },
180
200
  { id: "grid", name: "Grid View", href: "#" },
181
201
  { id: "calendar", name: "Calendar View", href: "#" },
202
+ { id: "timeline", name: "Timeline View", href: "#" },
182
203
  { id: "customList", name: "Custom List View", href: "#" },
183
204
  ]}
184
205
  onClick={(id) => {
@@ -458,6 +479,34 @@ const DTWGroupTestSchema = [
458
479
  groupId: "1. Operation",
459
480
  optional: true,
460
481
  },
482
+ {
483
+ id: "operationCrew",
484
+ name: "Operation Crew",
485
+ type: "listObject" as const,
486
+ tabId: "A. Basic Info",
487
+ groupId: "1. Operation",
488
+ listObjectApplyGroup: true,
489
+ listObjectTableView: true,
490
+ optional: true,
491
+ objectData: [
492
+ { id: "name", name: "Name", type: "string" as const },
493
+ { id: "role", name: "Role", type: "string" as const },
494
+ ],
495
+ },
496
+ {
497
+ id: "operationEquipment",
498
+ name: "Operation Equipment (Ungrouped)",
499
+ type: "listObject" as const,
500
+ tabId: "A. Basic Info",
501
+ groupId: "1. Operation",
502
+ listObjectApplyGroup: false,
503
+ listObjectTableView: true,
504
+ optional: true,
505
+ objectData: [
506
+ { id: "name", name: "Equipment", type: "string" as const },
507
+ { id: "quantity", name: "Quantity", type: "number" as const },
508
+ ],
509
+ },
461
510
  {
462
511
  id: "status",
463
512
  name: "Status",
@@ -523,6 +572,19 @@ const DTWGroupTestSchema = [
523
572
  groupId: "2. Coverage",
524
573
  optional: true,
525
574
  },
575
+ {
576
+ id: "sprayedPlots",
577
+ name: "Sprayed Plots",
578
+ type: "listObject" as const,
579
+ tabId: "B. Pesticide",
580
+ groupId: "2. Coverage",
581
+ listObjectApplyGroup: true,
582
+ optional: true,
583
+ objectData: [
584
+ { id: "plot", name: "Plot", type: "string" as const },
585
+ { id: "hectares", name: "Hectares", type: "number" as const },
586
+ ],
587
+ },
526
588
  // Tab C: Flight Metrics
527
589
  {
528
590
  id: "totalFlight",
@@ -654,6 +716,18 @@ const handleBuildWorkOrderRecord = (index: number) => {
654
716
  endDate: start.add(6, "hour").toISOString(),
655
717
  totalFlight: 2 + (index % 6),
656
718
  totalHours: 3 + (index % 5),
719
+ operationCrew: [
720
+ { id: `crew-${index}-1`, name: "Ali", role: "Pilot" },
721
+ { id: `crew-${index}-2`, name: "Siti", role: "Ground Crew" },
722
+ ],
723
+ operationEquipment: [
724
+ { id: `equipment-${index}-1`, name: "Sprayer Drone", quantity: 1 },
725
+ { id: `equipment-${index}-2`, name: "Spare Battery", quantity: 3 },
726
+ ],
727
+ sprayedPlots: [
728
+ { id: `plot-${index}-1`, plot: "North Field", hectares: 2.5 },
729
+ { id: `plot-${index}-2`, plot: "South Field", hectares: 1.8 },
730
+ ],
657
731
  };
658
732
  };
659
733
 
@@ -680,7 +754,9 @@ export const DTWGroupTest = () => {
680
754
  <div className="text-sm font-light mb-4 text-gray-500">
681
755
  Multi-tab + multi-group schema demo. List + read view renders sections
682
756
  by tabId / groupId; Create / Update / Delete write to the in-memory
683
- story backend.
757
+ story backend. Operation Crew (table) and Sprayed Plots (list) use
758
+ listObjectApplyGroup to appear inside their sections. Operation
759
+ Equipment has grouping disabled for comparison.
684
760
  </div>
685
761
  <DataTemplateWrapper
686
762
  ref={dataRefreshRef}
@@ -706,7 +782,7 @@ export const DTWGroupTest = () => {
706
782
  sort: { sortId: "startDate", type: "desc" as const },
707
783
  breadcrumbType: "button",
708
784
  groupRead: "groupId",
709
- tabAlign: "left",
785
+ // tabAlign: "left",
710
786
  filterComponentNewRow: true,
711
787
  // INFO: removed showExport to hide the ellipsis (⋮) menu
712
788
  }}
@@ -1039,6 +1115,135 @@ export const DataWrapperDrawerTest = () => {
1039
1115
  };
1040
1116
  DataWrapperDrawerTest.storyName = "DTW Drawer Test";
1041
1117
 
1118
+ // ===================================================================================
1119
+ // SECTION: Timeline view — GanttChart-backed date-range view
1120
+ // ===================================================================================
1121
+
1122
+ // INFO: seeds records that each carry a start (projectStart) and end (projectEnd)
1123
+ // spanning a few days, so the timeline draws a readable bar per row. One row is
1124
+ // intentionally left without an end date to show that incomplete rows are skipped
1125
+ // rather than dropping every bar around them.
1126
+ const TimelineStorySchema = [
1127
+ {
1128
+ id: "projectName",
1129
+ name: "Project",
1130
+ type: "string" as const,
1131
+ },
1132
+ {
1133
+ id: "team",
1134
+ name: "Team",
1135
+ type: "list" as const,
1136
+ listSelection: ["Design", "Engineering", "QA"],
1137
+ },
1138
+ {
1139
+ id: "projectStart",
1140
+ name: "Start Date",
1141
+ type: "date" as const,
1142
+ },
1143
+ {
1144
+ id: "projectEnd",
1145
+ name: "End Date",
1146
+ type: "date" as const,
1147
+ optional: true,
1148
+ },
1149
+ ];
1150
+
1151
+ const TIMELINE_TEAMS = ["Design", "Engineering", "QA"];
1152
+
1153
+ const handleBuildTimelineRecord = (index: number) => {
1154
+ // INFO: stagger each project's start so the bars cascade down the axis the way
1155
+ // the Velocity-style timeline in the brief does
1156
+ const start = dayjs()
1157
+ .startOf("month")
1158
+ .add(index * 3, "day");
1159
+ // INFO: every 5th row is left open-ended (no projectEnd) to demonstrate the
1160
+ // skip — it must not render a bar, and must not hide the rows after it
1161
+ const isOpenEnded = index % 5 === 4;
1162
+ return {
1163
+ _id: `timeline-${index}`,
1164
+ projectName: `Project ${String.fromCharCode(65 + (index % 26))}${index}`,
1165
+ team: TIMELINE_TEAMS[index % TIMELINE_TEAMS.length],
1166
+ projectStart: start.toISOString(),
1167
+ projectEnd: isOpenEnded
1168
+ ? undefined
1169
+ : start.add(4 + (index % 6), "day").toISOString(),
1170
+ };
1171
+ };
1172
+
1173
+ export const Timeline = () => {
1174
+ const mount = useStoryBackend({
1175
+ collections: {
1176
+ "dtw-timeline-collection": handleBuildStoryRecords(
1177
+ 12,
1178
+ handleBuildTimelineRecord,
1179
+ ),
1180
+ },
1181
+ yieldToE2E: true,
1182
+ });
1183
+ const dataRefreshRef: any = useRef(null);
1184
+ const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
1185
+ const [selectedId, setSelectedId] = useState<string | undefined>(undefined);
1186
+
1187
+ const handleNavigate = (type: any, id: any) => {
1188
+ setViewType(type);
1189
+ setSelectedId(id);
1190
+ };
1191
+
1192
+ if (!mount) {
1193
+ return <Loading />;
1194
+ }
1195
+
1196
+ return (
1197
+ <div className="w-full">
1198
+ <div className="font-semibold mb-1 text-gray-900">DTW Timeline</div>
1199
+ <div className="text-sm font-light mb-4 text-gray-500">
1200
+ The timeline view renders each record as a horizontal bar spanning its
1201
+ start and end date, powered by GanttChart. Bars are coloured by team via{" "}
1202
+ <code>timelineSettings.groupColorMap</code>. Rows missing an end date are
1203
+ skipped (every 5th project here is open-ended), and clicking a bar
1204
+ navigates to that record. Backed by 12 in-memory records — no dev server
1205
+ needed.
1206
+ </div>
1207
+ <DataTemplateWrapper
1208
+ ref={dataRefreshRef}
1209
+ title="Projects"
1210
+ type={viewType}
1211
+ id={selectedId}
1212
+ collectionId={"dtw-timeline-collection"}
1213
+ schema={TimelineStorySchema}
1214
+ workspace={storyWorkspace()}
1215
+ edit={{ allEditable: true }}
1216
+ bareSettings={{
1217
+ bareCompactMode: true,
1218
+ bareOffline: false,
1219
+ }}
1220
+ organise={{
1221
+ sort: { sortId: "projectStart", type: "asc" as const },
1222
+ breadcrumbType: "button",
1223
+ }}
1224
+ handleNavigate={handleNavigate}
1225
+ viewSettings={{
1226
+ timelineSettings: {
1227
+ title: "projectName",
1228
+ startDate: "projectStart",
1229
+ endDate: "projectEnd",
1230
+ // INFO: fixed colour per team so the same team is always the same
1231
+ // colour across refetches, matching the calendar's grouping behaviour
1232
+ groupId: "team",
1233
+ groupColorMap: {
1234
+ Design: "violet",
1235
+ Engineering: "blue",
1236
+ QA: "green",
1237
+ },
1238
+ },
1239
+ }}
1240
+ limit={50}
1241
+ />
1242
+ </div>
1243
+ );
1244
+ };
1245
+ Timeline.storyName = "DTW Timeline";
1246
+
1042
1247
  // ===================================================================================
1043
1248
  // SECTION: Offline demo — DTW with no backend required
1044
1249
  // ===================================================================================
@@ -109,7 +109,7 @@ What changes when `borderless` is on:
109
109
  | | Default | `borderless` |
110
110
  | --------- | -------------------------------------------------------- | -------------------------------------------------------------------------------- |
111
111
  | Container | `rounded`, `bg-white`, `border border-gray-300` | none |
112
- | Toolbar | Always shown when not `readOnly` | Hidden — use `/` and keyboard shortcuts |
112
+ | Toolbar | Always shown when not `readOnly` | Hidden — floating toolbar on text selection, `/` and keyboard shortcuts |
113
113
  | Height | `heightClassName ?? "h-72"` with inner `overflow-y-auto` | `heightClassName ?? "min-h-[12rem]"`, grows with content, page scrolls |
114
114
  | Padding | `p-4` | `p-0` |
115
115
  | Label row | Always rendered | Rendered only if `label`, `subLabel`, `customElement`, or `infoElement` is given |
@@ -118,6 +118,8 @@ On error the editor draws a red outline (`border border-red-500 rounded`) — th
118
118
 
119
119
  > **`heightClassName` in borderless mode must be a `min-h-*` class**, not a fixed `h-*`. Borderless has no inner `overflow-y-auto`, so a fixed height clips content instead of scrolling it. Leave it unset unless you need a taller empty state.
120
120
 
121
+ Selecting text in borderless mode shows a floating toolbar above the selection — Bold, Italic, Underline, Strikethrough, Inline code, Link and Clear formatting. It works inside table cells, and does not show in read-only mode, in a code block, or when an image or embed is selected. The boxed editor keeps its static toolbar instead.
122
+
121
123
  > **Note:** hiding the toolbar also hides its media-upload button. In borderless mode insert images and videos by **dragging or pasting** into the editor, or supply your own control via `customElement`.
122
124
 
123
125
  ##### Read-Only Mode (Preview)
@@ -163,6 +165,7 @@ On error the editor draws a red outline (`border border-red-500 rounded`) — th
163
165
  | `lib` | `{ axios?: any, mermaid?: () => Promise<any> }` | `null` | External libraries — axios for API calls; `mermaid` loader (e.g. `() => import("mermaid")`) to draw diagrams |
164
166
  | `uploadEndpoint` | `string` | `null` | API endpoint for uploading media files |
165
167
  | `onHeadingsChange` | `(headings: MarkdownHeading[]) => void` | `null` | Reports the document's `h1`–`h3` outline whenever it changes — feed it to `MarkdownOutline` |
168
+ | `textSize` | `"sm"` | `null` | 14px text on every screen. On phones the editor keeps a real 16px font and is drawn at 87.5% with `transform: scale`, the same trick as `TextInput` xs, so iOS Safari does not zoom on focus. `MarkdownStatic` takes the same prop |
166
169
 
167
170
  ##### Ref Methods
168
171
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blixify-ui-web-mcp",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "license": "MIT",
5
5
  "main": "lib/mcp/server.js",
6
6
  "bin": {