@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.
- package/.build-stamp +1 -1
- package/{abstractCompactor-ghh1q9an.js → abstractCompactor-e5yq60qz.js} +4 -4
- package/{agent.command-de4r6vk8.js → agent.command-yv00p0s0.js} +8 -7
- package/{application.command-nfsfm96y.js → application.command-0qsczf4k.js} +6 -6
- package/{applicationBuildRunner-554a2vnk.js → applicationBuildRunner-ct2pxyvw.js} +3 -2
- package/{applicationReleasePackager-pjbxc793.js → applicationReleasePackager-gg8ccbeh.js} +3 -3
- package/buildBatch.proc.js +8 -7
- package/{capacitorApp-yg9b4ajz.js → capacitorApp-4r9sm3qg.js} +3 -3
- package/{cloud.command-1qf95x67.js → cloud.command-dxfv18sa.js} +10 -10
- package/{context.command-hhjyjtp7.js → context.command-8d9e3k6b.js} +16 -15
- package/{guideline.command-vj956kfr.js → guideline.command-3jbkxzh6.js} +10 -10
- package/guidelines/componentRule/componentRule.instruction.md +2 -2
- package/guidelines/cssRule/cssRule.generate.json +1 -1
- package/guidelines/cssRule/cssRule.instruction.md +57 -34
- package/guidelines/framework/framework.instruction.md +1 -1
- package/guidelines/recipeRule/recipeRule.generate.json +31 -0
- package/guidelines/recipeRule/recipeRule.instruction.md +72 -0
- package/guidelines/ssrRule/ssrRule.generate.json +31 -0
- package/guidelines/ssrRule/ssrRule.instruction.md +108 -0
- package/incrementalBuilder.proc.js +6 -6
- package/{index-nsj2ftxj.js → index-0cj2zxbm.js} +3 -3
- package/{index-y1e79mz3.js → index-1pvex4az.js} +13 -13
- package/{index-h971erkm.js → index-2q1f5j9k.js} +3 -3
- package/{index-s9s0n5fz.js → index-7c6k4yvg.js} +2 -2
- package/{index-d0h48d2f.js → index-8hsbnyes.js} +1 -1
- package/{index-v9jqrdd1.js → index-a00jg8x8.js} +100 -2
- package/{index-dgeaqwmw.js → index-bbkek1bp.js} +1 -1
- package/{index-acwxfncv.js → index-bekddz9q.js} +4 -4
- package/{index-fbv96xaw.js → index-e16gnn26.js} +39 -16
- package/{index-z5v766nx.js → index-ec09pws3.js} +6 -6
- package/{index-zn9rh7sh.js → index-ej426mcr.js} +73 -24
- package/{index-xwfg9bam.js → index-hgna5rjy.js} +120 -4
- package/{index-z36qzgzp.js → index-k95xkh4p.js} +3 -4
- package/{index-7ppn9v4n.js → index-mr5s7mqd.js} +3 -3
- package/{index-6pz1j0zj.js → index-mxvakhsm.js} +6 -2
- package/{index-h6eav7zx.js → index-nm6wghb6.js} +6 -6
- package/{index-7k9sj7j5.js → index-pyt2b4nw.js} +212 -0
- package/{index-pfnh87f9.js → index-rzwcpcc3.js} +1 -1
- package/{index-zfp1gz9f.js → index-t3723f99.js} +1 -1
- package/{index-xftstr5s.js → index-tdbas9r0.js} +1 -1
- package/index-xjd3rqzj.js +129 -0
- package/index-xys926f2.js +136 -0
- package/{index-04as43rp.js → index-yzmhasdb.js} +16 -15
- package/{index-h5cvg9z9.js → index-zasydjxd.js} +4 -4
- package/index.js +19 -19
- package/{library.command-68ah07a3.js → library.command-ebdj1vnm.js} +4 -4
- package/{localRegistry.command-84dz000g.js → localRegistry.command-gb178ne8.js} +9 -9
- package/{module.command-t68xhamx.js → module.command-9c1bnsx4.js} +8 -8
- package/{package.command-00ncttab.js → package.command-t93p4jkj.js} +4 -4
- package/package.json +2 -3
- package/{page.command-1096s18n.js → page.command-k3k0meh2.js} +4 -4
- package/{primitive.command-02eqpx6t.js → primitive.command-de6nktdt.js} +9 -9
- package/{quality.command-r9cageqb.js → quality.command-z5kth2wa.js} +451 -48
- package/recipeScanner-tghkzrrt.js +12 -0
- package/{repair.command-7dynpnf1.js → repair.command-ehf2g112.js} +7 -6
- package/{scalar.command-3kxy3fkh.js → scalar.command-7a2jtv33.js} +7 -7
- package/templates/app/page/_index.tsx +55 -52
- package/templates/app/page/styles.css.template +12 -71
- package/templates/appSample/lib/task/Task.Template.tsx +2 -2
- package/templates/appSample/lib/task/Task.Unit.tsx +11 -11
- package/templates/appSample/lib/task/Task.Util.tsx +27 -28
- package/templates/appSample/lib/task/Task.View.tsx +22 -19
- package/templates/appSample/lib/task/Task.Zone.tsx +3 -3
- package/templates/appSample/page/task/[taskId]/_index.tsx +3 -3
- package/templates/appSample/page/task/[taskId]/edit.tsx +5 -5
- package/templates/appSample/page/task/_index.tsx +4 -4
- package/templates/appSample/page/task/_layout.tsx +1 -1
- package/templates/appSample/page/task/new.tsx +5 -5
- package/templates/appSample/ui/GlobalLoading.tsx +4 -4
- package/templates/appSample/ui/QuantityControl.tsx +5 -4
- package/templates/crudPages/[__model__Id]/page.tsx +1 -1
- package/templates/crudPages/page.tsx +1 -1
- package/templates/crudSinglePage/page.tsx +1 -1
- package/templates/module/__Model__.Template.tsx +2 -1
- package/templates/module/__Model__.Unit.tsx +2 -1
- package/templates/module/__Model__.Util.tsx +2 -1
- package/templates/module/__Model__.View.tsx +4 -3
- package/templates/module/__Model__.Zone.tsx +2 -1
- package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +1 -0
- package/templates/workspaceRoot/.cursor/rules/ssr-first.mdc.template +59 -0
- package/templates/workspaceRoot/AGENTS.md.template +139 -20
- package/templates/workspaceRoot/biome.json.template +1 -1
- package/{workflow.command-yy0d9gk3.js → workflow.command-e0tt0vk2.js} +15 -14
- 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 {
|
|
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-
|
|
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={
|
|
30
|
+
<div className={cn("flex w-full flex-col gap-4", className)}>
|
|
30
31
|
<div>
|
|
31
|
-
<h1 className="font-bold text-2xl text-
|
|
32
|
-
<div className={
|
|
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-
|
|
37
|
-
<p className="whitespace-pre-wrap text-
|
|
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-
|
|
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-
|
|
50
|
-
<h3 className="mb-3 font-semibold text-
|
|
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={
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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-
|
|
65
|
-
{entry.note && <span className="ml-2 text-
|
|
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={
|
|
82
|
+
// <div className={cn("text-sm", className)}>
|
|
80
83
|
// <span className="font-medium">{task.title}</span>
|
|
81
|
-
// {task.due && <span className="text-
|
|
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-
|
|
31
|
+
<div className="text-foreground/40 text-lg">{l("task.taskNoTasks")}</div>
|
|
32
32
|
<Link href="/task/new">
|
|
33
|
-
<button className="
|
|
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="
|
|
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="
|
|
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="
|
|
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-
|
|
30
|
-
<p className="mt-1 text-
|
|
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-
|
|
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-
|
|
22
|
-
<p className="mt-1 text-
|
|
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="
|
|
24
|
+
<Link href="/task/new" className={buttonRecipe({ variant: "primary", size: "sm" })}>
|
|
25
25
|
{l("task.taskNew")}
|
|
26
26
|
</Link>
|
|
27
27
|
</div>
|
|
@@ -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="
|
|
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-
|
|
28
|
-
<p className="mt-1 text-
|
|
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-
|
|
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 {
|
|
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={
|
|
23
|
-
<span className="
|
|
24
|
-
<span className="text-
|
|
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 {
|
|
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={
|
|
32
|
+
<div className={cn("inline-flex items-center gap-1", className)}>
|
|
32
33
|
<button
|
|
33
|
-
className="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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 "@${
|
|
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 "@${
|
|
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 "@${
|
|
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 {
|
|
13
|
-
import { cnst, usePage } from "@${
|
|
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={
|
|
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 "@${
|
|
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.
|