@akanjs/cli 2.3.1-rc.2 → 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.
- package/index.js +9 -2
- package/package.json +2 -2
- package/templates/appSample/common/formatters.ts +32 -0
- package/templates/appSample/common/validators.ts +32 -0
- package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +47 -0
- package/templates/appSample/lib/__scalar/workHistory/workHistory.constant.ts +38 -0
- package/templates/appSample/lib/__scalar/workHistory/workHistory.dictionary.ts +28 -0
- package/templates/appSample/lib/_noti/noti.abstract.ts +38 -0
- package/templates/appSample/lib/_noti/noti.dictionary.ts +25 -0
- package/templates/appSample/lib/_noti/noti.service.ts +31 -0
- package/templates/appSample/lib/_noti/noti.signal.ts +30 -0
- package/templates/appSample/lib/_noti/noti.store.ts +38 -0
- package/templates/appSample/lib/task/Task.Template.tsx +49 -0
- package/templates/appSample/lib/task/Task.Unit.tsx +70 -0
- package/templates/appSample/lib/task/Task.Util.tsx +103 -0
- package/templates/appSample/lib/task/Task.View.tsx +94 -0
- package/templates/appSample/lib/task/Task.Zone.tsx +66 -0
- package/templates/appSample/lib/task/task.abstract.ts +40 -0
- package/templates/appSample/lib/task/task.constant.ts +54 -0
- package/templates/appSample/lib/task/task.dictionary.ts +85 -0
- package/templates/appSample/lib/task/task.document.ts +54 -0
- package/templates/appSample/lib/task/task.service.ts +48 -0
- package/templates/appSample/lib/task/task.signal.ts +49 -0
- package/templates/appSample/lib/task/task.store.ts +45 -0
- package/templates/appSample/page/task/[taskId]/_index.tsx +39 -0
- package/templates/appSample/page/task/[taskId]/edit.tsx +34 -0
- package/templates/appSample/page/task/_index.tsx +38 -0
- package/templates/appSample/page/task/_layout.tsx +24 -0
- package/templates/appSample/page/task/new.tsx +34 -0
- package/templates/appSample/srvkit/AuthGuard.ts +35 -0
- package/templates/appSample/srvkit/SessionInternalArg.ts +32 -0
- package/templates/appSample/ui/GlobalLoading.tsx +30 -0
- package/templates/appSample/ui/QuantityControl.tsx +52 -0
- package/templates/appSample/webkit/useDebounce.ts +41 -0
- package/templates/workspaceRoot/AGENTS.md.template +470 -16
- package/templates/workspaceRoot/package.json.template +4 -1
- package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +62 -0
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
|
|
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
|
+
"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.
|
|
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
|
+
}
|