southpaw 0.2.7

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 (169) hide show
  1. package/README.md +51 -0
  2. package/api/client.ts +83 -0
  3. package/api/helpers/fs.ts +98 -0
  4. package/api/helpers/stream.ts +6 -0
  5. package/api/index.ts +50 -0
  6. package/api/server.ts +566 -0
  7. package/constants/fonts.ts +1583 -0
  8. package/constants/icons.ts +2124 -0
  9. package/constants/langs.ts +2592 -0
  10. package/constants/ssr-loading.tsx +17 -0
  11. package/contexts/dev.tsx +124 -0
  12. package/hooks/useDebounceCb.ts +21 -0
  13. package/modules/app.tsx +102 -0
  14. package/modules/behaviors.ts +161 -0
  15. package/modules/component.tsx +483 -0
  16. package/modules/config/index.ts +8 -0
  17. package/modules/database.ts +25 -0
  18. package/modules/document.tsx +23 -0
  19. package/modules/error.tsx +16 -0
  20. package/modules/page/autozoom.tsx +92 -0
  21. package/modules/page/browser.tsx +166 -0
  22. package/modules/page/index.tsx +327 -0
  23. package/modules/page/insert-modal.tsx +225 -0
  24. package/modules/page/inspect/index.tsx +237 -0
  25. package/modules/page/inspect/wrapper.tsx +88 -0
  26. package/modules/page/knobs/action.tsx +58 -0
  27. package/modules/page/knobs/color.tsx +78 -0
  28. package/modules/page/knobs/helpers.ts +71 -0
  29. package/modules/page/knobs/icon.tsx +85 -0
  30. package/modules/page/knobs/linker.tsx +188 -0
  31. package/modules/page/knobs/list-json.tsx +149 -0
  32. package/modules/page/knobs/list-text.tsx +129 -0
  33. package/modules/page/knobs/number.tsx +84 -0
  34. package/modules/page/knobs/segmented.tsx +81 -0
  35. package/modules/page/knobs/select.tsx +89 -0
  36. package/modules/page/knobs/size.tsx +106 -0
  37. package/modules/page/knobs/switch.tsx +75 -0
  38. package/modules/page/knobs/text.tsx +93 -0
  39. package/modules/page/left-tabs.tsx +36 -0
  40. package/modules/page/right-tabs.tsx +31 -0
  41. package/modules/page/seo.tsx +69 -0
  42. package/modules/page/tree.tsx +860 -0
  43. package/modules/state.ts +787 -0
  44. package/modules/store.ts +37 -0
  45. package/modules/theme.ts +105 -0
  46. package/modules/translations.ts +15 -0
  47. package/package.json +68 -0
  48. package/scripts/bin.js +607 -0
  49. package/scripts/test.js +145 -0
  50. package/themes/base.tsx +8962 -0
  51. package/themes/index.ts +4 -0
  52. package/themes/m2.ts +39 -0
  53. package/themes/newspaper.ts +39 -0
  54. package/types/api.ts +57 -0
  55. package/types/fiber_node.ts +69 -0
  56. package/types/react_element.ts +14 -0
  57. package/ui/accordion.tsx +70 -0
  58. package/ui/actionicon.tsx +41 -0
  59. package/ui/affix.tsx +14 -0
  60. package/ui/alert.tsx +37 -0
  61. package/ui/anchor.tsx +52 -0
  62. package/ui/arrow.tsx +116 -0
  63. package/ui/aspectratio.tsx +21 -0
  64. package/ui/autocomplete.tsx +34 -0
  65. package/ui/avatar.tsx +36 -0
  66. package/ui/backgroundimage.tsx +16 -0
  67. package/ui/badge.tsx +28 -0
  68. package/ui/blockquote.tsx +15 -0
  69. package/ui/blocks/faq.tsx +145 -0
  70. package/ui/blocks/features.tsx +167 -0
  71. package/ui/blocks/footer.tsx +182 -0
  72. package/ui/blocks/herobanner.tsx +391 -0
  73. package/ui/blocks/index.ts +44 -0
  74. package/ui/blocks/leftnav.tsx +141 -0
  75. package/ui/blocks/pricing.tsx +210 -0
  76. package/ui/blocks/testimonials.tsx +149 -0
  77. package/ui/blocks/uidocs.tsx +1429 -0
  78. package/ui/box.tsx +27 -0
  79. package/ui/breadcrumbs.tsx +33 -0
  80. package/ui/burger.tsx +23 -0
  81. package/ui/button.tsx +85 -0
  82. package/ui/card.tsx +28 -0
  83. package/ui/center.tsx +26 -0
  84. package/ui/checkbox.tsx +35 -0
  85. package/ui/chip.tsx +32 -0
  86. package/ui/closebutton.tsx +15 -0
  87. package/ui/code.tsx +104 -0
  88. package/ui/collapse.tsx +16 -0
  89. package/ui/colorinput.tsx +32 -0
  90. package/ui/colorpicker.tsx +26 -0
  91. package/ui/colorswatch.tsx +17 -0
  92. package/ui/combobox.tsx +80 -0
  93. package/ui/container.tsx +27 -0
  94. package/ui/dialog.tsx +15 -0
  95. package/ui/divider.tsx +58 -0
  96. package/ui/drawer.tsx +28 -0
  97. package/ui/fieldset.tsx +15 -0
  98. package/ui/filebutton.tsx +23 -0
  99. package/ui/fileinput.tsx +36 -0
  100. package/ui/flex.tsx +139 -0
  101. package/ui/focustrap.tsx +15 -0
  102. package/ui/form.tsx +26 -0
  103. package/ui/grid.tsx +69 -0
  104. package/ui/highlight.tsx +15 -0
  105. package/ui/hoverable.tsx +66 -0
  106. package/ui/hovercard.tsx +45 -0
  107. package/ui/icon.tsx +76 -0
  108. package/ui/image.tsx +32 -0
  109. package/ui/index.ts +1355 -0
  110. package/ui/indicator.tsx +42 -0
  111. package/ui/infobox.tsx +200 -0
  112. package/ui/input.tsx +18 -0
  113. package/ui/inviewport.tsx +23 -0
  114. package/ui/jsoninput.tsx +35 -0
  115. package/ui/kbd.tsx +20 -0
  116. package/ui/list.tsx +33 -0
  117. package/ui/loader.tsx +26 -0
  118. package/ui/loadingoverlay.tsx +22 -0
  119. package/ui/mark.tsx +15 -0
  120. package/ui/menu.tsx +89 -0
  121. package/ui/modal.tsx +27 -0
  122. package/ui/multiselect.tsx +42 -0
  123. package/ui/nativeselect.tsx +17 -0
  124. package/ui/navlink.tsx +40 -0
  125. package/ui/notification.tsx +25 -0
  126. package/ui/numberformatter.tsx +15 -0
  127. package/ui/numberinput.tsx +37 -0
  128. package/ui/overlay.tsx +15 -0
  129. package/ui/pagination.tsx +21 -0
  130. package/ui/paper.tsx +33 -0
  131. package/ui/passwordinput.tsx +36 -0
  132. package/ui/pill.tsx +17 -0
  133. package/ui/pillsinput.tsx +37 -0
  134. package/ui/pininput.tsx +30 -0
  135. package/ui/popover.tsx +43 -0
  136. package/ui/portal.tsx +15 -0
  137. package/ui/progress.tsx +49 -0
  138. package/ui/props.ts +91 -0
  139. package/ui/propsDef.ts +38 -0
  140. package/ui/radio.tsx +30 -0
  141. package/ui/rating.tsx +25 -0
  142. package/ui/ringprogress.tsx +17 -0
  143. package/ui/scrollarea.tsx +27 -0
  144. package/ui/segmentedcontrol.tsx +38 -0
  145. package/ui/select.tsx +43 -0
  146. package/ui/simplegrid.tsx +23 -0
  147. package/ui/skeleton.tsx +15 -0
  148. package/ui/slider.tsx +31 -0
  149. package/ui/space.tsx +19 -0
  150. package/ui/spoiler.tsx +23 -0
  151. package/ui/stepper.tsx +63 -0
  152. package/ui/switch.tsx +29 -0
  153. package/ui/table.tsx +93 -0
  154. package/ui/tabs.tsx +80 -0
  155. package/ui/tagsinput.tsx +19 -0
  156. package/ui/terminal.tsx +44 -0
  157. package/ui/text.tsx +168 -0
  158. package/ui/textarea.tsx +36 -0
  159. package/ui/textinput.tsx +34 -0
  160. package/ui/timeline.tsx +30 -0
  161. package/ui/title.tsx +42 -0
  162. package/ui/tooltip.tsx +32 -0
  163. package/ui/transition.tsx +21 -0
  164. package/ui/tree.tsx +42 -0
  165. package/ui/typographystylesprovider.tsx +20 -0
  166. package/ui/unstyledbutton.tsx +18 -0
  167. package/ui/video.tsx +16 -0
  168. package/ui/visuallyhidden.tsx +15 -0
  169. package/utils/index.ts +76 -0
@@ -0,0 +1,1429 @@
1
+ import { ReactElement, type ComponentType } from "react";
2
+ import {
3
+ type ComponentParams,
4
+ create_component,
5
+ } from "southpaw/modules/component";
6
+ import { UI } from "..";
7
+ import { UI_CATEGORIES } from "../../../src/constants/ui-categories";
8
+ import { UI_EXAMPLES } from "../../../src/constants/ui-examples";
9
+
10
+ type Props = {
11
+ ui?: Record<string, ComponentType<any>>;
12
+ blocks?: Record<string, ComponentType<any>>;
13
+ components?: Record<string, ComponentType<any>>;
14
+ };
15
+
16
+ type UIDefinition = {
17
+ params: ComponentParams<any, any, any, any, any>;
18
+ name: string;
19
+ description: string;
20
+ examples: string[];
21
+ category: string;
22
+ library: "ui" | "blocks" | "components";
23
+ };
24
+
25
+ type State = {
26
+ definitions: UIDefinition[];
27
+ searchQuery: string;
28
+ loading: boolean;
29
+ error: string | null;
30
+ previewZoom: number;
31
+ previewGrid: boolean;
32
+ previewRuler: boolean;
33
+ previewViewport: string;
34
+ previewTheme: "light" | "dark";
35
+ knobValues: Record<string, any>;
36
+ activeComponentId: string | null;
37
+ activeStoryId: number;
38
+ mobileMenuOpened: boolean;
39
+ inspectorOpened: boolean;
40
+ isMobile: boolean;
41
+ activeTab: "controls" | "actions" | "code" | "props";
42
+ activeLibrary: "all" | "ui" | "blocks" | "components";
43
+ expandedCategories: string[];
44
+ actionLog: Array<{ id: string; name: string; data?: any; timestamp: number }>;
45
+ };
46
+
47
+ type Computed = {
48
+ filteredDefinitions: (this: any) => UIDefinition[];
49
+ categorizedDefinitions: (
50
+ this: any,
51
+ ) => Record<string, Record<string, UIDefinition[]>>;
52
+ };
53
+
54
+ type Methods = {
55
+ categorizeComponent: (this: any, name: string, library: string) => string;
56
+ createFailedDefinition: (
57
+ this: any,
58
+ name: string,
59
+ library: "ui" | "blocks" | "components",
60
+ ) => UIDefinition;
61
+ extractExamples: (this: any, name: string) => string[];
62
+ renderPropType: (
63
+ this: any,
64
+ propDef: {
65
+ type: string;
66
+ options?: Array<string | { value: string; label?: string }>;
67
+ multiline?: boolean;
68
+ },
69
+ ) => string;
70
+ getCommonProps: (this: any) => Array<{
71
+ name: string;
72
+ type: string;
73
+ description: string;
74
+ required: boolean;
75
+ }>;
76
+ getComponentSpecificProps: (
77
+ this: any,
78
+ def: UIDefinition,
79
+ ) => Array<{
80
+ name: string;
81
+ type: string;
82
+ description: string;
83
+ required: boolean;
84
+ options?: Array<string | { value: string; label?: string }>;
85
+ }>;
86
+ handleSearchChange: (this: any, value: string) => void;
87
+ toggleGrid: (this: any) => void;
88
+ toggleRuler: (this: any) => void;
89
+ setZoom: (this: any, zoom: number) => void;
90
+ setViewport: (this: any, width: string) => void;
91
+ resetPreview: (this: any) => void;
92
+ initializeKnobs: (this: any, componentId: string, defaults: any) => void;
93
+ selectStory: (this: any, index: number) => void;
94
+ logAction: (this: any, name: string, data?: any) => void;
95
+ toggleCategory: (this: any, category: string) => void;
96
+ updateKnob: (this: any, name: string, value: any) => void;
97
+ renderKnobInput: (
98
+ this: any,
99
+ name: string,
100
+ propDef: any,
101
+ ) => ReactElement | null;
102
+ toggleMobileMenu: (this: any) => void;
103
+ closeMobileMenu: (this: any) => void;
104
+ setActiveTab: (
105
+ this: any,
106
+ tab: "controls" | "actions" | "code" | "props",
107
+ ) => void;
108
+ };
109
+
110
+ const Component = create_component<Props, State, Computed, any, Methods>({
111
+ name: "UIDocs",
112
+ description:
113
+ "A comprehensive UI documentation catalog for Southpaw UI elements",
114
+ props: {
115
+ ui: {},
116
+ } as Props,
117
+ state: {
118
+ definitions: [],
119
+ searchQuery: "",
120
+ loading: true,
121
+ error: null,
122
+ previewZoom: 1,
123
+ previewGrid: true,
124
+ previewRuler: false,
125
+ previewViewport: "Responsive",
126
+ previewTheme: "light",
127
+ knobValues: {},
128
+ activeComponentId: null,
129
+ activeStoryId: 0,
130
+ mobileMenuOpened: false,
131
+ inspectorOpened: false,
132
+ isMobile: false,
133
+ activeTab: "controls",
134
+ activeLibrary: "all",
135
+ expandedCategories: [],
136
+ actionLog: [],
137
+ },
138
+ computed: {
139
+ filteredDefinitions() {
140
+ return this.state.definitions.filter((def: UIDefinition) => {
141
+ const matchesSearch =
142
+ this.state.searchQuery === "" ||
143
+ def.name
144
+ .toLowerCase()
145
+ .includes(this.state.searchQuery.toLowerCase()) ||
146
+ def.description
147
+ .toLowerCase()
148
+ .includes(this.state.searchQuery.toLowerCase());
149
+
150
+ return matchesSearch;
151
+ });
152
+ },
153
+ categorizedDefinitions() {
154
+ const grouped: Record<string, Record<string, UIDefinition[]>> = {
155
+ ui: {},
156
+ blocks: {},
157
+ components: {},
158
+ };
159
+
160
+ this.computed.filteredDefinitions.forEach((def: any) => {
161
+ const lib = def.library;
162
+ if (!grouped[lib][def.category]) {
163
+ grouped[lib][def.category] = [];
164
+ }
165
+ grouped[lib][def.category].push(def);
166
+ });
167
+ return grouped;
168
+ },
169
+ },
170
+ methods: {
171
+ categorizeComponent(name: string, library: string): string {
172
+ if (library === "blocks") return "Page Blocks";
173
+ if (library === "components") return "App Components";
174
+
175
+ const lowerName = name.toLowerCase();
176
+ if (UI_CATEGORIES.layout.includes(lowerName)) return "Layout";
177
+ if (UI_CATEGORIES.inputs.includes(lowerName)) return "Inputs";
178
+ if (UI_CATEGORIES.buttons.includes(lowerName)) return "Buttons";
179
+ if (UI_CATEGORIES.navigation.includes(lowerName)) return "Navigation";
180
+ if (UI_CATEGORIES.display.includes(lowerName)) return "Display";
181
+ if (UI_CATEGORIES.containers.includes(lowerName)) return "Containers";
182
+ if (UI_CATEGORIES.utilities.includes(lowerName)) return "Utilities";
183
+ if (UI_CATEGORIES.data.includes(lowerName)) return "Data";
184
+ return "General Primitives";
185
+ },
186
+
187
+ createFailedDefinition(
188
+ name: string,
189
+ library: "ui" | "blocks" | "components",
190
+ ): UIDefinition {
191
+ return {
192
+ params: {
193
+ name,
194
+ description: "Failed to load component",
195
+ propsDef: {},
196
+ render: () => null,
197
+ },
198
+ name,
199
+ description: "Failed to load component",
200
+ examples: [`<UI.${name} />`],
201
+ category: "Other",
202
+ library,
203
+ };
204
+ },
205
+
206
+ extractExamples(name: string): string[] {
207
+ return (
208
+ UI_EXAMPLES[name] ||
209
+ UI_EXAMPLES.default.map((example) => example.replace("${name}", name))
210
+ );
211
+ },
212
+
213
+ renderPropType(propDef: {
214
+ type: string;
215
+ options?: Array<string | { value: string; label?: string }>;
216
+ multiline?: boolean;
217
+ }): string {
218
+ switch (propDef.type) {
219
+ case "segmented":
220
+ return propDef.options
221
+ ? `"${propDef.options
222
+ .map((opt) =>
223
+ typeof opt === "object" ? opt.value || opt : opt,
224
+ )
225
+ .join('" | "')}"`
226
+ : "string";
227
+ case "select":
228
+ return "string | number";
229
+ case "switch":
230
+ return "boolean";
231
+ case "number":
232
+ case "size":
233
+ return "number";
234
+ case "color":
235
+ return "string (color)";
236
+ case "icon":
237
+ return "string (icon name)";
238
+ case "text":
239
+ return propDef.multiline ? "string (multiline)" : "string";
240
+ default:
241
+ return propDef.type || "any";
242
+ }
243
+ },
244
+
245
+ getCommonProps(): Array<{
246
+ name: string;
247
+ type: string;
248
+ description: string;
249
+ required: boolean;
250
+ }> {
251
+ return [
252
+ {
253
+ name: "className",
254
+ type: "string",
255
+ description: "Additional CSS class names",
256
+ required: false,
257
+ },
258
+ {
259
+ name: "style",
260
+ type: "CSSProperties",
261
+ description: "Inline styles",
262
+ required: false,
263
+ },
264
+ {
265
+ name: "id",
266
+ type: "string",
267
+ description: "Element ID",
268
+ required: false,
269
+ },
270
+ {
271
+ name: "mount_animation",
272
+ type: "string | AnimationConfig",
273
+ description: "Mount animation configuration",
274
+ required: false,
275
+ },
276
+ {
277
+ name: "p",
278
+ type: "number | string",
279
+ description: "Padding",
280
+ required: false,
281
+ },
282
+ {
283
+ name: "m",
284
+ type: "number | string",
285
+ description: "Margin",
286
+ required: false,
287
+ },
288
+ {
289
+ name: "w",
290
+ type: "number | string",
291
+ description: "Width",
292
+ required: false,
293
+ },
294
+ {
295
+ name: "h",
296
+ type: "number | string",
297
+ description: "Height",
298
+ required: false,
299
+ },
300
+ {
301
+ name: "bg",
302
+ type: "string",
303
+ description: "Background color",
304
+ required: false,
305
+ },
306
+ {
307
+ name: "c",
308
+ type: "string",
309
+ description: "Text color",
310
+ required: false,
311
+ },
312
+ ];
313
+ },
314
+
315
+ getComponentSpecificProps(def: UIDefinition): Array<{
316
+ name: string;
317
+ type: string;
318
+ description: string;
319
+ required: boolean;
320
+ options?: Array<string | { value: string; label?: string }>;
321
+ }> {
322
+ const propsDef = (def.params.propsDef || {}) as Record<string, any>;
323
+ return Object.entries(propsDef).map(([propName, propDef]) => ({
324
+ name: propName,
325
+ type: this.methods.renderPropType(propDef),
326
+ description: propDef.label || "No description available",
327
+ required: false,
328
+ options:
329
+ propDef.type === "segmented"
330
+ ? propDef.options
331
+ : propDef.type === "select"
332
+ ? propDef.options
333
+ : undefined,
334
+ }));
335
+ },
336
+
337
+ handleSearchChange(value: string) {
338
+ this.state.searchQuery = value;
339
+ },
340
+ toggleGrid() {
341
+ this.state.previewGrid = !this.state.previewGrid;
342
+ },
343
+ toggleRuler() {
344
+ this.state.previewRuler = !this.state.previewRuler;
345
+ },
346
+ setZoom(zoom: number) {
347
+ this.state.previewZoom = zoom;
348
+ },
349
+ setViewport(width: string) {
350
+ this.state.previewViewport = width;
351
+ },
352
+ resetPreview() {
353
+ this.state.previewZoom = 1;
354
+ },
355
+ initializeKnobs(componentId: string, defaults: any) {
356
+ if (this.state.activeComponentId === componentId) return;
357
+
358
+ this.state.activeComponentId = componentId;
359
+ this.state.activeStoryId = 0;
360
+ this.state.actionLog = [];
361
+ this.state.knobValues = { ...defaults };
362
+ },
363
+ selectStory(index: number) {
364
+ this.state.activeStoryId = index;
365
+ const def = this.state.definitions.find(
366
+ (d: any) => d.name === this.state.activeComponentId,
367
+ );
368
+ if (!def) return;
369
+
370
+ const example = def.examples[index];
371
+ const props: Record<string, any> = {};
372
+
373
+ const propRegex = /(\w+)=(?:{([^}]+)}|"([^"]+)")/g;
374
+ let match;
375
+ while ((match = propRegex.exec(example)) !== null) {
376
+ const [, key, curlyValue, quotedValue] = match;
377
+ const val = curlyValue !== undefined ? curlyValue : quotedValue;
378
+
379
+ if (curlyValue !== undefined) {
380
+ try {
381
+ if (val === "true") props[key] = true;
382
+ else if (val === "false") props[key] = false;
383
+ else if (!isNaN(Number(val))) props[key] = Number(val);
384
+ else props[key] = JSON.parse(val.replace(/'/g, '"'));
385
+ } catch {
386
+ props[key] = val;
387
+ }
388
+ } else {
389
+ props[key] = val;
390
+ }
391
+ }
392
+
393
+ const childrenMatch = example.match(/>([^<]+)<\/UI\./);
394
+ if (childrenMatch) {
395
+ props["children"] = childrenMatch[1].trim();
396
+ }
397
+
398
+ this.state.knobValues = { ...def.params.props, ...props };
399
+ },
400
+ logAction(name: string, data?: any) {
401
+ const entry = {
402
+ id: Math.random().toString(36).substring(2, 9),
403
+ name,
404
+ data,
405
+ timestamp: Date.now(),
406
+ };
407
+ this.state.actionLog = [entry, ...this.state.actionLog].slice(0, 50);
408
+ },
409
+ toggleCategory(category: string) {
410
+ if (this.state.expandedCategories.includes(category)) {
411
+ this.state.expandedCategories = this.state.expandedCategories.filter(
412
+ (c: string) => c !== category,
413
+ );
414
+ } else {
415
+ this.state.expandedCategories = [
416
+ ...this.state.expandedCategories,
417
+ category,
418
+ ];
419
+ }
420
+ },
421
+ updateKnob(name: string, value: any) {
422
+ this.state.knobValues = {
423
+ ...this.state.knobValues,
424
+ [name]: value,
425
+ };
426
+ },
427
+ renderKnobInput(
428
+ name: string,
429
+ propDef: {
430
+ type: string;
431
+ options?: Array<string | { value: string; label?: string }>;
432
+ multiline?: boolean;
433
+ label?: string;
434
+ required?: boolean;
435
+ min?: number;
436
+ max?: number;
437
+ step?: number;
438
+ },
439
+ ): ReactElement | null {
440
+ const value = this.state.knobValues[name];
441
+ const label = (propDef.label || name) + (propDef.required ? " *" : "");
442
+ switch (propDef.type) {
443
+ case "text":
444
+ return propDef.multiline ? (
445
+ <UI.textarea
446
+ label={label}
447
+ value={value || ""}
448
+ onInput={() =>
449
+ this.methods.updateKnob(name, this.state.knobValues[name])
450
+ }
451
+ />
452
+ ) : (
453
+ <UI.textinput
454
+ label={label}
455
+ value={value || ""}
456
+ onInput={() =>
457
+ this.methods.updateKnob(name, this.state.knobValues[name])
458
+ }
459
+ />
460
+ );
461
+ case "number":
462
+ case "size":
463
+ return (
464
+ <UI.numberinput
465
+ label={label}
466
+ value={value}
467
+ min={propDef.min}
468
+ max={propDef.max}
469
+ step={propDef.step}
470
+ onChange={(val) => this.methods.updateKnob(name, Number(val))}
471
+ />
472
+ );
473
+ case "switch":
474
+ case "boolean":
475
+ return (
476
+ <UI.switch
477
+ label={label}
478
+ checked={!!value}
479
+ onChange={(e) =>
480
+ this.methods.updateKnob(name, e.currentTarget.checked)
481
+ }
482
+ />
483
+ );
484
+ case "select":
485
+ return (
486
+ <UI.select
487
+ label={label}
488
+ data={propDef.options as any}
489
+ value={String(value)}
490
+ onChange={(val) => this.methods.updateKnob(name, val)}
491
+ />
492
+ );
493
+ case "segmented":
494
+ return (
495
+ <UI.segmentedcontrol
496
+ data={propDef.options as any}
497
+ value={String(value)}
498
+ onChange={(val) => this.methods.updateKnob(name, val)}
499
+ />
500
+ );
501
+ case "color":
502
+ return (
503
+ <UI.colorinput
504
+ label={label}
505
+ value={value || ""}
506
+ onChange={(val) => this.methods.updateKnob(name, val)}
507
+ />
508
+ );
509
+ case "list-text":
510
+ const items = (value as any[] | undefined) || [];
511
+ return (
512
+ <UI.flex direction="column" gap="xs">
513
+ <UI.text text={label} size="sm" fw={500} />
514
+ {items.map((item: any, idx: number) => {
515
+ const itemLabel =
516
+ typeof item === "object"
517
+ ? item.label || item.children || item.value || ""
518
+ : String(item);
519
+ const itemContent =
520
+ typeof item === "object" ? item.content || "" : "";
521
+
522
+ return (
523
+ <UI.card key={idx} withBorder p="xs">
524
+ <UI.flex direction="column" gap="xs">
525
+ <UI.flex justify="space-between" align="center">
526
+ <UI.text
527
+ text={`Item ${idx + 1}`}
528
+ size="xs"
529
+ c="dimmed"
530
+ />
531
+ <UI.actionicon
532
+ color="red"
533
+ variant="subtle"
534
+ size="sm"
535
+ onClick={() => {
536
+ const newItems = items.filter((_, i) => i !== idx);
537
+ this.methods.updateKnob(name, newItems);
538
+ }}
539
+ >
540
+ <UI.icon icon="remove" />
541
+ </UI.actionicon>
542
+ </UI.flex>
543
+ <UI.textinput
544
+ label="Label"
545
+ value={itemLabel}
546
+ onInput={(e) => {
547
+ const newValue = e.currentTarget.value;
548
+ const newItems = [...items];
549
+ if (typeof newItems[idx] === "object") {
550
+ newItems[idx] = {
551
+ ...newItems[idx],
552
+ label: newValue,
553
+ ...(newItems[idx].children !== undefined
554
+ ? { children: newValue }
555
+ : {}),
556
+ ...(newItems[idx].value !== undefined
557
+ ? { value: newValue }
558
+ : {}),
559
+ };
560
+ } else {
561
+ newItems[idx] = newValue;
562
+ }
563
+ this.methods.updateKnob(name, newItems);
564
+ }}
565
+ />
566
+ <UI.textarea
567
+ label="Content"
568
+ minRows={2}
569
+ value={itemContent}
570
+ onInput={(e) => {
571
+ const newValue = e.currentTarget.value;
572
+ const newItems = [...items];
573
+ if (typeof newItems[idx] === "object") {
574
+ newItems[idx] = {
575
+ ...newItems[idx],
576
+ content: newValue,
577
+ };
578
+ }
579
+ this.methods.updateKnob(name, newItems);
580
+ }}
581
+ />
582
+ </UI.flex>
583
+ </UI.card>
584
+ );
585
+ })}
586
+ <UI.button
587
+ variant="outline"
588
+ size="xs"
589
+ onClick={() => {
590
+ const newIdx = items.length;
591
+ const newItem = {
592
+ id: String(Date.now()),
593
+ label: `Item ${newIdx + 1}`,
594
+ content: `Item ${newIdx + 1} content`,
595
+ children: `Item ${newIdx + 1}`,
596
+ value: String(newIdx),
597
+ };
598
+ this.methods.updateKnob(name, [...items, newItem]);
599
+ }}
600
+ >
601
+ <UI.icon icon="add" />
602
+ </UI.button>
603
+ </UI.flex>
604
+ );
605
+ default:
606
+ return null;
607
+ }
608
+ },
609
+ toggleMobileMenu() {
610
+ this.state.mobileMenuOpened = !this.state.mobileMenuOpened;
611
+ },
612
+ closeMobileMenu() {
613
+ this.state.mobileMenuOpened = false;
614
+ },
615
+ setActiveTab(tab: "controls" | "actions" | "code" | "props") {
616
+ this.state.activeTab = tab;
617
+ },
618
+ },
619
+ lifecycle: {
620
+ async onMount() {
621
+ // Handle mobile detection
622
+ const handleResize = () => {
623
+ this.state.isMobile = window.innerWidth < 768;
624
+ };
625
+ handleResize();
626
+ window.addEventListener("resize", handleResize);
627
+ (this.refs as any)._handleResize = handleResize;
628
+
629
+ try {
630
+ this.state.loading = true;
631
+ this.state.error = null;
632
+
633
+ const allDefinitions: UIDefinition[] = [];
634
+
635
+ // 1. UI Primitives
636
+ if (this.props.ui) {
637
+ const uiDefs = await Promise.all(
638
+ Object.keys(this.props.ui).map(async (name) => {
639
+ try {
640
+ const { default: Comp } = (await import(
641
+ `southpaw/ui/${name}`
642
+ )) as any;
643
+ return {
644
+ params: Comp.params || {
645
+ name,
646
+ description: "",
647
+ propsDef: {},
648
+ },
649
+ name,
650
+ description: Comp.params?.description || "",
651
+ examples: this.methods.extractExamples(name),
652
+ category: this.methods.categorizeComponent(name, "ui"),
653
+ library: "ui" as const,
654
+ };
655
+ } catch {
656
+ return this.methods.createFailedDefinition(name, "ui");
657
+ }
658
+ }),
659
+ );
660
+ allDefinitions.push(...uiDefs);
661
+ }
662
+
663
+ // 2. Blocks
664
+ if (this.props.blocks) {
665
+ const blockDefs = await Promise.all(
666
+ Object.keys(this.props.blocks).map(async (name) => {
667
+ try {
668
+ const { default: Comp } = (await import(
669
+ `southpaw/ui/blocks/${name}`
670
+ )) as any;
671
+ return {
672
+ params: Comp.params || {
673
+ name,
674
+ description: "",
675
+ propsDef: {},
676
+ },
677
+ name,
678
+ description: Comp.params?.description || "",
679
+ examples: this.methods.extractExamples(name),
680
+ category: this.methods.categorizeComponent(name, "blocks"),
681
+ library: "blocks" as const,
682
+ };
683
+ } catch {
684
+ return this.methods.createFailedDefinition(name, "blocks");
685
+ }
686
+ }),
687
+ );
688
+ allDefinitions.push(...blockDefs);
689
+ }
690
+
691
+ // 3. Components
692
+ if (this.props.components) {
693
+ const compDefs = await Promise.all(
694
+ Object.keys(this.props.components).map(async (name) => {
695
+ try {
696
+ const { default: Comp } = (await import(
697
+ `../../../src/components/${name}`
698
+ )) as any;
699
+ return {
700
+ params: Comp.params || {
701
+ name,
702
+ description: "",
703
+ propsDef: {},
704
+ },
705
+ name,
706
+ description: Comp.params?.description || "",
707
+ examples: this.methods.extractExamples(name),
708
+ category: this.methods.categorizeComponent(
709
+ name,
710
+ "components",
711
+ ),
712
+ library: "components" as const,
713
+ };
714
+ } catch {
715
+ return this.methods.createFailedDefinition(name, "components");
716
+ }
717
+ }),
718
+ );
719
+ allDefinitions.push(...compDefs);
720
+ }
721
+
722
+ this.state.definitions = allDefinitions.sort((a, b) =>
723
+ a.name.localeCompare(b.name),
724
+ );
725
+ } catch (error) {
726
+ this.state.error = "Failed to load documentation libraries";
727
+ console.error("UIDocs loading error:", error);
728
+ } finally {
729
+ this.state.loading = false;
730
+ }
731
+ },
732
+ onUnmount() {
733
+ if ((this.refs as any)._handleResize) {
734
+ window.removeEventListener("resize", (this.refs as any)._handleResize);
735
+ }
736
+ },
737
+ },
738
+ render() {
739
+ const sectionId = this.router.query.section as string;
740
+ const activeDefinition = this.state.definitions.find(
741
+ (d: UIDefinition) => d.name === sectionId,
742
+ );
743
+
744
+ if (
745
+ activeDefinition &&
746
+ this.state.activeComponentId !== activeDefinition.name
747
+ ) {
748
+ this.methods.initializeKnobs(
749
+ activeDefinition.name,
750
+ activeDefinition.params.props || {},
751
+ );
752
+ }
753
+
754
+ const conceptsContent = (
755
+ <UI.flex
756
+ direction="column"
757
+ gap="xl"
758
+ p="xl"
759
+ style={{ maxWidth: 800, margin: "0 auto" }}
760
+ >
761
+ <UI.box>
762
+ <UI.title order={1} text="Southpaw UI" fz={48} fw={900} />
763
+ <UI.text
764
+ size="lg"
765
+ c="dimmed"
766
+ text="A modern, behavior-driven component library for high-performance web applications."
767
+ />
768
+ </UI.box>
769
+
770
+ <UI.grid gutter="xl" cells={[{ span: 6 }, { span: 6 }]}>
771
+ <UI.card withBorder p="xl" radius="md">
772
+ <UI.icon icon="bolt" c="yellow" fz={32} />
773
+ <UI.title order={3} text="Performance First" mt="md" />
774
+ <UI.text
775
+ size="sm"
776
+ c="dimmed"
777
+ mt="xs"
778
+ text="Lightweight components built with zero-overhead principles for lightning-fast interactions."
779
+ />
780
+ </UI.card>
781
+ <UI.card withBorder p="xl" radius="md">
782
+ <UI.icon icon="palette" c="blue" fz={32} />
783
+ <UI.title order={3} text="Design Tokens" mt="md" />
784
+ <UI.text
785
+ size="sm"
786
+ c="dimmed"
787
+ mt="xs"
788
+ text="Strict adherence to design tokens ensuring visual cohesion across your entire application."
789
+ />
790
+ </UI.card>
791
+ </UI.grid>
792
+
793
+ <UI.box mt="xl">
794
+ <UI.title order={2} text="Getting Started" />
795
+ <UI.text
796
+ mt="md"
797
+ text="Select a component from the sidebar to explore its props, see live examples, and copy implementation code."
798
+ />
799
+ </UI.box>
800
+ </UI.flex>
801
+ );
802
+
803
+ const sidebarContent = (
804
+ <>
805
+ <UI.flex p="md" align="center" gap="sm">
806
+ <UI.icon icon="architecture" fz={24} c="blue" />
807
+ <UI.text fw={700} text="UIDocs" fz="lg" />
808
+ </UI.flex>
809
+
810
+ <UI.box px="md" pb="sm">
811
+ <UI.textinput
812
+ placeholder="Search components..."
813
+ leftSection={<UI.icon icon="search" fz={16} />}
814
+ value={this.state.searchQuery}
815
+ onInput={(e) =>
816
+ this.methods.handleSearchChange(e.currentTarget.value)
817
+ }
818
+ size="xs"
819
+ />
820
+ </UI.box>
821
+
822
+ <UI.box style={{ flex: 1, overflowY: "auto" }} px="xs">
823
+ <UI.navlink
824
+ label="Introduction"
825
+ leftSection={<UI.icon icon="home" fz={18} />}
826
+ active={!sectionId || sectionId === "introduction"}
827
+ onClick={() => {
828
+ this.router.push("/ui?section=introduction");
829
+ this.state.mobileMenuOpened = false;
830
+ }}
831
+ />
832
+
833
+ {Object.entries(this.computed.categorizedDefinitions).map(
834
+ ([libKey, categories]) => {
835
+ const libraryLabels: Record<string, string> = {
836
+ ui: "UI Primitives",
837
+ blocks: "Page Blocks",
838
+ components: "App Components",
839
+ };
840
+
841
+ if (Object.keys(categories).length === 0) return null;
842
+
843
+ return (
844
+ <UI.box key={libKey}>
845
+ <UI.divider
846
+ my="xs"
847
+ label={libraryLabels[libKey]}
848
+ labelPosition="center"
849
+ variant="dashed"
850
+ />
851
+ {Object.entries(categories).map(([category, defs]) => (
852
+ <UI.box key={category}>
853
+ <UI.navlink
854
+ label={category}
855
+ leftSection={<UI.icon icon="folder" fz={18} />}
856
+ opened={this.state.expandedCategories.includes(
857
+ category,
858
+ )}
859
+ onClick={() => this.methods.toggleCategory(category)}
860
+ >
861
+ {defs.map((def: UIDefinition) => (
862
+ <UI.navlink
863
+ key={def.name}
864
+ label={def.name}
865
+ active={sectionId === def.name}
866
+ onClick={() => {
867
+ this.router.push(`/ui?section=${def.name}`);
868
+ this.state.mobileMenuOpened = false;
869
+ }}
870
+ pl="xl"
871
+ />
872
+ ))}
873
+ </UI.navlink>
874
+ </UI.box>
875
+ ))}
876
+ </UI.box>
877
+ );
878
+ },
879
+ )}
880
+ </UI.box>
881
+ </>
882
+ );
883
+
884
+ const inspectorContent = activeDefinition ? (
885
+ <UI.tabs
886
+ value={this.state.activeTab}
887
+ onChange={(val) => this.methods.setActiveTab(val as any)}
888
+ style={{ flex: 1, display: "flex", flexDirection: "column" }}
889
+ tabs={[
890
+ {
891
+ value: "controls",
892
+ header: (
893
+ <UI.flex align="center" gap={4}>
894
+ <UI.icon icon="tune" fz={14} />
895
+ <UI.text size="xs" text="Controls" />
896
+ </UI.flex>
897
+ ),
898
+ content: (
899
+ <UI.box p="md" style={{ overflowY: "auto", flex: 1 }}>
900
+ <UI.flex direction="column" gap="md">
901
+ {Object.entries(activeDefinition.params.propsDef || {}).map(
902
+ ([name, def]) => (
903
+ <UI.box key={name}>
904
+ {this.methods.renderKnobInput(name, def as any)}
905
+ </UI.box>
906
+ ),
907
+ )}
908
+ {Object.keys(activeDefinition.params.propsDef || {})
909
+ .length === 0 && (
910
+ <UI.center h={100}>
911
+ <UI.text
912
+ size="sm"
913
+ c="dimmed"
914
+ fs="italic"
915
+ text="No controls available"
916
+ />
917
+ </UI.center>
918
+ )}
919
+ </UI.flex>
920
+ </UI.box>
921
+ ),
922
+ },
923
+ {
924
+ value: "actions",
925
+ header: (
926
+ <UI.flex align="center" gap={4}>
927
+ <UI.icon icon="dynamic_form" fz={14} />
928
+ <UI.text size="xs" text="Actions" />
929
+ </UI.flex>
930
+ ),
931
+ content: (
932
+ <UI.box p="md" style={{ overflowY: "auto", flex: 1 }}>
933
+ {this.state.actionLog.length === 0 ? (
934
+ <UI.center h={100}>
935
+ <UI.text
936
+ size="sm"
937
+ c="dimmed"
938
+ fs="italic"
939
+ text="No actions logged yet. Interact with the component."
940
+ />
941
+ </UI.center>
942
+ ) : (
943
+ <UI.flex direction="column" gap="xs">
944
+ {this.state.actionLog.slice(0, 20).map((log) => (
945
+ <UI.card key={log.id} withBorder p="xs" radius="xs">
946
+ <UI.flex justify="space-between">
947
+ <UI.text
948
+ fw={700}
949
+ size="xs"
950
+ text={log.name}
951
+ c="blue"
952
+ />
953
+ <UI.text
954
+ size="xs"
955
+ c="dimmed"
956
+ text={new Date(log.timestamp).toLocaleTimeString()}
957
+ />
958
+ </UI.flex>
959
+ {log.data && log.data.length > 0 && (
960
+ <UI.code
961
+ code={JSON.stringify(log.data, null, 2)}
962
+ codeLang="json"
963
+ block
964
+ mt="xs"
965
+ />
966
+ )}
967
+ </UI.card>
968
+ ))}
969
+ </UI.flex>
970
+ )}
971
+ </UI.box>
972
+ ),
973
+ },
974
+ {
975
+ value: "code",
976
+ header: (
977
+ <UI.flex align="center" gap={4}>
978
+ <UI.icon icon="code" fz={14} />
979
+ <UI.text size="xs" text="Code" />
980
+ </UI.flex>
981
+ ),
982
+ content: (
983
+ <UI.box p="md" style={{ overflowY: "auto", flex: 1 }}>
984
+ {(() => {
985
+ const propsCode = Object.entries(this.state.knobValues)
986
+ .filter(
987
+ ([key, value]) =>
988
+ key !== "children" && value !== undefined,
989
+ )
990
+ .map(([key, value]) => {
991
+ if (value === true) return key;
992
+ if (value === false) return null;
993
+ if (typeof value === "string") return `${key}="${value}"`;
994
+ if (typeof value === "number") return `${key}={${value}}`;
995
+ if (typeof value === "object")
996
+ return `${key}={${JSON.stringify(value)}}`;
997
+ return null;
998
+ })
999
+ .filter(Boolean);
1000
+
1001
+ const propsString = propsCode.length
1002
+ ? `\n ${propsCode.join("\n ")}\n`
1003
+ : "";
1004
+ const childrenValue = this.state.knobValues["children"];
1005
+ const childrenCode =
1006
+ typeof childrenValue === "string" ? childrenValue : null;
1007
+
1008
+ const code = childrenCode
1009
+ ? `<UI.${activeDefinition.name}${propsString}>\n ${childrenCode}\n</UI.${activeDefinition.name}>`
1010
+ : `<UI.${activeDefinition.name}${propsString}/>`;
1011
+
1012
+ return (
1013
+ <UI.box style={{ position: "relative" }}>
1014
+ <UI.code code={code} codeLang="tsx" block />
1015
+ <UI.actionicon
1016
+ size="sm"
1017
+ variant="subtle"
1018
+ style={{ position: "absolute", top: 8, right: 8 }}
1019
+ onClick={() => navigator.clipboard.writeText(code)}
1020
+ >
1021
+ <UI.icon icon="content_copy" fz={14} />
1022
+ </UI.actionicon>
1023
+ </UI.box>
1024
+ );
1025
+ })()}
1026
+ </UI.box>
1027
+ ),
1028
+ },
1029
+ {
1030
+ value: "props",
1031
+ header: (
1032
+ <UI.flex align="center" gap={4}>
1033
+ <UI.icon icon="description" fz={14} />
1034
+ <UI.text size="xs" text="Props" />
1035
+ </UI.flex>
1036
+ ),
1037
+ content: (
1038
+ <UI.box style={{ overflowY: "auto", flex: 1 }}>
1039
+ <UI.table
1040
+ striped
1041
+ columns={["Prop", "Type", "Desc"]}
1042
+ rows={[
1043
+ ...this.methods
1044
+ .getCommonProps()
1045
+ .map((p) => [p.name, p.type, p.description]),
1046
+ ...this.methods
1047
+ .getComponentSpecificProps(activeDefinition)
1048
+ .map((p) => [p.name, p.type, p.description]),
1049
+ ]}
1050
+ />
1051
+ </UI.box>
1052
+ ),
1053
+ },
1054
+ ]}
1055
+ />
1056
+ ) : (
1057
+ <UI.center h="100%">
1058
+ <UI.text c="dimmed" text="Select a component to inspect" />
1059
+ </UI.center>
1060
+ );
1061
+
1062
+ return (
1063
+ <UI.flex
1064
+ h="100vh"
1065
+ direction="column"
1066
+ style={{
1067
+ overflow: "hidden",
1068
+ backgroundColor: "var(--mantine-color-gray-0)",
1069
+ }}
1070
+ >
1071
+ {/* MOBILE HEADER */}
1072
+ {this.state.isMobile && (
1073
+ <UI.flex
1074
+ p="sm"
1075
+ bg="white"
1076
+ align="center"
1077
+ justify="space-between"
1078
+ style={{ borderBottom: "1px solid var(--mantine-color-gray-3)" }}
1079
+ >
1080
+ <UI.burger
1081
+ opened={this.state.mobileMenuOpened}
1082
+ onClick={() =>
1083
+ (this.state.mobileMenuOpened = !this.state.mobileMenuOpened)
1084
+ }
1085
+ size="sm"
1086
+ />
1087
+ <UI.flex align="center" gap="xs">
1088
+ <UI.icon icon="architecture" fz={20} c="blue" />
1089
+ <UI.text fw={700} text="UIDocs" />
1090
+ </UI.flex>
1091
+ <UI.actionicon
1092
+ variant="subtle"
1093
+ onClick={() =>
1094
+ (this.state.inspectorOpened = !this.state.inspectorOpened)
1095
+ }
1096
+ >
1097
+ <UI.icon icon="tune" fz={20} />
1098
+ </UI.actionicon>
1099
+ </UI.flex>
1100
+ )}
1101
+
1102
+ <UI.flex direction="row" flex={1} style={{ overflow: "hidden" }}>
1103
+ {/* SIDEBAR */}
1104
+ {this.state.isMobile ? (
1105
+ <UI.drawer
1106
+ opened={this.state.mobileMenuOpened}
1107
+ onClose={() => (this.state.mobileMenuOpened = false)}
1108
+ title="Navigation"
1109
+ padding="md"
1110
+ size="280px"
1111
+ >
1112
+ <UI.flex direction="column" h="100%">
1113
+ {sidebarContent}
1114
+ </UI.flex>
1115
+ </UI.drawer>
1116
+ ) : (
1117
+ <UI.box
1118
+ w={280}
1119
+ h="100%"
1120
+ style={{
1121
+ borderRight: "1px solid var(--mantine-color-gray-3)",
1122
+ backgroundColor: "white",
1123
+ display: "flex",
1124
+ flexDirection: "column",
1125
+ }}
1126
+ >
1127
+ {sidebarContent}
1128
+ </UI.box>
1129
+ )}
1130
+
1131
+ {/* MAIN CANVAS */}
1132
+ <UI.flex
1133
+ direction="column"
1134
+ flex={1}
1135
+ h="100%"
1136
+ style={{ position: "relative" }}
1137
+ >
1138
+ {activeDefinition ? (
1139
+ <>
1140
+ <UI.box
1141
+ style={{
1142
+ position: "absolute",
1143
+ top: this.state.isMobile ? 10 : 20,
1144
+ left: "50%",
1145
+ transform: "translateX(-50%)",
1146
+ zIndex: 100,
1147
+ backgroundColor: "rgba(255, 255, 255, 0.8)",
1148
+ backdropFilter: "blur(8px)",
1149
+ border: "1px solid var(--mantine-color-gray-3)",
1150
+ borderRadius: "var(--mantine-radius-xl)",
1151
+ padding: this.state.isMobile ? "2px 8px" : "4px 16px",
1152
+ boxShadow: "var(--mantine-shadow-md)",
1153
+ maxWidth: "95vw",
1154
+ overflow: "hidden",
1155
+ }}
1156
+ >
1157
+ <UI.flex gap="xs" align="center">
1158
+ <UI.segmentedcontrol
1159
+ size={this.state.isMobile ? "xs" : "xs"}
1160
+ value={this.state.previewViewport}
1161
+ onChange={(val) => this.methods.setViewport(val)}
1162
+ data={[
1163
+ {
1164
+ label: <UI.icon icon="aspect_ratio" fz={16} />,
1165
+ value: "Responsive",
1166
+ },
1167
+ {
1168
+ label: <UI.icon icon="smartphone" fz={16} />,
1169
+ value: "375px",
1170
+ },
1171
+ {
1172
+ label: <UI.icon icon="tablet_mac" fz={16} />,
1173
+ value: "768px",
1174
+ },
1175
+ {
1176
+ label: <UI.icon icon="desktop_windows" fz={16} />,
1177
+ value: "1024px",
1178
+ },
1179
+ ]}
1180
+ />
1181
+ {!this.state.isMobile && (
1182
+ <>
1183
+ <UI.divider orientation="vertical" />
1184
+ <UI.actionicon
1185
+ variant="subtle"
1186
+ size="sm"
1187
+ onClick={() =>
1188
+ this.methods.setZoom(
1189
+ Math.max(0.25, this.state.previewZoom - 0.25),
1190
+ )
1191
+ }
1192
+ >
1193
+ <UI.icon icon="remove" fz={16} />
1194
+ </UI.actionicon>
1195
+ <UI.text
1196
+ size="xs"
1197
+ w={35}
1198
+ ta="center"
1199
+ text={`${Math.round(this.state.previewZoom * 100)}%`}
1200
+ />
1201
+ <UI.actionicon
1202
+ variant="subtle"
1203
+ size="sm"
1204
+ onClick={() =>
1205
+ this.methods.setZoom(
1206
+ Math.min(3, this.state.previewZoom + 0.25),
1207
+ )
1208
+ }
1209
+ >
1210
+ <UI.icon icon="add" fz={16} />
1211
+ </UI.actionicon>
1212
+ <UI.divider orientation="vertical" />
1213
+ <UI.actionicon
1214
+ variant={this.state.previewGrid ? "light" : "subtle"}
1215
+ size="sm"
1216
+ onClick={this.methods.toggleGrid}
1217
+ color={this.state.previewGrid ? "blue" : "gray"}
1218
+ >
1219
+ <UI.icon icon="grid_on" fz={16} />
1220
+ </UI.actionicon>
1221
+ </>
1222
+ )}
1223
+ </UI.flex>
1224
+ </UI.box>
1225
+
1226
+ <UI.box
1227
+ style={{
1228
+ position: "absolute",
1229
+ bottom: this.state.isMobile ? 10 : 20,
1230
+ left: this.state.isMobile ? 10 : 20,
1231
+ zIndex: 100,
1232
+ backgroundColor: "rgba(255, 255, 255, 0.8)",
1233
+ backdropFilter: "blur(8px)",
1234
+ border: "1px solid var(--mantine-color-gray-3)",
1235
+ borderRadius: "var(--mantine-radius-md)",
1236
+ padding: "8px",
1237
+ boxShadow: "var(--mantine-shadow-sm)",
1238
+ maxWidth: this.state.isMobile ? "calc(100vw - 20px)" : 300,
1239
+ }}
1240
+ >
1241
+ <UI.text
1242
+ size="xs"
1243
+ fw={700}
1244
+ c="dimmed"
1245
+ mb={4}
1246
+ text="Examples"
1247
+ />
1248
+ <UI.flex
1249
+ direction={this.state.isMobile ? "row" : "column"}
1250
+ gap={4}
1251
+ style={{ overflowX: "auto" }}
1252
+ >
1253
+ {activeDefinition.examples.map(
1254
+ (ex: string, idx: number) => {
1255
+ const UnstyledButtonComp = UI.unstyledbutton as any;
1256
+ return (
1257
+ <UnstyledButtonComp
1258
+ key={idx}
1259
+ onClick={() => this.methods.selectStory(idx)}
1260
+ style={
1261
+ {
1262
+ padding: "4px 8px",
1263
+ borderRadius: "var(--mantine-radius-xs)",
1264
+ backgroundColor:
1265
+ this.state.activeStoryId === idx
1266
+ ? "var(--mantine-color-blue-0)"
1267
+ : "transparent",
1268
+ borderLeft:
1269
+ !this.state.isMobile &&
1270
+ this.state.activeStoryId === idx
1271
+ ? "2px solid var(--mantine-color-blue-6)"
1272
+ : "2px solid transparent",
1273
+ borderBottom:
1274
+ this.state.isMobile &&
1275
+ this.state.activeStoryId === idx
1276
+ ? "2px solid var(--mantine-color-blue-6)"
1277
+ : "2px solid transparent",
1278
+ cursor: "pointer",
1279
+ display: "block",
1280
+ whiteSpace: "nowrap",
1281
+ } as any
1282
+ }
1283
+ >
1284
+ <UI.text
1285
+ size="xs"
1286
+ fw={this.state.activeStoryId === idx ? 600 : 400}
1287
+ text={`Example ${idx + 1}`}
1288
+ />
1289
+ </UnstyledButtonComp>
1290
+ );
1291
+ },
1292
+ )}
1293
+ </UI.flex>
1294
+ </UI.box>
1295
+
1296
+ <UI.box
1297
+ bg={this.state.previewTheme === "light" ? "white" : "#1A1B1E"}
1298
+ flex={1}
1299
+ style={{
1300
+ overflow: "auto",
1301
+ display: "flex",
1302
+ alignItems: "center",
1303
+ justifyContent: "center",
1304
+ position: "relative",
1305
+ }}
1306
+ >
1307
+ <UI.box
1308
+ style={{
1309
+ transform: `scale(${this.state.previewZoom})`,
1310
+ transition: "transform 0.2s ease, width 0.3s ease",
1311
+ width:
1312
+ this.state.previewViewport === "Responsive"
1313
+ ? "100%"
1314
+ : this.state.previewViewport,
1315
+ padding: this.state.isMobile ? "40px 10px" : "60px",
1316
+ display: "flex",
1317
+ alignItems: "center",
1318
+ justifyContent: "center",
1319
+ }}
1320
+ >
1321
+ {(() => {
1322
+ const libData =
1323
+ activeDefinition.library === "ui"
1324
+ ? this.props.ui
1325
+ : activeDefinition.library === "blocks"
1326
+ ? this.props.blocks
1327
+ : this.props.components;
1328
+
1329
+ const TargetComp = (libData as any)?.[
1330
+ activeDefinition.name
1331
+ ];
1332
+
1333
+ if (!TargetComp)
1334
+ return (
1335
+ <UI.flex direction="column" align="center" gap="md">
1336
+ <UI.icon icon="error_outline" fz={48} c="red" />
1337
+ <UI.text
1338
+ text={`Component '${activeDefinition.name}' not found in ${activeDefinition.library}`}
1339
+ fw={700}
1340
+ />
1341
+ <UI.text
1342
+ size="xs"
1343
+ c="dimmed"
1344
+ text="This usually happens if the component was renamed or the import failed during the documentation scanning phase."
1345
+ />
1346
+ </UI.flex>
1347
+ );
1348
+
1349
+ const propsWithLogging = { ...this.state.knobValues };
1350
+ Object.keys(propsWithLogging).forEach((key) => {
1351
+ if (
1352
+ key.startsWith("on") &&
1353
+ typeof propsWithLogging[key] === "function"
1354
+ ) {
1355
+ const original = propsWithLogging[key];
1356
+ propsWithLogging[key] = (...args: any[]) => {
1357
+ this.methods.logAction(key, args);
1358
+ original?.(...args);
1359
+ };
1360
+ }
1361
+ });
1362
+
1363
+ return <TargetComp {...propsWithLogging} />;
1364
+ })()}
1365
+ </UI.box>
1366
+
1367
+ {this.state.previewGrid && (
1368
+ <UI.box
1369
+ style={{
1370
+ position: "absolute",
1371
+ height: "300vh",
1372
+ inset: 0,
1373
+ pointerEvents: "none",
1374
+ zIndex: 10,
1375
+ backgroundImage: `linear-gradient(to right, ${
1376
+ this.state.previewTheme === "light"
1377
+ ? "var(--mantine-color-gray-4)"
1378
+ : "#2C2E33"
1379
+ } 1px, transparent 1px), linear-gradient(to bottom, ${
1380
+ this.state.previewTheme === "light"
1381
+ ? "var(--mantine-color-gray-4)"
1382
+ : "#2C2E33"
1383
+ } 1px, transparent 1px)`,
1384
+ backgroundSize: "20px 20px",
1385
+ }}
1386
+ />
1387
+ )}
1388
+ </UI.box>
1389
+ </>
1390
+ ) : (
1391
+ conceptsContent
1392
+ )}
1393
+ </UI.flex>
1394
+
1395
+ {/* INSPECTOR */}
1396
+ {this.state.isMobile ? (
1397
+ <UI.drawer
1398
+ opened={this.state.inspectorOpened}
1399
+ onClose={() => (this.state.inspectorOpened = false)}
1400
+ title="Inspector"
1401
+ padding="md"
1402
+ size="lg"
1403
+ position="right"
1404
+ >
1405
+ <UI.flex direction="column" h="100%">
1406
+ {inspectorContent}
1407
+ </UI.flex>
1408
+ </UI.drawer>
1409
+ ) : (
1410
+ <UI.box
1411
+ w={350}
1412
+ h="100%"
1413
+ style={{
1414
+ borderLeft: "1px solid var(--mantine-color-gray-3)",
1415
+ backgroundColor: "white",
1416
+ display: "flex",
1417
+ flexDirection: "column",
1418
+ }}
1419
+ >
1420
+ {inspectorContent}
1421
+ </UI.box>
1422
+ )}
1423
+ </UI.flex>
1424
+ </UI.flex>
1425
+ );
1426
+ },
1427
+ });
1428
+
1429
+ export default Component;