blixify-ui-web-mcp 0.1.25 → 0.1.26

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.
@@ -0,0 +1,256 @@
1
+ import React, { useState } from "react";
2
+ import { NewGrid as NewGridImpl, NewGridItem } from ".";
3
+ import { StorySection } from "../../storyUtils";
4
+
5
+ const meta = { title: "Display/New Grid" };
6
+ export default meta;
7
+
8
+ // INFO: identical sample data to legacy Display/Grid for side-by-side comparison
9
+ const data: NewGridItem[] = [
10
+ {
11
+ id: "1",
12
+ title: "Nike Court Vision",
13
+ description: "Nike Men's Court Vision Low Next Nature Shoes - Black",
14
+ tags: ["Sports"],
15
+ imageUrl: "https://cf.shopee.com.my/file/fef57a9b0d40a0626def6fdc993c442a",
16
+ },
17
+ {
18
+ id: "2",
19
+ title: "Nike Air Max 2X",
20
+ description: "Nike Men's Air Max 2X Shoes - White",
21
+ tags: ["Sports", "Promotions"],
22
+ imageUrl: "https://cf.shopee.com.my/file/6df4da8ff019bfa08c1bcba4bbe3c983",
23
+ },
24
+ {
25
+ id: "3",
26
+ title: "Nike Air Max 2X",
27
+ description: "Nike Men's Air Max 2X Shoes - White",
28
+ tags: ["Sports", "Promotions"],
29
+ imageUrl: "https://cf.shopee.com.my/file/6df4da8ff019bfa08c1bcba4bbe3c983",
30
+ },
31
+ {
32
+ id: "4",
33
+ title: "Nike Air Max 2X",
34
+ description: "Nike Men's Air Max 2X Shoes - White",
35
+ tags: ["Sports", "Promotions"],
36
+ ribbonText: "SALE",
37
+ imageUrl: "https://cf.shopee.com.my/file/6df4da8ff019bfa08c1bcba4bbe3c983",
38
+ },
39
+ {
40
+ id: "5",
41
+ title: "Nike Air Max 2X",
42
+ description: "Nike Men's Air Max 2X Shoes - White",
43
+ tags: ["Sports"],
44
+ imageUrl: "https://cf.shopee.com.my/file/6df4da8ff019bfa08c1bcba4bbe3c983",
45
+ },
46
+ {
47
+ id: "6",
48
+ title: "Nike Air Max 2X",
49
+ description: "Nike Men's Air Max 2X Shoes - White",
50
+ tags: ["Sports", "Promotions"],
51
+ imageUrl: "https://cf.shopee.com.my/file/6df4da8ff019bfa08c1bcba4bbe3c983",
52
+ },
53
+ {
54
+ id: "7",
55
+ title: "Nike Air Max 2X",
56
+ description: "Nike Men's Air Max 2X Shoes - White",
57
+ tags: ["Sports"],
58
+ imageUrl: "https://cf.shopee.com.my/file/6df4da8ff019bfa08c1bcba4bbe3c983",
59
+ },
60
+ {
61
+ id: "8",
62
+ title: "Nike Air Max 2X",
63
+ description: "Nike Men's Air Max 2X Shoes - White",
64
+ tags: ["Sports", "Promotions"],
65
+ imageUrl: "https://cf.shopee.com.my/file/6df4da8ff019bfa08c1bcba4bbe3c983",
66
+ },
67
+ ];
68
+
69
+ const renderImgRibbon = (id: string) => {
70
+ if (id === "1" || id === "2") {
71
+ return (
72
+ <div className="absolute top-5 -left-9 w-[50%] bg-yellow-500 text-white text-xs font-bold p-2 text-center -rotate-45">
73
+ <span>WINNER</span>
74
+ </div>
75
+ );
76
+ }
77
+ return null;
78
+ };
79
+
80
+ export const NewGrid = () => {
81
+ const [pageIndex, setPageIndex] = useState(0);
82
+ const limit = 5;
83
+ const cursor = true;
84
+
85
+ const handlePagination = (value: string | number) => {
86
+ if (typeof value === "number") setPageIndex(value);
87
+ else setPageIndex((prev) => (value === "next" ? prev + 1 : prev - 1));
88
+ };
89
+
90
+ return (
91
+ <div className="font-sans p-6">
92
+ <StorySection
93
+ title="NewGrid — same data as legacy Display/Grid"
94
+ description="NewGridItem = GridClass. Drop-in swap from <Grid data={d}> to <NewGridImpl data={d}> — modernised card chrome (rounded-2xl, tag chips, shared NewPagination footer)."
95
+ >
96
+ <NewGridImpl
97
+ data={data}
98
+ limit={limit}
99
+ loading={false}
100
+ paginationType="page"
101
+ size={data.length}
102
+ pageIndex={pageIndex}
103
+ handleUpdatePageIndex={handlePagination}
104
+ renderImgRibbon={renderImgRibbon}
105
+ />
106
+ </StorySection>
107
+
108
+ <StorySection
109
+ title="NewGrid — Horizontal scroll pagination"
110
+ description="Use paginationType='scroll' with horizontalScroll for an infinite grid that auto-loads the next page when the user scrolls to the right edge. Grab-to-pan drag is enabled."
111
+ >
112
+ <NewGridImpl
113
+ data={data}
114
+ limit={limit}
115
+ loading={false}
116
+ paginationType="scroll"
117
+ cursor={cursor}
118
+ pageIndex={pageIndex}
119
+ size={data.length}
120
+ horizontalScroll
121
+ handleUpdatePageIndex={handlePagination}
122
+ renderImgRibbon={renderImgRibbon}
123
+ />
124
+ </StorySection>
125
+
126
+ <StorySection
127
+ title="NewGrid — Cursor pagination"
128
+ description="Use paginationType='cursor' for prev/next navigation with a truthy cursor while more data exists."
129
+ >
130
+ <NewGridImpl
131
+ data={data}
132
+ limit={limit}
133
+ loading={false}
134
+ paginationType="cursor"
135
+ cursor={cursor}
136
+ pageIndex={pageIndex}
137
+ size={data.length}
138
+ handleUpdatePageIndex={handlePagination}
139
+ onClickData={(id) => console.log("clicked", id)}
140
+ />
141
+ </StorySection>
142
+
143
+ <StorySection
144
+ title="NewGrid — Custom grid item"
145
+ description="Pass customGridItems with a map of id → ReactElement to replace a specific card with fully custom content. The key must match the item's id in data."
146
+ >
147
+ <NewGridImpl
148
+ data={data.slice(0, 4)}
149
+ limit={limit}
150
+ loading={false}
151
+ customGridItems={{
152
+ "2": (
153
+ <div className="flex h-full flex-col items-center justify-center bg-primary-700 p-4 font-thin text-white">
154
+ <h1 className="text-3xl">Custom item</h1>
155
+ <p className="mt-6 text-sm">
156
+ This card replaces the default grid item for id="2".
157
+ </p>
158
+ </div>
159
+ ),
160
+ }}
161
+ />
162
+ </StorySection>
163
+
164
+ <StorySection
165
+ title="NewGrid — Selected items"
166
+ description="Pass selectedItemsId and selectedItemClassName to highlight specific cards."
167
+ >
168
+ <NewGridImpl
169
+ data={data.slice(0, 4)}
170
+ limit={limit}
171
+ loading={false}
172
+ selectedItemsId={["1", "3"]}
173
+ selectedItemClassName="ring-2 ring-primary-500"
174
+ onClickData={(id) => console.log("clicked", id)}
175
+ />
176
+ </StorySection>
177
+
178
+ <StorySection
179
+ title="NewGrid — Dark mode"
180
+ description="darkMode uses the solid bg-gray-900 surface (same as NewList/NewKanban). Pass darkModeBg to override the default surface."
181
+ dark
182
+ >
183
+ <div className="w-full min-w-0 rounded-lg bg-gray-950 p-6">
184
+ <NewGridImpl
185
+ data={data}
186
+ limit={limit}
187
+ loading={false}
188
+ paginationType="page"
189
+ size={data.length}
190
+ pageIndex={pageIndex}
191
+ darkMode
192
+ handleUpdatePageIndex={handlePagination}
193
+ renderImgRibbon={renderImgRibbon}
194
+ />
195
+ </div>
196
+ </StorySection>
197
+
198
+ <StorySection
199
+ title="NewGrid — Loading state"
200
+ description="Pass loading=true to show a loading indicator while data is being fetched."
201
+ >
202
+ <NewGridImpl data={[]} loading={true} />
203
+ </StorySection>
204
+
205
+ <StorySection
206
+ title="NewGrid — Card links open in a new tab"
207
+ description="Pass newTabUrl alongside onClickData: a plain click navigates in place via onClickData, while cmd/ctrl-click falls through to the underlying link and opens a new tab."
208
+ >
209
+ <NewGridImpl
210
+ data={data.slice(0, 3)}
211
+ limit={limit}
212
+ loading={false}
213
+ newTabUrl="?record="
214
+ onClickData={(id) => console.log("clicked", id)}
215
+ />
216
+ </StorySection>
217
+
218
+ <StorySection
219
+ title="NewGrid — Empty and loading keep the pagination footer"
220
+ description="Loading and empty states render inside the wrapper, so the NewPagination footer stays mounted. Without it an empty page on a non-zero pageIndex would leave no control to navigate back."
221
+ >
222
+ <div className="w-full space-y-6">
223
+ <NewGridImpl
224
+ data={[]}
225
+ loading={false}
226
+ paginationType="page"
227
+ limit={limit}
228
+ size={0}
229
+ pageIndex={2}
230
+ emptyTitle="No records match the current filter"
231
+ handleUpdatePageIndex={handlePagination}
232
+ />
233
+ <NewGridImpl
234
+ data={[]}
235
+ loading={true}
236
+ paginationType="page"
237
+ limit={limit}
238
+ size={0}
239
+ pageIndex={2}
240
+ handleUpdatePageIndex={handlePagination}
241
+ />
242
+ </div>
243
+ </StorySection>
244
+
245
+ <StorySection
246
+ title="Comparing with the legacy Grid"
247
+ description="Open Display/Grid in another tab — same sample data renders. NewGridItem is exactly GridClass — drop-in data swap."
248
+ >
249
+ <div className="rounded-lg bg-gray-50 p-6 text-center text-sm text-gray-500">
250
+ → See <code>Display/Grid</code> in the sidebar for the legacy
251
+ component.
252
+ </div>
253
+ </StorySection>
254
+ </div>
255
+ );
256
+ };
@@ -6,14 +6,13 @@ import { StorySection } from "../../storyUtils";
6
6
  const meta = { title: "Display/New Kanban" };
7
7
  export default meta;
8
8
 
9
-
10
9
  // INFO: identical sample data to legacy Display/Kanban for side-by-side comparison
11
10
  const initialData: NewKanbanCard[] = [
12
11
  {
13
12
  id: "1",
14
13
  title: "DC-1",
15
14
  description: "Desc",
16
- groupBy: "Open",
15
+ status: "Open",
17
16
  tags: ["Sports", "Promotions"],
18
17
  createdAt: dayjs("12/24/2020").toDate(),
19
18
  } as any,
@@ -22,7 +21,7 @@ const initialData: NewKanbanCard[] = [
22
21
  title: "DC-2",
23
22
  description:
24
23
  "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.",
25
- groupBy: "In progress",
24
+ status: "In progress",
26
25
  tags: ["Sports", "Promotions"],
27
26
  createdAt: dayjs("12/24/2020").toDate(),
28
27
  } as any,
@@ -30,7 +29,7 @@ const initialData: NewKanbanCard[] = [
30
29
  id: "3",
31
30
  title: "DC-3",
32
31
  description: "test",
33
- groupBy: "To be tested",
32
+ status: "To be tested",
34
33
  tags: ["Sports", "Promotions"],
35
34
  createdAt: dayjs("12/24/2020").toDate(),
36
35
  } as any,
@@ -38,39 +37,47 @@ const initialData: NewKanbanCard[] = [
38
37
  id: "4",
39
38
  title: "DC-4",
40
39
  description: "test",
41
- groupBy: "Reopen",
40
+ status: "Reopen",
42
41
  createdAt: dayjs("12/24/2020").toDate(),
43
42
  } as any,
44
43
  {
45
44
  id: "5",
46
45
  title: "DC-5",
47
46
  description: "test",
48
- groupBy: "Reopen",
47
+ status: "Reopen",
49
48
  createdAt: dayjs("12/24/2020").toDate(),
50
49
  } as any,
51
50
  {
52
51
  id: "6",
53
52
  title: "DC-6",
54
53
  description: "test",
55
- groupBy: "Open",
54
+ status: "Open",
56
55
  createdAt: dayjs("12/24/2020").toDate(),
57
56
  } as any,
58
57
  {
59
58
  id: "7",
60
59
  title: "DC-7",
61
60
  description: "test",
62
- groupBy: "Open",
61
+ status: "Open",
63
62
  createdAt: dayjs("12/24/2020").toDate(),
64
63
  } as any,
65
64
  {
66
65
  id: "8",
67
66
  title: "DC-8",
68
67
  description: "test",
69
- groupBy: "Open",
68
+ status: "Open",
69
+ createdAt: dayjs("12/24/2020").toDate(),
70
+ } as any, // INFO: no status, so it lands in Unassigned
71
+ {
72
+ id: "9",
73
+ title: "DC-9",
74
+ description: "No status yet",
70
75
  createdAt: dayjs("12/24/2020").toDate(),
71
76
  } as any,
72
77
  ];
73
78
 
79
+ const STATUSES = ["Open", "In progress", "To be tested", "Reopen"];
80
+
74
81
  export const NewKanban = () => {
75
82
  // INFO: stateful demo for drag-and-drop — onUpdateData mutates the local copy
76
83
  // so dropping a card visually moves it. Matches legacy <Kanban onUpdateData>.
@@ -78,54 +85,105 @@ export const NewKanban = () => {
78
85
  const handleUpdate = (cardId: string, _from: string, toCol: string) => {
79
86
  setData((prev) =>
80
87
  prev.map((c) =>
81
- c.id === cardId ? ({ ...c, groupBy: toCol } as any) : c,
88
+ c.id === cardId
89
+ ? ({
90
+ ...c,
91
+ status: toCol || undefined,
92
+ } as any)
93
+ : c,
82
94
  ),
83
95
  );
84
96
  };
85
97
 
86
98
  return (
87
- <div className="font-sans p-6">
88
- <StorySection
89
- title="NewKanban — same data as legacy Display/Kanban"
90
- description="NewKanbanCard = GridClass. Pass flat GridClass[] data + groupBy directly — drop-in swap from <Kanban data={d} groupBy={['groupBy']}> to <NewKanbanImpl data={d} groupBy={['groupBy']}>."
91
- >
92
- <NewKanbanImpl data={data} groupBy={["groupBy"]} />
93
- </StorySection>
99
+ <div className="font-sans p-6">
100
+ <StorySection
101
+ title="NewKanban — same data as legacy Display/Kanban"
102
+ description="Same props as legacy <Kanban>: groupId is the field to group by, groupBy lists the column values in order, and nullColumn always shows Unassigned for cards with no value."
103
+ >
104
+ <NewKanbanImpl
105
+ data={data}
106
+ groupId="status"
107
+ groupBy={STATUSES}
108
+ nullColumn
109
+ />
110
+ </StorySection>
111
+
112
+ <StorySection
113
+ title="NewKanban — Drag and drop (desktop)"
114
+ description="draggable=true + onUpdateData enables HTML5 drag between columns. Drop a card on another column to move it. Column highlights when a draggable card hovers over it."
115
+ >
116
+ <NewKanbanImpl
117
+ data={data}
118
+ groupId="status"
119
+ groupBy={STATUSES}
120
+ nullColumn
121
+ draggable
122
+ onUpdateData={handleUpdate}
123
+ />
124
+ </StorySection>
125
+
126
+ <StorySection
127
+ title="NewKanban — Mobile (tab switcher) + touch drag"
128
+ description="One column at a time. Tabs replace horizontal scroll. With draggable=true, long-press a card and drag onto a tab button to move it to that column. The active tab also switches automatically."
129
+ >
130
+ <NewKanbanImpl
131
+ data={data}
132
+ groupId="status"
133
+ groupBy={STATUSES}
134
+ nullColumn
135
+ mobileView
136
+ draggable
137
+ onUpdateData={handleUpdate}
138
+ onAddCard={() => {}}
139
+ />
140
+ </StorySection>
94
141
 
95
- <StorySection
96
- title="NewKanban — Drag and drop (desktop)"
97
- description="draggable=true + onUpdateData enables HTML5 drag between columns. Drop a card on another column to move it. Column highlights when a draggable card hovers over it."
98
- >
99
- <NewKanbanImpl
100
- data={data}
101
- groupBy={["groupBy"]}
102
- draggable
103
- onUpdateData={handleUpdate}
104
- />
105
- </StorySection>
142
+ <StorySection
143
+ title="NewKanban — Dark mode"
144
+ description="darkMode uses the solid bg-gray-900 surface (same as Grid/List/DetailList) instead of a translucent white overlay. Columns show a subtle border only, so there is no washed-out overlay behind the cards. Pass darkModeBg to override the default surface."
145
+ >
146
+ <div className="w-full min-w-0 rounded-lg bg-gray-950 p-6">
147
+ <NewKanbanImpl
148
+ data={data}
149
+ groupId="status"
150
+ groupBy={STATUSES}
151
+ nullColumn
152
+ draggable
153
+ onUpdateData={handleUpdate}
154
+ darkMode
155
+ />
156
+ </div>
157
+ </StorySection>
106
158
 
107
- <StorySection
108
- title="NewKanban — Mobile (tab switcher) + touch drag"
109
- description="One column at a time. Tabs replace horizontal scroll. With draggable=true, long-press a card and drag onto a tab button to move it to that column. The active tab also switches automatically."
110
- >
111
- <NewKanbanImpl
112
- data={data}
113
- groupBy={["groupBy"]}
114
- mobileView
115
- draggable
116
- onUpdateData={handleUpdate}
117
- onAddCard={() => {}}
118
- />
119
- </StorySection>
159
+ <StorySection
160
+ title="NewKanban — Dark mode (mobile)"
161
+ description="Mobile tab switcher in dark mode. Tab bar and count badges use the gray-800/700 palette, cards use bg-gray-900."
162
+ >
163
+ <div className="w-full min-w-0 rounded-lg bg-gray-950 p-6">
164
+ <NewKanbanImpl
165
+ data={data}
166
+ groupId="status"
167
+ groupBy={STATUSES}
168
+ nullColumn
169
+ mobileView
170
+ draggable
171
+ onUpdateData={handleUpdate}
172
+ onAddCard={() => {}}
173
+ darkMode
174
+ />
175
+ </div>
176
+ </StorySection>
120
177
 
121
- <StorySection
122
- title="Comparing with the legacy Kanban"
123
- description="Open Display/Kanban in another tab — same sample data renders. NewKanbanCard is exactly GridClass — drop-in data swap. NewKanbanImpl renders the same data with modernised card styling (image strip, tag chips, rounded corners) and a mobile tab switcher instead of horizontal scroll."
124
- >
125
- <div className="rounded-lg bg-gray-50 p-6 text-center text-sm text-gray-500">
126
- → See <code>Display/Kanban</code> in the sidebar for the legacy component.
127
- </div>
128
- </StorySection>
129
- </div>
130
- );
178
+ <StorySection
179
+ title="Comparing with the legacy Kanban"
180
+ description="Open Display/Kanban in another tab — same sample data renders. NewKanbanCard is exactly GridClass — drop-in data swap. NewKanbanImpl renders the same data with modernised card styling (image strip, tag chips, rounded corners) and a mobile tab switcher instead of horizontal scroll."
181
+ >
182
+ <div className="rounded-lg bg-gray-50 p-6 text-center text-sm text-gray-500">
183
+ → See <code>Display/Kanban</code> in the sidebar for the legacy
184
+ component.
185
+ </div>
186
+ </StorySection>
187
+ </div>
188
+ );
131
189
  };
@@ -82,6 +82,7 @@ const ref = useRef(null);
82
82
  | `mobileView` | `boolean` | `false` | Renders a compact inline layout for mobile (label left, value right) |
83
83
  | `darkMode` | `boolean` | `false` | Applies dark colour scheme to labels |
84
84
  | `className` | `tailwind class` | — | Class applied to the outer container |
85
+ | `size` | `"xs"` | — | Compact input that looks 12px everywhere: on phones it keeps 16px text drawn at 75%, so iOS Safari does not zoom; from `sm` up it is a plain 12px input. Uses important utilities so an app's global date-picker input rule does not override it |
85
86
  | `onChange` | `(id: string, value: Date \| undefined, type?: string) => void` | — | Called on date selection. `type` is `"start"` or `"end"` in range mode |
86
87
  | `cellInput` | `CellInput` | — | Refer to Input Utils — enables cell/table editing mode |
87
88
 
@@ -92,6 +92,7 @@ Async / database search:
92
92
  | `infoElement` | `ReactElement` | — | Info element rendered in the label area |
93
93
  | `containerClassName` | `string` | — | Tailwind classes for the outer container |
94
94
  | `selectClassName` | `string` | — | Tailwind classes for the select trigger element |
95
+ | `size` | `"xs"` | — | Compact field that looks 12px everywhere: on phones it keeps 16px text and draws the trigger at 75%, so iOS Safari does not zoom on focus; from `sm` up it is a plain 12px field |
95
96
  | `dropdownClassName` | `string` | — | Tailwind classes for the dropdown list |
96
97
  | `imageClassName` | `string` | — | Tailwind classes for option images (used with `type="button"`) |
97
98
  | `onChange` | `(value: string \| string[]) => void` | — | Called with the updated value when selection changes |
@@ -54,7 +54,9 @@ const ref = useRef(null);
54
54
  | ----------------------- | ------------------ | ---------- | ---------------------------------------------------------------------------- |
55
55
  | `id` | `string` | — | HTML id attribute |
56
56
  | `subLabel` | `string` | — | Secondary label shown below the main label |
57
- | `size` | `string` | — | `"small"` (px-3 py-2) or `"mini"` (p-2); default is standard (px-5 py-3) |
57
+ | `size` | `string` | — | `"small"` (px-3 py-2), `"mini"` (p-2) or `"xs"`; default is standard (px-5 py-3). `"xs"` looks 12px everywhere: on phones it keeps 16px text and draws the field at 75%, so iOS Safari does not zoom on focus; from `sm` up it is a plain 12px field. Give it a width through `containerClassName` when it sits in a row |
58
+ | `enterKeyHint` | `string` | — | Label for a phone keyboard's enter key, e.g. `"done"`, `"search"`, `"go"` |
59
+ | `onKeyDown` | `(e) => void` | — | Passed to the `<input>` — e.g. Enter to commit, Escape to cancel |
58
60
  | `labelExtra` | `ReactElement` | — | Extra element rendered inline with the label |
59
61
  | `infoElement` | `ReactElement` | — | Content shown in an InfoWindow tooltip next to the label |
60
62
  | `customElement` | `ReactElement` | — | Custom element rendered to the right of the label row |
@@ -18,6 +18,7 @@ export const TextInput = () => {
18
18
 
19
19
  const [email, setEmail] = useState("");
20
20
  const [password, setPassword] = useState("");
21
+ const [xsValue, setXsValue] = useState("");
21
22
  const [number, setNumber] = useState("");
22
23
  const [mobile, setMobile] = useState("");
23
24
  const [capText, setCapText] = useState("");
@@ -51,6 +52,32 @@ export const TextInput = () => {
51
52
  </div>
52
53
  </StorySection>
53
54
 
55
+ <StorySection
56
+ title="Extra small (xs)"
57
+ description="size='xs' looks 12px everywhere. On a phone it keeps 16px text and draws the field at 75%, so iOS Safari does not zoom when it is focused; from sm up it is a plain 12px field. enterKeyHint='done' labels the phone keyboard's enter key, and onKeyDown handles Enter here."
58
+ >
59
+ <form
60
+ className="flex items-center gap-2"
61
+ onSubmit={(event) => {
62
+ event.preventDefault();
63
+ event.stopPropagation();
64
+ }}
65
+ >
66
+ <TextInputImpl
67
+ id="xs-input"
68
+ value={xsValue}
69
+ type="text"
70
+ label=""
71
+ size="xs"
72
+ placeholder="View name"
73
+ enterKeyHint="done"
74
+ containerClassName="w-32 sm:w-44"
75
+ onChange={(e) => setXsValue(e.target.value)}
76
+ />
77
+ <span className="text-xs text-gray-500">Compact, no iOS zoom</span>
78
+ </form>
79
+ </StorySection>
80
+
54
81
  <StorySection
55
82
  title="Password (show/hide toggle)"
56
83
  description="type='password' renders an eye icon to toggle visibility. Validates 6–30 characters, no spaces."
@@ -127,6 +127,10 @@ const navigation = [
127
127
  | `dashboardContentClassName` | `string` | — | Tailwind classes applied to the main content `<main>` element |
128
128
  | `linkType` | `"react" \| "gatsby" \| "next" \| "external" \| "default"` | — | Router adapter passed through to nav links |
129
129
  | `custom` | `any` | — | Custom link component used when `linkType` is `"react"`, `"gatsby"`, or `"next"` |
130
+ | `compactMode` | `boolean` | `false` | Tighter rows, smaller icons and a smaller expand chevron, keeping every label |
131
+ | `searchable` | `boolean` | `false` | Shows a filter box. Matches are listed flat, each with the path it sits under (e.g. `Sales › Reports`) |
132
+ | `persistKey` | `string` | — | localStorage key that remembers open groups and pages. A new key loads that key's stored state. |
133
+ | `defaultExpandedIds` | `string[]` | — | Groups (by `groupId`) and pages (by `id`) open when nothing is stored for them |
130
134
 
131
135
  ##### Navigation
132
136
 
@@ -137,6 +141,7 @@ const navigation = [
137
141
  | `href` | `string` | URL used when `handleBarsOnClick` is not provided |
138
142
  | `icon` | `any` | Heroicon or SVG component rendered beside the label |
139
143
  | `groupId` | `string` | Optional — items sharing the same `groupId` are nested under a collapsible group labelled with that string |
144
+ | `parentId` | `string` | Optional — `id` of the item this one nests under. A row with children shows a chevron and, with nothing stored, opens when it holds `selectedId`. Rows are keyed by `id`, so names may repeat. |
140
145
 
141
146
  ##### Transition
142
147
 
@@ -77,6 +77,21 @@ const twoGroupNavigation = [
77
77
  { id: "vol3", name: "Volume 3", href: "/j/3", icon: FolderIcon, groupId: "Journals" },
78
78
  ];
79
79
 
80
+ // INFO: the shape Space builds — workspace rows, group rows under them, then
81
+ // spaces and sub-pages, all through parentId. "Reports" and "Charts" repeat
82
+ // across workspaces on purpose.
83
+ const workspaceNavigation = [
84
+ { id: "ws-sales", name: "Sales", href: "/ws/sales", icon: UsersIcon },
85
+ { id: "sales-reports", name: "Reports", href: "/g/sales-reports", icon: FolderIcon, parentId: "ws-sales" },
86
+ { id: "sales-charts", name: "Charts", href: "/s/sales-charts", icon: FolderIcon, parentId: "sales-reports" },
87
+ { id: "sales-q3", name: "Q3 Review", href: "/s/sales-q3", icon: FolderIcon, parentId: "sales-charts" },
88
+ { id: "ws-marketing", name: "Marketing", href: "/ws/marketing", icon: UsersIcon },
89
+ { id: "marketing-reports", name: "Reports", href: "/g/marketing-reports", icon: FolderIcon, parentId: "ws-marketing" },
90
+ { id: "marketing-charts", name: "Charts", href: "/s/marketing-charts", icon: FolderIcon, parentId: "marketing-reports" },
91
+ { id: "ws-ops", name: "Operations", href: "/ws/ops", icon: UsersIcon },
92
+ { id: "ops-charts", name: "Charts", href: "/s/ops-charts", icon: FolderIcon, parentId: "ws-ops" },
93
+ ];
94
+
80
95
  const renderLogo = (collapse?: boolean) => (
81
96
  <div className="flex items-center flex-shrink-0 px-6 cursor-pointer">
82
97
  <img
@@ -108,6 +123,8 @@ export const LeftNav = () => {
108
123
  // INFO: starts on an item inside a group, so the story shows the group already
109
124
  // open on mount — the case a route change reproduces in a real app
110
125
  const [groupedSelectedId, setGroupedSelectedId] = useState("engineering");
126
+ // INFO: starts three levels down, so the story shows its branch open on mount
127
+ const [workspaceSelectedId, setWorkspaceSelectedId] = useState("sales-q3");
111
128
 
112
129
  return (
113
130
  <>
@@ -298,6 +315,27 @@ export const LeftNav = () => {
298
315
  </div>
299
316
  </StorySection>
300
317
 
318
+ <StorySection
319
+ title="Workspace Tree"
320
+ description="Workspaces, groups and pages nested through parentId, in compact mode with search — the shape Blixify Space builds. The selected page (Sales › Reports › Charts › Q3 Review) starts with its whole branch open; other workspaces start closed, and every row with children shows a chevron. 'Reports' and 'Charts' repeat across workspaces: rows are keyed by id, so they never collide. Type 'charts' — each match shows the path it came from — then clear the box and the tree comes back exactly as it was."
321
+ >
322
+ <div className="w-full h-96 relative overflow-hidden border rounded-lg">
323
+ <LeftNavImpl
324
+ selectedId={workspaceSelectedId}
325
+ navigation={workspaceNavigation}
326
+ leftNavMobileMenu={false}
327
+ compactMode={true}
328
+ searchable={true}
329
+ searchPlaceholder="Filter spaces"
330
+ handleBarsOnClick={(id) => setWorkspaceSelectedId(id)}
331
+ handleToggleLeftNavMobileMenu={() => {}}
332
+ renderDashboardLogo={() => renderLogo()}
333
+ renderDashboardContent={renderContent}
334
+ renderDashboardHeaderContent={renderHeader}
335
+ />
336
+ </div>
337
+ </StorySection>
338
+
301
339
  <StorySection
302
340
  title="Persistent Expand State"
303
341
  description="persistKey names a localStorage entry that remembers which groups and pages are open. Expand a group, reload the page, and it is still open — without it expansion lasts one mount only, so anything opened is shut again by the next navigation. Groups are keyed by groupId and pages by id. Omit persistKey and no storage is touched at all."
@@ -1 +1 @@
1
- {"version":3,"file":"getBackendComponent.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/getBackendComponent.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA6DxB,eAAO,MAAM,uBAAuB;;;;;;;;;;CA6DnC,CAAC"}
1
+ {"version":3,"file":"getBackendComponent.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/getBackendComponent.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAkExB,eAAO,MAAM,uBAAuB;;;;;;;;;;CA6DnC,CAAC"}