akanjs 2.0.0-rc.7 → 2.0.0

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 (87) hide show
  1. package/base/primitiveRegistry.ts +28 -2
  2. package/cli/application/application.command.ts +11 -3
  3. package/cli/application/application.runner.ts +17 -1
  4. package/cli/guidelines/databaseModule/databaseModule.instruction.md +1 -1
  5. package/cli/guidelines/modelConstant/modelConstant.instruction.md +5 -5
  6. package/cli/guidelines/modelDocument/modelDocument.instruction.md +34 -61
  7. package/cli/guidelines/modelService/modelService.instruction.md +1 -1
  8. package/cli/index.js +9321 -19222
  9. package/cli/library/library.runner.ts +14 -13
  10. package/cli/package/package.runner.ts +31 -6
  11. package/cli/package/package.script.ts +2 -2
  12. package/cli/templates/app/page/_index.tsx +200 -79
  13. package/cli/templates/app/page/_layout.tsx +0 -1
  14. package/cli/templates/app/public/favicon.ico.template +0 -0
  15. package/cli/templates/app/public/logo.png.template +0 -0
  16. package/cli/templates/module/__Model__.Zone.tsx +1 -1
  17. package/cli/templates/module/__model__.document.ts +1 -1
  18. package/cli/templates/workspaceRoot/.gitignore.template +1 -11
  19. package/cli/templates/workspaceRoot/biome.json.template +16 -0
  20. package/cli/templates/workspaceRoot/package.json.template +1 -5
  21. package/cli/workspace/workspace.command.ts +7 -9
  22. package/cli/workspace/workspace.runner.ts +3 -13
  23. package/cli/workspace/workspace.script.ts +24 -9
  24. package/client/csrTypes.ts +1 -1
  25. package/constant/fieldInfo.ts +1 -1
  26. package/constant/serialize.ts +7 -1
  27. package/devkit/capacitor.base.config.ts +1 -1
  28. package/devkit/capacitorApp.ts +5 -1
  29. package/devkit/commandDecorators/argMeta.ts +28 -14
  30. package/devkit/commandDecorators/command.ts +41 -15
  31. package/devkit/commandDecorators/commandBuilder.ts +78 -42
  32. package/devkit/commandDecorators/helpFormatter.ts +7 -4
  33. package/devkit/dependencyScanner.ts +121 -15
  34. package/devkit/executors.ts +35 -23
  35. package/devkit/frontendBuild/cssCompiler.ts +9 -3
  36. package/devkit/incrementalBuilder/incrementalBuilder.proc.ts +2 -1
  37. package/devkit/lint/no-deep-internal-import.grit +25 -0
  38. package/devkit/lint/no-import-external-library.grit +1 -0
  39. package/devkit/mobile/mobileTarget.ts +48 -8
  40. package/devkit/scanInfo.ts +4 -1
  41. package/devkit/src/capacitorApp.ts +277 -0
  42. package/devkit/transforms/barrelImportsPlugin.ts +6 -0
  43. package/fetch/client/fetchClient.ts +1 -0
  44. package/fetch/client/httpClient.ts +13 -1
  45. package/package.json +37 -31
  46. package/server/akanServer.ts +21 -7
  47. package/server/hmr/clientScript.ts +8 -5
  48. package/server/resolver/resolver.contract.fixture.ts +1 -1
  49. package/test/index.ts +14 -0
  50. package/test/signalTest.preload.ts +10 -0
  51. package/test/signalTestRuntime.ts +126 -0
  52. package/test/testServer.ts +130 -25
  53. package/ui/Constant/Doc.tsx +696 -0
  54. package/ui/Constant/Mermaid.tsx +149 -0
  55. package/ui/Constant/index.ts +6 -0
  56. package/ui/Constant/schemaDoc.ts +324 -0
  57. package/ui/Field.tsx +0 -1
  58. package/ui/Portal.tsx +2 -0
  59. package/ui/System/CSR.tsx +6 -5
  60. package/ui/System/SSR.tsx +1 -1
  61. package/ui/System/SelectLanguage.tsx +1 -1
  62. package/ui/index.ts +1 -0
  63. package/ui/styles.css +0 -1
  64. package/webkit/bootCsr.tsx +8 -5
  65. package/base/test-globals.d.ts +0 -4
  66. package/cli/templates/app/common/commonLogic.ts +0 -12
  67. package/cli/templates/app/common/index.ts +0 -10
  68. package/cli/templates/app/public/favicon.ico +0 -0
  69. package/cli/templates/app/public/icons/icon-128x128.png +0 -0
  70. package/cli/templates/app/public/icons/icon-144x144.png +0 -0
  71. package/cli/templates/app/public/icons/icon-152x152.png +0 -0
  72. package/cli/templates/app/public/icons/icon-192x192.png +0 -0
  73. package/cli/templates/app/public/icons/icon-256x256.png +0 -0
  74. package/cli/templates/app/public/icons/icon-384x384.png +0 -0
  75. package/cli/templates/app/public/icons/icon-48x48.png +0 -0
  76. package/cli/templates/app/public/icons/icon-512x512.png +0 -0
  77. package/cli/templates/app/public/icons/icon-72x72.png +0 -0
  78. package/cli/templates/app/public/icons/icon-96x96.png +0 -0
  79. package/cli/templates/app/public/logo.svg +0 -70
  80. package/cli/templates/app/public/manifest.json.template +0 -67
  81. package/cli/templates/app/srvkit/backendLogic.ts +0 -12
  82. package/cli/templates/app/srvkit/index.ts +0 -10
  83. package/cli/templates/app/ui/UiComponent.ts +0 -16
  84. package/cli/templates/app/ui/index.ts +0 -10
  85. package/cli/templates/app/webkit/frontendLogic.ts +0 -12
  86. package/cli/templates/app/webkit/index.ts +0 -10
  87. package/cli/templates/module/index.tsx +0 -44
@@ -0,0 +1,696 @@
1
+ "use client";
2
+
3
+ import { usePage } from "akanjs/client";
4
+ import { capitalize } from "akanjs/common";
5
+ import { useMemo, useState } from "react";
6
+ import { AiOutlineInfoCircle, AiOutlineSearch } from "react-icons/ai";
7
+ import { BiNetworkChart, BiTable } from "react-icons/bi";
8
+
9
+ import { Input } from "../Input";
10
+ import { Modal } from "../Modal";
11
+ import { Mermaid } from "./Mermaid";
12
+ import {
13
+ type DatabaseModelVariant,
14
+ type DatabaseSchema,
15
+ databaseModelVariants,
16
+ type FieldSchema,
17
+ getConstantSchemaDoc,
18
+ getDefaultVariant,
19
+ getVariantTitle,
20
+ type ScalarSchema,
21
+ } from "./schemaDoc";
22
+
23
+ export default function Doc() {
24
+ return <div />;
25
+ }
26
+
27
+ interface ZoneProps {
28
+ models?: string[];
29
+ scalars?: string[];
30
+ enums?: string[];
31
+ openAll?: boolean;
32
+ }
33
+
34
+ const Zone = ({ models, scalars, enums, openAll }: ZoneProps) => {
35
+ const schemaDoc = useMemo(() => getConstantSchemaDoc({ models, scalars, enums }), [models, scalars, enums]);
36
+ const [query, setQuery] = useState("");
37
+ const [viewMode, setViewMode] = useState<"table" | "diagram">("table");
38
+ const filteredDatabases = useMemo(
39
+ () => schemaDoc.databases.filter((database) => matchesQuery(database.refName, query)),
40
+ [schemaDoc.databases, query],
41
+ );
42
+ const filteredScalars = useMemo(
43
+ () => schemaDoc.scalars.filter((scalar) => matchesQuery(scalar.refName, query)),
44
+ [schemaDoc.scalars, query],
45
+ );
46
+ const filteredEnums = useMemo(
47
+ () =>
48
+ schemaDoc.enums.filter(
49
+ (enumSchema) => matchesQuery(enumSchema.refName, query) || matchesQuery(enumSchema.key, query),
50
+ ),
51
+ [schemaDoc.enums, query],
52
+ );
53
+ return (
54
+ <div className="flex break-after-page flex-col gap-4">
55
+ <div>
56
+ <div className="font-bold text-3xl">Constant Schema Docs</div>
57
+ <div className="text-base-content/70">
58
+ Database models, scalar models, enums, and relations from ConstantRegistry.
59
+ </div>
60
+ </div>
61
+ <div className="grid grid-cols-2 gap-2 md:grid-cols-4">
62
+ <SummaryCard title="Database Models" value={filteredDatabases.length} />
63
+ <SummaryCard title="Scalar Models" value={filteredScalars.length} />
64
+ <SummaryCard title="Enums" value={filteredEnums.length} />
65
+ <SummaryCard title="Relations" value={schemaDoc.relations.length} />
66
+ </div>
67
+ <div className="flex flex-wrap items-center justify-between gap-2 rounded-xl bg-base-200 p-3">
68
+ <Input
69
+ nullable
70
+ value={query}
71
+ onChange={setQuery}
72
+ inputClassName="w-72"
73
+ icon={<AiOutlineSearch />}
74
+ placeholder="Search models or enums"
75
+ />
76
+ <div className="join">
77
+ <button
78
+ className={`btn join-item btn-sm ${viewMode === "table" ? "btn-primary" : "btn-outline"}`}
79
+ onClick={() => setViewMode("table")}
80
+ >
81
+ <BiTable /> Table
82
+ </button>
83
+ <button
84
+ className={`btn join-item btn-sm ${viewMode === "diagram" ? "btn-primary" : "btn-outline"}`}
85
+ onClick={() => setViewMode("diagram")}
86
+ >
87
+ <BiNetworkChart /> Diagram
88
+ </button>
89
+ </div>
90
+ </div>
91
+ {viewMode === "diagram" ? (
92
+ <Diagram databases={filteredDatabases} scalars={filteredScalars} />
93
+ ) : (
94
+ <div className="flex flex-col gap-4">
95
+ {filteredDatabases.map((database) => (
96
+ <Model key={database.refName} database={database} openAll={openAll} />
97
+ ))}
98
+ {filteredScalars.length ? (
99
+ <div className="flex flex-col gap-3">
100
+ <div className="font-bold text-2xl">Scalar Models</div>
101
+ {filteredScalars.map((scalar) => (
102
+ <Scalar key={scalar.refName} scalar={scalar} openAll={openAll} />
103
+ ))}
104
+ </div>
105
+ ) : null}
106
+ {filteredEnums.length ? (
107
+ <div className="flex flex-col gap-3">
108
+ <div className="font-bold text-2xl">Enums</div>
109
+ <EnumList enums={filteredEnums} />
110
+ </div>
111
+ ) : null}
112
+ </div>
113
+ )}
114
+ </div>
115
+ );
116
+ };
117
+ Doc.Zone = Zone;
118
+
119
+ const Print = ({ models, scalars, enums }: ZoneProps) => {
120
+ const schemaDoc = useMemo(() => getConstantSchemaDoc({ models, scalars, enums }), [models, scalars, enums]);
121
+ return (
122
+ <div className="flex flex-col gap-10 bg-base-100 text-base-content print:bg-white print:text-black">
123
+ <div className="break-after-page">
124
+ <div className="font-bold text-4xl">Constant Schema Definition</div>
125
+ <div className="mt-2 text-base-content/70 print:text-black">
126
+ Database models, scalar models, enums, and relations from ConstantRegistry.
127
+ </div>
128
+ <div className="mt-6 grid grid-cols-2 gap-2 md:grid-cols-4">
129
+ <SummaryCard title="Database Models" value={schemaDoc.databases.length} />
130
+ <SummaryCard title="Scalar Models" value={schemaDoc.scalars.length} />
131
+ <SummaryCard title="Enums" value={schemaDoc.enums.length} />
132
+ <SummaryCard title="Relations" value={schemaDoc.relations.length} />
133
+ </div>
134
+ </div>
135
+ {schemaDoc.databases.map((database) => (
136
+ <PrintDatabase key={database.refName} database={database} />
137
+ ))}
138
+ {schemaDoc.scalars.length ? (
139
+ <section className="flex break-before-page flex-col gap-4">
140
+ <PrintSectionTitle title="Scalar Models" />
141
+ {schemaDoc.scalars.map((scalar) => (
142
+ <PrintScalar key={scalar.refName} scalar={scalar} />
143
+ ))}
144
+ </section>
145
+ ) : null}
146
+ {schemaDoc.enums.length ? (
147
+ <section className="flex break-before-page flex-col gap-4">
148
+ <PrintSectionTitle title="Enums" />
149
+ <PrintEnumTable enums={schemaDoc.enums} />
150
+ </section>
151
+ ) : null}
152
+ </div>
153
+ );
154
+ };
155
+ Doc.Print = Print;
156
+
157
+ interface ModelProps {
158
+ refName?: string;
159
+ database?: DatabaseSchema;
160
+ openAll?: boolean;
161
+ }
162
+
163
+ const Model = ({ refName, database: databaseProp, openAll }: ModelProps) => {
164
+ const database = useMemo(
165
+ () => databaseProp ?? getConstantSchemaDoc({ models: refName ? [refName] : [] }).databases.at(0),
166
+ [databaseProp, refName],
167
+ );
168
+ const [variant, setVariant] = useState<DatabaseModelVariant>("full");
169
+ const { l } = usePage();
170
+ if (!database) return null;
171
+ const activeVariant = database.variants[variant] ?? getDefaultVariant(database);
172
+ return (
173
+ <div className="collapse-arrow collapse bg-base-200">
174
+ <input type="checkbox" defaultChecked={openAll} />
175
+ <div className="collapse-title">
176
+ <div className="flex flex-wrap items-center gap-2">
177
+ <div className="font-bold text-xl">{database.modelName}</div>
178
+ <div className="badge badge-primary">{database.refName}</div>
179
+ <div className="text-base-content/70 text-sm">{l._(`${database.refName}.modelDesc`)}</div>
180
+ </div>
181
+ </div>
182
+ <div className="collapse-content flex flex-col gap-3">
183
+ <VariantTabs variant={variant} onChange={setVariant} />
184
+ <ModelVariantTable variant={activeVariant} />
185
+ </div>
186
+ </div>
187
+ );
188
+ };
189
+ Doc.Model = Model;
190
+
191
+ interface ScalarProps {
192
+ refName?: string;
193
+ scalar?: ScalarSchema;
194
+ openAll?: boolean;
195
+ }
196
+
197
+ const Scalar = ({ refName, scalar: scalarProp, openAll }: ScalarProps) => {
198
+ const scalar = useMemo(
199
+ () => scalarProp ?? getConstantSchemaDoc({ scalars: refName ? [refName] : [] }).scalars.at(0),
200
+ [scalarProp, refName],
201
+ );
202
+ const { l } = usePage();
203
+ if (!scalar) return null;
204
+ return (
205
+ <div className="collapse-arrow collapse bg-base-200">
206
+ <input type="checkbox" defaultChecked={openAll} />
207
+ <div className="collapse-title">
208
+ <div className="flex flex-wrap items-center gap-2">
209
+ <div className="font-bold text-xl">{scalar.modelName}</div>
210
+ <div className="badge badge-secondary">{scalar.refName}</div>
211
+ <div className="text-base-content/70 text-sm">{l._(`${scalar.refName}.modelDesc`)}</div>
212
+ </div>
213
+ </div>
214
+ <div className="collapse-content">
215
+ <FieldTable refName={scalar.refName} fields={scalar.fields} />
216
+ </div>
217
+ </div>
218
+ );
219
+ };
220
+ Doc.Scalar = Scalar;
221
+
222
+ interface EnumProps {
223
+ enums?: ReturnType<typeof getConstantSchemaDoc>["enums"];
224
+ }
225
+
226
+ const EnumList = ({ enums = getConstantSchemaDoc().enums }: EnumProps) => {
227
+ const { l } = usePage();
228
+ return (
229
+ <div className="overflow-x-auto rounded-xl bg-base-200 p-3">
230
+ <table className="table">
231
+ <thead>
232
+ <tr>
233
+ <th>Key</th>
234
+ <th>Ref Name</th>
235
+ <th>Type</th>
236
+ <th>Values</th>
237
+ <th>Used By</th>
238
+ </tr>
239
+ </thead>
240
+ <tbody>
241
+ {enums.map((enumSchema) => (
242
+ <tr key={enumSchema.key}>
243
+ <td>{enumSchema.key}</td>
244
+ <td>{enumSchema.refName}</td>
245
+ <td>{enumSchema.typeName}</td>
246
+ <td>
247
+ <div className="flex flex-wrap gap-1">
248
+ {enumSchema.values.map((value) => (
249
+ <div
250
+ key={String(value)}
251
+ className="tooltip tooltip-primary"
252
+ data-tip={l._(`${enumSchema.refName}.${value}`)}
253
+ >
254
+ <button className="btn btn-xs">{String(value)}</button>
255
+ </div>
256
+ ))}
257
+ </div>
258
+ </td>
259
+ <td>
260
+ <div className="flex flex-wrap gap-1">
261
+ {enumSchema.usedBy.length
262
+ ? enumSchema.usedBy.map((usage) => (
263
+ <span
264
+ key={`${usage.refName}-${usage.variant}-${usage.fieldKey}`}
265
+ className="badge badge-outline"
266
+ >
267
+ {usage.refName}.{usage.fieldKey}
268
+ </span>
269
+ ))
270
+ : "-"}
271
+ </div>
272
+ </td>
273
+ </tr>
274
+ ))}
275
+ </tbody>
276
+ </table>
277
+ </div>
278
+ );
279
+ };
280
+ Doc.Enum = EnumList;
281
+
282
+ const VariantTabs = ({
283
+ variant,
284
+ onChange,
285
+ }: {
286
+ variant: DatabaseModelVariant;
287
+ onChange: (variant: DatabaseModelVariant) => void;
288
+ }) => (
289
+ <div className="tabs tabs-box w-fit">
290
+ {databaseModelVariants.map((item) => (
291
+ <button key={item} className={`tab ${variant === item ? "tab-active" : ""}`} onClick={() => onChange(item)}>
292
+ {getVariantTitle(item)}
293
+ </button>
294
+ ))}
295
+ </div>
296
+ );
297
+
298
+ const ModelVariantTable = ({ variant }: { variant: ReturnType<typeof getDefaultVariant> }) => (
299
+ <div className="flex flex-col gap-2">
300
+ <div className="flex flex-wrap items-center gap-2">
301
+ <div className="font-extrabold text-lg">{variant.modelName}</div>
302
+ <div className="badge badge-outline">{getVariantTitle(variant.variant)}</div>
303
+ <div className="text-base-content/60 text-sm">{variant.fields.length} fields</div>
304
+ </div>
305
+ <FieldTable refName={variant.refName} fields={variant.fields} />
306
+ </div>
307
+ );
308
+
309
+ const PrintDatabase = ({ database }: { database: DatabaseSchema }) => {
310
+ const { l } = usePage();
311
+ return (
312
+ <section className="flex break-after-page flex-col gap-5">
313
+ <div>
314
+ <div className="flex flex-wrap items-center gap-2">
315
+ <div className="font-bold text-3xl">{database.modelName}</div>
316
+ <div className="badge badge-primary print:border print:border-black print:bg-white print:text-black">
317
+ {database.refName}
318
+ </div>
319
+ </div>
320
+ <div className="mt-2 text-base-content/70 print:text-black">{l._(`${database.refName}.modelDesc`)}</div>
321
+ </div>
322
+ {databaseModelVariants.map((variantKey) => {
323
+ const variant = database.variants[variantKey];
324
+ return (
325
+ <div key={variantKey} className="flex flex-col gap-2">
326
+ <PrintVariantHeader
327
+ title={variant.modelName}
328
+ badge={getVariantTitle(variant.variant)}
329
+ fields={variant.fields.length}
330
+ />
331
+ <PrintFieldTable refName={variant.refName} fields={variant.fields} />
332
+ </div>
333
+ );
334
+ })}
335
+ </section>
336
+ );
337
+ };
338
+
339
+ const PrintScalar = ({ scalar }: { scalar: ScalarSchema }) => {
340
+ const { l } = usePage();
341
+ return (
342
+ <section className="flex break-inside-avoid flex-col gap-3">
343
+ <div>
344
+ <div className="flex flex-wrap items-center gap-2">
345
+ <div className="font-bold text-2xl">{scalar.modelName}</div>
346
+ <div className="badge badge-secondary print:border print:border-black print:bg-white print:text-black">
347
+ {scalar.refName}
348
+ </div>
349
+ </div>
350
+ <div className="mt-1 text-base-content/70 print:text-black">{l._(`${scalar.refName}.modelDesc`)}</div>
351
+ </div>
352
+ <PrintFieldTable refName={scalar.refName} fields={scalar.fields} />
353
+ </section>
354
+ );
355
+ };
356
+
357
+ const PrintSectionTitle = ({ title }: { title: string }) => <div className="font-bold text-3xl">{title}</div>;
358
+
359
+ const PrintVariantHeader = ({ title, badge, fields }: { title: string; badge: string; fields: number }) => (
360
+ <div className="flex flex-wrap items-center gap-2">
361
+ <div className="font-extrabold text-xl">{title}</div>
362
+ <div className="badge badge-outline print:border print:border-black">{badge}</div>
363
+ <div className="text-base-content/60 text-sm print:text-black">{fields} fields</div>
364
+ </div>
365
+ );
366
+
367
+ const FieldTable = ({ refName, fields }: { refName: string; fields: FieldSchema[] }) => {
368
+ const { l } = usePage();
369
+ const [selectedField, setSelectedField] = useState<FieldSchema | null>(null);
370
+ return (
371
+ <>
372
+ <div className="overflow-x-auto rounded-xl bg-base-100 p-3">
373
+ <table className="table">
374
+ <thead>
375
+ <tr>
376
+ <th>Key</th>
377
+ <th>Type</th>
378
+ <th>Required</th>
379
+ <th>Field Type</th>
380
+ <th>Relation</th>
381
+ <th>Default</th>
382
+ <th>Constraints</th>
383
+ <th>Enum</th>
384
+ <th>Description</th>
385
+ <th />
386
+ </tr>
387
+ </thead>
388
+ <tbody>
389
+ {fields.map((field) => (
390
+ <tr key={field.key}>
391
+ <td>
392
+ <div className="font-bold">{field.key}</div>
393
+ <div className="text-base-content/60 text-xs">{l._(`${refName}.${field.key}`)}</div>
394
+ </td>
395
+ <td>
396
+ <span className={field.typeKind === "primitive" ? "" : "badge badge-primary badge-outline"}>
397
+ {field.typeLabel}
398
+ </span>
399
+ </td>
400
+ <td>
401
+ {field.required ? (
402
+ <span className="badge badge-error">Required</span>
403
+ ) : (
404
+ <span className="badge">Optional</span>
405
+ )}
406
+ </td>
407
+ <td>
408
+ <span className="badge badge-outline">{field.fieldType}</span>
409
+ {!field.select ? <span className="badge badge-warning ml-1">select:false</span> : null}
410
+ </td>
411
+ <td>
412
+ {field.relationLabel ? <span className="badge badge-secondary">{field.relationLabel}</span> : "-"}
413
+ </td>
414
+ <td className="max-w-48 truncate">{field.defaultLabel ?? "-"}</td>
415
+ <td>
416
+ <div className="flex flex-wrap gap-1">
417
+ {field.constraints.length
418
+ ? field.constraints.map((constraint) => (
419
+ <span key={constraint} className="badge badge-outline">
420
+ {constraint}
421
+ </span>
422
+ ))
423
+ : "-"}
424
+ </div>
425
+ </td>
426
+ <td>
427
+ {field.enumValues ? (
428
+ <div className="flex flex-wrap gap-1">
429
+ {field.enumValues.map((value) => (
430
+ <span key={String(value)} className="badge">
431
+ {String(value)}
432
+ </span>
433
+ ))}
434
+ </div>
435
+ ) : (
436
+ "-"
437
+ )}
438
+ </td>
439
+ <td className="min-w-52">{l._(`${refName}.${field.key}.desc`)}</td>
440
+ <td>
441
+ <button className="btn btn-ghost btn-xs" onClick={() => setSelectedField(field)}>
442
+ <AiOutlineInfoCircle /> Detail
443
+ </button>
444
+ </td>
445
+ </tr>
446
+ ))}
447
+ </tbody>
448
+ </table>
449
+ </div>
450
+ <FieldDetailModal refName={refName} field={selectedField} onClose={() => setSelectedField(null)} />
451
+ </>
452
+ );
453
+ };
454
+
455
+ const PrintFieldTable = ({ refName, fields }: { refName: string; fields: FieldSchema[] }) => {
456
+ const { l } = usePage();
457
+ return (
458
+ <div className="overflow-x-auto rounded-xl bg-base-100 p-3 print:overflow-visible print:rounded-none print:p-0">
459
+ <table className="table-sm table">
460
+ <thead>
461
+ <tr>
462
+ <th>Key</th>
463
+ <th>Type</th>
464
+ <th>Required</th>
465
+ <th>Field Type</th>
466
+ <th>Relation</th>
467
+ <th>Default</th>
468
+ <th>Constraints</th>
469
+ <th>Enum</th>
470
+ <th>Description</th>
471
+ <th>Detail</th>
472
+ </tr>
473
+ </thead>
474
+ <tbody>
475
+ {fields.map((field) => (
476
+ <tr key={field.key} className="break-inside-avoid">
477
+ <td>
478
+ <div className="font-bold">{field.key}</div>
479
+ <div className="text-base-content/60 text-xs print:text-black">{l._(`${refName}.${field.key}`)}</div>
480
+ </td>
481
+ <td>{field.typeLabel}</td>
482
+ <td>{field.required ? "Required" : "Optional"}</td>
483
+ <td>
484
+ <div>{field.fieldType}</div>
485
+ {!field.select ? <div>select:false</div> : null}
486
+ {field.immutable ? <div>immutable</div> : null}
487
+ </td>
488
+ <td>{getPrintRelation(field)}</td>
489
+ <td>{field.defaultLabel ?? "-"}</td>
490
+ <td>{field.constraints.length ? field.constraints.join(", ") : "-"}</td>
491
+ <td>{field.enumValues ? `${field.enumRefName ?? "enum"}: ${field.enumValues.join(", ")}` : "-"}</td>
492
+ <td>{l._(`${refName}.${field.key}.desc`)}</td>
493
+ <td>
494
+ <PrintFieldDetail field={field} />
495
+ </td>
496
+ </tr>
497
+ ))}
498
+ </tbody>
499
+ </table>
500
+ </div>
501
+ );
502
+ };
503
+
504
+ const PrintFieldDetail = ({ field }: { field: FieldSchema }) => {
505
+ const details = [
506
+ field.ref ? `ref: ${field.ref}` : null,
507
+ field.refPath ? `refPath: ${field.refPath}` : null,
508
+ field.refType ? `refType: ${field.refType}` : null,
509
+ field.exampleLabel ? `example: ${field.exampleLabel}` : null,
510
+ Object.keys(field.meta).length ? `meta: ${JSON.stringify(field.meta)}` : null,
511
+ ].filter((detail): detail is string => !!detail);
512
+ return details.length ? <div className="whitespace-pre-wrap text-xs">{details.join("\n")}</div> : "-";
513
+ };
514
+
515
+ const PrintEnumTable = ({ enums }: { enums: ReturnType<typeof getConstantSchemaDoc>["enums"] }) => {
516
+ const { l } = usePage();
517
+ return (
518
+ <div className="overflow-x-auto rounded-xl bg-base-100 p-3 print:overflow-visible print:rounded-none print:p-0">
519
+ <table className="table-sm table">
520
+ <thead>
521
+ <tr>
522
+ <th>Key</th>
523
+ <th>Ref Name</th>
524
+ <th>Type</th>
525
+ <th>Values</th>
526
+ <th>Descriptions</th>
527
+ <th>Used By</th>
528
+ </tr>
529
+ </thead>
530
+ <tbody>
531
+ {enums.map((enumSchema) => (
532
+ <tr key={enumSchema.key} className="break-inside-avoid">
533
+ <td>{enumSchema.key}</td>
534
+ <td>{enumSchema.refName}</td>
535
+ <td>{enumSchema.typeName}</td>
536
+ <td>{enumSchema.values.join(", ")}</td>
537
+ <td>
538
+ {enumSchema.values.map((value) => (
539
+ <div key={String(value)}>
540
+ {String(value)}: {l._(`${enumSchema.refName}.${value}`)}
541
+ </div>
542
+ ))}
543
+ </td>
544
+ <td>
545
+ {enumSchema.usedBy.length
546
+ ? enumSchema.usedBy
547
+ .map((usage) => `${usage.refName}.${usage.fieldKey} (${getVariantTitle(usage.variant)})`)
548
+ .join(", ")
549
+ : "-"}
550
+ </td>
551
+ </tr>
552
+ ))}
553
+ </tbody>
554
+ </table>
555
+ </div>
556
+ );
557
+ };
558
+
559
+ const FieldDetailModal = ({
560
+ refName,
561
+ field,
562
+ onClose,
563
+ }: {
564
+ refName: string;
565
+ field: FieldSchema | null;
566
+ onClose: () => void;
567
+ }) => {
568
+ const { l } = usePage();
569
+ if (!field) return null;
570
+ const detail = {
571
+ key: field.key,
572
+ type: field.typeLabel,
573
+ required: field.required,
574
+ fieldType: field.fieldType,
575
+ select: field.select,
576
+ immutable: field.immutable,
577
+ ref: field.ref,
578
+ refPath: field.refPath,
579
+ refType: field.refType,
580
+ default: field.defaultLabel,
581
+ example: field.exampleLabel,
582
+ constraints: field.constraints,
583
+ enum: field.enumValues,
584
+ meta: field.meta,
585
+ };
586
+ return (
587
+ <Modal
588
+ title={`${refName}.${field.key}`}
589
+ open={!!field}
590
+ onCancel={onClose}
591
+ className="max-w-3xl"
592
+ bodyClassName="flex flex-col gap-4"
593
+ >
594
+ <div>
595
+ <div className="font-bold text-lg">{l._(`${refName}.${field.key}`)}</div>
596
+ <div className="text-base-content/70">{l._(`${refName}.${field.key}.desc`)}</div>
597
+ </div>
598
+ <pre className="max-h-[60vh] overflow-auto rounded-xl bg-base-200 p-4 text-sm">
599
+ {JSON.stringify(detail, null, 2)}
600
+ </pre>
601
+ </Modal>
602
+ );
603
+ };
604
+
605
+ const getPrintRelation = (field: FieldSchema) => {
606
+ const parts = [
607
+ field.relationLabel,
608
+ field.typeRefName ? `target: ${field.typeRefName}` : null,
609
+ field.ref ? `ref: ${field.ref}` : null,
610
+ field.refPath ? `path: ${field.refPath}` : null,
611
+ ].filter((part): part is string => !!part);
612
+ return parts.length ? parts.join("\n") : "-";
613
+ };
614
+
615
+ const SummaryCard = ({ title, value }: { title: string; value: number }) => (
616
+ <div className="rounded-xl bg-base-200 p-4">
617
+ <div className="text-base-content/60 text-sm">{title}</div>
618
+ <div className="font-bold text-2xl">{value}</div>
619
+ </div>
620
+ );
621
+
622
+ const Diagram = ({ databases, scalars }: { databases: DatabaseSchema[]; scalars: ScalarSchema[] }) => {
623
+ const [selectedNode, setSelectedNode] = useState<string | null>(
624
+ databases.at(0)?.refName ?? scalars.at(0)?.refName ?? null,
625
+ );
626
+ const graph = useMemo(() => makeDiagram(databases, scalars), [databases, scalars]);
627
+ const selectedRefName = selectedNode ? graph.nodeRefNames.get(selectedNode) : undefined;
628
+ const selectedDatabase = selectedRefName
629
+ ? databases.find((database) => database.refName === selectedRefName)
630
+ : undefined;
631
+ const selectedScalar = selectedRefName ? scalars.find((scalar) => scalar.refName === selectedRefName) : undefined;
632
+ return (
633
+ <div className="grid gap-4 lg:grid-cols-[1fr_360px]">
634
+ <Mermaid
635
+ title="Schema Relationship Diagram"
636
+ chart={graph.chart}
637
+ highlightNodes={selectedNode ? [selectedNode] : []}
638
+ onSelectNode={setSelectedNode}
639
+ />
640
+ <div className="rounded-xl bg-base-200 p-4">
641
+ <div className="font-bold text-xl">Selected Model</div>
642
+ {selectedDatabase ? (
643
+ <Model database={selectedDatabase} openAll />
644
+ ) : selectedScalar ? (
645
+ <Scalar scalar={selectedScalar} openAll />
646
+ ) : selectedRefName ? (
647
+ <div className="mt-4">
648
+ <div className="badge badge-outline">External</div>
649
+ <div className="mt-2 font-bold">{selectedRefName}</div>
650
+ </div>
651
+ ) : (
652
+ <div className="mt-4 text-base-content/60">Select a node in the diagram.</div>
653
+ )}
654
+ </div>
655
+ </div>
656
+ );
657
+ };
658
+
659
+ const makeDiagram = (databases: DatabaseSchema[], scalars: ScalarSchema[]) => {
660
+ const schemaDoc = getConstantSchemaDoc({
661
+ models: databases.map((database) => database.refName),
662
+ scalars: scalars.map((scalar) => scalar.refName),
663
+ });
664
+ const nodeRefNames = new Map<string, string>();
665
+ const nodeLines = new Map<string, string>();
666
+ const addNode = (refName: string, label: string) => {
667
+ const nodeId = toMermaidNodeId(refName);
668
+ nodeRefNames.set(nodeId, refName);
669
+ nodeLines.set(nodeId, ` ${nodeId}["${escapeMermaidLabel(label)}"]`);
670
+ };
671
+ databases.forEach((database) => {
672
+ addNode(database.refName, `${database.modelName}\\n${database.refName}`);
673
+ });
674
+ scalars.forEach((scalar) => {
675
+ addNode(scalar.refName, `${scalar.modelName}\\n${scalar.refName}`);
676
+ });
677
+ schemaDoc.relations.forEach((relation) => {
678
+ if (!nodeLines.has(toMermaidNodeId(relation.targetRefName))) {
679
+ addNode(relation.targetRefName, `${capitalize(relation.targetRefName)}\\nexternal`);
680
+ }
681
+ });
682
+ const edgeLines = schemaDoc.relations.map((relation) => {
683
+ const from = toMermaidNodeId(relation.sourceRefName);
684
+ const to = toMermaidNodeId(relation.targetRefName);
685
+ const label = escapeMermaidLabel(`${relation.fieldKey}: ${relation.relationType}`);
686
+ return ` ${from} -->|"${label}"| ${to}`;
687
+ });
688
+ const chart = ["flowchart LR", ...nodeLines.values(), ...edgeLines].join("\n");
689
+ return { chart, nodeRefNames };
690
+ };
691
+
692
+ const toMermaidNodeId = (refName: string) => `schema_${refName.replace(/[^a-zA-Z0-9_]/g, "_")}`;
693
+
694
+ const escapeMermaidLabel = (label: string) => label.replace(/"/g, '\\"');
695
+
696
+ const matchesQuery = (value: string, query: string) => value.toLowerCase().includes(query.trim().toLowerCase());