@zerotal/admin 1.8.1 → 1.9.0

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/CHANGELOG.md CHANGED
@@ -8,6 +8,28 @@ follows the Zerotal monorepo's unified versioning.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.9.0] — 2026-08-29
12
+
13
+ ### Documented
14
+
15
+ - **Every promised export is documented.** The `docs-coverage` gate reads `maturity: stable` as a
16
+ promise about a package's exports, and measures how much of that promise is written down. It
17
+ was 798 gaps across the suite; it is now zero. This package's share is covered on its own
18
+ pages — types named, options shapes described, and the decisions behind them recorded where
19
+ somebody looking for them will find them.
20
+
21
+ ### Changed
22
+
23
+ - **INTERNAL: the panel's own page machinery is marked `@internal`.** The `make*Page` factories,
24
+ the page classes they produce (`ResourceListPage`, `RecordViewPage`, `ResourceFormPage`,
25
+ `RolesPage`, `MediaPage`, `NotificationsPage`, `ConsolePage`, …), the action and widget
26
+ renderers, and `AdminPanelHost`.
27
+
28
+ **Nothing is removed and nothing breaks** — they are still exported and still work. An app
29
+ declares a `Resource` and the panel builds these pages from it; none of them has a caller
30
+ outside `@zerotal/admin` or a line in the guide, because writing one was never the way to use
31
+ this package.
32
+
11
33
  ## [1.6.0] — 2026-08-15
12
34
 
13
35
  ### Fixed
package/api-surface.md CHANGED
@@ -1114,12 +1114,6 @@ interface ActionContext = {
1114
1114
  slug: string
1115
1115
  }
1116
1116
 
1117
- interface ActionPage = {
1118
- download?: (filename: string, content: string | Uint8Array, mime?: string) => unknown
1119
- flash: (message: string, level?: string) => unknown
1120
- redirect: (url: string) => { withSuccess(message: string): unknown;}
1121
- }
1122
-
1123
1117
  interface AdminAuthConfig = {
1124
1118
  authMiddleware?: MiddlewareClass[]
1125
1119
  authenticateWhen?: (user: unknown) => boolean | Promise<boolean>
@@ -1170,18 +1164,6 @@ interface AdminNotification = {
1170
1164
  title: string
1171
1165
  }
1172
1166
 
1173
- interface AdminPanelHost = {
1174
- console: (contribution: ConsoleContribution) => void
1175
- enabled: (id: string) => boolean
1176
- navItem: (contribution: NavContribution) => void
1177
- page: (contribution: PageContribution) => void
1178
- renderHook: (name: string, hook: (context: RenderHookContext) => HtmlNode | string | null) => void
1179
- searchProvider: (provider: PanelSearchProvider) => void
1180
- topbarSlot: (slot: TopbarSlot) => void
1181
- userMenuItem: (item: UserMenuContribution) => void
1182
- widget: (contribution: WidgetContribution) => void
1183
- }
1184
-
1185
1167
  interface AdminPlugin = {
1186
1168
  id: string
1187
1169
  install: (panel: AdminPanelHost) => void | Promise<void>
@@ -1492,22 +1474,6 @@ interface PageContribution = {
1492
1474
  title: string
1493
1475
  }
1494
1476
 
1495
- interface PanelPage = {
1496
- ability: string | undefined
1497
- cluster?: typeof Cluster | undefined
1498
- navigationBadge?: (() => Promise<string | number | null> | string | number | null) | undefined
1499
- navigationBadgeColor?: BadgeTone | undefined
1500
- navigationGroup: string | undefined
1501
- navigationIcon: string
1502
- navigationLabel: string
1503
- navigationSort: number
1504
- page: PanelPageClass
1505
- routeParams: string[]
1506
- showInNavigation: boolean
1507
- slug: string
1508
- title: string
1509
- }
1510
-
1511
1477
  interface PanelSearchProvider = {
1512
1478
  ability: string
1513
1479
  icon?: string
@@ -1527,19 +1493,6 @@ interface PivotColumn = {
1527
1493
  label?: string
1528
1494
  }
1529
1495
 
1530
- interface RecordPage = {
1531
- lastPage: number
1532
- page: number
1533
- perPage: number
1534
- rows: Record<string, unknown>[]
1535
- total: number
1536
- }
1537
-
1538
- interface RenderableCell = {
1539
- badge?: BadgeTone | undefined
1540
- text: string
1541
- }
1542
-
1543
1496
  interface RenderHookContext = {
1544
1497
  page?: 'list' | 'record' | 'form' | 'dashboard' | undefined
1545
1498
  recordId?: string | undefined
@@ -1663,8 +1616,6 @@ type ActionVisible = (record: AdminRecord | undefined, ctx: ActionContext) => bo
1663
1616
 
1664
1617
  type AdminAuthorizer = (ability: string) => boolean | Promise<boolean>
1665
1618
 
1666
- type AdminPageClass = typeof AdminPage & (new () => AdminPage)
1667
-
1668
1619
  type BadgeTone = 'default' | 'primary' | 'success' | 'muted' | 'destructive'
1669
1620
 
1670
1621
  type CalloutTone = 'default' | 'primary' | 'success' | 'warning' | 'destructive'
@@ -1675,8 +1626,6 @@ type ChartResolver = () => Promise<ChartData> | ChartData
1675
1626
 
1676
1627
  type ChartType = 'line' | 'bar' | 'doughnut' | 'pie'
1677
1628
 
1678
- type ClusterClass = typeof Cluster
1679
-
1680
1629
  type ColumnKind = 'text' | 'toggle' | 'select' | 'input' | 'image' | 'color' | 'icon'
1681
1630
 
1682
1631
  type Conjunction = 'and' | 'or'
@@ -1711,8 +1660,6 @@ type FormComponent = FormSection | FormTabs | Wizard | FormSplit | Callout | Pri
1711
1660
 
1712
1661
  type InfolistComponent = Section | Entry
1713
1662
 
1714
- type PanelPageClass = new () => object
1715
-
1716
1663
  type PrimeKind = 'text' | 'html' | 'image'
1717
1664
 
1718
1665
  type QueryModifier = (query: AdminQuery) => AdminQuery
@@ -1727,8 +1674,6 @@ type RenderHookName = 'body.start' | 'body.end' | 'sidebar.start' | 'sidebar.end
1727
1674
 
1728
1675
  type ResourceClass = typeof Resource
1729
1676
 
1730
- type ResourceFormClass = new () => Form & Record<string, unknown>
1731
-
1732
1677
  type StatsResolver = () => Promise<Stat[]> | Stat[]
1733
1678
 
1734
1679
  type SummaryKind = 'sum' | 'avg' | 'count' | 'min' | 'max' | 'range'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/admin",
3
- "version": "1.8.1",
3
+ "version": "1.9.0",
4
4
  "license": "MIT",
5
5
  "maturity": "stable",
6
6
  "private": false,
@@ -32,16 +32,16 @@
32
32
  "typecheck": "tsc --noEmit"
33
33
  },
34
34
  "dependencies": {
35
- "@zerotal/cache": "1.8.1",
36
- "@zerotal/core": "1.8.1",
37
- "@zerotal/flow": "1.8.1",
38
- "@zerotal/flow-ui": "1.8.1",
39
- "@zerotal/validator": "1.8.1"
35
+ "@zerotal/cache": "1.9.0",
36
+ "@zerotal/core": "1.9.0",
37
+ "@zerotal/flow": "1.9.0",
38
+ "@zerotal/flow-ui": "1.9.0",
39
+ "@zerotal/validator": "1.9.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@zerotal/orm": "1.8.1",
43
- "@zerotal/auth": "1.8.1",
44
- "@zerotal/queue": "1.8.1"
42
+ "@zerotal/orm": "1.9.0",
43
+ "@zerotal/auth": "1.9.0",
44
+ "@zerotal/queue": "1.9.0"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@zerotal/orm": "^1.0.0",
package/src/Cluster.ts CHANGED
@@ -46,5 +46,9 @@ export abstract class Cluster {
46
46
  }
47
47
  }
48
48
 
49
- /** A Cluster subclass (used by its static surface — never instantiated). */
49
+ /**
50
+ * A Cluster subclass (used by its static surface — never instantiated).
51
+ *
52
+ * @internal
53
+ */
50
54
  export type ClusterClass = typeof Cluster;
@@ -72,6 +72,8 @@ export interface NavGroup {
72
72
  * A registered custom page, normalized from either door — an {@link AdminPage}
73
73
  * subclass registered by the app, or a {@link PageContribution} pushed in by a
74
74
  * package.
75
+ *
76
+ * @internal
75
77
  */
76
78
  export interface PanelPage {
77
79
  slug: string;
package/src/Resource.ts CHANGED
@@ -114,6 +114,7 @@ export interface EmptyState {
114
114
  actions?: ActionItem[];
115
115
  }
116
116
 
117
+ /** @internal */
117
118
  export interface RecordPage {
118
119
  rows: Record<string, unknown>[];
119
120
  total: number;
@@ -25,7 +25,11 @@ import { flattenFields } from "../form/index.ts";
25
25
 
26
26
  export type ActionColor = "default" | "primary" | "success" | "muted" | "destructive";
27
27
 
28
- /** Minimal view of the host page an action handler can drive (flash/redirect). */
28
+ /**
29
+ * Minimal view of the host page an action handler can drive (flash/redirect).
30
+ *
31
+ * @internal
32
+ */
29
33
  export interface ActionPage {
30
34
  flash(message: string, level?: string): unknown;
31
35
  redirect(url: string): { withSuccess(message: string): unknown };
@@ -27,7 +27,11 @@ export interface RenderActionOptions {
27
27
  args?: unknown[];
28
28
  }
29
29
 
30
- /** Render a single action. Link actions become `<a navigate>`, callbacks `<button>`. */
30
+ /**
31
+ * Render a single action. Link actions become `<a navigate>`, callbacks `<button>`.
32
+ *
33
+ * @internal
34
+ */
31
35
  export function renderAction(
32
36
  a: Action,
33
37
  ctx: ActionContext,
@@ -92,6 +96,8 @@ export function renderAction(
92
96
  * Returns `null` when nothing inside is visible, so an empty menu never appears.
93
97
  * `argsFor` builds each member's handler arguments, since those carry the
94
98
  * action's own key.
99
+ *
100
+ * @internal
95
101
  */
96
102
  export function renderActionGroup(
97
103
  group: ActionGroup,
@@ -128,6 +134,8 @@ export function renderActionGroup(
128
134
  /**
129
135
  * Render an action as a full-width dropdown menu row (icon + label). Used by the
130
136
  * row-action overflow menu when a row has more actions than fit inline.
137
+ *
138
+ * @internal
131
139
  */
132
140
  export function renderActionMenuItem(
133
141
  a: Action,
@@ -7,6 +7,7 @@ import type { HtmlNode } from "@zerotal/flow";
7
7
  import { Panel } from "../Panel.ts";
8
8
  import { adminHead } from "../theme.ts";
9
9
 
10
+ /** @internal */
10
11
  export class AuthLayout extends Layout {
11
12
  static override get head(): string {
12
13
  const cfg = Panel.config();
@@ -16,6 +16,7 @@ import type { RuleBuilder, FieldRule } from "@zerotal/validator";
16
16
  import type { Field, FieldMode } from "./Field.ts";
17
17
  import { type FormComponent, flattenFields } from "./Section.ts";
18
18
 
19
+ /** @internal */
19
20
  export type ResourceFormClass = new () => Form & Record<string, unknown>;
20
21
 
21
22
  /** @internal */
@@ -96,5 +96,7 @@ export abstract class AdminPage extends Component {
96
96
  /**
97
97
  * A concrete {@link AdminPage} subclass — the static metadata above, plus the
98
98
  * zero-argument constructor Flow builds the page with.
99
+ *
100
+ * @internal
99
101
  */
100
102
  export type AdminPageClass = typeof AdminPage & (new () => AdminPage);
@@ -36,6 +36,7 @@ const ALIGN: Record<string, string> = {
36
36
  end: "text-right tabular-nums",
37
37
  };
38
38
 
39
+ /** @internal */
39
40
  export class ConsolePage extends Component {
40
41
  static layout = AdminLayout;
41
42
 
@@ -304,6 +305,8 @@ function cell(c: ConsoleColumn, row: ConsoleRow): HtmlNode | string {
304
305
  * shared class resolving itself from the route — keeps the console on a static
305
306
  * and matches how resource list pages are generated. The explicit name matters:
306
307
  * Flow's component registry is keyed by constructor name.
308
+ *
309
+ * @internal
307
310
  */
308
311
  export function makeConsolePage(
309
312
  console: ConsoleContribution,
@@ -253,7 +253,11 @@ export class DashboardPage extends Component {
253
253
  }
254
254
  }
255
255
 
256
- /** The dashboard for one panel, showing only that panel's resources and widgets. */
256
+ /**
257
+ * The dashboard for one panel, showing only that panel's resources and widgets.
258
+ *
259
+ * @internal
260
+ */
257
261
  export function makeDashboardPage(panel: PanelInstance = Panel.default()): typeof DashboardPage {
258
262
  const Page = class extends DashboardPage {
259
263
  static override panel = panel;
@@ -15,6 +15,7 @@ import { deleteMedia, formatSize, isImage, isUpload, mediaUrl, storeMedia } from
15
15
  /** How many files the grid shows before the "load more" step. */
16
16
  const PAGE_SIZE = 60;
17
17
 
18
+ /** @internal */
18
19
  export class MediaPage extends Component {
19
20
  static layout = AdminLayout;
20
21
  /** The panel this page belongs to — set by each generated subclass. */
@@ -338,7 +339,11 @@ export class MediaPage extends Component {
338
339
  }
339
340
  }
340
341
 
341
- /** Build a MediaPage bound to one panel. */
342
+ /**
343
+ * Build a MediaPage bound to one panel.
344
+ *
345
+ * @internal
346
+ */
342
347
  export function makeMediaPage(panel: PanelInstance = Panel.default()): typeof MediaPage {
343
348
  return class BoundMediaPage extends MediaPage {
344
349
  static override panel = panel;
@@ -10,6 +10,7 @@ import { Icon } from "../ui/icons.tsx";
10
10
  import { Panel } from "../Panel.ts";
11
11
  import type { PanelInstance } from "../PanelInstance.ts";
12
12
 
13
+ /** @internal */
13
14
  export class NotificationsPage extends Component {
14
15
  static layout = AdminLayout;
15
16
  /** The panel this page belongs to — set by each generated subclass. */
@@ -142,7 +143,11 @@ export class NotificationsPage extends Component {
142
143
  }
143
144
  }
144
145
 
145
- /** The notification centre for one panel, reading that panel's provider. */
146
+ /**
147
+ * The notification centre for one panel, reading that panel's provider.
148
+ *
149
+ * @internal
150
+ */
146
151
  export function makeNotificationsPage(
147
152
  panel: PanelInstance = Panel.default(),
148
153
  ): typeof NotificationsPage {
@@ -95,6 +95,7 @@ interface PivotProxy {
95
95
  all?: () => unknown;
96
96
  }
97
97
 
98
+ /** @internal */
98
99
  export class RecordViewPage extends Component {
99
100
  static layout = AdminLayout;
100
101
  /** Set by each generated subclass. */
@@ -925,6 +926,8 @@ export class RecordViewPage extends Component {
925
926
  /**
926
927
  * Build a uniquely-named View page subclass bound to a resource — mirrors
927
928
  * {@link makeResourceListPage} so Flow keeps component identity stable.
929
+ *
930
+ * @internal
928
931
  */
929
932
  export function makeRecordViewPage(
930
933
  resource: ResourceClass,
@@ -63,7 +63,11 @@ const _formRegistry = new Map<string, FormMeta>();
63
63
 
64
64
  const formKey = (panelId: string, slug: string): string => `${panelId}:${slug}`;
65
65
 
66
- /** Register a resource's form config so the shared page can resolve it. */
66
+ /**
67
+ * Register a resource's form config so the shared page can resolve it.
68
+ *
69
+ * @internal
70
+ */
67
71
  export function registerResourceForm(panelId: string, slug: string, meta: FormMeta): void {
68
72
  _formRegistry.set(formKey(panelId, slug), meta);
69
73
  }
@@ -72,6 +76,7 @@ const SPAN_CLASS = ["", "", "sm:col-span-2", "sm:col-span-3", "sm:col-span-4"];
72
76
  const INPUT_CLASS =
73
77
  "mt-1.5 block w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground shadow-sm outline-none transition placeholder:text-muted-foreground focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background disabled:cursor-not-allowed disabled:opacity-60";
74
78
 
79
+ /** @internal */
75
80
  export class ResourceFormPage extends Component {
76
81
  @locked slug = "";
77
82
  @locked mode: FieldMode = "create";
@@ -104,6 +104,7 @@ function parseFilters(s: string): Record<string, string> {
104
104
  }
105
105
  }
106
106
 
107
+ /** @internal */
107
108
  export class ResourceListPage extends Component {
108
109
  static layout = AdminLayout;
109
110
  /** Set by each generated subclass. */
@@ -2535,6 +2536,8 @@ export class ResourceListPage extends Component {
2535
2536
  /**
2536
2537
  * Build a uniquely-named List page subclass bound to a resource. The distinct
2537
2538
  * class name keeps Flow's snapshot/component identity stable per resource.
2539
+ *
2540
+ * @internal
2538
2541
  */
2539
2542
  export function makeResourceListPage(
2540
2543
  resource: ResourceClass,
@@ -12,6 +12,7 @@ import type { PanelInstance } from "../PanelInstance.ts";
12
12
  import type { Permission, Role } from "../roles.ts";
13
13
  import { groupPermissions, panelPermissions, roleHas } from "../roles.ts";
14
14
 
15
+ /** @internal */
15
16
  export class RolesPage extends Component {
16
17
  static layout = AdminLayout;
17
18
  /** The panel this page belongs to — set by each generated subclass. */
@@ -317,7 +318,11 @@ export class RolesPage extends Component {
317
318
  }
318
319
  }
319
320
 
320
- /** Build a RolesPage bound to one panel. */
321
+ /**
322
+ * Build a RolesPage bound to one panel.
323
+ *
324
+ * @internal
325
+ */
321
326
  export function makeRolesPage(panel: PanelInstance = Panel.default()): typeof RolesPage {
322
327
  return class BoundRolesPage extends RolesPage {
323
328
  static override panel = panel;
@@ -158,7 +158,11 @@ export class SearchPage extends Component {
158
158
  }
159
159
  }
160
160
 
161
- /** The search page for one panel, searching only that panel's resources. */
161
+ /**
162
+ * The search page for one panel, searching only that panel's resources.
163
+ *
164
+ * @internal
165
+ */
162
166
  export function makeSearchPage(panel: PanelInstance = Panel.default()): typeof SearchPage {
163
167
  const Page = class extends SearchPage {
164
168
  static override panel = panel;
package/src/plugin.ts CHANGED
@@ -40,6 +40,8 @@ import type { RenderHookContext } from "./renderHooks.ts";
40
40
  * over without importing anything from `@zerotal/admin`. The panel gives the page
41
41
  * its own layout when it mounts the route, so a contributed page renders only its
42
42
  * content and inherits the panel's chrome.
43
+ *
44
+ * @internal
43
45
  */
44
46
  export type PanelPageClass = new () => object;
45
47
 
@@ -245,6 +247,8 @@ export interface UserMenuContribution {
245
247
  * Contributors should declare their own minimal copy of the members they use
246
248
  * rather than importing this type, so they depend on the admin package at build
247
249
  * time not at all.
250
+ *
251
+ * @internal
248
252
  */
249
253
  export interface AdminPanelHost {
250
254
  /**
@@ -37,6 +37,7 @@ export interface SummaryResult {
37
37
  text: string;
38
38
  }
39
39
 
40
+ /** @internal */
40
41
  export interface RenderableCell {
41
42
  /** Pre-escaped/plain text, or a badge descriptor. */
42
43
  text: string;
@@ -15,6 +15,7 @@ import { Icon } from "./icons.tsx";
15
15
  import { Command, Toaster, Sidebar } from "@zerotal/flow-ui";
16
16
  import type { CommandItem, SidebarItem, SidebarGroup } from "@zerotal/flow-ui";
17
17
 
18
+ /** @internal */
18
19
  export class AdminLayout extends Layout {
19
20
  /**
20
21
  * The panel this shell belongs to. The base class serves whichever panel owns
@@ -335,6 +336,8 @@ export class AdminLayout extends Layout {
335
336
  * The router keys shell persistence on the layout class name, so each panel needs
336
337
  * a class of its own: without that, navigating from one panel to another would
337
338
  * swap the content while leaving the previous panel's sidebar in place.
339
+ *
340
+ * @internal
338
341
  */
339
342
  export function makeAdminLayout(panel: PanelInstance): typeof AdminLayout {
340
343
  const cached = LAYOUTS.get(panel);
@@ -45,6 +45,8 @@ export function chartInitScript(configs: unknown[]): string {
45
45
  *
46
46
  * Returns `null` when there is nothing to draw, so a caller can drop the whole
47
47
  * block rather than leaving an empty gap above its table.
48
+ *
49
+ * @internal
48
50
  */
49
51
  export async function renderWidgets(
50
52
  widgets: DashboardWidget[],