@akanjs/cli 2.3.1-rc.3 → 2.3.1-rc.5

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 (37) hide show
  1. package/index.js +9 -2
  2. package/package.json +2 -2
  3. package/templates/appSample/common/formatters.ts +32 -0
  4. package/templates/appSample/common/validators.ts +32 -0
  5. package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +47 -0
  6. package/templates/appSample/lib/__scalar/workHistory/workHistory.constant.ts +35 -0
  7. package/templates/appSample/lib/__scalar/workHistory/workHistory.dictionary.ts +25 -0
  8. package/templates/appSample/lib/__scalar/workHistory/workHistory.document.ts +10 -0
  9. package/templates/appSample/lib/_noti/noti.abstract.ts +38 -0
  10. package/templates/appSample/lib/_noti/noti.dictionary.ts +23 -0
  11. package/templates/appSample/lib/_noti/noti.service.ts +27 -0
  12. package/templates/appSample/lib/_noti/noti.signal.ts +26 -0
  13. package/templates/appSample/lib/_noti/noti.store.ts +35 -0
  14. package/templates/appSample/lib/task/Task.Template.tsx +45 -0
  15. package/templates/appSample/lib/task/Task.Unit.tsx +65 -0
  16. package/templates/appSample/lib/task/Task.Util.tsx +105 -0
  17. package/templates/appSample/lib/task/Task.View.tsx +86 -0
  18. package/templates/appSample/lib/task/Task.Zone.tsx +54 -0
  19. package/templates/appSample/lib/task/task.abstract.ts +40 -0
  20. package/templates/appSample/lib/task/task.constant.ts +53 -0
  21. package/templates/appSample/lib/task/task.dictionary.ts +77 -0
  22. package/templates/appSample/lib/task/task.document.ts +51 -0
  23. package/templates/appSample/lib/task/task.service.ts +44 -0
  24. package/templates/appSample/lib/task/task.signal.ts +54 -0
  25. package/templates/appSample/lib/task/task.store.ts +42 -0
  26. package/templates/appSample/page/task/[taskId]/_index.tsx +39 -0
  27. package/templates/appSample/page/task/[taskId]/edit.tsx +28 -0
  28. package/templates/appSample/page/task/_index.tsx +34 -0
  29. package/templates/appSample/page/task/_layout.tsx +24 -0
  30. package/templates/appSample/page/task/new.tsx +26 -0
  31. package/templates/appSample/srvkit/AuthGuard.ts +35 -0
  32. package/templates/appSample/srvkit/SessionInternalArg.ts +32 -0
  33. package/templates/appSample/ui/GlobalLoading.tsx +30 -0
  34. package/templates/appSample/ui/QuantityControl.tsx +52 -0
  35. package/templates/appSample/webkit/useDebounce.ts +41 -0
  36. package/templates/module/__model__.document.ts +1 -1
  37. package/templates/workspaceRoot/AGENTS.md.template +470 -16
@@ -0,0 +1,86 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "Task.View.tsx",
6
+ content: `import { type cnst, usePage } from "@apps/${dict.appName}/client";
7
+ import { clsx } from "akanjs/client";
8
+
9
+ // ===== Task.View.tsx =====
10
+ // Convention: lib/<module>/ — PascalCase .tsx, View suffix = detail display component.
11
+ // View receives the Full Model (cnst.Task) as props — gives access to all defined fields.
12
+ // Uses usePage().l() for i18n — the framework convention for dictionary-based translations.
13
+ // Invoked by Akan's data-loading zone chain: Task.Zone → Load.View → Task.View.
14
+
15
+ interface GeneralProps {
16
+ className?: string;
17
+ task: cnst.Task;
18
+ }
19
+
20
+ export const General = ({ className, task }: GeneralProps) => {
21
+ const { l } = usePage();
22
+ const statusColor = {
23
+ todo: "text-base-content/50",
24
+ inProgress: "text-primary",
25
+ completed: "text-success",
26
+ }[task.status];
27
+
28
+ return (
29
+ <div className={clsx("flex w-full flex-col gap-4", className)}>
30
+ <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>
33
+ </div>
34
+
35
+ {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>
38
+ </div>
39
+ )}
40
+
41
+ <div className="flex items-center gap-4 text-base-content/60 text-sm">
42
+ <div>
43
+ <span className="font-medium">{l("task.taskDueLabel")} </span>
44
+ {task.due ? task.due.toDate().toLocaleDateString() : l("task.taskNoDue")}
45
+ </div>
46
+ </div>
47
+
48
+ {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>
51
+ <ul className="space-y-2">
52
+ {task.workHistory.map((entry, i) => (
53
+ <li key={i} className="flex items-start gap-3 text-sm">
54
+ <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
+ })}
60
+ >
61
+ {l(\`workHistoryAction.\${entry.action}\`)}
62
+ </span>
63
+ <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>}
66
+ </div>
67
+ </li>
68
+ ))}
69
+ </ul>
70
+ </div>
71
+ )}
72
+ </div>
73
+ );
74
+ };
75
+
76
+ // ---- Expandable additional fields: ----
77
+ // Compact: for narrow spaces like sidebars or modals
78
+ // export const Compact = ({ className, task }: CompactProps) => (
79
+ // <div className={clsx("text-sm", className)}>
80
+ // <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>}
82
+ // </div>
83
+ // );
84
+ `,
85
+ };
86
+ }
@@ -0,0 +1,54 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "Task.Zone.tsx",
6
+ content: `"use client";
7
+ import { Task, usePage } from "@apps/${dict.appName}/client";
8
+ import type { ClientInit, ClientView } from "akanjs/fetch";
9
+ import { Link, Load } from "akanjs/ui";
10
+
11
+ import * as cnst from "../cnst";
12
+
13
+ // ===== Task.Zone.tsx =====
14
+ // Convention: lib/<module>/ — PascalCase .tsx, Zone suffix = composition layer between pages and UI.
15
+ // Zone components use Load.Units / Load.View from akanjs/ui — the framework convention for data-bound zones.
16
+ // Takes ClientInit / ClientView props from page loader — data is fetched server-side.
17
+ // Uses usePage().l() for i18n — the framework convention for dictionary-based translations.
18
+ // Invoked directly from page components: <Task.Zone.Card init={...} />, <Task.Zone.View view={...} />.
19
+
20
+ interface CardProps {
21
+ className?: string;
22
+ init: ClientInit<"task", cnst.LightTask>;
23
+ }
24
+
25
+ export const Card = ({ className, init }: CardProps) => {
26
+ const { l } = usePage();
27
+ return (
28
+ <Load.Units
29
+ className={className}
30
+ init={init}
31
+ renderEmpty={() => (
32
+ <div className="flex flex-col items-center gap-4 py-16 text-center">
33
+ <div className="text-base-content/40 text-lg">{l("task.taskNoTasks")}</div>
34
+ <Link href="/task/new">
35
+ <button className="btn btn-primary btn-sm">{l("task.taskCreateFirst")}</button>
36
+ </Link>
37
+ </div>
38
+ )}
39
+ renderItem={(task) => <Task.Unit.Card key={task.id} task={task} href={\`/task/\${task.id}\`} />}
40
+ />
41
+ );
42
+ };
43
+
44
+ interface ViewProps {
45
+ className?: string;
46
+ view: ClientView<"task", cnst.Task>;
47
+ }
48
+
49
+ export const View = ({ className, view }: ViewProps) => {
50
+ return <Load.View className={className} view={view} renderView={(task) => <Task.View.General task={task} />} />;
51
+ };
52
+ `,
53
+ };
54
+ }
@@ -0,0 +1,40 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "task.abstract.md",
6
+ content: `# Task Abstract
7
+
8
+ ## Akan.js Module Pattern
9
+
10
+ Task is a **database-backed domain module** — the standard Akan.js pattern for persisted entities.
11
+ This module demonstrates the full Akan.js module lifecycle:
12
+ constant -> document -> service -> signal -> store -> UI.
13
+
14
+ Every database module in Akan.js follows this layered architecture:
15
+ - task.constant.ts — data shape (enum, Input, Object, Light, Full layers via via())
16
+ - task.document.ts — DB queries, filters, sorts, indexes, Document chain methods
17
+ - task.dictionary.ts — i18n labels, error messages, UI translations
18
+ - task.service.ts — business logic orchestration bound to DB via serve(db.task, ...)
19
+ - task.signal.ts — API surface (query, mutation, pubsub endpoints bound via endpoint(srv.task, ...))
20
+ - task.store.ts — client state management bound to signal via store(sig.task, ...)
21
+ - Task.*.tsx — UI components (Zone -> Load -> Unit / View / Template / Util)
22
+
23
+ ## Convention: lib/<module>/ directory naming
24
+
25
+ - Database modules live under lib/<module>/ (no underscore prefix)
26
+ - Files follow <module>.<layer>.ts naming — e.g., task.constant.ts, task.service.ts
27
+ - UI files use PascalCase with explicit suffix: Task.Zone.tsx, Task.Unit.tsx, Task.View.tsx
28
+
29
+ ## Convention: scan-registered barrels
30
+
31
+ akan scan auto-discovers each file and registers it in the corresponding barrel:
32
+ cnst.ts, db.ts, dict.ts, srv.ts, sig.ts, st.ts
33
+
34
+ ## Related Modules
35
+
36
+ - lib/__scalar/workHistory/ — standard scalar modules with double-underscore prefix (reusable field sets)
37
+ - lib/_noti/ — pure service module (compare structure differences: no constant/document, serve without DB binding)
38
+ `,
39
+ };
40
+ }
@@ -0,0 +1,53 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return `import { enumOf } from "akanjs/base";
5
+ import { via } from "akanjs/constant";
6
+
7
+ import { WorkHistory } from "../__scalar/workHistory/workHistory.constant";
8
+
9
+ // ===== task.constant.ts =====
10
+ // Convention: <module>.constant.ts — the data shape layer of an Akan.js database module.
11
+ // Import scalar primitives from akanjs/base; define model layers with via() from akanjs/constant.
12
+ // Scalars are embedded via field([ScalarType], ...) — see WorkHistory embedding below.
13
+ // Layer order: enum → Input → Object → Light → Full.
14
+ // Input = user-provided fields; Object = Input + system fields + embedded scalars; Light = subset for list views; Full = Object + Light.
15
+ // Registered by akan scan into cnst.ts barrel.
16
+
17
+ export class TaskStatus extends enumOf("taskStatus", [
18
+ "todo",
19
+ "inProgress",
20
+ "completed",
21
+ ] as const) {}
22
+
23
+ export class TaskInput extends via((field) => ({
24
+ title: field(String),
25
+ content: field(String, { default: "" }),
26
+ })) {}
27
+
28
+ // TaskObject embeds WorkHistory as a list field — the scalar embedding pattern.
29
+ // field([WorkHistory]) stores a list of scalar objects in the parent document.
30
+ // Each status change (create, start, complete) pushes a new entry into this list.
31
+ export class TaskObject extends via(TaskInput, (field) => ({
32
+ status: field(TaskStatus, { default: "todo" }),
33
+ due: field(Date).optional(),
34
+ workHistory: field([WorkHistory]),
35
+ })) {}
36
+
37
+ export class LightTask extends via(TaskObject, ["title", "status", "due"] as const, (resolve) => ({})) {}
38
+
39
+ export class Task extends via(TaskObject, LightTask, (resolve) => ({})) {}
40
+
41
+ export class TaskInsight extends via(Task, (field) => ({})) {}
42
+
43
+ // ---- Expandable additional fields: ----
44
+ // ===== Add to TaskInput =====
45
+ // - priority: field(TaskPriority, { default: "medium" })
46
+ // - tags: field(String).list().optional()
47
+ // ===== Add to TaskObject =====
48
+ // - assignee: field(ID).optional()
49
+ // - completedAt: field(Date).optional()
50
+ // ===== Add to Task =====
51
+ // isOverdue(): boolean { return this.due && this.due < new Date() }
52
+ `;
53
+ }
@@ -0,0 +1,77 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return `import { modelDictionary } from "akanjs/dictionary";
5
+
6
+ import type { Task, TaskStatus } from "./task.constant";
7
+ import type { TaskFilter } from "./task.document";
8
+
9
+ // ===== task.dictionary.ts =====
10
+ // Convention: <module>.dictionary.ts — i18n labels and messages for a database module.
11
+ // Uses modelDictionary(["en", "ko"]) from akanjs/dictionary — the framework convention for bilingual module dictionaries.
12
+ // Sections: .of() module name, .model() field labels, .lightModel() list-view labels, .query() filter labels,
13
+ // .sort() labels, .enum() value labels, .error() messages (thrown via Err()), .translate() UI messages (used in store via msg.xxx).
14
+ // Registered by akan scan into dict.ts barrel.
15
+
16
+ export const dictionary = modelDictionary(["en", "ko"])
17
+ .of((t) =>
18
+ t(["Task", "할 일"])
19
+ .desc(["A unit of work to be completed", "완료해야 할 작업 단위"]),
20
+ )
21
+ .model<Task>((t) => ({
22
+ title: t(["Title", "제목"]).desc(["Task title", "할 일 제목"]),
23
+ content: t(["Content", "내용"]).desc(["Detailed task description", "상세 작업 설명"]),
24
+ status: t(["Status", "상태"]).desc(["Current task status", "현재 작업 상태"]),
25
+ due: t(["Due Date", "마감일"]).desc(["Deadline for completion", "완료 마감일"]),
26
+ workHistory: t(["Work History", "작업 이력"]).desc(["Status change log", "상태 변경 기록"]),
27
+ }))
28
+ .query<TaskFilter>((fn) => ({
29
+ byStatus: fn(["By Status", "상태별"]).arg((t) => ({
30
+ status: t(["Status", "상태"]),
31
+ })),
32
+ dueBefore: fn(["Due Before", "이전 마감"]).arg((t) => ({
33
+ before: t(["Before Date", "기준일"]),
34
+ })),
35
+ }))
36
+ .sort<TaskFilter>((t) => ({
37
+ byDue: t(["By Due Date", "마감일순"]),
38
+ newest: t(["Newest First", "최신순"]),
39
+ }))
40
+ .enum<TaskStatus>("taskStatus", (t) => ({
41
+ todo: t(["To Do", "할 일"]),
42
+ inProgress: t(["In Progress", "진행 중"]),
43
+ completed: t(["Completed", "완료"]),
44
+ }))
45
+ .error({
46
+ cannotStartFromNonTodo: [
47
+ "Task can only start from todo status",
48
+ "할 일 상태에서만 시작할 수 있습니다",
49
+ ],
50
+ cannotCompleteFromNonInProgress: [
51
+ "Task can only complete from in-progress status",
52
+ "진행 중 상태에서만 완료할 수 있습니다",
53
+ ],
54
+ })
55
+ .translate({
56
+ createTaskLoading: ["Creating task…", "할 일 생성 중…"],
57
+ createTaskSuccess: ["Task created", "할 일이 생성되었습니다"],
58
+ startTaskLoading: ["Starting task…", "작업 시작 중…"],
59
+ startTaskSuccess: ["Task started", "작업이 시작되었습니다"],
60
+ completeTaskLoading: ["Completing task…", "작업 완료 중…"],
61
+ completeTaskSuccess: ["Task completed", "작업이 완료되었습니다"],
62
+ removeTaskLoading: ["Removing task…", "할 일 삭제 중…"],
63
+ removeTaskSuccess: ["Task removed", "할 일이 삭제되었습니다"],
64
+ taskDueLabel: ["Due:", "마감:"],
65
+ taskNoDue: ["No deadline", "마감 없음"],
66
+ taskWorkHistoryTitle: ["Work History", "작업 이력"],
67
+ taskStart: ["Start", "시작"],
68
+ taskComplete: ["Complete", "완료"],
69
+ taskRemove: ["Remove", "삭제"],
70
+ taskNoTasks: ["No tasks yet", "할 일이 없습니다"],
71
+ taskCreateFirst: ["+ Create your first task", "+ 첫 할 일 만들기"],
72
+ taskBackToTasks: ["← Back to Tasks", "← 할 일 목록으로"],
73
+ taskEdit: ["Edit", "수정"],
74
+ taskNew: ["+ New Task", "+ 새 할 일"],
75
+ });
76
+ `;
77
+ }
@@ -0,0 +1,51 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return `import { dayjs } from "akanjs/base";
5
+ import { by, from, into } from "akanjs/document";
6
+
7
+ import { Err } from "../dict";
8
+ import * as cnst from "../cnst";
9
+
10
+ // ===== task.document.ts =====
11
+ // Convention: <module>.document.ts — the database query and persistence layer.
12
+ // Import from/into/by from akanjs/document — the framework convention for Filter, Document, and Model classes.
13
+ // Filter defines query/sort conditions (auto-generates List/Find/Pick/Exists/Count methods via akan scan).
14
+ // Document defines chainable per-document methods (e.g., task.start().save()).
15
+ // Model = into(Document, Filter, cnst.module, ...) — collection-level operations, schema hooks.
16
+ // Registered by akan scan into db.ts barrel.
17
+
18
+ export class TaskFilter extends from(cnst.Task, (filter) => ({
19
+ query: {
20
+ byStatus: filter()
21
+ .arg("status", cnst.TaskStatus)
22
+ .query((status) => ({ status })),
23
+ dueBefore: filter()
24
+ .arg("before", Date)
25
+ .query((before, q) => ({ due: q.lte(before) })),
26
+ },
27
+ sort: {
28
+ byDue: { due: 1 },
29
+ newest: { createdAt: -1 },
30
+ },
31
+ })) {}
32
+
33
+ export class Task extends by(cnst.Task) {
34
+ start() {
35
+ if (this.status !== "todo") throw new Err("task.error.cannotStartFromNonTodo");
36
+ this.status = "inProgress";
37
+ this.workHistory.push({ action: "completed", at: dayjs(), note: "" });
38
+ return this;
39
+ }
40
+
41
+ complete() {
42
+ if (this.status !== "inProgress") throw new Err("task.error.cannotCompleteFromNonInProgress");
43
+ this.status = "completed";
44
+ this.workHistory.push({ action: "completed", at: dayjs(), note: "" });
45
+ return this;
46
+ }
47
+ }
48
+
49
+ export class TaskModel extends into(Task, TaskFilter, cnst.task, () => ({})) {}
50
+ `;
51
+ }
@@ -0,0 +1,44 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return `import { dayjs } from "akanjs/base";
5
+ import type { DataInputOf } from "akanjs/document";
6
+ import { serve } from "akanjs/service";
7
+ import * as db from "../db";
8
+
9
+ // ===== task.service.ts =====
10
+ // Convention: <module>.service.ts — business logic orchestration for a database module.
11
+ // Extends serve(db.<module>, depsCallback) from akanjs/service — binds to the DB model, receives DI deps.
12
+ // Auto-generated by akan scan (do not write manually):
13
+ // getTask(id), createModel(data), updateModel(id, data), removeModel(id),
14
+ // listByStatus(status), searchDocs(text), and all filter+query methods from document.ts.
15
+ // Manual below: lifecycle hooks, custom business logic methods.
16
+ // Registered by akan scan into srv.ts barrel.
17
+
18
+ export class TaskService extends serve(db.task, () => ({})) {
19
+ // Lifecycle hook: runs before every document creation.
20
+ // Auto-injects the initial workHistory entry for the scalar embedding pattern.
21
+ override _preCreate(data: DataInputOf<db.TaskInput, db.Task>): DataInputOf<db.TaskInput, db.Task> {
22
+ return {
23
+ ...data,
24
+ workHistory: [{ action: "created", at: dayjs(), note: "" }],
25
+ };
26
+ }
27
+
28
+ async startTask(taskId: string) {
29
+ const task = await this.getTask(taskId);
30
+ return task.start().save();
31
+ }
32
+
33
+ async completeTask(taskId: string) {
34
+ const task = await this.getTask(taskId);
35
+ return task.complete().save();
36
+ }
37
+ }
38
+
39
+ // ---- Expandable additional fields: ----
40
+ // export class TaskService extends serve(db.task, ({ service, use, signal, plug, env, memory }) => ({
41
+ // notiService: service<srv.NotiService>(),
42
+ // })) {
43
+ `;
44
+ }
@@ -0,0 +1,54 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return `import { endpoint, internal, Public, slice } from "akanjs/signal";
5
+
6
+ import * as cnst from "../cnst";
7
+ import * as srv from "../srv";
8
+
9
+ // ===== task.signal.ts =====
10
+ // Convention: <module>.signal.ts — the API surface for a database module.
11
+ // Extends endpoint(srv.<module>, ...) from akanjs/signal — binds to the service to define query/mutation/pubsub.
12
+ // endpoint() receives: { query, mutation, pubsub, message } — each typed with the model's return type.
13
+ // .param() = URL path parameter, .body() = request body, .search() = query string.
14
+ //
15
+ // Auto-generated by akan scan (do not write manually):
16
+ // viewTask(id) — fetch single task for detail view
17
+ // editTask(id) — fetch task for edit view
18
+ // mergeTask(id, data) — create/update task
19
+ // Manual endpoints below: only define endpoints that need custom business logic.
20
+ // Registered by akan scan into sig.ts barrel.
21
+
22
+ export class TaskInternal extends internal(srv.task, ({ interval }) => ({})) {}
23
+
24
+ export class TaskSlice extends slice(srv.task, { guards: { root: Public, get: Public, cru: Public } }, (init) => ({
25
+ inPublic: init().exec(function () {
26
+ return this.taskService.queryAny();
27
+ }),
28
+ })) {}
29
+
30
+ export class TaskEndpoint extends endpoint(srv.task, ({ mutation }) => ({
31
+ startTask: mutation(cnst.Task)
32
+ .param("taskId", String)
33
+ .exec(async function (taskId) {
34
+ return await this.taskService.startTask(taskId);
35
+ }),
36
+
37
+ completeTask: mutation(cnst.Task)
38
+ .param("taskId", String)
39
+ .exec(async function (taskId) {
40
+ return await this.taskService.completeTask(taskId);
41
+ }),
42
+ })) {}
43
+
44
+ // ---- Expandable additional fields: ----
45
+ // // message: server→client real-time message (WebSocket)
46
+ // taskCreated: message(cnst.Task).room("taskId", String).exec(...),
47
+ // // pubsub: publish-subscribe pattern
48
+ // taskEvent: pubsub(cnst.Task).exec(...),
49
+ // // search: search parameters (optional, URL query string)
50
+ // searchTask: query(cnst.LightTask).search("keyword", String).exec(...),
51
+ // // guards: authentication/authorization guards
52
+ // // export class TaskEndpoint extends endpoint(srv.task, { guards: { root: SignedIn } }, ({ query, mutation }) => ({})) {}
53
+ `;
54
+ }
@@ -0,0 +1,42 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return `import { store } from "akanjs/store";
5
+
6
+ import { fetch, msg, sig } from "../useClient";
7
+
8
+ // ===== task.store.ts =====
9
+ // Convention: <module>.store.ts — client-side state management for a database module.
10
+ // Extends store(sig.<module>, ...) from akanjs/store — binds to the signal to auto-generate model states/actions.
11
+ // Auto-generated by akan scan (do not write manually):
12
+ // taskForm — form state bound to model fields (create + edit)
13
+ // setTitleOnTask, setContentOnTask, setDueOnTask — auto-setters for form fields
14
+ // createTask(data), updateTask(id, data), removeTask(id) — CRUD actions
15
+ // task (cached model), taskLoading, taskModal — model display states
16
+ // Manual below: custom actions wrapping auto-generated fetch with toast feedback.
17
+ // Registered by akan scan into st.ts barrel.
18
+
19
+ export class TaskStore extends store(sig.task, () => ({})) {
20
+ async startTask(taskId: string) {
21
+ msg.loading("task.startTaskLoading", { key: "startTask" });
22
+ const task = await fetch.startTask(taskId);
23
+ msg.success("task.startTaskSuccess", { key: "startTask" });
24
+ this.setTask(task);
25
+ }
26
+
27
+ async completeTask(taskId: string) {
28
+ msg.loading("task.completeTaskLoading", { key: "completeTask" });
29
+ const task = await fetch.completeTask(taskId);
30
+ msg.success("task.completeTaskSuccess", { key: "completeTask" });
31
+ this.setTask(task);
32
+ }
33
+ }
34
+
35
+ // ---- Expandable additional fields: ----
36
+ // Custom state (if needed):
37
+ // export class TaskStore extends store(sig.task, () => ({
38
+ // filterKeyword: "",
39
+ // })) { ... }
40
+ // The auto-generated taskForm handles title, content, due states automatically.
41
+ `;
42
+ }
@@ -0,0 +1,39 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "_index.tsx",
6
+ content: `
7
+ import { fetch, usePage } from "@apps/${dict.appName}/client";
8
+ import { Task } from "@apps/${dict.appName}/lib/task";
9
+ import { Link } from "akanjs/ui";
10
+
11
+ // ===== page/task/[taskId]/_index.tsx =====
12
+ // Convention: Akan.js file-based routing with dynamic segments.
13
+ // [taskId] folder = URL path parameter extracted via useParams() from akanjs/client.
14
+ // Uses usePage().l() for i18n — the framework convention for dictionary-based translations.
15
+
16
+ interface PageProps {
17
+ params: { taskId: string };
18
+ }
19
+ export default async function Page({ params: { taskId } }: PageProps) {
20
+ const { l } = usePage();
21
+ const { taskView } = await fetch.viewTask(taskId);
22
+ return (
23
+ <main className="mx-auto max-w-4xl px-6 py-8">
24
+ <div className="mb-6 flex items-center justify-between">
25
+ <a href="/task" className="btn btn-ghost btn-sm">
26
+ {l("task.taskBackToTasks")}
27
+ </a>
28
+ <Link href={\`/task/\${taskId}/edit\`} className="btn btn-primary btn-sm">
29
+ {l("task.taskEdit")}
30
+ </Link>
31
+ </div>
32
+
33
+ <Task.Zone.View className="max-w-2xl" view={taskView} />
34
+ </main>
35
+ );
36
+ }
37
+ `,
38
+ };
39
+ }
@@ -0,0 +1,28 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "edit.tsx",
6
+ content: `import { fetch, Task } from "@apps/${dict.appName}/client";
7
+ import { Load } from "akanjs/ui";
8
+
9
+ // ===== page/task/[taskId]/edit.tsx =====
10
+ // Convention: Server-side edit form page using Load.Edit from akanjs/ui.
11
+ // async Page() — fetches model data on the server via fetch.editTask(params.taskId).
12
+ // The same Template (Task.Template.General) is reused here — no separate edit template needed.
13
+ // Load.Edit with type="form" provides the submit/cancel wrapper with the pre-loaded model.
14
+
15
+ interface PageProps {
16
+ params: { taskId: string };
17
+ }
18
+ export default async function Page({ params: { taskId } }: PageProps) {
19
+ const { taskEdit } = await fetch.editTask(taskId);
20
+ return (
21
+ <Load.Edit slice={fetch.slice.taskInPublic} edit={taskEdit} type="form" onSubmit={\`/task/\${taskId}\`}>
22
+ <Task.Template.General />
23
+ </Load.Edit>
24
+ );
25
+ }
26
+ `,
27
+ };
28
+ }
@@ -0,0 +1,34 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "_index.tsx",
6
+ content: `import { fetch, Task, usePage } from "@apps/${dict.appName}/client";
7
+ import { Link } from "akanjs/ui";
8
+
9
+ // ===== page/task/_index.tsx =====
10
+ // Convention: Akan.js file-based routing — _index.tsx is the index page for /task.
11
+ // Server-side data loading via loader() at the page level; passes init/view props to Zone components.
12
+ // Uses usePage().l() for i18n — the framework convention for dictionary-based translations.
13
+
14
+ export default async function Page() {
15
+ const { l } = usePage();
16
+ const { taskInitInPublic } = await fetch.initTaskInPublic();
17
+ return (
18
+ <main className="mx-auto max-w-4xl px-6 py-8">
19
+ <div className="mb-6 flex items-center justify-between">
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>
23
+ </div>
24
+ <Link href="/task/new" className="btn btn-primary btn-sm">
25
+ {l("task.taskNew")}
26
+ </Link>
27
+ </div>
28
+
29
+ <Task.Zone.Card className="flex flex-col gap-3" init={taskInitInPublic} />
30
+ </main>
31
+ );
32
+ }`,
33
+ };
34
+ }
@@ -0,0 +1,24 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "_layout.tsx",
6
+ content: `
7
+ // ===== page/task/_layout.tsx =====
8
+ // Convention: Akan.js file-based routing — _layout.tsx wraps every sub-page under /task/*.
9
+ // The underscores mark it as a routing file; children = sub-page component from nested routes.
10
+ // Rendered by Akan's router: URL → _layout.tsx → children (e.g., _index.tsx or [taskId]/_index.tsx).
11
+
12
+ interface LayoutProps {
13
+ children: React.ReactNode;
14
+ }
15
+ export default function Layout({ children }: LayoutProps) {
16
+ return (
17
+ <div className="min-h-screen bg-base-200">
18
+ {children}
19
+ </div>
20
+ );
21
+ }
22
+ `,
23
+ };
24
+ }
@@ -0,0 +1,26 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "new.tsx",
6
+ content: `import { type cnst, fetch, Task } from "@apps/${dict.appName}/client";
7
+ import { Load } from "akanjs/ui";
8
+
9
+ // ===== page/task/new.tsx =====
10
+ // Convention: Server-side form page using Load.Edit from akanjs/ui.
11
+ // async Page() — server-side component that pre-initializes form data before rendering.
12
+ // Load.Edit with type="form" renders the Template inside a form wrapper with submit/cancel actions.
13
+ // onCancel="back" navigates to the previous page; onSubmit specifies the redirect after success.
14
+ // Template is reused — same Task.Template.General used for create, edit, and client-side modals.
15
+
16
+ export default async function Page() {
17
+ const taskForm: Partial<cnst.Task> = { status: "todo" };
18
+
19
+ return (
20
+ <Load.Edit slice={fetch.slice.taskInPublic} edit={taskForm} type="form" onCancel="back" onSubmit="/task">
21
+ <Task.Template.General />
22
+ </Load.Edit>
23
+ );
24
+ }`,
25
+ };
26
+ }