@akanjs/cli 2.4.2-rc.3 → 3.0.0-alpha.1

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 (84) hide show
  1. package/.build-stamp +1 -1
  2. package/{abstractCompactor-ghh1q9an.js → abstractCompactor-e5yq60qz.js} +4 -4
  3. package/{agent.command-de4r6vk8.js → agent.command-yv00p0s0.js} +8 -7
  4. package/{application.command-nfsfm96y.js → application.command-0qsczf4k.js} +6 -6
  5. package/{applicationBuildRunner-554a2vnk.js → applicationBuildRunner-ct2pxyvw.js} +3 -2
  6. package/{applicationReleasePackager-pjbxc793.js → applicationReleasePackager-gg8ccbeh.js} +3 -3
  7. package/buildBatch.proc.js +8 -7
  8. package/{capacitorApp-yg9b4ajz.js → capacitorApp-4r9sm3qg.js} +3 -3
  9. package/{cloud.command-1qf95x67.js → cloud.command-dxfv18sa.js} +10 -10
  10. package/{context.command-hhjyjtp7.js → context.command-8d9e3k6b.js} +16 -15
  11. package/{guideline.command-vj956kfr.js → guideline.command-3jbkxzh6.js} +10 -10
  12. package/guidelines/componentRule/componentRule.instruction.md +2 -2
  13. package/guidelines/cssRule/cssRule.generate.json +1 -1
  14. package/guidelines/cssRule/cssRule.instruction.md +57 -34
  15. package/guidelines/framework/framework.instruction.md +1 -1
  16. package/guidelines/recipeRule/recipeRule.generate.json +31 -0
  17. package/guidelines/recipeRule/recipeRule.instruction.md +72 -0
  18. package/guidelines/ssrRule/ssrRule.generate.json +31 -0
  19. package/guidelines/ssrRule/ssrRule.instruction.md +108 -0
  20. package/incrementalBuilder.proc.js +6 -6
  21. package/{index-nsj2ftxj.js → index-0cj2zxbm.js} +3 -3
  22. package/{index-y1e79mz3.js → index-1pvex4az.js} +13 -13
  23. package/{index-h971erkm.js → index-2q1f5j9k.js} +3 -3
  24. package/{index-s9s0n5fz.js → index-7c6k4yvg.js} +2 -2
  25. package/{index-d0h48d2f.js → index-8hsbnyes.js} +1 -1
  26. package/{index-v9jqrdd1.js → index-a00jg8x8.js} +100 -2
  27. package/{index-dgeaqwmw.js → index-bbkek1bp.js} +1 -1
  28. package/{index-acwxfncv.js → index-bekddz9q.js} +4 -4
  29. package/{index-fbv96xaw.js → index-e16gnn26.js} +39 -16
  30. package/{index-z5v766nx.js → index-ec09pws3.js} +6 -6
  31. package/{index-zn9rh7sh.js → index-ej426mcr.js} +73 -24
  32. package/{index-xwfg9bam.js → index-hgna5rjy.js} +120 -4
  33. package/{index-z36qzgzp.js → index-k95xkh4p.js} +3 -4
  34. package/{index-7ppn9v4n.js → index-mr5s7mqd.js} +3 -3
  35. package/{index-6pz1j0zj.js → index-mxvakhsm.js} +6 -2
  36. package/{index-h6eav7zx.js → index-nm6wghb6.js} +6 -6
  37. package/{index-7k9sj7j5.js → index-pyt2b4nw.js} +212 -0
  38. package/{index-pfnh87f9.js → index-rzwcpcc3.js} +1 -1
  39. package/{index-zfp1gz9f.js → index-t3723f99.js} +1 -1
  40. package/{index-xftstr5s.js → index-tdbas9r0.js} +1 -1
  41. package/index-xjd3rqzj.js +129 -0
  42. package/index-xys926f2.js +136 -0
  43. package/{index-04as43rp.js → index-yzmhasdb.js} +16 -15
  44. package/{index-h5cvg9z9.js → index-zasydjxd.js} +4 -4
  45. package/index.js +19 -19
  46. package/{library.command-68ah07a3.js → library.command-ebdj1vnm.js} +4 -4
  47. package/{localRegistry.command-84dz000g.js → localRegistry.command-gb178ne8.js} +9 -9
  48. package/{module.command-t68xhamx.js → module.command-9c1bnsx4.js} +8 -8
  49. package/{package.command-00ncttab.js → package.command-t93p4jkj.js} +4 -4
  50. package/package.json +2 -3
  51. package/{page.command-1096s18n.js → page.command-k3k0meh2.js} +4 -4
  52. package/{primitive.command-02eqpx6t.js → primitive.command-de6nktdt.js} +9 -9
  53. package/{quality.command-r9cageqb.js → quality.command-z5kth2wa.js} +451 -48
  54. package/recipeScanner-tghkzrrt.js +12 -0
  55. package/{repair.command-7dynpnf1.js → repair.command-ehf2g112.js} +7 -6
  56. package/{scalar.command-3kxy3fkh.js → scalar.command-7a2jtv33.js} +7 -7
  57. package/templates/app/page/_index.tsx +55 -52
  58. package/templates/app/page/styles.css.template +12 -71
  59. package/templates/appSample/lib/task/Task.Template.tsx +2 -2
  60. package/templates/appSample/lib/task/Task.Unit.tsx +11 -11
  61. package/templates/appSample/lib/task/Task.Util.tsx +27 -28
  62. package/templates/appSample/lib/task/Task.View.tsx +22 -19
  63. package/templates/appSample/lib/task/Task.Zone.tsx +3 -3
  64. package/templates/appSample/page/task/[taskId]/_index.tsx +3 -3
  65. package/templates/appSample/page/task/[taskId]/edit.tsx +5 -5
  66. package/templates/appSample/page/task/_index.tsx +4 -4
  67. package/templates/appSample/page/task/_layout.tsx +1 -1
  68. package/templates/appSample/page/task/new.tsx +5 -5
  69. package/templates/appSample/ui/GlobalLoading.tsx +4 -4
  70. package/templates/appSample/ui/QuantityControl.tsx +5 -4
  71. package/templates/crudPages/[__model__Id]/page.tsx +1 -1
  72. package/templates/crudPages/page.tsx +1 -1
  73. package/templates/crudSinglePage/page.tsx +1 -1
  74. package/templates/module/__Model__.Template.tsx +2 -1
  75. package/templates/module/__Model__.Unit.tsx +2 -1
  76. package/templates/module/__Model__.Util.tsx +2 -1
  77. package/templates/module/__Model__.View.tsx +4 -3
  78. package/templates/module/__Model__.Zone.tsx +2 -1
  79. package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +1 -0
  80. package/templates/workspaceRoot/.cursor/rules/ssr-first.mdc.template +59 -0
  81. package/templates/workspaceRoot/AGENTS.md.template +139 -20
  82. package/templates/workspaceRoot/biome.json.template +1 -1
  83. package/{workflow.command-yy0d9gk3.js → workflow.command-e0tt0vk2.js} +15 -14
  84. package/{workspace.command-stv9g4a0.js → workspace.command-fpda0evj.js} +87 -27
@@ -4,7 +4,8 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
4
4
  return {
5
5
  filename: "Task.View.tsx",
6
6
  content: `import { type cnst, usePage } from "@apps/${dict.appName}/client";
7
- import { clsx } from "akanjs/client";
7
+ import { cn } from "akanjs/client";
8
+ import { badgeRecipe } from "akanjs/ui";
8
9
 
9
10
  // ===== Task.View.tsx =====
10
11
  // Convention: lib/<module>/ — PascalCase .tsx, View suffix = detail display component.
@@ -20,25 +21,25 @@ interface GeneralProps {
20
21
  export const General = ({ className, task }: GeneralProps) => {
21
22
  const { l } = usePage();
22
23
  const statusColor = {
23
- todo: "text-base-content/50",
24
+ todo: "text-foreground/50",
24
25
  inProgress: "text-primary",
25
26
  completed: "text-success",
26
27
  }[task.status];
27
28
 
28
29
  return (
29
- <div className={clsx("flex w-full flex-col gap-4", className)}>
30
+ <div className={cn("flex w-full flex-col gap-4", className)}>
30
31
  <div>
31
- <h1 className="font-bold text-2xl text-base-content">{task.title}</h1>
32
- <div className={clsx("mt-1 font-medium text-sm", statusColor)}>{l(\`taskStatus.\${task.status}\`)}</div>
32
+ <h1 className="font-bold text-2xl text-foreground">{task.title}</h1>
33
+ <div className={cn("mt-1 font-medium text-sm", statusColor)}>{l(\`taskStatus.\${task.status}\`)}</div>
33
34
  </div>
34
35
 
35
36
  {task.content && (
36
- <div className="rounded-lg border border-base-content/10 bg-base-100 p-4">
37
- <p className="whitespace-pre-wrap text-base-content/80 text-sm">{task.content}</p>
37
+ <div className="rounded-lg border border-foreground/10 bg-background p-4">
38
+ <p className="whitespace-pre-wrap text-foreground/80 text-sm">{task.content}</p>
38
39
  </div>
39
40
  )}
40
41
 
41
- <div className="flex items-center gap-4 text-base-content/60 text-sm">
42
+ <div className="flex items-center gap-4 text-foreground/60 text-sm">
42
43
  <div>
43
44
  <span className="font-medium">{l("task.taskDueLabel")} </span>
44
45
  {task.due ? task.due.toDate().toLocaleDateString() : l("task.taskNoDue")}
@@ -46,23 +47,25 @@ export const General = ({ className, task }: GeneralProps) => {
46
47
  </div>
47
48
 
48
49
  {task.workHistory && task.workHistory.length > 0 && (
49
- <div className="mt-2 border-base-content/10 border-t pt-4">
50
- <h3 className="mb-3 font-semibold text-base-content text-sm">{l("task.taskWorkHistoryTitle")}</h3>
50
+ <div className="mt-2 border-foreground/10 border-t pt-4">
51
+ <h3 className="mb-3 font-semibold text-foreground text-sm">{l("task.taskWorkHistoryTitle")}</h3>
51
52
  <ul className="space-y-2">
52
53
  {task.workHistory.map((entry, i) => (
53
54
  <li key={i} className="flex items-start gap-3 text-sm">
54
55
  <span
55
- className={clsx("badge badge-xs mt-0.5 shrink-0", {
56
- "badge-ghost": entry.action === "created",
57
- "badge-primary": entry.action === "started",
58
- "badge-success": entry.action === "completed",
59
- })}
56
+ className={cn(
57
+ badgeRecipe({
58
+ variant:
59
+ entry.action === "started" ? "primary" : entry.action === "completed" ? "success" : "default",
60
+ }),
61
+ "mt-0.5 shrink-0",
62
+ )}
60
63
  >
61
64
  {l(\`workHistoryAction.\${entry.action}\`)}
62
65
  </span>
63
66
  <div>
64
- <span className="text-base-content/60">{entry.at.toDate().toLocaleString()}</span>
65
- {entry.note && <span className="ml-2 text-base-content/50">{entry.note}</span>}
67
+ <span className="text-foreground/60">{entry.at.toDate().toLocaleString()}</span>
68
+ {entry.note && <span className="ml-2 text-foreground/50">{entry.note}</span>}
66
69
  </div>
67
70
  </li>
68
71
  ))}
@@ -76,9 +79,9 @@ export const General = ({ className, task }: GeneralProps) => {
76
79
  // ---- Expandable additional fields: ----
77
80
  // Compact: for narrow spaces like sidebars or modals
78
81
  // export const Compact = ({ className, task }: CompactProps) => (
79
- // <div className={clsx("text-sm", className)}>
82
+ // <div className={cn("text-sm", className)}>
80
83
  // <span className="font-medium">{task.title}</span>
81
- // {task.due && <span className="text-base-content/50 ml-2">Due: {new Date(task.due).toLocaleDateString()}</span>}
84
+ // {task.due && <span className="text-foreground/50 ml-2">Due: {new Date(task.due).toLocaleDateString()}</span>}
82
85
  // </div>
83
86
  // );
84
87
  `,
@@ -6,7 +6,7 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
6
6
  content: `"use client";
7
7
  import { type cnst, Task, usePage } from "@apps/${dict.appName}/client";
8
8
  import type { ClientInit, ClientView } from "akanjs/fetch";
9
- import { Link, Load } from "akanjs/ui";
9
+ import { Link, Load, buttonRecipe } from "akanjs/ui";
10
10
 
11
11
  // ===== Task.Zone.tsx =====
12
12
  // Convention: lib/<module>/ — PascalCase .tsx, Zone suffix = composition layer between pages and UI.
@@ -28,9 +28,9 @@ export const Card = ({ className, init }: CardProps) => {
28
28
  init={init}
29
29
  renderEmpty={() => (
30
30
  <div className="flex flex-col items-center gap-4 py-16 text-center">
31
- <div className="text-base-content/40 text-lg">{l("task.taskNoTasks")}</div>
31
+ <div className="text-foreground/40 text-lg">{l("task.taskNoTasks")}</div>
32
32
  <Link href="/task/new">
33
- <button className="btn btn-primary btn-sm">{l("task.taskCreateFirst")}</button>
33
+ <button className={buttonRecipe({ variant: "primary", size: "sm" })}>{l("task.taskCreateFirst")}</button>
34
34
  </Link>
35
35
  </div>
36
36
  )}
@@ -6,7 +6,7 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
6
6
  content: `
7
7
  import { fetch, usePage } from "@apps/${dict.appName}/client";
8
8
  import { Task } from "@apps/${dict.appName}/lib/task";
9
- import { Link } from "akanjs/ui";
9
+ import { Link, buttonRecipe } from "akanjs/ui";
10
10
 
11
11
  // ===== page/task/[taskId]/_index.tsx =====
12
12
  // Convention: Akan.js file-based routing with dynamic segments.
@@ -22,10 +22,10 @@ export default async function Page({ params: { taskId } }: PageProps) {
22
22
  return (
23
23
  <main className="mx-auto max-w-4xl px-6 py-8">
24
24
  <div className="mb-6 flex items-center justify-between">
25
- <a href="/task" className="btn btn-ghost btn-sm">
25
+ <a href="/task" className={buttonRecipe({ variant: "ghost", size: "sm" })}>
26
26
  {l("task.taskBackToTasks")}
27
27
  </a>
28
- <Link href={\`/task/\${taskId}/edit\`} className="btn btn-primary btn-sm">
28
+ <Link href={\`/task/\${taskId}/edit\`} className={buttonRecipe({ variant: "primary", size: "sm" })}>
29
29
  {l("task.taskEdit")}
30
30
  </Link>
31
31
  </div>
@@ -4,7 +4,7 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
4
4
  return {
5
5
  filename: "edit.tsx",
6
6
  content: `import { fetch, Task } from "@apps/${dict.appName}/client";
7
- import { Load, Link } from "akanjs/ui";
7
+ import { Load, Link, buttonRecipe } from "akanjs/ui";
8
8
 
9
9
  // ===== page/task/[taskId]/edit.tsx =====
10
10
  // Convention: Server-side edit form page using Load.Edit from akanjs/ui.
@@ -21,15 +21,15 @@ export default async function Page({ params: { taskId } }: PageProps) {
21
21
  return (
22
22
  <main className="mx-auto max-w-2xl px-6 py-8">
23
23
  <div className="mb-6">
24
- <Link href={\`/task/\${taskId}\`} className="btn btn-ghost btn-sm">
24
+ <Link href={\`/task/\${taskId}\`} className={buttonRecipe({ variant: "ghost", size: "sm" })}>
25
25
  ← Back to Task
26
26
  </Link>
27
27
  </div>
28
28
  <div className="mb-6">
29
- <h1 className="font-extrabold text-3xl text-base-content">Edit Task</h1>
30
- <p className="mt-1 text-base-content/60 text-sm">Update the task details</p>
29
+ <h1 className="font-extrabold text-3xl text-foreground">Edit Task</h1>
30
+ <p className="mt-1 text-foreground/60 text-sm">Update the task details</p>
31
31
  </div>
32
- <div className="rounded-xl border border-base-content/10 bg-base-100 p-6 shadow-sm">
32
+ <div className="rounded-xl border border-foreground/10 bg-background p-6 shadow-sm">
33
33
  <Load.Edit slice={fetch.slice.taskInPublic} edit={taskEdit} type="form" onSubmit={\`/task/\${taskId}\`}>
34
34
  <Task.Template.General />
35
35
  </Load.Edit>
@@ -4,7 +4,7 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
4
4
  return {
5
5
  filename: "_index.tsx",
6
6
  content: `import { fetch, Task, usePage } from "@apps/${dict.appName}/client";
7
- import { Link } from "akanjs/ui";
7
+ import { Link, buttonRecipe } from "akanjs/ui";
8
8
 
9
9
  // ===== page/task/_index.tsx =====
10
10
  // Convention: Akan.js file-based routing — _index.tsx is the index page for /task.
@@ -18,10 +18,10 @@ export default async function Page() {
18
18
  <main className="mx-auto max-w-4xl px-6 py-8">
19
19
  <div className="mb-6 flex items-center justify-between">
20
20
  <div>
21
- <h1 className="font-extrabold text-3xl text-base-content">{l("task.modelName")}</h1>
22
- <p className="mt-1 text-base-content/60 text-sm">{l("task.modelDesc")}</p>
21
+ <h1 className="font-extrabold text-3xl text-foreground">{l("task.modelName")}</h1>
22
+ <p className="mt-1 text-foreground/60 text-sm">{l("task.modelDesc")}</p>
23
23
  </div>
24
- <Link href="/task/new" className="btn btn-primary btn-sm">
24
+ <Link href="/task/new" className={buttonRecipe({ variant: "primary", size: "sm" })}>
25
25
  {l("task.taskNew")}
26
26
  </Link>
27
27
  </div>
@@ -14,7 +14,7 @@ interface LayoutProps {
14
14
  }
15
15
  export default function Layout({ children }: LayoutProps) {
16
16
  return (
17
- <div className="min-h-screen bg-base-200">
17
+ <div className="min-h-screen bg-muted">
18
18
  {children}
19
19
  </div>
20
20
  );
@@ -4,7 +4,7 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
4
4
  return {
5
5
  filename: "new.tsx",
6
6
  content: `import { type cnst, fetch, Task, usePage } from "@apps/${dict.appName}/client";
7
- import { Load, Link } from "akanjs/ui";
7
+ import { Load, Link, buttonRecipe } from "akanjs/ui";
8
8
 
9
9
  // ===== page/task/new.tsx =====
10
10
  // Convention: Server-side form page using Load.Edit from akanjs/ui.
@@ -19,15 +19,15 @@ export default async function Page() {
19
19
  return (
20
20
  <main className="mx-auto max-w-2xl px-6 py-8">
21
21
  <div className="mb-6">
22
- <Link href="/task" className="btn btn-ghost btn-sm">
22
+ <Link href="/task" className={buttonRecipe({ variant: "ghost", size: "sm" })}>
23
23
  ← Tasks
24
24
  </Link>
25
25
  </div>
26
26
  <div className="mb-6">
27
- <h1 className="font-extrabold text-3xl text-base-content">New Task</h1>
28
- <p className="mt-1 text-base-content/60 text-sm">Create a new task with title and description</p>
27
+ <h1 className="font-extrabold text-3xl text-foreground">New Task</h1>
28
+ <p className="mt-1 text-foreground/60 text-sm">Create a new task with title and description</p>
29
29
  </div>
30
- <div className="rounded-xl border border-base-content/10 bg-base-100 p-6 shadow-sm">
30
+ <div className="rounded-xl border border-foreground/10 bg-background p-6 shadow-sm">
31
31
  <Load.Edit slice={fetch.slice.taskInPublic} edit={taskForm} type="form" onCancel="back" onSubmit="/task">
32
32
  <Task.Template.General />
33
33
  </Load.Edit>
@@ -5,7 +5,7 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
5
5
  filename: "GlobalLoading.tsx",
6
6
  content: `"use client";
7
7
 
8
- import { clsx } from "akanjs/client";
8
+ import { cn } from "akanjs/client";
9
9
 
10
10
  // ===== GlobalLoading.tsx =====
11
11
  // Convention: ui/ folder — reusable visual components. PascalCase .tsx, "use client" directive.
@@ -19,9 +19,9 @@ interface GlobalLoadingProps {
19
19
 
20
20
  export const GlobalLoading = ({ className, message = "Loading..." }: GlobalLoadingProps) => {
21
21
  return (
22
- <div className={clsx("flex flex-col items-center justify-center gap-4 py-32", className)}>
23
- <span className="loading loading-spinner loading-lg text-primary" />
24
- <span className="text-base-content/60 text-sm">{message}</span>
22
+ <div className={cn("flex flex-col items-center justify-center gap-4 py-32", className)}>
23
+ <span className="inline-block size-8 animate-spin rounded-full border-2 border-primary border-t-transparent" />
24
+ <span className="text-foreground/60 text-sm">{message}</span>
25
25
  </div>
26
26
  );
27
27
  };
@@ -5,7 +5,8 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
5
5
  filename: "QuantityControl.tsx",
6
6
  content: `"use client";
7
7
 
8
- import { clsx } from "akanjs/client";
8
+ import { cn } from "akanjs/client";
9
+ import { buttonRecipe } from "akanjs/ui";
9
10
 
10
11
  // ===== QuantityControl.tsx =====
11
12
  // Convention: ui/ folder — reusable visual components. PascalCase .tsx, "use client" directive.
@@ -28,9 +29,9 @@ export const QuantityControl = ({
28
29
  max = 99,
29
30
  }: QuantityControlProps) => {
30
31
  return (
31
- <div className={clsx("inline-flex items-center gap-1", className)}>
32
+ <div className={cn("inline-flex items-center gap-1", className)}>
32
33
  <button
33
- className="btn btn-circle btn-outline btn-xs"
34
+ className={buttonRecipe({ variant: "outline", size: "icon" }, "size-6 rounded-full")}
34
35
  disabled={value <= min}
35
36
  onClick={() => onChange(Math.max(min, value - 1))}
36
37
  >
@@ -38,7 +39,7 @@ export const QuantityControl = ({
38
39
  </button>
39
40
  <span className="w-8 text-center font-medium tabular-nums">{value}</span>
40
41
  <button
41
- className="btn btn-circle btn-outline btn-xs"
42
+ className={buttonRecipe({ variant: "outline", size: "icon" }, "size-6 rounded-full")}
42
43
  disabled={value >= max}
43
44
  onClick={() => onChange(Math.min(max, value + 1))}
44
45
  >
@@ -38,7 +38,7 @@ export default async function Page({ params }: PageProps) {
38
38
  <div className="flex gap-4 font-bold text-lg items-center">
39
39
  <${dict.Model}.Zone.View view={${dict.model}View} />
40
40
  <Link href={\`/${dict.model}/\${${dict.model}.id}/edit\`}>
41
- <button className="btn">
41
+ <button className="inline-flex items-center justify-center gap-2 rounded-field bg-primary px-4 py-2 font-medium text-primary-foreground text-sm transition-colors hover:bg-primary/90">
42
42
  {l("base.updateModel", { model: l("${dict.model}.modelName") })}
43
43
  </button>
44
44
  </Link>
@@ -23,7 +23,7 @@ export default async function Page() {
23
23
  <div className="animate-fadeIn px-4 pt-4 flex gap-4 items-center">
24
24
  <div className="font-bold text-lg md:text-4xl">${dict.Model}s</div>
25
25
  <Link href={\`/${dict.model}/new\`}>
26
- <button className="btn">+ {l("base.createModel", { model: l("${dict.model}.modelName") })}</button>
26
+ <button className="inline-flex items-center justify-center gap-2 rounded-field bg-primary px-4 py-2 font-medium text-primary-foreground text-sm transition-colors hover:bg-primary/90">+ {l("base.createModel", { model: l("${dict.model}.modelName") })}</button>
27
27
  </Link>
28
28
  </div>
29
29
  <div>{l("${dict.model}.modelDesc")}</div>
@@ -20,7 +20,7 @@ export default async function Page() {
20
20
  <>
21
21
  <div className="animate-fadeIn flex items-center gap-4 px-4 pt-4">
22
22
  <div className="text-lg font-bold md:text-4xl">{l("${dict.model}.modelName")}</div>
23
- <Model.New className="btn btn-ghost" slice={fetch.slice["${dict.model}InPublic"]} renderTitle="id">
23
+ <Model.New className="inline-flex items-center gap-2 rounded-field px-3 py-1.5 text-sm transition-colors hover:bg-muted" slice={fetch.slice["${dict.model}InPublic"]} renderTitle="id">
24
24
  <${dict.Model}.Template.General />
25
25
  </Model.New>
26
26
  </div>
@@ -4,6 +4,7 @@ interface Dict {
4
4
  Model: string;
5
5
  model: string;
6
6
  sysName: string;
7
+ sysType: string;
7
8
  }
8
9
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dict) {
9
10
  return {
@@ -11,7 +12,7 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
11
12
  content: `
12
13
  "use client";
13
14
  import { Field, Layout } from "akanjs/ui";
14
- import { st, usePage } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
15
+ import { st, usePage } from "@${dict.sysType}s/${dict.sysName}/client";
15
16
 
16
17
  interface GeneralProps {
17
18
  className?: string;
@@ -4,13 +4,14 @@ interface Dict {
4
4
  Model: string;
5
5
  model: string;
6
6
  sysName: string;
7
+ sysType: string;
7
8
  }
8
9
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dict) {
9
10
  return {
10
11
  filename: `${dict.Model}.Unit.tsx`,
11
12
  content: `
12
13
  import type { ModelProps } from "akanjs/client";
13
- import { cnst, usePage } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
14
+ import { cnst, usePage } from "@${dict.sysType}s/${dict.sysName}/client";
14
15
  import { Link } from "akanjs/ui";
15
16
 
16
17
  export const Card = ({ ${dict.model}, href }: ModelProps<"${dict.model}", cnst.Light${dict.Model}>) => {
@@ -4,6 +4,7 @@ interface Dict {
4
4
  Model: string;
5
5
  model: string;
6
6
  sysName: string;
7
+ sysType: string;
7
8
  }
8
9
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dict) {
9
10
  return {
@@ -11,7 +12,7 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
11
12
  content: `
12
13
  "use client";
13
14
  import { Model } from "akanjs/ui";
14
- import { fetch, usePage } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
15
+ import { fetch, usePage } from "@${dict.sysType}s/${dict.sysName}/client";
15
16
  import { BiTrash } from "react-icons/bi";
16
17
 
17
18
  interface RemoveProps {
@@ -4,13 +4,14 @@ interface Dict {
4
4
  Model: string;
5
5
  model: string;
6
6
  sysName: string;
7
+ sysType: string;
7
8
  }
8
9
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dict) {
9
10
  return {
10
11
  filename: `${dict.Model}.View.tsx`,
11
12
  content: `
12
- import { clsx } from "akanjs/client";
13
- import { cnst, usePage } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
13
+ import { cn } from "akanjs/client";
14
+ import { cnst, usePage } from "@${dict.sysType}s/${dict.sysName}/client";
14
15
 
15
16
  interface GeneralProps {
16
17
  className?: string;
@@ -19,7 +20,7 @@ interface GeneralProps {
19
20
  export const General = ({ className, ${dict.model} }: GeneralProps) => {
20
21
  const { l } = usePage();
21
22
  return (
22
- <div className={clsx("w-full", className)}>
23
+ <div className={cn("w-full", className)}>
23
24
  <div>{l("${dict.model}.name")}: {${dict.model}.name}</div>
24
25
  </div>
25
26
  );
@@ -4,6 +4,7 @@ interface Dict {
4
4
  Model: string;
5
5
  model: string;
6
6
  sysName: string;
7
+ sysType: string;
7
8
  }
8
9
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dict) {
9
10
  return {
@@ -13,7 +14,7 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
13
14
  import { Load } from "akanjs/ui";
14
15
  // Alias the domain namespace so the Card/View exports below never collide with the model name
15
16
  // (a model literally named "card" or "view" would otherwise shadow this import).
16
- import { type cnst, ${dict.Model} as ${dict.Model}Domain } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
17
+ import { type cnst, ${dict.Model} as ${dict.Model}Domain } from "@${dict.sysType}s/${dict.sysName}/client";
17
18
  import type { ClientInit, ClientView, SliceMeta } from "akanjs/fetch";
18
19
 
19
20
  interface CardProps {
@@ -15,3 +15,4 @@ alwaysApply: false
15
15
  - In domain UI the boundary is mechanical, not a judgment call: `Template`, `Zone`, and `Util` are always client components with `"use client"` on line 1; `Unit` and `View` are always server components and never carry the directive. `usePage()` is still legal in server files.
16
16
  - Preserve established domain file roles such as `.document.ts`, `.service.ts`, `.store.ts`, `.constant.ts`, and `.client.ts`.
17
17
  - When unsure, inspect nearby files in the same app or package before introducing a new boundary pattern.
18
+ - The boundary is only half the rule: how *little* ends up on the client is the other half. See `ssr-first.mdc`.
@@ -0,0 +1,59 @@
1
+ ---
2
+ description: SSR-first rendering rules — keep JSX on the server and justify every "use client"
3
+ globs: apps/**/*.tsx,libs/**/*.tsx
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # SSR First — Server Rendering Is The Default
8
+
9
+ Akan is SSR-first. A JSX element rendered on the server ships as HTML and costs nothing to hydrate; an element
10
+ behind `"use client"` ships twice — as markup and as bundled JS that re-runs in the browser. The boundary question
11
+ is not which file *may* be client, it is **how little** ends up on the client side.
12
+
13
+ - **The default is server. `"use client"` is a cost you justify per component, not a habit.** A component earns the
14
+ directive only by using a client-only capability: a React hook, a JSX event handler, the store (`st.use.*` /
15
+ `st.do.*`), a browser global, or a client-only third-party package. Rendering markup, mapping over data, reading a
16
+ route param, and calling `l()` are server work — `usePage()` and `l()` never force a boundary.
17
+ - The file role decides *where* the boundary sits (`Template`/`Zone`/`Util` are client, `Unit`/`View` are server);
18
+ it does not decide how much markup sits behind it. A `Zone` that hoards markup is still an SSR regression.
19
+ - Measure with `akan quality ssr` (`--format json` for tooling). It reports the server render share per app and lib.
20
+ Treat **50% server share as the floor** and a falling share as a regression to justify or revert.
21
+
22
+ ## What `akan quality ssr` Flags
23
+
24
+ - `akan.ssr.unnecessary-use-client` — the directive is there but nothing in the file needs it. Delete it.
25
+ - `akan.ssr.client-static-component` — a component in a client file renders real markup with zero client-only
26
+ capability: pure server work sitting in the bundle.
27
+ - `akan.ssr.client-static-markup` — a large subtree wraps one or two interactive touches. Split it.
28
+ - `akan.ssr.client-mount-load` — a `useEffect(…, [])` loads server data the page could fetch first.
29
+ - `akan.ssr.module-missing-server-view` — a module renders only from `Template`/`Zone`/`Util`, with no `Unit`/`View`.
30
+ - `akan.ssr.template-client-state` — a `Template` holds form state in `useState` instead of the store.
31
+
32
+ A third-party client package and the `ui/<Folder>/index_.tsx` + `lazy()` boundary both justify the directive and are
33
+ not flagged. Interaction-driven `fetch.*` inside an `onClick` is not flagged either — only mount-time loads are.
34
+
35
+ ## Server-Side Implementation Playbook
36
+
37
+ 1. **Wrap the interaction, not the UI.** The smallest useful client component adds one behaviour and renders
38
+ `children` untouched, so the markup inside never reaches the bundle:
39
+ `export const ClickWrapper = ({ children, onPick }: ClickWrapperProps) => <div onClick={onPick}>{children}</div>;`
40
+ 2. **Split compound components so panels stay on the server.** A tab or accordion needs client state only for
41
+ *which* part is visible. `Tab` / `Tab.Menus` / `Tab.Menu` / `Tab.Panel` from `akanjs/ui` is the shape: only the
42
+ provider and menu hold state, and `<Tab.Panel>` renders children as-is, so a server `Unit`/`View` passed in stays
43
+ server-rendered. Never one client file with a mode `useState` and every panel body inlined.
44
+ 3. **Sync state instead of fetching it.** The page calls `fetch.initTaskInTodo()` / `fetch.viewTask(id)` and passes
45
+ the serializable result into a `Zone`; `Load.Units` / `Load.View` hydrate the store from it. Never a
46
+ `useEffect(…, [])` that fetches on mount.
47
+ 4. **Push the boundary down to the leaf that needs it.** A store-reading `Zone` holds zero markup and delegates to a
48
+ server `View`: `export const Self = () => <Task.View.General task={st.use.task()} />;`
49
+ 5. **Hand the promise across, not the awaited value.** `ClientInit` / `ClientView` are `PromiseOrObject<T>`, so a
50
+ page may pass an unawaited `fetch.initTaskInTodo()` and `Load.*` resolves it behind a skeleton.
51
+ 6. **Use named `ReactNode` slots, not just `children`.** `Layout.Navbar` takes `title`, `back`, `left`, `right`, and
52
+ `children`, so a client shell composes server content in five places.
53
+ 7. **Let the server do the derived work.** Display and predicate logic lives on `LightTask` (`isNew()`,
54
+ `canWrite(user?)`); enum→class lookups live in a module-scope `as const` map.
55
+ 8. **Gate auth on the server.** Check the session in `_layout.tsx` and redirect there, before any HTML is sent.
56
+ 9. **Prefer CSS over client state for pure visibility.** A `data-*` attribute plus `group-data-[…]` variants, or
57
+ `<details>`/`<summary>`, keeps both branches server-rendered.
58
+ 10. **Keep the heavy island out of the first load.** Put a large client-only widget behind the
59
+ `ui/<Folder>/index_.tsx` + `lazy()` pair.