@akanjs/cli 2.3.1-rc.4 → 2.3.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 (24) hide show
  1. package/package.json +2 -2
  2. package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +5 -5
  3. package/templates/appSample/lib/__scalar/workHistory/workHistory.constant.ts +6 -9
  4. package/templates/appSample/lib/__scalar/workHistory/workHistory.dictionary.ts +4 -7
  5. package/templates/appSample/lib/__scalar/workHistory/workHistory.document.ts +10 -0
  6. package/templates/appSample/lib/_noti/noti.dictionary.ts +4 -6
  7. package/templates/appSample/lib/_noti/noti.service.ts +2 -6
  8. package/templates/appSample/lib/_noti/noti.signal.ts +4 -8
  9. package/templates/appSample/lib/_noti/noti.store.ts +4 -7
  10. package/templates/appSample/lib/task/Task.Template.tsx +3 -7
  11. package/templates/appSample/lib/task/Task.Unit.tsx +4 -9
  12. package/templates/appSample/lib/task/Task.Util.tsx +6 -4
  13. package/templates/appSample/lib/task/Task.View.tsx +16 -24
  14. package/templates/appSample/lib/task/Task.Zone.tsx +7 -19
  15. package/templates/appSample/lib/task/task.constant.ts +9 -10
  16. package/templates/appSample/lib/task/task.dictionary.ts +3 -11
  17. package/templates/appSample/lib/task/task.document.ts +6 -9
  18. package/templates/appSample/lib/task/task.service.ts +5 -9
  19. package/templates/appSample/lib/task/task.signal.ts +10 -5
  20. package/templates/appSample/lib/task/task.store.ts +3 -6
  21. package/templates/appSample/page/task/[taskId]/edit.tsx +18 -10
  22. package/templates/appSample/page/task/_index.tsx +6 -10
  23. package/templates/appSample/page/task/new.tsx +19 -14
  24. package/templates/module/__model__.document.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "2.3.1-rc.4",
3
+ "version": "2.3.1",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -35,7 +35,7 @@
35
35
  "@langchain/openai": "^1.4.6",
36
36
  "@tailwindcss/node": "^4.3.0",
37
37
  "@trapezedev/project": "^7.1.4",
38
- "akanjs": "2.3.1-rc.4",
38
+ "akanjs": "2.3.1",
39
39
  "chalk": "^5.6.2",
40
40
  "commander": "^14.0.3",
41
41
  "daisyui": "^5.5.20",
@@ -12,7 +12,7 @@ Scalar modules are embedded into models via field([ScalarType]), not stored inde
12
12
  They live under the double-underscore prefix to distinguish them from database models.
13
13
 
14
14
  Standard scalar layers:
15
- - workHistory.constant.ts — enum (WorkHistoryAction) + entry class (WorkHistoryEntry) defined via via() + enumOf()
15
+ - workHistory.constant.ts — enum (WorkHistoryAction) + entry class (WorkHistory) defined via via() + enumOf()
16
16
  - workHistory.dictionary.ts — i18n via scalarDictionary() (fewer layers than modelDictionary)
17
17
  - workHistory.abstract.md — embedding rules and intent
18
18
 
@@ -20,14 +20,14 @@ Standard scalar layers:
20
20
 
21
21
  - Scalar modules use double-underscore prefix: lib/__scalar/workHistory/
22
22
  - Files drop the underscores: workHistory.constant.ts, not __workHistory.constant.ts
23
- - Models embed scalars via field([WorkHistoryEntry], { default: [] }) — list-of-scalar embedding
23
+ - Models embed scalars via field([WorkHistory]) — list-of-scalar embedding
24
24
 
25
25
  ## Embedding Example (in task.constant.ts)
26
26
 
27
- TaskObject embeds a list of WorkHistoryEntry:
28
- import { WorkHistoryEntry } from "../../__scalar/workHistory/workHistory.constant";
27
+ TaskObject embeds a list of WorkHistory:
28
+ import { WorkHistory } from "../../__scalar/workHistory/workHistory.constant";
29
29
  export class TaskObject extends via(TaskInput, (field) => ({
30
- workHistory: field([WorkHistoryEntry], { default: [] }),
30
+ workHistory: field([WorkHistory]),
31
31
  }))
32
32
 
33
33
  Each status change (create, start, complete) pushes a new entry.
@@ -1,14 +1,12 @@
1
1
  import type { AppInfo, LibInfo } from "akanjs";
2
2
 
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
- return {
5
- filename: "workHistory.constant.ts",
6
- content: `import { dayjs, enumOf } from "akanjs/base";
4
+ return `import { dayjs, enumOf } from "akanjs/base";
7
5
  import { via } from "akanjs/constant";
8
6
 
9
7
  // ===== workHistory.constant.ts =====
10
8
  // Convention: lib/__scalar/<type>/ — scalar modules use double-underscore prefix.
11
- // Scalars are embedded value shapes, reused across models via field([ScalarType], { default: [] }).
9
+ // Scalars are embedded value shapes, reused across models via field([ScalarType]).
12
10
  // Define enums with enumOf() and entry classes with via() from akanjs/constant.
13
11
 
14
12
  // ---- Enum ----
@@ -21,10 +19,10 @@ export class WorkHistoryAction extends enumOf("workHistoryAction", [
21
19
  ] as const) {}
22
20
 
23
21
  // ---- Scalar Entry ----
24
- // WorkHistoryEntry: a single entry in a model's workHistory list
22
+ // WorkHistory: a single entry in a model's workHistory list
25
23
  // Each status change in the owning model pushes a new entry into the list
26
- // field([WorkHistoryEntry], { default: [] }) in the parent model's Object layer
27
- export class WorkHistoryEntry extends via((field) => ({
24
+ // field([WorkHistory]) in the parent model's Object layer
25
+ export class WorkHistory extends via((field) => ({
28
26
  action: field(WorkHistoryAction),
29
27
  at: field(Date, { default: () => dayjs() }),
30
28
  note: field(String, { default: "" }),
@@ -33,6 +31,5 @@ export class WorkHistoryEntry extends via((field) => ({
33
31
  // ---- Expandable additional fields: ----
34
32
  // - actor: field(String).optional() — who performed the action
35
33
  // - previousStatus: field(TaskStatus).optional() — status before the change
36
- `,
37
- };
34
+ `;
38
35
  }
@@ -1,18 +1,16 @@
1
1
  import type { AppInfo, LibInfo } from "akanjs";
2
2
 
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
- return {
5
- filename: "workHistory.dictionary.ts",
6
- content: `import { scalarDictionary } from "akanjs/dictionary";
4
+ return `import { scalarDictionary } from "akanjs/dictionary";
7
5
 
8
- import type { WorkHistoryAction, WorkHistoryEntry } from "./workHistory.constant";
6
+ import type { WorkHistoryAction, WorkHistory } from "./workHistory.constant";
9
7
 
10
8
  // ===== workHistory.dictionary.ts =====
11
9
  // Convention: scalarDictionary() for scalar module i18n — fewer layers than modelDictionary().
12
10
  // Scalar dictionaries define field labels and enum value labels only; no model/insight/query/sort layers.
13
11
 
14
12
  export const dictionary = scalarDictionary(["en", "ko"])
15
- .model<WorkHistoryEntry>((t) => ({
13
+ .model<WorkHistory>((t) => ({
16
14
  action: t(["Action", "작업"]).desc(["Type of work history event", "작업 이력 이벤트 유형"]),
17
15
  at: t(["At", "시간"]).desc(["When the event occurred", "이벤트 발생 시간"]),
18
16
  note: t(["Note", "메모"]).desc(["Optional note about the event", "이벤트에 대한 선택적 메모"]),
@@ -23,6 +21,5 @@ export const dictionary = scalarDictionary(["en", "ko"])
23
21
  completed: t(["Completed", "완료됨"]).desc(["Task was completed", "할 일이 완료됨"]),
24
22
  }))
25
23
  .translate({});
26
- `,
27
- };
24
+ `;
28
25
  }
@@ -0,0 +1,10 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return `import { by } from "akanjs/document";
5
+
6
+ import * as cnst from "./workHistory.constant";
7
+
8
+ export class WorkHistory extends by(cnst.WorkHistory) {}
9
+ `;
10
+ }
@@ -1,9 +1,8 @@
1
1
  import type { AppInfo, LibInfo } from "akanjs";
2
2
 
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
- return {
5
- filename: "noti.dictionary.ts",
6
- content: `import { serviceDictionary } from "akanjs/dictionary";
4
+ return `import { serviceDictionary } from "akanjs/dictionary";
5
+ import type { NotiEndpoint } from "./noti.signal";
7
6
 
8
7
  // ===== noti.dictionary.ts =====
9
8
  // Convention: <module>.dictionary.ts for a pure service module.
@@ -13,13 +12,12 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
13
12
  // Registered by akan scan into dict.ts barrel.
14
13
 
15
14
  export const dictionary = serviceDictionary(["en", "ko"])
16
- .endpoint<typeof import("./noti.signal").NotiEndpoint>((fn) => ({
15
+ .endpoint<NotiEndpoint>((fn) => ({
17
16
  send: fn(["Send Notification", "알림 보내기"]),
18
17
  }))
19
18
  .translate({
20
19
  notiReceived: ["New notification", "새 알림이 도착했습니다"],
21
20
  notiMarkAllRead: ["Mark all as read", "모두 읽음으로 표시"],
22
21
  });
23
- `,
24
- };
22
+ `;
25
23
  }
@@ -1,10 +1,7 @@
1
1
  import type { AppInfo, LibInfo } from "akanjs";
2
2
 
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
- return {
5
- filename: "noti.service.ts",
6
- content: `import { serve } from "akanjs/service";
7
- import { dayjs } from "akanjs/base";
4
+ return `import { serve } from "akanjs/service";
8
5
 
9
6
  // ===== noti.service.ts =====
10
7
  // Convention: <module>.service.ts for a pure service module.
@@ -26,6 +23,5 @@ export class NotiService extends serve("noti" as const, () => ({})) {}
26
23
  // return this.pushApi.send({ token, title, body });
27
24
  // }
28
25
  // }
29
- `,
30
- };
26
+ `;
31
27
  }
@@ -1,10 +1,7 @@
1
1
  import type { AppInfo, LibInfo } from "akanjs";
2
2
 
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
- return {
5
- filename: "noti.signal.ts",
6
- content: `import { dayjs } from "akanjs/base";
7
- import { endpoint } from "akanjs/signal";
4
+ return `import { internal, endpoint } from "akanjs/signal";
8
5
 
9
6
  import * as srv from "../srv";
10
7
 
@@ -15,9 +12,9 @@ import * as srv from "../srv";
15
12
  // Client subscribes via fetch.subscribeSend((data) => { ... }).
16
13
  // Registered by akan scan into sig.ts barrel.
17
14
 
18
- export class NotiEndpoint extends endpoint(srv.noti, ({ pubsub }) => ({
15
+ export class NotiInternal extends internal(srv.noti, () => ({})) {}
19
16
 
20
- })) {}
17
+ export class NotiEndpoint extends endpoint(srv.noti, () => ({})) {}
21
18
 
22
19
  // ---- Expandable additional fields: ----
23
20
  // history: query(NotiHistory)
@@ -25,6 +22,5 @@ export class NotiEndpoint extends endpoint(srv.noti, ({ pubsub }) => ({
25
22
  // .exec(async function (userId) {
26
23
  // return await this.notiService.getHistory(userId);
27
24
  // }),
28
- `,
29
- };
25
+ `;
30
26
  }
@@ -1,9 +1,7 @@
1
1
  import type { AppInfo, LibInfo } from "akanjs";
2
2
 
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
- return {
5
- filename: "noti.store.ts",
6
- content: `import { dayjs } from "akanjs/base";
4
+ return `import type { Dayjs } from "akanjs/base";
7
5
  import { store } from "akanjs/store";
8
6
 
9
7
  // ===== noti.store.ts =====
@@ -14,10 +12,10 @@ import { store } from "akanjs/store";
14
12
  // Registered by akan scan into st.ts barrel.
15
13
 
16
14
  export class NotiStore extends store("noti" as const, () => ({
17
- notiList: [] as { id: string; type: string; message: string; sentAt: dayjs.Dayjs }[],
15
+ notiList: [] as { id: string; type: string; message: string; sentAt: Dayjs }[],
18
16
  unreadCount: 0,
19
17
  })) {
20
- addNoti(noti: { type: string; message: string; sentAt: dayjs.Dayjs }) {
18
+ addNoti(noti: { type: string; message: string; sentAt: Dayjs }) {
21
19
  const id = Math.random().toString(36).slice(2);
22
20
  this.set({
23
21
  notiList: [...(this.get().notiList ?? []), { ...noti, id }],
@@ -33,6 +31,5 @@ export class NotiStore extends store("noti" as const, () => ({
33
31
  });
34
32
  }
35
33
  }
36
- `,
37
- };
34
+ `;
38
35
  }
@@ -4,12 +4,10 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
4
4
  return {
5
5
  filename: "Task.Template.tsx",
6
6
  content: `"use client";
7
-
7
+ import { st, usePage } from "@apps/${dict.appName}/client";
8
8
  import { clsx } from "akanjs/client";
9
9
  import { Field, Layout } from "akanjs/ui";
10
10
 
11
- import { st, usePage } from "@apps/${dict.appName}/client";
12
-
13
11
  // ===== Task.Template.tsx =====
14
12
  // Convention: lib/<module>/ — PascalCase .tsx, Template suffix = form/edit template component.
15
13
  // Uses akanjs/ui Field and Layout components: the framework convention for controlled form UIs.
@@ -34,16 +32,14 @@ export const General = ({ className }: TaskEditProps) => {
34
32
  value={taskForm.title}
35
33
  onChange={st.do.setTitleOnTask}
36
34
  />
37
- <Field.Text
35
+ <Field.TextArea
38
36
  label={l("task.content")}
39
37
  desc={l("task.content.desc")}
40
38
  value={taskForm.content}
41
39
  onChange={st.do.setContentOnTask}
42
- multiline
43
40
  />
44
41
  </Layout.Template>
45
42
  );
46
- };
47
- `,
43
+ };`,
48
44
  };
49
45
  }
@@ -3,12 +3,10 @@ import type { AppInfo, LibInfo } from "akanjs";
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
4
  return {
5
5
  filename: "Task.Unit.tsx",
6
- content: `import { type ModelProps, clsx } from "akanjs/client";
6
+ content: `import { type cnst, usePage } from "@apps/${dict.appName}/client";
7
+ import { type ModelProps, clsx } from "akanjs/client";
7
8
  import { Layout } from "akanjs/ui";
8
9
 
9
- import * as cnst from "@apps/${dict.appName}/lib/cnst";
10
- import { usePage } from "@apps/${dict.appName}/client";
11
-
12
10
  // ===== Task.Unit.tsx =====
13
11
  // Convention: lib/<module>/ — PascalCase .tsx, Unit suffix = card/list-item component.
14
12
  // Unit receives LightModel as props via the ModelProps<modelName, LightModelType> generic from akanjs/client.
@@ -39,9 +37,7 @@ export const Card = ({ task, className, href }: CardProps) => {
39
37
  >
40
38
  <div className="flex items-start justify-between gap-2">
41
39
  <span className="font-semibold text-base-content">{task.title}</span>
42
- <span className={clsx("badge badge-sm shrink-0", statusBadge)}>
43
- {l(\`taskStatus.\${task.status}\`)}
44
- </span>
40
+ <span className={clsx("badge badge-sm shrink-0", statusBadge)}>{l(\`taskStatus.\${task.status}\`)}</span>
45
41
  </div>
46
42
  {task.due && (
47
43
  <div className="mt-2 text-base-content/60 text-xs">
@@ -64,7 +60,6 @@ export const Card = ({ task, className, href }: CardProps) => {
64
60
  // Abstract: abstracted summary display (search results, previews)
65
61
  // export const Abstract = ({ task }: AbstractProps) => (
66
62
  // <div className="text-sm">{task.title} — {l("taskStatus", task.status)}</div>
67
- // );
68
- `,
63
+ // );`,
69
64
  };
70
65
  }
@@ -4,8 +4,7 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
4
4
  return {
5
5
  filename: "Task.Util.tsx",
6
6
  content: `"use client";
7
-
8
- import { st, usePage } from "@apps/${dict.appName}/client";
7
+ import { fetch, st, usePage } from "@apps/${dict.appName}/client";
9
8
  import { Model } from "akanjs/ui";
10
9
 
11
10
  // ===== Task.Util.tsx =====
@@ -46,7 +45,7 @@ interface RemoveProps {
46
45
  }
47
46
 
48
47
  export const Remove = ({ taskId }: RemoveProps) => (
49
- <Model.Remove modelId={taskId}>
48
+ <Model.Remove modelId={taskId} slice={fetch.slice.task}>
50
49
  <button className="btn btn-xs btn-ghost text-error">{usePage().l("task.taskRemove")}</button>
51
50
  </Model.Remove>
52
51
  );
@@ -63,7 +62,10 @@ export const Toolbox = ({ taskId, status }: ToolboxProps) => {
63
62
  <button tabIndex={0} className="btn btn-xs btn-ghost">
64
63
  ···
65
64
  </button>
66
- <ul tabIndex={0} className="dropdown-content menu rounded-box z-[1] w-40 border border-base-content/10 bg-base-100 p-2 shadow">
65
+ <ul
66
+ tabIndex={0}
67
+ className="dropdown-content menu z-[1] w-40 rounded-box border border-base-content/10 bg-base-100 p-2 shadow"
68
+ >
67
69
  {status === "todo" && (
68
70
  <li>
69
71
  <button onClick={() => st.do.startTask(taskId)}>{l("task.taskStart")}</button>
@@ -3,10 +3,8 @@ import type { AppInfo, LibInfo } from "akanjs";
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
4
  return {
5
5
  filename: "Task.View.tsx",
6
- content: `import { clsx } from "akanjs/client";
7
-
8
- import * as cnst from "@apps/${dict.appName}/lib/cnst";
9
- import { usePage } from "@apps/${dict.appName}/client";
6
+ content: `import { type cnst, usePage } from "@apps/${dict.appName}/client";
7
+ import { clsx } from "akanjs/client";
10
8
 
11
9
  // ===== Task.View.tsx =====
12
10
  // Convention: lib/<module>/ — PascalCase .tsx, View suffix = detail display component.
@@ -31,46 +29,40 @@ export const General = ({ className, task }: GeneralProps) => {
31
29
  <div className={clsx("flex w-full flex-col gap-4", className)}>
32
30
  <div>
33
31
  <h1 className="font-bold text-2xl text-base-content">{task.title}</h1>
34
- <div className={clsx("mt-1 font-medium text-sm", statusColor)}>
35
- {l(\`taskStatus.\${task.status}\`)}
36
- </div>
32
+ <div className={clsx("mt-1 font-medium text-sm", statusColor)}>{l(\`taskStatus.\${task.status}\`)}</div>
37
33
  </div>
38
34
 
39
35
  {task.content && (
40
36
  <div className="rounded-lg border border-base-content/10 bg-base-100 p-4">
41
- <p className="whitespace-pre-wrap text-base-content/80 text-sm">
42
- {task.content}
43
- </p>
37
+ <p className="whitespace-pre-wrap text-base-content/80 text-sm">{task.content}</p>
44
38
  </div>
45
39
  )}
46
40
 
47
41
  <div className="flex items-center gap-4 text-base-content/60 text-sm">
48
42
  <div>
49
- <span className="font-medium">
50
- {l("task.taskDueLabel")}{" "}
51
- </span>
52
- {task.due
53
- ? task.due.toDate().toLocaleDateString()
54
- : l("task.taskNoDue")}
43
+ <span className="font-medium">{l("task.taskDueLabel")} </span>
44
+ {task.due ? task.due.toDate().toLocaleDateString() : l("task.taskNoDue")}
55
45
  </div>
56
46
  </div>
57
47
 
58
48
  {task.workHistory && task.workHistory.length > 0 && (
59
- <div className="mt-2 border-t border-base-content/10 pt-4">
60
- <h3 className="font-semibold text-base-content text-sm mb-3">{l("task.taskWorkHistoryTitle")}</h3>
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>
61
51
  <ul className="space-y-2">
62
52
  {task.workHistory.map((entry, i) => (
63
53
  <li key={i} className="flex items-start gap-3 text-sm">
64
- <span className={clsx("badge badge-xs mt-0.5 shrink-0", {
65
- "badge-ghost": entry.action === "created",
66
- "badge-primary": entry.action === "started",
67
- "badge-success": entry.action === "completed",
68
- })}>
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
+ >
69
61
  {l(\`workHistoryAction.\${entry.action}\`)}
70
62
  </span>
71
63
  <div>
72
64
  <span className="text-base-content/60">{entry.at.toDate().toLocaleString()}</span>
73
- {entry.note && <span className="text-base-content/50 ml-2">{entry.note}</span>}
65
+ {entry.note && <span className="ml-2 text-base-content/50">{entry.note}</span>}
74
66
  </div>
75
67
  </li>
76
68
  ))}
@@ -4,10 +4,9 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
4
4
  return {
5
5
  filename: "Task.Zone.tsx",
6
6
  content: `"use client";
7
-
8
- import { fetch, Task, usePage } from "@apps/${dict.appName}/client";
9
- import { Load } from "akanjs/ui";
7
+ import { Task, usePage } from "@apps/${dict.appName}/client";
10
8
  import type { ClientInit, ClientView } from "akanjs/fetch";
9
+ import { Link, Load } from "akanjs/ui";
11
10
 
12
11
  import * as cnst from "../cnst";
13
12
 
@@ -32,16 +31,12 @@ export const Card = ({ className, init }: CardProps) => {
32
31
  renderEmpty={() => (
33
32
  <div className="flex flex-col items-center gap-4 py-16 text-center">
34
33
  <div className="text-base-content/40 text-lg">{l("task.taskNoTasks")}</div>
35
- <button className="btn btn-primary btn-sm">{l("task.taskCreateFirst")}</button>
34
+ <Link href="/task/new">
35
+ <button className="btn btn-primary btn-sm">{l("task.taskCreateFirst")}</button>
36
+ </Link>
36
37
  </div>
37
38
  )}
38
- renderItem={(task) => (
39
- <Task.Unit.Card
40
- key={task.id}
41
- task={task}
42
- href={\`/task/\${task.id}\`}
43
- />
44
- )}
39
+ renderItem={(task) => <Task.Unit.Card key={task.id} task={task} href={\`/task/\${task.id}\`} />}
45
40
  />
46
41
  );
47
42
  };
@@ -52,15 +47,8 @@ interface ViewProps {
52
47
  }
53
48
 
54
49
  export const View = ({ className, view }: ViewProps) => {
55
- return (
56
- <Load.View
57
- className={className}
58
- view={view}
59
- renderView={(task) => <Task.View.General task={task} />}
60
- />
61
- );
50
+ return <Load.View className={className} view={view} renderView={(task) => <Task.View.General task={task} />} />;
62
51
  };
63
-
64
52
  `,
65
53
  };
66
54
  }
@@ -1,17 +1,15 @@
1
1
  import type { AppInfo, LibInfo } from "akanjs";
2
2
 
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
- return {
5
- filename: "task.constant.ts",
6
- content: `import { enumOf } from "akanjs/base";
4
+ return `import { enumOf } from "akanjs/base";
7
5
  import { via } from "akanjs/constant";
8
6
 
9
- import { WorkHistoryEntry } from "../__scalar/workHistory/workHistory.constant";
7
+ import { WorkHistory } from "../__scalar/workHistory/workHistory.constant";
10
8
 
11
9
  // ===== task.constant.ts =====
12
10
  // Convention: <module>.constant.ts — the data shape layer of an Akan.js database module.
13
11
  // Import scalar primitives from akanjs/base; define model layers with via() from akanjs/constant.
14
- // Scalars are embedded via field([ScalarType], ...) — see WorkHistoryEntry embedding below.
12
+ // Scalars are embedded via field([ScalarType], ...) — see WorkHistory embedding below.
15
13
  // Layer order: enum → Input → Object → Light → Full.
16
14
  // Input = user-provided fields; Object = Input + system fields + embedded scalars; Light = subset for list views; Full = Object + Light.
17
15
  // Registered by akan scan into cnst.ts barrel.
@@ -27,19 +25,21 @@ export class TaskInput extends via((field) => ({
27
25
  content: field(String, { default: "" }),
28
26
  })) {}
29
27
 
30
- // TaskObject embeds WorkHistoryEntry as a list field — the scalar embedding pattern.
31
- // field([WorkHistoryEntry], { default: [] }) stores a list of scalar objects in the parent document.
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.
32
30
  // Each status change (create, start, complete) pushes a new entry into this list.
33
31
  export class TaskObject extends via(TaskInput, (field) => ({
34
32
  status: field(TaskStatus, { default: "todo" }),
35
33
  due: field(Date).optional(),
36
- workHistory: field([WorkHistoryEntry], { default: [] }),
34
+ workHistory: field([WorkHistory]),
37
35
  })) {}
38
36
 
39
37
  export class LightTask extends via(TaskObject, ["title", "status", "due"] as const, (resolve) => ({})) {}
40
38
 
41
39
  export class Task extends via(TaskObject, LightTask, (resolve) => ({})) {}
42
40
 
41
+ export class TaskInsight extends via(Task, (field) => ({})) {}
42
+
43
43
  // ---- Expandable additional fields: ----
44
44
  // ===== Add to TaskInput =====
45
45
  // - priority: field(TaskPriority, { default: "medium" })
@@ -49,6 +49,5 @@ export class Task extends via(TaskObject, LightTask, (resolve) => ({})) {}
49
49
  // - completedAt: field(Date).optional()
50
50
  // ===== Add to Task =====
51
51
  // isOverdue(): boolean { return this.due && this.due < new Date() }
52
- `,
53
- };
52
+ `;
54
53
  }
@@ -1,11 +1,9 @@
1
1
  import type { AppInfo, LibInfo } from "akanjs";
2
2
 
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
- return {
5
- filename: "task.dictionary.ts",
6
- content: `import { modelDictionary } from "akanjs/dictionary";
4
+ return `import { modelDictionary } from "akanjs/dictionary";
7
5
 
8
- import type { Task, LightTask } from "./task.constant";
6
+ import type { Task, TaskStatus } from "./task.constant";
9
7
  import type { TaskFilter } from "./task.document";
10
8
 
11
9
  // ===== task.dictionary.ts =====
@@ -27,11 +25,6 @@ export const dictionary = modelDictionary(["en", "ko"])
27
25
  due: t(["Due Date", "마감일"]).desc(["Deadline for completion", "완료 마감일"]),
28
26
  workHistory: t(["Work History", "작업 이력"]).desc(["Status change log", "상태 변경 기록"]),
29
27
  }))
30
- .lightModel<LightTask>((t) => ({
31
- title: t(["Title", "제목"]),
32
- status: t(["Status", "상태"]),
33
- due: t(["Due", "마감"]),
34
- }))
35
28
  .query<TaskFilter>((fn) => ({
36
29
  byStatus: fn(["By Status", "상태별"]).arg((t) => ({
37
30
  status: t(["Status", "상태"]),
@@ -80,6 +73,5 @@ export const dictionary = modelDictionary(["en", "ko"])
80
73
  taskEdit: ["Edit", "수정"],
81
74
  taskNew: ["+ New Task", "+ 새 할 일"],
82
75
  });
83
- `,
84
- };
76
+ `;
85
77
  }
@@ -1,9 +1,7 @@
1
1
  import type { AppInfo, LibInfo } from "akanjs";
2
2
 
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
- return {
5
- filename: "task.document.ts",
6
- content: `import { dayjs } from "akanjs/base";
4
+ return `import { dayjs } from "akanjs/base";
7
5
  import { by, from, into } from "akanjs/document";
8
6
 
9
7
  import { Err } from "../dict";
@@ -32,23 +30,22 @@ export class TaskFilter extends from(cnst.Task, (filter) => ({
32
30
  },
33
31
  })) {}
34
32
 
35
- export class TaskDocument extends by(cnst.Task) {
33
+ export class Task extends by(cnst.Task) {
36
34
  start() {
37
35
  if (this.status !== "todo") throw new Err("task.error.cannotStartFromNonTodo");
38
36
  this.status = "inProgress";
39
- this.workHistory.push({ action: cnst.WorkHistoryAction.started, at: dayjs() } as cnst.WorkHistoryEntry);
37
+ this.workHistory.push({ action: "completed", at: dayjs(), note: "" });
40
38
  return this;
41
39
  }
42
40
 
43
41
  complete() {
44
42
  if (this.status !== "inProgress") throw new Err("task.error.cannotCompleteFromNonInProgress");
45
43
  this.status = "completed";
46
- this.workHistory.push({ action: cnst.WorkHistoryAction.completed, at: dayjs() } as cnst.WorkHistoryEntry);
44
+ this.workHistory.push({ action: "completed", at: dayjs(), note: "" });
47
45
  return this;
48
46
  }
49
47
  }
50
48
 
51
- export class TaskModel extends into(TaskDocument, TaskFilter, cnst.task, () => ({})) {}
52
- `,
53
- };
49
+ export class TaskModel extends into(Task, TaskFilter, cnst.task, () => ({})) {}
50
+ `;
54
51
  }
@@ -1,12 +1,9 @@
1
1
  import type { AppInfo, LibInfo } from "akanjs";
2
2
 
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
- return {
5
- filename: "task.service.ts",
6
- content: `import { dayjs } from "akanjs/base";
4
+ return `import { dayjs } from "akanjs/base";
5
+ import type { DataInputOf } from "akanjs/document";
7
6
  import { serve } from "akanjs/service";
8
-
9
- import * as cnst from "../cnst";
10
7
  import * as db from "../db";
11
8
 
12
9
  // ===== task.service.ts =====
@@ -21,10 +18,10 @@ import * as db from "../db";
21
18
  export class TaskService extends serve(db.task, () => ({})) {
22
19
  // Lifecycle hook: runs before every document creation.
23
20
  // Auto-injects the initial workHistory entry for the scalar embedding pattern.
24
- _preCreate(data: cnst.TaskInput): cnst.TaskInput {
21
+ override _preCreate(data: DataInputOf<db.TaskInput, db.Task>): DataInputOf<db.TaskInput, db.Task> {
25
22
  return {
26
23
  ...data,
27
- workHistory: [{ action: cnst.WorkHistoryAction.created, at: dayjs(), note: "" } as cnst.WorkHistoryEntry],
24
+ workHistory: [{ action: "created", at: dayjs(), note: "" }],
28
25
  };
29
26
  }
30
27
 
@@ -43,6 +40,5 @@ export class TaskService extends serve(db.task, () => ({})) {
43
40
  // export class TaskService extends serve(db.task, ({ service, use, signal, plug, env, memory }) => ({
44
41
  // notiService: service<srv.NotiService>(),
45
42
  // })) {
46
- `,
47
- };
43
+ `;
48
44
  }
@@ -1,9 +1,7 @@
1
1
  import type { AppInfo, LibInfo } from "akanjs";
2
2
 
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
- return {
5
- filename: "task.signal.ts",
6
- content: `import { endpoint } from "akanjs/signal";
4
+ return `import { endpoint, internal, Public, slice } from "akanjs/signal";
7
5
 
8
6
  import * as cnst from "../cnst";
9
7
  import * as srv from "../srv";
@@ -21,6 +19,14 @@ import * as srv from "../srv";
21
19
  // Manual endpoints below: only define endpoints that need custom business logic.
22
20
  // Registered by akan scan into sig.ts barrel.
23
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
+
24
30
  export class TaskEndpoint extends endpoint(srv.task, ({ mutation }) => ({
25
31
  startTask: mutation(cnst.Task)
26
32
  .param("taskId", String)
@@ -44,6 +50,5 @@ export class TaskEndpoint extends endpoint(srv.task, ({ mutation }) => ({
44
50
  // searchTask: query(cnst.LightTask).search("keyword", String).exec(...),
45
51
  // // guards: authentication/authorization guards
46
52
  // // export class TaskEndpoint extends endpoint(srv.task, { guards: { root: SignedIn } }, ({ query, mutation }) => ({})) {}
47
- `,
48
- };
53
+ `;
49
54
  }
@@ -1,11 +1,9 @@
1
1
  import type { AppInfo, LibInfo } from "akanjs";
2
2
 
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
- return {
5
- filename: "task.store.ts",
6
- content: `import { store } from "akanjs/store";
4
+ return `import { store } from "akanjs/store";
7
5
 
8
- import { fetch, sig, msg } from "../useClient";
6
+ import { fetch, msg, sig } from "../useClient";
9
7
 
10
8
  // ===== task.store.ts =====
11
9
  // Convention: <module>.store.ts — client-side state management for a database module.
@@ -40,6 +38,5 @@ export class TaskStore extends store(sig.task, () => ({})) {
40
38
  // filterKeyword: "",
41
39
  // })) { ... }
42
40
  // The auto-generated taskForm handles title, content, due states automatically.
43
- `,
44
- };
41
+ `;
45
42
  }
@@ -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 } from "akanjs/ui";
7
+ import { Load, Link } 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.
@@ -16,17 +16,25 @@ interface PageProps {
16
16
  params: { taskId: string };
17
17
  }
18
18
  export default async function Page({ params: { taskId } }: PageProps) {
19
- const taskEdit = await fetch.editTask(taskId);
19
+ const { taskEdit } = await fetch.editTask(taskId);
20
20
 
21
21
  return (
22
- <Load.Edit
23
- slice={fetch.slice.task}
24
- edit={taskEdit}
25
- type="form"
26
- onSubmit={\`/task/\${params.taskId}\`}
27
- >
28
- <Task.Template.General />
29
- </Load.Edit>
22
+ <main className="mx-auto max-w-2xl px-6 py-8">
23
+ <div className="mb-6">
24
+ <Link href={\`/task/\${taskId}\`} className="btn btn-ghost btn-sm">
25
+ ← Back to Task
26
+ </Link>
27
+ </div>
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>
31
+ </div>
32
+ <div className="rounded-xl border border-base-content/10 bg-base-100 p-6 shadow-sm">
33
+ <Load.Edit slice={fetch.slice.taskInPublic} edit={taskEdit} type="form" onSubmit={\`/task/\${taskId}\`}>
34
+ <Task.Template.General />
35
+ </Load.Edit>
36
+ </div>
37
+ </main>
30
38
  );
31
39
  }
32
40
  `,
@@ -3,9 +3,7 @@ import type { AppInfo, LibInfo } from "akanjs";
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
4
  return {
5
5
  filename: "_index.tsx",
6
- content: `"use client";
7
-
8
- import { fetch, Task, usePage } from "@apps/${dict.appName}/client";
6
+ content: `import { fetch, Task, usePage } from "@apps/${dict.appName}/client";
9
7
  import { Link } from "akanjs/ui";
10
8
 
11
9
  // ===== page/task/_index.tsx =====
@@ -13,26 +11,24 @@ import { Link } from "akanjs/ui";
13
11
  // Server-side data loading via loader() at the page level; passes init/view props to Zone components.
14
12
  // Uses usePage().l() for i18n — the framework convention for dictionary-based translations.
15
13
 
16
- export default function Page() {
14
+ export default async function Page() {
17
15
  const { l } = usePage();
16
+ const { taskInitInPublic } = await fetch.initTaskInPublic();
18
17
  return (
19
18
  <main className="mx-auto max-w-4xl px-6 py-8">
20
19
  <div className="mb-6 flex items-center justify-between">
21
20
  <div>
22
21
  <h1 className="font-extrabold text-3xl text-base-content">{l("task.modelName")}</h1>
23
- <p className="mt-1 text-base-content/60 text-sm">
24
- {l("task.modelDesc")}
25
- </p>
22
+ <p className="mt-1 text-base-content/60 text-sm">{l("task.modelDesc")}</p>
26
23
  </div>
27
24
  <Link href="/task/new" className="btn btn-primary btn-sm">
28
25
  {l("task.taskNew")}
29
26
  </Link>
30
27
  </div>
31
28
 
32
- <Task.Zone.Card className="flex flex-col gap-3" init={fetch.slice.task} />
29
+ <Task.Zone.Card className="flex flex-col gap-3" init={taskInitInPublic} />
33
30
  </main>
34
31
  );
35
- }
36
- `,
32
+ }`,
37
33
  };
38
34
  }
@@ -3,9 +3,8 @@ import type { AppInfo, LibInfo } from "akanjs";
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
4
  return {
5
5
  filename: "new.tsx",
6
- content: `import { fetch, usePage } from "@apps/${dict.appName}/client";
7
- import { Load } from "akanjs/ui";
8
- import { cnst, Task } from "@apps/${dict.appName}/client";
6
+ content: `import { type cnst, fetch, Task, usePage } from "@apps/${dict.appName}/client";
7
+ import { Load, Link } from "akanjs/ui";
9
8
 
10
9
  // ===== page/task/new.tsx =====
11
10
  // Convention: Server-side form page using Load.Edit from akanjs/ui.
@@ -18,17 +17,23 @@ export default async function Page() {
18
17
  const taskForm: Partial<cnst.Task> = { status: "todo" };
19
18
 
20
19
  return (
21
- <Load.Edit
22
- slice={fetch.slice.task}
23
- edit={taskForm}
24
- type="form"
25
- onCancel="back"
26
- onSubmit="/task"
27
- >
28
- <Task.Template.General />
29
- </Load.Edit>
20
+ <main className="mx-auto max-w-2xl px-6 py-8">
21
+ <div className="mb-6">
22
+ <Link href="/task" className="btn btn-ghost btn-sm">
23
+ ← Tasks
24
+ </Link>
25
+ </div>
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>
29
+ </div>
30
+ <div className="rounded-xl border border-base-content/10 bg-base-100 p-6 shadow-sm">
31
+ <Load.Edit slice={fetch.slice.taskInPublic} edit={taskForm} type="form" onCancel="back" onSubmit="/task">
32
+ <Task.Template.General />
33
+ </Load.Edit>
34
+ </div>
35
+ </main>
30
36
  );
31
- }
32
- `,
37
+ }`,
33
38
  };
34
39
  }
@@ -7,7 +7,7 @@ interface Dict {
7
7
  }
8
8
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dict) {
9
9
  return `
10
- import { by, from, into, type SchemaOf } from "akanjs/document";
10
+ import { by, from, into } from "akanjs/document";
11
11
 
12
12
  import * as cnst from "../cnst";
13
13