akanjs 3.0.0-alpha.25 → 3.0.0-alpha.26

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 (43) hide show
  1. package/client/clientRuntime.ts +2 -0
  2. package/dictionary/base.dictionary.ts +5 -0
  3. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  4. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  5. package/package.json +1 -1
  6. package/signal/serializer/fetch.serializer.ts +4 -0
  7. package/store/agent/AgentContext.ts +2 -2
  8. package/store/agent/StoreCatalogue.ts +2 -23
  9. package/types/client/clientRuntime.d.ts +2 -0
  10. package/types/dictionary/base.dictionary.d.ts +1 -1
  11. package/types/dictionary/dictionary.d.ts +8 -8
  12. package/types/store/agent/AgentContext.d.ts +2 -2
  13. package/types/store/agent/StoreCatalogue.d.ts +1 -1
  14. package/types/ui/Data/Dashboard.d.ts +3 -2
  15. package/types/ui/Data/Insight.d.ts +1 -1
  16. package/types/ui/Data/ListContainer.d.ts +3 -3
  17. package/types/ui/Data/dataExport.d.ts +5 -0
  18. package/types/ui/Data/dataText.d.ts +6 -0
  19. package/types/ui/Model/AdminPanel.d.ts +6 -2
  20. package/ui/BottomSheet.tsx +1 -1
  21. package/ui/Data/CardList.tsx +14 -3
  22. package/ui/Data/Dashboard.tsx +34 -50
  23. package/ui/Data/Insight.tsx +18 -12
  24. package/ui/Data/Item.tsx +42 -56
  25. package/ui/Data/ListContainer.tsx +116 -116
  26. package/ui/Data/TableList.tsx +22 -13
  27. package/ui/Data/dataExport.ts +54 -0
  28. package/ui/Data/dataText.ts +22 -0
  29. package/ui/KeyboardAvoiding.tsx +2 -2
  30. package/ui/Layout/Sider.tsx +1 -1
  31. package/ui/Link/CsrLink.tsx +1 -1
  32. package/ui/Menu.tsx +1 -1
  33. package/ui/Model/AdminPanel.tsx +29 -11
  34. package/ui/Signal/Doc.tsx +2 -2
  35. package/ui/Signal/RestApi.tsx +2 -2
  36. package/ui/System/CSR.tsx +1 -1
  37. package/ui/System/Client.tsx +5 -5
  38. package/ui/System/DevModeToggle.tsx +1 -1
  39. package/ui/System/Gtag.tsx +2 -2
  40. package/ui/System/Messages.tsx +2 -2
  41. package/ui/System/ThemeToggle.tsx +43 -63
  42. package/ui/Table.tsx +1 -1
  43. package/webkit/useFrameRuntime.ts +1 -1
@@ -47,6 +47,8 @@ type RuntimeFetch = typeof globalThis.fetch & {
47
47
  serializedSignal: Record<string, unknown>;
48
48
  setJwt: (jwt: string | null) => void;
49
49
  slice: Record<string, SliceMeta>;
50
+ /** Sort keys per model refName, registered from each serialized signal's filter. */
51
+ sortKeyMap?: Map<string, string[]>;
50
52
  ws: RuntimeWs;
51
53
  [key: string]: unknown;
52
54
  };
@@ -89,6 +89,11 @@ export const baseDictionary = serviceDictionary(["en", "ko"])
89
89
  removeMsg: ["Are you sure to remove?", "정말로 삭제하시겠습니까?"],
90
90
  confirmMsg: ["Are you sure to {actionType}", "진행하시겠습니까? ({actionType})"],
91
91
  perPage: ["/page", "/페이지"],
92
+ refresh: ["Refresh", "새로고침"],
93
+ cardView: ["Card view", "카드 보기"],
94
+ tableView: ["Table view", "표 보기"],
95
+ exportCsv: ["Export CSV", "CSV 내보내기"],
96
+ exportJson: ["Export JSON", "JSON 내보내기"],
92
97
  actions: ["Actions", "작업"],
93
98
  new: ["New", "신규"],
94
99
  edit: ["Edit", "수정"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.25",
3
+ "version": "3.0.0-alpha.26",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -1,6 +1,7 @@
1
1
  import { type Cls, ENDPOINT_META, PrimitiveRegistry, type PrimitiveScalar, SLICE_META } from "akanjs/base";
2
2
  import { Logger } from "akanjs/common";
3
3
  import { ConstantRegistry, type ConstantType } from "akanjs/constant";
4
+ import { getFilterMeta } from "akanjs/document";
4
5
  import type { LiveRegistry } from "akanjs/service";
5
6
  import type {
6
7
  ArgInfo,
@@ -90,9 +91,12 @@ export class FetchSerializer {
90
91
  for (const [key, endpointInfo] of Object.entries(endpointMeta)) {
91
92
  endpoint[key] = FetchSerializer.#serializeEndpoint(endpointInfo);
92
93
  }
94
+
95
+ const sortKeys = Object.keys(getFilterMeta(sliceCls.srv.db.filter, { allowEmpty: true })?.sort ?? {});
93
96
  return {
94
97
  ...(prefix ? { prefix } : {}),
95
98
  ...(Object.keys(slice).length ? { slice } : {}),
99
+ ...(sortKeys.length ? { filter: { filter: {}, sortKeys } } : {}),
96
100
  ...(sliceCls.getGuards.filter((g) => g.name !== "None").length
97
101
  ? { getGuards: sliceCls.getGuards.map((g) => g.name) }
98
102
  : {}),
@@ -8,8 +8,8 @@ import { ensureStoreSurface } from "./storeSurface";
8
8
  * The akan default context for a turn: where the user is (route), what is on screen (scopes and their curated
9
9
  * resources), and which store keys the mounted components are reading (live keys). Live entries carry names and
10
10
  * small primitives only — anything bigger is one `readState` call away, masked — so the block cannot bloat with
11
- * what happens to be in the store. Base-store keys are excluded by the base store's own `agent: false`
12
- * declaration, and the route block carries the three of them that matter.
11
+ * what happens to be in the store. Base-store plumbing is kept off the surface by `st.use.x({ agent: false })`
12
+ * at each call site, and the route block carries the three of them that matter.
13
13
  */
14
14
  export class AgentContext {
15
15
  static of(): AgentContext {
@@ -1,10 +1,9 @@
1
- import { type Cls, STATE_META } from "akanjs/base";
1
+ import type { Cls } from "akanjs/base";
2
2
  import { ConstantRegistry } from "akanjs/constant";
3
3
  import type { AgentRefusal } from "akanjs/signal";
4
4
  import { databaseStateModelTypes, databaseStateNames } from "../databaseStateNames";
5
5
  import type { SliceStateKey } from "../state";
6
6
  import type { StoreInstance } from "../storeInstance";
7
- import { StoreRegistry } from "../storeRegistry";
8
7
  import type { SerializedStoreState } from "./types";
9
8
 
10
9
  /** Which of the model's classes each slice state key holds. The rest hold a primitive or a query descriptor. */
@@ -26,7 +25,7 @@ const sliceStateModelTypes: { [key in SliceStateKey]?: SerializedStoreState["mod
26
25
  *
27
26
  * Reads stay store-derived because a read has to be masked and only the store declares which model masks it. Which
28
27
  * of these keys is readable at a given moment is liveness rather than this catalogue — the keys the mounted
29
- * components subscribe. The framework's own store is the one thing held back whatever subscribes it.
28
+ * components subscribe. Plumbing in the base store opts out at the call site with `{ agent: false }`.
30
29
  *
31
30
  * It is derived on the client rather than shipped from the server: the store classes are in the bundle already,
32
31
  * and a second copy over the wire is a second thing to keep in step.
@@ -42,30 +41,10 @@ export class StoreCatalogue {
42
41
  this.state = this.#state();
43
42
  }
44
43
 
45
- /**
46
- * The framework's own store keys, held back however widely they are subscribed.
47
- *
48
- * `st.use.path()` and friends are read all over an app, so publishing them would put plumbing in the context
49
- * block of nearly every screen — and `tryJwt` / `tryAccount` hold the caller's credential and are genuinely
50
- * subscribed (the API explorer's `Doc` and `RestApi` both read them), so a screen reading one must not be able
51
- * to hand it to a model. The route context block carries the three of these an agent legitimately needs. This
52
- * is a fixed rule rather than a declaration a store makes, because forgetting it leaks a credential.
53
- */
54
- static #frameworkKeys() {
55
- return new Set(Object.keys(StoreRegistry.get("base")?.[STATE_META] ?? {}));
56
- }
57
-
58
44
  #state(): { [key: string]: SerializedStoreState } {
59
45
  const state = this.#instance.get();
60
46
  const declared = StoreCatalogue.#declaredStateModels();
61
- const framework = StoreCatalogue.#frameworkKeys();
62
- if (framework.size)
63
- this.refusals.push({
64
- key: "base",
65
- reason: "the framework's own store: routing plumbing an agent cannot act on, and the caller's credential.",
66
- });
67
47
  const entries = Object.keys(state)
68
- .filter((key) => !framework.has(key))
69
48
  .sort()
70
49
  .map((key): [string, SerializedStoreState] => {
71
50
  const role = this.#instance.sliceStateRoles.get(key);
@@ -39,6 +39,8 @@ type RuntimeFetch = typeof globalThis.fetch & {
39
39
  serializedSignal: Record<string, unknown>;
40
40
  setJwt: (jwt: string | null) => void;
41
41
  slice: Record<string, SliceMeta>;
42
+ /** Sort keys per model refName, registered from each serialized signal's filter. */
43
+ sortKeyMap?: Map<string, string[]>;
42
44
  ws: RuntimeWs;
43
45
  [key: string]: unknown;
44
46
  };
@@ -1 +1 @@
1
- export declare const baseDictionary: import("./dictInfo.d.ts").ServiceDictInfo<[string, string], "ping" | "pingBody" | "pingParam" | "pingQuery" | "wsPing" | "pubsubPing", never, "error" | "remove" | "save" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "edit" | "view">;
1
+ export declare const baseDictionary: import("./dictInfo.d.ts").ServiceDictInfo<[string, string], "ping" | "pingBody" | "pingParam" | "pingQuery" | "wsPing" | "pubsubPing", never, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view">;
@@ -1,13 +1,13 @@
1
1
  import type { AgentEndpoint, AgentTurn, BaseEndpoint } from "akanjs/signal";
2
2
  export declare const dictionary: {
3
- base: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "edit" | "view">, never>;
3
+ base: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view">, never>;
4
4
  agentTurn: import("./locale.d.ts").DictModule<import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc`, never>;
5
5
  agent: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">;
6
6
  };
7
- export declare const Err: import("./trans.d.ts").ErrConstructor<"agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">, translate: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja", key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, data?: import("./trans.d.ts").TranslationData) => string, msg: {
8
- info: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
9
- success: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
10
- error: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
11
- warning: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
12
- loading: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
13
- }, getDictionary: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja") => object, getAllDictionary: () => import("./trans.d.ts").RootDictionary, __Dict_Key__: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, __Error_Key__: "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed";
7
+ export declare const Err: import("./trans.d.ts").ErrConstructor<"agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">, translate: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja", key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, data?: import("./trans.d.ts").TranslationData) => string, msg: {
8
+ info: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
9
+ success: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
10
+ error: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
11
+ warning: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
12
+ loading: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
13
+ }, getDictionary: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja") => object, getAllDictionary: () => import("./trans.d.ts").RootDictionary, __Dict_Key__: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, __Error_Key__: "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed";
@@ -5,8 +5,8 @@ import type { AgentBridge } from "./AgentBridge.d.ts";
5
5
  * The akan default context for a turn: where the user is (route), what is on screen (scopes and their curated
6
6
  * resources), and which store keys the mounted components are reading (live keys). Live entries carry names and
7
7
  * small primitives only — anything bigger is one `readState` call away, masked — so the block cannot bloat with
8
- * what happens to be in the store. Base-store keys are excluded by the base store's own `agent: false`
9
- * declaration, and the route block carries the three of them that matter.
8
+ * what happens to be in the store. Base-store plumbing is kept off the surface by `st.use.x({ agent: false })`
9
+ * at each call site, and the route block carries the three of them that matter.
10
10
  */
11
11
  export declare class AgentContext {
12
12
  #private;
@@ -11,7 +11,7 @@ import type { SerializedStoreState } from "./types.d.ts";
11
11
  *
12
12
  * Reads stay store-derived because a read has to be masked and only the store declares which model masks it. Which
13
13
  * of these keys is readable at a given moment is liveness rather than this catalogue — the keys the mounted
14
- * components subscribe. The framework's own store is the one thing held back whatever subscribes it.
14
+ * components subscribe. Plumbing in the base store opts out at the call site with `{ agent: false }`.
15
15
  *
16
16
  * It is derived on the client rather than shipped from the server: the store classes are in the bundle already,
17
17
  * and a second copy over the wire is a second thing to keep in step.
@@ -3,9 +3,10 @@ export interface DashboardProps<T extends string, State> {
3
3
  className?: string;
4
4
  summary: Record<string, unknown>;
5
5
  slice: SliceMeta;
6
- queryMap: Record<string, unknown>;
6
+ /** Columns that narrow the listing when clicked. A column absent from the map renders as a plain tile. */
7
+ queryMap?: Record<string, unknown>;
7
8
  columns?: string[];
8
9
  presents?: string[];
9
10
  hidePresents?: boolean;
10
11
  }
11
- export default function Dashboard<T extends string, State>({ className, summary, slice, queryMap, columns, presents, hidePresents, }: DashboardProps<T, State>): import("react/jsx-runtime").JSX.Element;
12
+ export default function Dashboard<T extends string, State>({ className, summary, slice, queryMap, columns, presents, hidePresents, }: DashboardProps<T, State>): import("react/jsx-runtime").JSX.Element | null;
@@ -5,5 +5,5 @@ interface InsightProps<T extends string, Insight> {
5
5
  slice: SliceMeta;
6
6
  columns?: (keyof Insight)[];
7
7
  }
8
- export default function Insight<T extends string, Insight>({ className, insight, slice, columns, }: InsightProps<T, Insight>): import("react/jsx-runtime").JSX.Element;
8
+ export default function Insight<T extends string, Insight>({ className, insight, slice, columns, }: InsightProps<T, Insight>): import("react/jsx-runtime").JSX.Element | null;
9
9
  export {};
@@ -10,11 +10,11 @@ export interface ListContainerProps<T extends string, State, Input, Full extends
10
10
  className?: string;
11
11
  /** Additional classes for card-list rendering. */
12
12
  cardListClassName?: string;
13
- /** Render as cards or table/list. */
13
+ /** Initial rendering mode. The toolbar toggle switches it from here. */
14
14
  type?: "card" | "list";
15
- /** Static query object passed to the generated init action. */
15
+ /** Static query object passed as the first argument of the generated init action. */
16
16
  query?: Record<string, unknown>;
17
- /** Initial fetch form/filter values. */
17
+ /** Initial fetch form: page, limit, sort, and the default values a new model starts from. */
18
18
  init?: FetchInitForm<Input, any>;
19
19
  /** Generated slice metadata for the target model. */
20
20
  slice: SliceMeta;
@@ -0,0 +1,5 @@
1
+ import type { DataColumn } from "akanjs/client";
2
+ export declare const columnKey: (column: DataColumn<any>) => string;
3
+ export declare const toCsvBlob: (columns: DataColumn<any>[], models: Record<string, unknown>[], title: (column: DataColumn<any>) => string) => Blob;
4
+ export declare const toJsonBlob: (models: unknown[]) => Blob;
5
+ export declare const downloadBlob: (blob: Blob, filename: string) => void;
@@ -0,0 +1,6 @@
1
+ export declare const humanize: (name: string) => string;
2
+ /** A dictionary miss returns the key itself (`makeTrans`), so a raw `user.insight.count` would ship as the label. */
3
+ export declare const dictLabel: (translate: (key: string) => string, key: string, name: string) => string;
4
+ /** A column with no renderer still has to print something a React child accepts, objects and arrays included. */
5
+ export declare const formatCell: (value: unknown) => string;
6
+ export declare const formatStat: (value: unknown) => string;
@@ -6,6 +6,7 @@ interface AdminPanelProps<T extends string, State, Input, Full extends {
6
6
  id: string;
7
7
  }> extends ListContainerProps<T, State, Input, Full, Light> {
8
8
  slice: SliceMeta;
9
+ /** Generated `<Model>.Unit` / `.Template` / `.View` namespaces. A role without a `General` export is skipped. */
9
10
  components: {
10
11
  Template: {
11
12
  [key: string]: any;
@@ -17,15 +18,18 @@ interface AdminPanelProps<T extends string, State, Input, Full extends {
17
18
  [key: string]: any;
18
19
  };
19
20
  };
21
+ /** Summary column to query descriptor. Only a column this map names becomes a link to its filtered listing. */
20
22
  queryMap?: {
21
- [key: string]: any;
23
+ [key: string]: unknown;
22
24
  };
25
+ /** Keys of the app's `summary` state shown as dashboard tiles above the list. */
23
26
  summaryColumns?: string[];
27
+ /** Model insight keys shown above the list. The header already carries the total count. */
24
28
  insightColumns?: string[];
25
29
  }
26
30
  export default function AdminPanel<RefName extends string, State, Input, Full extends {
27
31
  id: string;
28
32
  }, Light extends {
29
33
  id: string;
30
- }>({ slice, components, summaryColumns, insightColumns, renderInsight, renderDashboard, ...props }: AdminPanelProps<RefName, State, Input, Full, Light>): import("react/jsx-runtime").JSX.Element;
34
+ }>({ slice, components, queryMap, summaryColumns, insightColumns, renderInsight, renderDashboard, ...props }: AdminPanelProps<RefName, State, Input, Full, Light>): import("react/jsx-runtime").JSX.Element;
31
35
  export {};
@@ -23,7 +23,7 @@ export interface BottomSheetRef {
23
23
  export const BottomSheet = forwardRef<BottomSheetRef, BottomSheetProps>(
24
24
  ({ open, onCancel, type = "half", children }: BottomSheetProps, bottomSheetRef) => {
25
25
  const ref = useRef<HTMLDivElement>(null);
26
- const pageState = st.use.pageState();
26
+ const pageState = st.use.pageState({ agent: false });
27
27
 
28
28
  const [{ y, opacity }, api] = useSpring(() => ({ y: window.innerHeight, opacity: 0 }));
29
29
 
@@ -6,11 +6,14 @@ import type { FilterInstance } from "akanjs/document";
6
6
  import type { FetchInitForm, SliceMeta } from "akanjs/fetch";
7
7
  import { st } from "akanjs/store";
8
8
  import { type ReactNode, useEffect } from "react";
9
+ import { Empty } from "../Empty";
9
10
  import { Loading } from "../Loading";
10
11
  import { Model } from "../Model";
11
12
  import DataItem from "./Item";
12
13
  import DataPagination from "./Pagination";
13
14
 
15
+ const gridClassName = "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4";
16
+
14
17
  type DataItemProps<T extends string, M extends { id: string }, L extends { id: string }> = {
15
18
  [key in T]: L;
16
19
  } & { slice: SliceMeta };
@@ -86,14 +89,22 @@ export default function CardList<
86
89
  return (
87
90
  <div className={className}>
88
91
  {modelListLoading ? (
89
- <div className={cn("grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5", cardListClassName)}>
92
+ <div className={cn(gridClassName, cardListClassName)}>
90
93
  {new Array(limitOfModel || 20)
91
94
  .fill(0)
92
95
  .map((_, idx) => `skeleton-${idx}`)
93
- .map((key) => (renderLoading ? renderLoading() : <Loading.Skeleton key={key} active />))}
96
+ .map((key) =>
97
+ renderLoading ? (
98
+ renderLoading()
99
+ ) : (
100
+ <Loading.Skeleton key={key} className="rounded-box border border-border bg-card p-4" active />
101
+ ),
102
+ )}
94
103
  </div>
104
+ ) : !modelList.length ? (
105
+ <Empty />
95
106
  ) : (
96
- <div className={cn("grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5", cardListClassName)}>
107
+ <div className={cn(gridClassName, cardListClassName)}>
97
108
  {modelList.map((model, idx) => {
98
109
  return (
99
110
  <DataItem
@@ -1,22 +1,24 @@
1
1
  "use client";
2
2
  import { cn, usePage } from "akanjs/client";
3
- import { capitalize } from "akanjs/common";
4
3
  import type { SliceMeta } from "akanjs/fetch";
5
4
  import { st } from "akanjs/store";
6
5
 
7
- import { buttonRecipe } from "../Button";
8
6
  import { Link } from "../Link";
7
+ import { dictLabel, formatStat } from "./dataText";
9
8
 
10
9
  export interface DashboardProps<T extends string, State> {
11
10
  className?: string;
12
11
  summary: Record<string, unknown>;
13
12
  slice: SliceMeta;
14
- queryMap: Record<string, unknown>;
13
+ /** Columns that narrow the listing when clicked. A column absent from the map renders as a plain tile. */
14
+ queryMap?: Record<string, unknown>;
15
15
  columns?: string[];
16
16
  presents?: string[];
17
17
  hidePresents?: boolean;
18
18
  }
19
19
 
20
+ const tileClassName = "flex min-w-40 flex-1 flex-col gap-1 rounded-box border px-4 py-3 text-left";
21
+
20
22
  export default function Dashboard<T extends string, State>({
21
23
  className,
22
24
  summary,
@@ -26,56 +28,38 @@ export default function Dashboard<T extends string, State>({
26
28
  presents,
27
29
  hidePresents,
28
30
  }: DashboardProps<T, State>) {
29
- const { refName, sliceName } = slice;
31
+ const { refName } = slice;
30
32
  const { l } = usePage();
31
- const searchParams = st.use.searchParams();
33
+ const searchParams = st.use.searchParams({ agent: false });
32
34
  const filter = Array.isArray(searchParams.filter) ? searchParams.filter[0] : searchParams.filter;
33
- const [modelName, modelClassName] = [refName, capitalize(refName)];
34
- const formatSummaryValue = (value: unknown) =>
35
- typeof value === "number" || typeof value === "string" ? value.toLocaleString() : "";
35
+ const shownColumns = (columns ?? []).filter((column) => summary[column] !== undefined);
36
+ const shownPresents = hidePresents ? [] : (presents ?? []).filter((column) => summary[column] !== undefined);
37
+ if (!shownColumns.length && !shownPresents.length) return null;
36
38
  return (
37
- <div className={cn("my-2 flex w-full flex-wrap justify-center py-0 shadow-sm", className)}>
38
- <div className="flex flex-wrap">
39
- {columns?.map(
40
- (column) =>
41
- summary[column] !== undefined &&
42
- queryMap[column] !== undefined && (
43
- <button
44
- key={column}
45
- className={buttonRecipe({ variant: "ghost" }, [
46
- "mx-1 h-32 w-48 rounded-none pt-3 hover:border",
47
- filter === column ? "border" : "border-0",
48
- ])}
49
- >
50
- <Link
51
- key={column}
52
- className="flex flex-col gap-1"
53
- href={`/admin?topMenu=data&subMenu=${modelName}&filter=${column}`}
54
- >
55
- <div className="text-foreground/60 text-xs">{l(`summary.${column}` as "base.new")}</div>
56
- <div className="font-semibold text-2xl text-primary">{formatSummaryValue(summary[column])}</div>
57
- </Link>
58
- </button>
59
- ),
60
- )}
61
- {!hidePresents
62
- ? presents?.map(
63
- (column) =>
64
- summary[column] !== undefined &&
65
- queryMap[column] !== undefined && (
66
- <button
67
- key={column}
68
- className={buttonRecipe({ variant: "ghost" }, "mx-1 h-32 w-48 rounded-none border-none pt-3")}
69
- >
70
- <div className="flex flex-col gap-1">
71
- <div className="text-foreground/60 text-xs">{l(`summary.${column}` as "base.new")}</div>
72
- <div className="font-semibold text-2xl text-primary">{formatSummaryValue(summary[column])}</div>
73
- </div>
74
- </button>
75
- ),
76
- )
77
- : null}
78
- </div>
39
+ <div className={cn("mb-4 flex flex-wrap gap-2", className)}>
40
+ {[...shownColumns, ...shownPresents].map((column) => {
41
+ const linkable = queryMap?.[column] !== undefined;
42
+ return (
43
+ <Link
44
+ key={column}
45
+ disabled={!linkable}
46
+ href={`/admin?topMenu=data&subMenu=${refName}&filter=${column}`}
47
+ className={cn(
48
+ tileClassName,
49
+ "bg-card",
50
+ linkable && "transition hover:border-primary/50",
51
+ filter === column && linkable ? "border-primary" : "border-border",
52
+ )}
53
+ >
54
+ <span className="truncate text-muted-foreground text-xs">
55
+ {dictLabel(l._, `summary.${column}`, column)}
56
+ </span>
57
+ <span className="truncate font-semibold text-2xl text-primary tabular-nums">
58
+ {formatStat(summary[column])}
59
+ </span>
60
+ </Link>
61
+ );
62
+ })}
79
63
  </div>
80
64
  );
81
65
  }
@@ -2,6 +2,8 @@
2
2
  import { cn, usePage } from "akanjs/client";
3
3
  import type { SliceMeta } from "akanjs/fetch";
4
4
 
5
+ import { dictLabel, formatStat } from "./dataText";
6
+
5
7
  interface InsightProps<T extends string, Insight> {
6
8
  className?: string;
7
9
  insight: Insight;
@@ -17,19 +19,23 @@ export default function Insight<T extends string, Insight>({
17
19
  }: InsightProps<T, Insight>) {
18
20
  const { l } = usePage();
19
21
  const { refName } = slice;
22
+ const cells = (columns ?? []).filter((column) => insight[column] !== undefined && insight[column] !== null);
23
+ if (!cells.length) return null;
20
24
  return (
21
- <div className={cn("my-2 flex w-full flex-wrap justify-center py-2 shadow-sm", className)}>
22
- <div className="flex flex-wrap">
23
- {columns?.map(
24
- (column: keyof Insight, idx) =>
25
- insight[column] !== undefined && (
26
- <div key={column.toString()} className="mx-1 flex items-center gap-2 rounded-none px-6 py-4">
27
- <div className="text-foreground/60 text-xs">{l._(`${refName}.insight.${column as string}`)}</div>
28
- <div className="font-semibold text-xl">{(insight[column] as string).toLocaleString()}</div>
29
- </div>
30
- ),
31
- )}
32
- </div>
25
+ <div className={cn("mb-4 grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4", className)}>
26
+ {cells.map((column) => {
27
+ const name = String(column);
28
+ return (
29
+ <div key={name} className="rounded-box border border-border bg-card px-4 py-3">
30
+ <div className="truncate text-muted-foreground text-xs">
31
+ {dictLabel(l._, `${refName}.insight.${name}`, name)}
32
+ </div>
33
+ <div className="mt-1 truncate font-semibold text-foreground text-xl tabular-nums">
34
+ {formatStat(insight[column])}
35
+ </div>
36
+ </div>
37
+ );
38
+ })}
33
39
  </div>
34
40
  );
35
41
  }