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

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 (35) 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 +38 -0
  7. package/templates/appSample/lib/__scalar/workHistory/workHistory.dictionary.ts +28 -0
  8. package/templates/appSample/lib/_noti/noti.abstract.ts +38 -0
  9. package/templates/appSample/lib/_noti/noti.dictionary.ts +25 -0
  10. package/templates/appSample/lib/_noti/noti.service.ts +31 -0
  11. package/templates/appSample/lib/_noti/noti.signal.ts +30 -0
  12. package/templates/appSample/lib/_noti/noti.store.ts +38 -0
  13. package/templates/appSample/lib/task/Task.Template.tsx +49 -0
  14. package/templates/appSample/lib/task/Task.Unit.tsx +70 -0
  15. package/templates/appSample/lib/task/Task.Util.tsx +103 -0
  16. package/templates/appSample/lib/task/Task.View.tsx +94 -0
  17. package/templates/appSample/lib/task/Task.Zone.tsx +66 -0
  18. package/templates/appSample/lib/task/task.abstract.ts +40 -0
  19. package/templates/appSample/lib/task/task.constant.ts +54 -0
  20. package/templates/appSample/lib/task/task.dictionary.ts +85 -0
  21. package/templates/appSample/lib/task/task.document.ts +54 -0
  22. package/templates/appSample/lib/task/task.service.ts +48 -0
  23. package/templates/appSample/lib/task/task.signal.ts +49 -0
  24. package/templates/appSample/lib/task/task.store.ts +45 -0
  25. package/templates/appSample/page/task/[taskId]/_index.tsx +39 -0
  26. package/templates/appSample/page/task/[taskId]/edit.tsx +34 -0
  27. package/templates/appSample/page/task/_index.tsx +38 -0
  28. package/templates/appSample/page/task/_layout.tsx +24 -0
  29. package/templates/appSample/page/task/new.tsx +34 -0
  30. package/templates/appSample/srvkit/AuthGuard.ts +35 -0
  31. package/templates/appSample/srvkit/SessionInternalArg.ts +32 -0
  32. package/templates/appSample/ui/GlobalLoading.tsx +30 -0
  33. package/templates/appSample/ui/QuantityControl.tsx +52 -0
  34. package/templates/appSample/webkit/useDebounce.ts +41 -0
  35. package/templates/workspaceRoot/AGENTS.md.template +470 -16
package/index.js CHANGED
@@ -10761,7 +10761,7 @@ class ApplicationRunner extends runner("application") {
10761
10761
  await workspace.applyTemplate({
10762
10762
  basePath: `apps/${appName}`,
10763
10763
  template: "app",
10764
- dict: { appName, companyName: workspace.repoName, startDomain: "localhost" },
10764
+ dict: { appName },
10765
10765
  options: { libs }
10766
10766
  });
10767
10767
  return AppExecutor.from(workspace, appName);
@@ -13996,6 +13996,12 @@ class WorkspaceScript extends script("workspace", [
13996
13996
  await this.libraryScript.installLibrary(workspace, "shared");
13997
13997
  }
13998
13998
  await this.applicationScript.createApplication(appName, workspace, { libs: installLibs ? ["util", "shared"] : [] });
13999
+ await workspace.applyTemplate({
14000
+ basePath: `apps/${appName}`,
14001
+ template: "appSample",
14002
+ dict: { appName },
14003
+ options: { libs: installLibs ? ["util", "shared"] : [] }
14004
+ });
13999
14005
  const gitSpinner = workspace.spinning("Initializing git repository and commit...");
14000
14006
  try {
14001
14007
  await workspace.commit("Initial commit", { init: true });
@@ -14063,7 +14069,8 @@ class WorkspaceCommand extends command("workspace", [WorkspaceScript], ({ public
14063
14069
  label: "Yes, I want to accelerate development by installing shared and util libraries (for akanjs experts)",
14064
14070
  value: true
14065
14071
  }
14066
- ]
14072
+ ],
14073
+ default: false
14067
14074
  }).option("init", Boolean, {
14068
14075
  desc: "Do you want to initialize the workspace? (Recommended)",
14069
14076
  default: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "2.3.1-rc.3",
3
+ "version": "2.3.1-rc.4",
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.3",
38
+ "akanjs": "2.3.1-rc.4",
39
39
  "chalk": "^5.6.2",
40
40
  "commander": "^14.0.3",
41
41
  "daisyui": "^5.5.20",
@@ -0,0 +1,32 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "formatters.ts",
6
+ content: `// ===== formatters.ts =====
7
+ // Convention: common/ folder — only pure functions that run on both server and client.
8
+ // Cannot import window, Bun, process.env, or any runtime-specific API.
9
+ // Naming: camelCase .ts, file name = primary export name.
10
+ // Scanned by akan scan into common/index.ts barrel automatically.
11
+
12
+ export function trimString(str: string, maxLength = 50, suffix = "...") {
13
+ if (str.length <= maxLength) return str;
14
+ return str.slice(0, maxLength - suffix.length) + suffix;
15
+ }
16
+
17
+ export function formatKebabToPascal(str: string) {
18
+ return str
19
+ .split("-")
20
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
21
+ .join("");
22
+ }
23
+
24
+ // ---- Expandable additional fields: ----
25
+ // File size formatter: 1024 → "1 KB"
26
+ // export function formatBytes(bytes: number, decimals = 2) { ... }
27
+ //
28
+ // Relative date time: "3 hours ago", "just now"
29
+ // export function formatRelativeTime(date: Date) { ... }
30
+ `,
31
+ };
32
+ }
@@ -0,0 +1,32 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "validators.ts",
6
+ content: `// ===== validators.ts =====
7
+ // Convention: common/ folder — only pure functions that run on both server and client.
8
+ // Cannot import window, Bun, process.env, or any runtime-specific API.
9
+ // Naming: camelCase .ts, file name = primary export name.
10
+ // Scanned by akan scan into common/index.ts barrel automatically.
11
+
12
+ export function isValidEmail(email: string): boolean {
13
+ const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
14
+ return emailRegex.test(email);
15
+ }
16
+
17
+ export function isValidTaskTitle(title: string): boolean {
18
+ return title.trim().length > 0;
19
+ }
20
+
21
+ // ---- Expandable additional fields: ----
22
+ // URL validation
23
+ // export function isValidUrl(url: string) { try { new URL(url); return true; } catch { return false; } }
24
+ //
25
+ // Password strength check (8+ chars, special character)
26
+ // export function isStrongPassword(password: string) { ... }
27
+ //
28
+ // Phone number format validation
29
+ // export function isValidPhone(phone: string, locale = "KR") { ... }
30
+ `,
31
+ };
32
+ }
@@ -0,0 +1,47 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "workHistory.abstract.md",
6
+ content: `# WorkHistory Scalar Abstract
7
+
8
+ ## Akan.js Module Pattern
9
+
10
+ WorkHistory is a **scalar module** — a reusable value type under lib/__scalar/.
11
+ Scalar modules are embedded into models via field([ScalarType]), not stored independently.
12
+ They live under the double-underscore prefix to distinguish them from database models.
13
+
14
+ Standard scalar layers:
15
+ - workHistory.constant.ts — enum (WorkHistoryAction) + entry class (WorkHistoryEntry) defined via via() + enumOf()
16
+ - workHistory.dictionary.ts — i18n via scalarDictionary() (fewer layers than modelDictionary)
17
+ - workHistory.abstract.md — embedding rules and intent
18
+
19
+ ## Convention: lib/__scalar/<type>/ directory naming
20
+
21
+ - Scalar modules use double-underscore prefix: lib/__scalar/workHistory/
22
+ - Files drop the underscores: workHistory.constant.ts, not __workHistory.constant.ts
23
+ - Models embed scalars via field([WorkHistoryEntry], { default: [] }) — list-of-scalar embedding
24
+
25
+ ## Embedding Example (in task.constant.ts)
26
+
27
+ TaskObject embeds a list of WorkHistoryEntry:
28
+ import { WorkHistoryEntry } from "../../__scalar/workHistory/workHistory.constant";
29
+ export class TaskObject extends via(TaskInput, (field) => ({
30
+ workHistory: field([WorkHistoryEntry], { default: [] }),
31
+ }))
32
+
33
+ Each status change (create, start, complete) pushes a new entry.
34
+
35
+ ## Agent Notes
36
+
37
+ - Read this abstract before modifying the WorkHistory scalar.
38
+ - Update when new action types or fields are added.
39
+ - Do not duplicate workHistory fields in individual models; always reuse this scalar.
40
+
41
+ ## Related Modules
42
+
43
+ - lib/task/ — database module embedding workHistory as a list field
44
+ - lib/_noti/ — pure service module (different module type, no scalar usage)
45
+ `,
46
+ };
47
+ }
@@ -0,0 +1,38 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
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";
7
+ import { via } from "akanjs/constant";
8
+
9
+ // ===== workHistory.constant.ts =====
10
+ // Convention: lib/__scalar/<type>/ — scalar modules use double-underscore prefix.
11
+ // Scalars are embedded value shapes, reused across models via field([ScalarType], { default: [] }).
12
+ // Define enums with enumOf() and entry classes with via() from akanjs/constant.
13
+
14
+ // ---- Enum ----
15
+ // WorkHistoryAction: the type of change recorded in a work history entry
16
+ // Scalar enums follow the same enumOf() convention as model enums
17
+ export class WorkHistoryAction extends enumOf("workHistoryAction", [
18
+ "created",
19
+ "started",
20
+ "completed",
21
+ ] as const) {}
22
+
23
+ // ---- Scalar Entry ----
24
+ // WorkHistoryEntry: a single entry in a model's workHistory list
25
+ // 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) => ({
28
+ action: field(WorkHistoryAction),
29
+ at: field(Date, { default: () => dayjs() }),
30
+ note: field(String, { default: "" }),
31
+ })) {}
32
+
33
+ // ---- Expandable additional fields: ----
34
+ // - actor: field(String).optional() — who performed the action
35
+ // - previousStatus: field(TaskStatus).optional() — status before the change
36
+ `,
37
+ };
38
+ }
@@ -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: "workHistory.dictionary.ts",
6
+ content: `import { scalarDictionary } from "akanjs/dictionary";
7
+
8
+ import type { WorkHistoryAction, WorkHistoryEntry } from "./workHistory.constant";
9
+
10
+ // ===== workHistory.dictionary.ts =====
11
+ // Convention: scalarDictionary() for scalar module i18n — fewer layers than modelDictionary().
12
+ // Scalar dictionaries define field labels and enum value labels only; no model/insight/query/sort layers.
13
+
14
+ export const dictionary = scalarDictionary(["en", "ko"])
15
+ .model<WorkHistoryEntry>((t) => ({
16
+ action: t(["Action", "작업"]).desc(["Type of work history event", "작업 이력 이벤트 유형"]),
17
+ at: t(["At", "시간"]).desc(["When the event occurred", "이벤트 발생 시간"]),
18
+ note: t(["Note", "메모"]).desc(["Optional note about the event", "이벤트에 대한 선택적 메모"]),
19
+ }))
20
+ .enum<WorkHistoryAction>("workHistoryAction", (t) => ({
21
+ created: t(["Created", "생성됨"]).desc(["Task was created", "할 일이 생성됨"]),
22
+ started: t(["Started", "시작됨"]).desc(["Task was started", "할 일이 시작됨"]),
23
+ completed: t(["Completed", "완료됨"]).desc(["Task was completed", "할 일이 완료됨"]),
24
+ }))
25
+ .translate({});
26
+ `,
27
+ };
28
+ }
@@ -0,0 +1,38 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
3
+ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
+ return {
5
+ filename: "noti.abstract.md",
6
+ content: `# Noti Abstract
7
+
8
+ ## Akan.js Module Pattern
9
+
10
+ Noti is a **pure service module** — not a database module. It demonstrates the Akan.js convention
11
+ for modules that orchestrate behavior without owning a persistent document model.
12
+
13
+ Both module types follow the same layer pattern, but service modules have fewer layers:
14
+ no constant.ts (no data shape), no document.ts (no DB queries), no UI components.
15
+
16
+ - noti.service.ts — business logic via serve("noti" as const, ...) (named service, no DB binding)
17
+ - noti.signal.ts — real-time pubsub endpoint bound via endpoint(srv.noti, ...)
18
+ - noti.store.ts — client state via store("noti" as const, ...) (named store, no signal binding)
19
+ - noti.dictionary.ts — i18n via serviceDictionary() (no model field labels)
20
+
21
+ ## Convention: lib/_<service>/ directory naming
22
+
23
+ - Service modules live under lib/_<service>/ — the underscore prefix signals "this is a service, not a database model."
24
+ - Files drop the underscore: lib/_noti/noti.service.ts, not lib/_noti/_noti.service.ts.
25
+ - Scan registers into the same barrel files: srv.ts, sig.ts, st.ts, dict.ts.
26
+
27
+ ## Convention: named serve/store (no DB binding)
28
+
29
+ - serve("noti" as const, ...) — service name passed as string, no db.<module> binding.
30
+ - store("noti" as const, ...) — store name passed as string, no sig.<module> binding.
31
+
32
+ ## Related Modules
33
+
34
+ - lib/task/ — database module reference (compare structure differences)
35
+ - lib/__scalar/workHistory/ — reusable scalar field modules (double-underscore prefix, embedded in models)
36
+ `,
37
+ };
38
+ }
@@ -0,0 +1,25 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
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";
7
+
8
+ // ===== noti.dictionary.ts =====
9
+ // Convention: <module>.dictionary.ts for a pure service module.
10
+ // Uses serviceDictionary(["en", "ko"]) from akanjs/dictionary — the framework convention for service dictionaries.
11
+ // Unlike modelDictionary, no model/lightModel/query/sort/enum sections (service modules have no DB model).
12
+ // Sections: .endpoint() for signal endpoint names, .translate() for UI messages.
13
+ // Registered by akan scan into dict.ts barrel.
14
+
15
+ export const dictionary = serviceDictionary(["en", "ko"])
16
+ .endpoint<typeof import("./noti.signal").NotiEndpoint>((fn) => ({
17
+ send: fn(["Send Notification", "알림 보내기"]),
18
+ }))
19
+ .translate({
20
+ notiReceived: ["New notification", "새 알림이 도착했습니다"],
21
+ notiMarkAllRead: ["Mark all as read", "모두 읽음으로 표시"],
22
+ });
23
+ `,
24
+ };
25
+ }
@@ -0,0 +1,31 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
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";
8
+
9
+ // ===== noti.service.ts =====
10
+ // Convention: <module>.service.ts for a pure service module.
11
+ // Extends serve("noti" as const, ...) — named service (string literal), no DB model binding.
12
+ // DB modules use serve(db.<module>, ...); service modules use serve("<name>" as const, ...).
13
+ // DI deps available: { service, use, signal, plug, env, memory }.
14
+ // Registered by akan scan into srv.ts barrel.
15
+
16
+ export class NotiService extends serve("noti" as const, () => ({})) {}
17
+
18
+ // ---- Expandable additional fields: ----
19
+ // export class NotiService extends serve("noti" as const, ({ plug, env, service }) => ({
20
+ // pushApi: plug(PushApi),
21
+ // fcmKey: env((options) => options.fcmKey),
22
+ // taskService: service<srv.TaskService>(),
23
+ // })) {
24
+ // async sendPush(userId: string, title: string, body: string) {
25
+ // const token = await this.getDeviceToken(userId);
26
+ // return this.pushApi.send({ token, title, body });
27
+ // }
28
+ // }
29
+ `,
30
+ };
31
+ }
@@ -0,0 +1,30 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
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";
8
+
9
+ import * as srv from "../srv";
10
+
11
+ // ===== noti.signal.ts =====
12
+ // Convention: <module>.signal.ts for a pure service module.
13
+ // Extends endpoint(srv.<module>, ...) — pubsub endpoint for real-time server→client communication.
14
+ // pubsub() is the Akan.js convention for publish-subscribe: server publishes, all connected clients receive.
15
+ // Client subscribes via fetch.subscribeSend((data) => { ... }).
16
+ // Registered by akan scan into sig.ts barrel.
17
+
18
+ export class NotiEndpoint extends endpoint(srv.noti, ({ pubsub }) => ({
19
+
20
+ })) {}
21
+
22
+ // ---- Expandable additional fields: ----
23
+ // history: query(NotiHistory)
24
+ // .param("userId", String)
25
+ // .exec(async function (userId) {
26
+ // return await this.notiService.getHistory(userId);
27
+ // }),
28
+ `,
29
+ };
30
+ }
@@ -0,0 +1,38 @@
1
+ import type { AppInfo, LibInfo } from "akanjs";
2
+
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";
7
+ import { store } from "akanjs/store";
8
+
9
+ // ===== noti.store.ts =====
10
+ // Convention: <module>.store.ts for a pure service module.
11
+ // Extends store("noti" as const, ...) — named store (string literal), no signal binding.
12
+ // DB modules use store(sig.<module>, ...); service modules use store("<name>" as const, ...).
13
+ // State definitions use direct property assignment. Actions use this.set() / this.get().
14
+ // Registered by akan scan into st.ts barrel.
15
+
16
+ export class NotiStore extends store("noti" as const, () => ({
17
+ notiList: [] as { id: string; type: string; message: string; sentAt: dayjs.Dayjs }[],
18
+ unreadCount: 0,
19
+ })) {
20
+ addNoti(noti: { type: string; message: string; sentAt: dayjs.Dayjs }) {
21
+ const id = Math.random().toString(36).slice(2);
22
+ this.set({
23
+ notiList: [...(this.get().notiList ?? []), { ...noti, id }],
24
+ unreadCount: (this.get().unreadCount ?? 0) + 1,
25
+ });
26
+ }
27
+ markAllRead() {
28
+ this.set({ unreadCount: 0 });
29
+ }
30
+ removeNoti(id: string) {
31
+ this.set({
32
+ notiList: (this.get().notiList ?? []).filter((n) => n.id !== id),
33
+ });
34
+ }
35
+ }
36
+ `,
37
+ };
38
+ }
@@ -0,0 +1,49 @@
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.Template.tsx",
6
+ content: `"use client";
7
+
8
+ import { clsx } from "akanjs/client";
9
+ import { Field, Layout } from "akanjs/ui";
10
+
11
+ import { st, usePage } from "@apps/${dict.appName}/client";
12
+
13
+ // ===== Task.Template.tsx =====
14
+ // Convention: lib/<module>/ — PascalCase .tsx, Template suffix = form/edit template component.
15
+ // Uses akanjs/ui Field and Layout components: the framework convention for controlled form UIs.
16
+ // "use client" directive required — form events (onChange) are client-side only.
17
+ // Uses st.use.taskForm() — auto-generated form state bound to the model (handles create + edit).
18
+ // Auto-generated setters: st.do.setTitleOnTask, st.do.setContentOnTask, st.do.setDueOnTask.
19
+ // Invoked by Akan's data-loading zone chain: Task.Zone → Load.Edit → Task.Template.
20
+
21
+ interface TaskEditProps {
22
+ className?: string;
23
+ }
24
+
25
+ export const General = ({ className }: TaskEditProps) => {
26
+ const taskForm = st.use.taskForm();
27
+ const { l } = usePage();
28
+
29
+ return (
30
+ <Layout.Template className={clsx("flex flex-col gap-4", className)}>
31
+ <Field.Text
32
+ label={l("task.title")}
33
+ desc={l("task.title.desc")}
34
+ value={taskForm.title}
35
+ onChange={st.do.setTitleOnTask}
36
+ />
37
+ <Field.Text
38
+ label={l("task.content")}
39
+ desc={l("task.content.desc")}
40
+ value={taskForm.content}
41
+ onChange={st.do.setContentOnTask}
42
+ multiline
43
+ />
44
+ </Layout.Template>
45
+ );
46
+ };
47
+ `,
48
+ };
49
+ }
@@ -0,0 +1,70 @@
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.Unit.tsx",
6
+ content: `import { type ModelProps, clsx } from "akanjs/client";
7
+ import { Layout } from "akanjs/ui";
8
+
9
+ import * as cnst from "@apps/${dict.appName}/lib/cnst";
10
+ import { usePage } from "@apps/${dict.appName}/client";
11
+
12
+ // ===== Task.Unit.tsx =====
13
+ // Convention: lib/<module>/ — PascalCase .tsx, Unit suffix = card/list-item component.
14
+ // Unit receives LightModel as props via the ModelProps<modelName, LightModelType> generic from akanjs/client.
15
+ // Wraps content in Layout.Unit — Akan.js convention for navigable cards in a list.
16
+ // Uses usePage().l() for i18n — the framework convention for dictionary-based translations.
17
+ // Enum values are accessed via l("taskStatus.todo") — dotted path to dictionary enum keys.
18
+ // Invoked by Akan's data-loading zone chain: Task.Zone → Load.Units → Task.Unit.
19
+
20
+ interface CardProps extends ModelProps<"task", cnst.LightTask> {
21
+ className?: string;
22
+ }
23
+
24
+ export const Card = ({ task, className, href }: CardProps) => {
25
+ const { l } = usePage();
26
+ const statusBadge = {
27
+ todo: "badge-ghost",
28
+ inProgress: "badge-primary",
29
+ completed: "badge-success",
30
+ }[task.status];
31
+
32
+ return (
33
+ <Layout.Unit
34
+ className={clsx(
35
+ "rounded-lg border border-base-content/10 bg-base-100 p-4 transition-shadow hover:shadow-md",
36
+ className,
37
+ )}
38
+ href={href}
39
+ >
40
+ <div className="flex items-start justify-between gap-2">
41
+ <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>
45
+ </div>
46
+ {task.due && (
47
+ <div className="mt-2 text-base-content/60 text-xs">
48
+ {l("task.taskDueLabel")} {task.due.toDate().toLocaleDateString()}
49
+ </div>
50
+ )}
51
+ </Layout.Unit>
52
+ );
53
+ };
54
+
55
+ // ---- Expandable additional fields: ----
56
+ // Mini: smaller inline display (tag, chip style)
57
+ // export const Mini = ({ task }: MiniProps) => (
58
+ // <span className="inline-flex items-center gap-1 text-sm">
59
+ // <span className={clsx("badge badge-xs", statusBadge)}>{task.status}</span>
60
+ // {task.title}
61
+ // </span>
62
+ // );
63
+ //
64
+ // Abstract: abstracted summary display (search results, previews)
65
+ // export const Abstract = ({ task }: AbstractProps) => (
66
+ // <div className="text-sm">{task.title} — {l("taskStatus", task.status)}</div>
67
+ // );
68
+ `,
69
+ };
70
+ }
@@ -0,0 +1,103 @@
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.Util.tsx",
6
+ content: `"use client";
7
+
8
+ import { st, usePage } from "@apps/${dict.appName}/client";
9
+ import { Model } from "akanjs/ui";
10
+
11
+ // ===== Task.Util.tsx =====
12
+ // Convention: lib/<module>/ — PascalCase .tsx, Util suffix = action buttons/utility components.
13
+ // "use client" directive required for onClick handlers.
14
+ // Dispatches to store actions via st.do.xxx() — the Akan.js convention for invoking client-side operations.
15
+ // Uses usePage().l() for i18n — the framework convention for dictionary-based translations.
16
+ // Wraps destructive actions in Model.Remove from akanjs/ui — the framework convention for confirmation dialogs.
17
+
18
+ interface StartProps {
19
+ taskId: string;
20
+ }
21
+
22
+ export const Start = ({ taskId }: StartProps) => (
23
+ <button
24
+ className="btn btn-xs border-primary/20 bg-primary/10 text-primary hover:bg-primary hover:text-primary-content"
25
+ onClick={() => st.do.startTask(taskId)}
26
+ >
27
+ {usePage().l("task.taskStart")}
28
+ </button>
29
+ );
30
+
31
+ interface CompleteProps {
32
+ taskId: string;
33
+ }
34
+
35
+ export const Complete = ({ taskId }: CompleteProps) => (
36
+ <button
37
+ className="btn btn-xs border-success/20 bg-success/10 text-success hover:bg-success hover:text-success-content"
38
+ onClick={() => st.do.completeTask(taskId)}
39
+ >
40
+ {usePage().l("task.taskComplete")}
41
+ </button>
42
+ );
43
+
44
+ interface RemoveProps {
45
+ taskId: string;
46
+ }
47
+
48
+ export const Remove = ({ taskId }: RemoveProps) => (
49
+ <Model.Remove modelId={taskId}>
50
+ <button className="btn btn-xs btn-ghost text-error">{usePage().l("task.taskRemove")}</button>
51
+ </Model.Remove>
52
+ );
53
+
54
+ interface ToolboxProps {
55
+ taskId: string;
56
+ status: string;
57
+ }
58
+
59
+ export const Toolbox = ({ taskId, status }: ToolboxProps) => {
60
+ const { l } = usePage();
61
+ return (
62
+ <div className="dropdown dropdown-end">
63
+ <button tabIndex={0} className="btn btn-xs btn-ghost">
64
+ ···
65
+ </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">
67
+ {status === "todo" && (
68
+ <li>
69
+ <button onClick={() => st.do.startTask(taskId)}>{l("task.taskStart")}</button>
70
+ </li>
71
+ )}
72
+ {status === "inProgress" && (
73
+ <li>
74
+ <button onClick={() => st.do.completeTask(taskId)}>{l("task.taskComplete")}</button>
75
+ </li>
76
+ )}
77
+ <li>
78
+ <button className="text-error" onClick={() => st.do.removeTask(taskId)}>
79
+ {l("task.taskRemove")}
80
+ </button>
81
+ </li>
82
+ </ul>
83
+ </div>
84
+ );
85
+ };
86
+
87
+ // ---- Expandable additional fields: ----
88
+ // Edit Button: Model.Edit wrapper — triggers modal edit form
89
+ // export const Edit = ({ taskId }: EditProps) => (
90
+ // <Model.Edit renderTitle="title" modelId={taskId}>
91
+ // <Task.Template.General />
92
+ // </Model.Edit>
93
+ // );
94
+ //
95
+ // New Task Button: Model.NewWrapper — triggers new Task creation form
96
+ // export const NewTask = () => (
97
+ // <Model.NewWrapper partial={{}}>
98
+ // <button className="btn btn-primary btn-sm">+ New Task</button>
99
+ // </Model.NewWrapper>
100
+ // );
101
+ `,
102
+ };
103
+ }