@object-ui/layout 17.6.0 → 17.7.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/README.md CHANGED
@@ -23,10 +23,9 @@ pnpm add @object-ui/layout
23
23
 
24
24
  ## Registration
25
25
 
26
- Importing this package registers its component keys (`page-header`, `page:card`,
27
- `responsive-grid`, `navigation-renderer`, `app-schema-renderer`) on the
28
- `ComponentRegistry` as a module load side effect, so the side-effect-only import
29
- is enough:
26
+ Importing this package registers its component keys (`page:card`,
27
+ `app-schema-renderer`) on the `ComponentRegistry` as a module load side effect,
28
+ so the side-effect-only import is enough:
30
29
 
31
30
  ```typescript
32
31
  import '@object-ui/layout';
@@ -39,6 +38,16 @@ used to say `"sideEffects": false`, and a bundler honouring it dropped the
39
38
  registration silently). `registerLayout()` is also exported for hosts that
40
39
  prefer to register explicitly.
41
40
 
41
+ The `page-header` key is retired (objectui#10859): author `page:header`, which
42
+ `@object-ui/components` registers, with the secondary line as `subtitle`. `PageHeader`
43
+ stays an export for JSX composition.
44
+
45
+ The `responsive-grid` and `navigation-renderer` keys are retired too (objectui#11441).
46
+ A breakpoint grid is the `grid` node with a breakpoint `columns` object
47
+ (`{ "type": "grid", "columns": { "xs": 1, "md": 2 }, "gap": 4 }`), and navigation is
48
+ application metadata: the app's `navigation` items, drawn by the shell.
49
+ `ResponsiveGrid` and `NavigationRenderer` stay exports for JSX composition.
50
+
42
51
  ## Components
43
52
 
44
53
  ### AppShell
@@ -199,10 +208,22 @@ composed in JSX rather than authored as a JSON node. Full guide:
199
208
  The layout components are designed to work seamlessly with React Router:
200
209
 
201
210
  ```typescript
202
- import { BrowserRouter, Routes, Route } from 'react-router-dom';
211
+ import type { ComponentType, ReactNode } from 'react';
203
212
  import { AppShell, SidebarNav } from '@object-ui/layout';
204
213
  import { Home, Users } from 'lucide-react';
205
214
 
215
+ // `react-router-dom` is a PEER dependency (see Installation above): your app
216
+ // installs it, this package does not. These three stand in for what you would
217
+ // import from it, so the composition below is still checked against the shipped
218
+ // `@object-ui/layout` types.
219
+ declare const BrowserRouter: ComponentType<{ children?: ReactNode }>;
220
+ declare const Routes: ComponentType<{ children?: ReactNode }>;
221
+ declare const Route: ComponentType<{ path: string; element: ReactNode }>;
222
+
223
+ // Your own page components, one per route.
224
+ declare const Dashboard: ComponentType;
225
+ declare const UsersPage: ComponentType;
226
+
206
227
  function App() {
207
228
  return (
208
229
  <BrowserRouter>
@@ -219,7 +240,7 @@ function App() {
219
240
  >
220
241
  <Routes>
221
242
  <Route path="/" element={<Dashboard />} />
222
- <Route path="/users" element={<Users />} />
243
+ <Route path="/users" element={<UsersPage />} />
223
244
  </Routes>
224
245
  </AppShell>
225
246
  </BrowserRouter>
@@ -227,6 +248,12 @@ function App() {
227
248
  }
228
249
  ```
229
250
 
251
+ A route's `element` takes **your page component**, never the `lucide-react` icon
252
+ of the same name. This example used to route `/users` to `Users` — the icon it
253
+ imports for the sidebar — so the page rendered a 24-pixel glyph where its content
254
+ belonged. Nothing catches that for you: an icon is a valid component, so the
255
+ route type-checks either way, which is why the two are named apart here.
256
+
230
257
  ## Customization
231
258
 
232
259
  `AppShell` takes a single `className`, and it lands on the `<main>` content
@@ -235,6 +262,8 @@ per-slot `headerClassName` / `sidebarClassName` — the navbar and the sidebar a
235
262
  nodes **you** build, so style them where you build them:
236
263
 
237
264
  ```typescript
265
+ import { AppShell } from '@object-ui/layout';
266
+
238
267
  <AppShell
239
268
  className="bg-gray-50"
240
269
  navbar={<div className="border-b px-4">My App</div>}
@@ -1,6 +1,6 @@
1
1
  import { default as React } from 'react';
2
2
  import { AppComponentSchema, NavigationItem } from '@object-ui/types';
3
- import { VisibilityEvaluator, PermissionChecker, CapabilityChecker } from './NavigationRenderer.js';
3
+ import { VisibilityEvaluator, PermissionChecker, CapabilityChecker, DocTargetChecker } from './NavigationRenderer.js';
4
4
  /**
5
5
  * Mobile navigation display mode — the two modes this renderer implements.
6
6
  *
@@ -34,6 +34,8 @@ export interface AppSchemaRendererProps {
34
34
  checkPermission?: PermissionChecker;
35
35
  /** Optional capability checker passed to NavigationRenderer (gates `requiresObject` / `requiresService`) */
36
36
  checkCapability?: CapabilityChecker;
37
+ /** Optional member-readability checker for `doc` entries, passed to NavigationRenderer (objectui#10188) */
38
+ checkDocTarget?: DocTargetChecker;
37
39
  /** Called when an action-type navigation item is clicked */
38
40
  onAction?: (item: NavigationItem) => void;
39
41
  /** Slot: top navbar content (rendered beside the sidebar trigger) */
@@ -65,7 +67,7 @@ export interface AppSchemaRendererProps {
65
67
  * Renders a complete application shell from an `AppSchema` JSON document.
66
68
  *
67
69
  * Responsibilities:
68
- * - Reads `name`, `title`, `description`, `logo`, `favicon` for branding
70
+ * - Reads `name`, `title`, `description`, `icon`, `branding.logo`, `branding.favicon` for branding
69
71
  * - Renders sidebar navigation from `navigation` or `areas[].navigation`
70
72
  * - Area switcher when multiple areas are VISIBLE — area visibility is
71
73
  * derived from the items inside, not authored (objectui#3311): an area
@@ -87,4 +89,4 @@ export interface AppSchemaRendererProps {
87
89
  * </AppSchemaRenderer>
88
90
  * ```
89
91
  */
90
- export declare function AppSchemaRenderer({ schema, basePath, mobileNavMode, evaluateVisibility: evalVisProp, checkPermission: checkPermProp, checkCapability: checkCapProp, onAction, navbar, sidebarHeader, sidebarFooter, sidebarExtra, children, className, defaultOpen, enableSearch, enablePinning, onPinToggle, enableReorder, onReorder, }: AppSchemaRendererProps): React.JSX.Element;
92
+ export declare function AppSchemaRenderer({ schema, basePath, mobileNavMode, evaluateVisibility: evalVisProp, checkPermission: checkPermProp, checkCapability: checkCapProp, checkDocTarget, onAction, navbar, sidebarHeader, sidebarFooter, sidebarExtra, children, className, defaultOpen, enableSearch, enablePinning, onPinToggle, enableReorder, onReorder, }: AppSchemaRendererProps): React.JSX.Element;
@@ -10,7 +10,11 @@ export interface AppShellBranding {
10
10
  accentColor?: string;
11
11
  /** Favicon URL — replaces the <link rel="icon"> href */
12
12
  favicon?: string;
13
- /** Page title suffix (sets document.title) */
13
+ /**
14
+ * Assigned to `document.title` as given. It is the whole title, not a suffix:
15
+ * the caller composes the string it wants (the console passes
16
+ * `"App label — Product name"`).
17
+ */
14
18
  title?: string;
15
19
  }
16
20
  export interface AppShellProps {
@@ -32,6 +36,29 @@ export interface AppShellProps {
32
36
  /**
33
37
  * Apply branding CSS custom properties to the document root.
34
38
  * This is extracted as a standalone hook so it can be re-used independently.
39
+ *
40
+ * It is also the ONE writer of `document.title` for as long as a shell is
41
+ * mounted (objectui#8637). Ownership is scoped, not permanent: the hook
42
+ * captures whatever the tab already said, writes `title` over it, and puts the
43
+ * captured string back when the shell unmounts or `title` changes. So a host
44
+ * that mounts a shell for part of its route tree gets the specific title while
45
+ * it is there and its previous title back when it leaves, without a second
46
+ * writer keyed on navigation — which is what the console used to do, and what
47
+ * reverted the composed title to the bare product name on every in-app
48
+ * navigation.
49
+ *
50
+ * ⚠️ The capture is a read of the live `document.title`, so a host that lets
51
+ * something else write the tab title WHILE a shell is mounted hands this hook a
52
+ * value it did not put there, and that value is what comes back on unmount.
53
+ * Nesting a second title-writing surface inside a mounted shell is the shape to
54
+ * avoid; the console's own auth surfaces sit outside the shell for this reason.
55
+ *
56
+ * The favicon is scoped the same way (objectui#10040): when `branding.favicon`
57
+ * is set and the page has an icon link, the hook captures that link's `href`
58
+ * attribute, writes the branded URL, and puts the captured attribute back (or
59
+ * removes it, if there was none) when the shell unmounts or the effect re-runs
60
+ * on changed inputs. The same replay caveat applies — an icon written by something else
61
+ * while the shell is mounted is overwritten by the restore.
35
62
  */
36
63
  export declare function useAppShellBranding(branding?: AppShellBranding, title?: string): void;
37
64
  export declare function AppShell({ sidebar, navbar, children, className, defaultOpen, branding, rightRail, }: AppShellProps): React.JSX.Element;
@@ -27,6 +27,59 @@ export type PermissionChecker = (permissions: string[]) => boolean;
27
27
  * runtime capability probe yet.
28
28
  */
29
29
  export type CapabilityChecker = (kind: 'object' | 'service', name: string) => boolean;
30
+ /** What a `type: 'doc'` navigation entry opens: a book, a page, or that page in that book. */
31
+ export interface DocNavTarget {
32
+ book?: string;
33
+ doc?: string;
34
+ }
35
+ /**
36
+ * Answers whether the signed-in member may read what a `type: 'doc'` entry
37
+ * opens (objectui#10188). `false` hides the entry.
38
+ *
39
+ * The server is the enforcer (ADR-0046 §6.7): its app read already drops a
40
+ * `doc` entry the caller may not read (objectstack#19790), and this checker is
41
+ * the renderer's defence in depth behind it (that ruling's point 2). This layer
42
+ * holds no audience rules, so the host answers from the member's own doc / book
43
+ * reads and never re-derives an audience. Kept apart from
44
+ * {@link CapabilityChecker} on purpose: that one asks whether the RUNTIME has a
45
+ * target, this one whether the MEMBER may read it.
46
+ *
47
+ * When not provided, `doc` entries pass — the server's answer stands.
48
+ */
49
+ export type DocTargetChecker = (target: DocNavTarget) => boolean;
50
+ /**
51
+ * What an UNLABELLED navigation entry inherits its text from (objectui#9868).
52
+ *
53
+ * `@objectstack/spec` 17.5.0 made a nav entry's `label` optional with a
54
+ * declared semantic: absent ⇒ the entry shows, at RENDER time, the CURRENT label
55
+ * of what it opens — the view's label when it names a view and that view is
56
+ * labelled, else the object's / dashboard's label. These are the three targets
57
+ * that sentence names; `resolveNavItemLabel` walks them in that order and asks a
58
+ * {@link NavTargetLabelResolver} about each one.
59
+ */
60
+ export type NavLabelTarget = {
61
+ kind: 'view';
62
+ objectName: string;
63
+ viewName: string;
64
+ } | {
65
+ kind: 'object';
66
+ objectName: string;
67
+ } | {
68
+ kind: 'dashboard';
69
+ dashboardName: string;
70
+ };
71
+ /**
72
+ * Answers the CURRENT display label of a nav target from the host's metadata —
73
+ * the object schema's `label`, the named view's `label`, the dashboard's
74
+ * `label` — or `undefined` when the target carries none (or is not loaded).
75
+ *
76
+ * This layer holds no metadata, so the host supplies it: the console shell
77
+ * reads the metadata cache it already loads (`useNavTargetLabel` in
78
+ * `@object-ui/app-shell`). Because it is asked on every render and nothing is
79
+ * stored, a renamed target shows its new name on the next render. Without it,
80
+ * an unlabelled entry falls through to its target's machine name.
81
+ */
82
+ export type NavTargetLabelResolver = (target: NavLabelTarget) => string | undefined;
30
83
  export interface NavigationRendererProps {
31
84
  /** Navigation items to render */
32
85
  items: NavigationItem[];
@@ -41,6 +94,8 @@ export interface NavigationRendererProps {
41
94
  checkPermission?: PermissionChecker;
42
95
  /** Optional runtime-capability checker for `requiresObject` / `requiresService` */
43
96
  checkCapability?: CapabilityChecker;
97
+ /** Optional member-readability checker for `doc` entries — see {@link DocTargetChecker} */
98
+ checkDocTarget?: DocTargetChecker;
44
99
  /**
45
100
  * Called when an `action`-type item is clicked.
46
101
  *
@@ -72,38 +127,35 @@ export interface NavigationRendererProps {
72
127
  /** Called when navigation items are reordered via drag */
73
128
  onReorder?: (reorderedItems: NavigationItem[]) => void;
74
129
  /**
75
- * Optional label resolver for object-type navigation items.
76
- * When provided, called with `(objectName, fallbackLabel)` for items
77
- * where `item.type === 'object'` and `item.label` is a plain string.
78
- * Enables convention-based i18n auto-resolution without coupling
79
- * the layout package to i18n.
80
- */
81
- resolveObjectLabel?: (objectName: string, fallbackLabel: string) => string;
82
- /**
83
- * Optional label resolver for dashboard-type navigation items.
84
- * Called with `(dashboardName, fallbackLabel)` for items where
85
- * `item.type === 'dashboard'` and `item.label` is a plain string.
86
- * Mirrors `resolveObjectLabel` for the convention-based i18n hook
87
- * `useObjectLabel().dashboardLabel`.
130
+ * The viewer's locale (a BCP-47 tag such as `zh-CN`), for a PRESENT label
131
+ * written as an inline locale map (`{ en: 'Accounts', 'zh-CN': '客户' }`,
132
+ * the spec's `I18nLabel`): the map renders this locale's entry, through the
133
+ * spec's own `resolveI18nLabel`. When the map has no entry for this locale,
134
+ * the fallback order is that resolver's and is not restated here
135
+ * (objectui#11299).
136
+ *
137
+ * Injected, like `t`: this layer carries no i18n dependency, so the host that
138
+ * knows the language passes it (the console passes its active UI language,
139
+ * the same value its sidebar resolves area labels in). Omitted ⇒ the
140
+ * resolver's documented no-locale default, `en`. A plain-string label and an
141
+ * absent one are unaffected.
88
142
  */
89
- resolveDashboardLabel?: (dashboardName: string, fallbackLabel: string) => string;
143
+ locale?: string;
90
144
  /**
91
- * Optional label resolver for object-type navigation items that target a
92
- * specific view (i.e. `viewName` is set). Called with
93
- * `(objectName, viewName, fallbackLabel)`. Mirrors
94
- * `useObjectLabel().viewLabel` and resolves
95
- * `{ns}.objects.{objectName}._views.{viewName}.label`.
96
- *
97
- * Without this resolver, an object item with a `viewName` falls back to
98
- * its schema-provided explicit label (which keeps it distinct from a bare
99
- * object-list entry under the same group — avoids visual duplicates such
100
- * as two `商机` rows where one is the list and the other is a Kanban view).
145
+ * Resolver for the text an entry with NO `label` inherits (objectui#9868):
146
+ * the current label of its view / object / dashboard, read from the host's
147
+ * metadata at render time. Consulted ONLY for an absent `label` — an authored
148
+ * label is never replaced by the target's metadata label, not even one
149
+ * spelled like the target's machine name (the ruling refused that sentinel).
150
+ * See {@link NavTargetLabelResolver} and {@link resolveNavItemLabel}.
101
151
  */
102
- resolveViewLabel?: (objectName: string, viewName: string, fallbackLabel: string) => string;
152
+ resolveTargetLabel?: NavTargetLabelResolver;
103
153
  /**
104
- * Optional i18n translation function for resolving I18nLabel objects
105
- * (`{ key, defaultValue }`). When provided, labels are translated
106
- * through i18next; otherwise falls back to `defaultValue`.
154
+ * Optional i18n translation function for resolving KEYED label objects
155
+ * (`{ key, defaultValue }`, see {@link resolveLabel}). When provided, labels
156
+ * are translated through i18next; otherwise falls back to `defaultValue`.
157
+ * An inline locale map (`{ en, 'zh-CN' }`) is not keyed and never reaches
158
+ * it — {@link resolveNavItemLabel} reads a map itself, in {@link locale}.
107
159
  */
108
160
  t?: (key: string, options?: any) => string;
109
161
  /**
@@ -137,6 +189,45 @@ export declare function resolveIcon(name?: string): React.ComponentType<any>;
137
189
  * wrongly for each other's input, silently (objectui#4167).
138
190
  */
139
191
  export declare function resolveLabel(label: string | KeyedI18nLabel, t?: (key: string, options?: any) => string): string;
192
+ /**
193
+ * Resolve a navigation item's display text, in the spec's one order
194
+ * (`@objectstack/spec` `BaseNavItemSchema.label`, objectstack#20849):
195
+ *
196
+ * 1. The id-keyed bundle entry `apps.APP.navigation.ID.label`. That rung runs
197
+ * UPSTREAM of this function, at the server-side `/meta` boundary:
198
+ * `translateApp` in `@objectstack/spec` (`src/system/i18n-resolver.ts`)
199
+ * rewrites a node's `label` by its `id` before the metadata reaches this
200
+ * renderer. App-navigation localization has that one owner — localize nav
201
+ * labels there, never here.
202
+ * 2. A PRESENT label, as authored ({@link presentNavItemLabel}): an inline
203
+ * locale map reads the entry for `locale` (the viewer's), and a string
204
+ * renders verbatim.
205
+ * 3. An ABSENT label inherits its target's CURRENT label (objectui#9868 —
206
+ * `@objectstack/spec` 17.5.0 made it optional, cloud#2021 letter-A ruling),
207
+ * resolved here at render time and never written back — see
208
+ * {@link inheritedNavItemLabel} for the ladder. That arm is keyed on
209
+ * ABSENCE alone, so an authored label is never swapped for the target's
210
+ * metadata label, however it is spelled.
211
+ *
212
+ * ⛔ Nothing here matches a label's text against a name (objectui#11201,
213
+ * ruling B). A present label equal to its target's machine name (`account`)
214
+ * renders `account` in every locale: text cannot tell a deliberate `account`
215
+ * from a machine-written one, so translation is keyed on identity (rung 1) or
216
+ * comes through inheritance (rung 3). The three convention resolvers that
217
+ * the retired rule consulted, and the three arguments that carried them, are
218
+ * gone (objectui#11299).
219
+ *
220
+ * EXPORTED since `969ba84f4`, for the same reason {@link resolveHref} is: a
221
+ * second surface now renders the same `NavigationItem[]`. `nav:menu` is the
222
+ * app's navigation tree as PAGE CONTENT (`app-shell/src/views/nav-menu-renderer.tsx`),
223
+ * and it cannot mount `NavigationRenderer` itself — that renders through
224
+ * `SidebarMenuButton`, whose `useSidebar()` throws outside the shell's
225
+ * `SidebarProvider`. Re-deriving the label rules there would put one nav entry
226
+ * under two names on two surfaces of one app, which is exactly the drift the
227
+ * "single source of truth" note on `resolveHref` exists to prevent. Nothing
228
+ * about the behaviour changed with the keyword.
229
+ */
230
+ export declare function resolveNavItemLabel(item: NavigationItem, t?: (key: string, options?: any) => string, targetLabel?: NavTargetLabelResolver, locale?: string): string;
140
231
  /** Guard callbacks for {@link hasVisibleNavigationItems}. */
141
232
  export interface NavigationVisibilityOptions {
142
233
  /** Evaluator for item `visible` expressions. Defaults to always-visible. */
@@ -145,6 +236,8 @@ export interface NavigationVisibilityOptions {
145
236
  checkPermission?: PermissionChecker;
146
237
  /** Checker for `requiresObject` / `requiresService`. Defaults to pass. */
147
238
  checkCapability?: CapabilityChecker;
239
+ /** Checker for what a `doc` entry opens ({@link DocTargetChecker}). Defaults to pass. */
240
+ checkDocTarget?: DocTargetChecker;
148
241
  /**
149
242
  * Whether the host wires an `onAction` dispatcher. Without one, `action`
150
243
  * items are not rendered at all (framework#4509 — a nav entry that looks
@@ -240,13 +333,19 @@ export declare function resolveActiveNavItem(items: NavigationItem[], pathname:
240
333
  /**
241
334
  * Recursively filter navigation items by search query (case-insensitive label match).
242
335
  * Groups are kept if any child matches, with non-matching children pruned.
336
+ *
337
+ * Matches the text the row SHOWS: `labelOf` defaults to
338
+ * {@link resolveNavItemLabel} with no resolvers, and `NavigationRenderer` passes
339
+ * its own resolvers and locale so an unlabelled entry is found by the label it
340
+ * inherits (objectui#9868), not by a `label` it does not have, and a map-valued
341
+ * one by its entry in the viewer's locale (objectui#11299).
243
342
  */
244
- export declare function filterNavigationItems(items: NavigationItem[], query: string): NavigationItem[];
343
+ export declare function filterNavigationItems(items: NavigationItem[], query: string, labelOf?: (item: NavigationItem) => string): NavigationItem[];
245
344
  /**
246
345
  * Renders a `NavigationItem[]` tree into Shadcn Sidebar components.
247
346
  *
248
347
  * Features:
249
- * - 7 navigation item types + separators
348
+ * - Every navigation item type + separators
250
349
  * - Nested collapsible groups
251
350
  * - Badge indicators
252
351
  * - Visibility expression evaluation
@@ -271,4 +370,4 @@ export declare function filterNavigationItems(items: NavigationItem[], query: st
271
370
  * />
272
371
  * ```
273
372
  */
274
- export declare function NavigationRenderer({ items, basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, onAction, searchQuery, enablePinning, onPinToggle, enableReorder, onReorder, resolveObjectLabel, resolveDashboardLabel, resolveViewLabel, t: tProp, templateContext, }: NavigationRendererProps): React.JSX.Element;
373
+ export declare function NavigationRenderer({ items, basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, checkDocTarget, onAction, searchQuery, enablePinning, onPinToggle, enableReorder, onReorder, resolveTargetLabel, locale, t: tProp, templateContext, }: NavigationRendererProps): React.JSX.Element;
@@ -32,7 +32,7 @@ export interface PageHeaderComponentProps extends React.HTMLAttributes<HTMLDivEl
32
32
  * was retired in objectui#3789, once the ADR-0087 D2 conversion
33
33
  * `page-header-subtitle-alias` was measured to reach EVERY spec-valid header
34
34
  * position on the load path (regions, slots, and containers nested to any
35
- * depth — objectstack#6775 / #6776 / PR #7034). Stored metadata authored with
35
+ * depth — objectstack#6775 / objectstack-ai/objectstack#6776 / PR objectstack-ai/objectstack#7034). Stored metadata authored with
36
36
  * `description` is rewritten to `subtitle` before it ever reaches a renderer,
37
37
  * so the second line survives without this side holding a second dialect open.
38
38
  */
@@ -1,25 +1,59 @@
1
1
  import { default as React } from 'react';
2
- import { BreakpointColumnMap, BreakpointOrderMap } from '@objectstack/spec/ui';
3
2
  /**
4
- * Breakpoint column map (`BreakpointColumnMapSchema`) and breakpoint order map
5
- * (`BreakpointOrderMapSchema`), both re-exported from `@objectstack/spec/ui`.
3
+ * Breakpoint column map — grid column counts per Tailwind-style breakpoint,
4
+ * owned by this package since objectui#7580 (maintainer ruling 2026-09-04,
5
+ * option A).
6
6
  *
7
7
  * These were hand copies until objectui#4167, each carrying the word "mirrors"
8
8
  * — the doc-comment shape objectstack#4115 catalogues, where the claim is what
9
- * the next agent reads as canonical and the code is what actually decides. rc.6
10
- * publishes both names, and both copies happened to still be exact: six
11
- * optional numbers keyed `xs`…`2xl`, on a `$strict` schema. "Still exact" is
12
- * the argument FOR binding them, not against — the copies had nothing to
13
- * protect, so the only thing they could do from here was drift, and a
14
- * breakpoint the spec adds now arrives instead of silently not existing.
15
- *
16
- * `BreakpointOrderMap` has no read point in this package (`ResponsiveGrid`
17
- * resolves columns only); it is published because `ResponsiveConfigSchema`
18
- * pairs the two and an author configuring order needs the type. Bound rather
19
- * than deleted for that reason — deleting a published type is a separate
20
- * decision from stopping it being a fork.
9
+ * the next agent reads as canonical and the code is what actually decides. They
10
+ * were then bound by reference to `@objectstack/spec/ui`, which was right for
11
+ * as long as the spec published them.
12
+ *
13
+ * objectstack#11027 retired the whole `ui/responsive` vocabulary upstream, so
14
+ * there is no longer anything to bind to — and, unlike the four types that left
15
+ * with the key, this one has a renderer that implements it:
16
+ * {@link resolveColumnClasses} applies it on `ResponsiveGrid`'s render path.
17
+ * The tombstone's own return condition — the vocabulary "returns if and when a
18
+ * renderer implements it" — is met here, which is why the type is re-homed
19
+ * rather than deleted.
20
+ *
21
+ * The node that once made it authorable from JSON is gone. The
22
+ * `responsive-grid` registration retired under objectui#11441 (the
23
+ * maintainer's ruling `5950208338`, letter B), which kept this declaration and
24
+ * named the `grid` node with a breakpoint `columns` object as the one
25
+ * authorable spelling. `ResponsiveGrid` is now a React-only export, and this
26
+ * map is the type of its `columns` prop.
27
+ *
28
+ * ⚠️ Members are the retired `BreakpointColumnMapSchema`'s, verbatim: six
29
+ * optional column counts keyed `xs`…`2xl`. The schema was `$strict`, so no
30
+ * index signature is restored here either — an unknown breakpoint key was a
31
+ * parse error upstream and stays a type error here.
32
+ *
33
+ * `BreakpointOrderMap` RETIRED with the key (same ruling, item 3) and is NOT
34
+ * re-homed. Its own comment in this file already recorded the reason: it had no
35
+ * read point in this package — `ResponsiveGrid` resolves columns only — and it
36
+ * was published solely because the spec's `ResponsiveConfigSchema` paired the
37
+ * two, so an author configuring `order` needed the type. That pairing is what
38
+ * objectstack#11027 deleted; with the schema gone there is no order vocabulary
39
+ * left for it to be the type OF, and re-homing it would have re-declared a key
40
+ * nothing reads on either side of the boundary — the declare-without-enforce
41
+ * shape ADR-0049 removes.
42
+ *
43
+ * ⏳ Interim, and it self-expires: the pin is still `@objectstack/spec` 17.2.0,
44
+ * which PRE-dates the retirement and still exports this name, so the collision
45
+ * is real today and carries a reasoned entry in
46
+ * `scripts/check-spec-symbol-derivation.mjs`. The guard's ratchet 3 fails an
47
+ * ALLOW entry that excuses nothing, so the pin bump is forced to delete it.
21
48
  */
22
- export type { BreakpointColumnMap, BreakpointOrderMap };
49
+ export interface BreakpointColumnMap {
50
+ xs?: number;
51
+ sm?: number;
52
+ md?: number;
53
+ lg?: number;
54
+ xl?: number;
55
+ '2xl'?: number;
56
+ }
23
57
  export interface ResponsiveGridProps {
24
58
  /** Grid column map per breakpoint */
25
59
  columns?: BreakpointColumnMap;
@@ -31,8 +65,9 @@ export interface ResponsiveGridProps {
31
65
  children: React.ReactNode;
32
66
  }
33
67
  /**
34
- * ResponsiveGrid — A layout component that consumes @objectstack/spec
35
- * BreakpointColumnMapSchema for responsive grid layouts.
68
+ * ResponsiveGrid — A layout component that consumes this package's own
69
+ * BreakpointColumnMap (the six keys xs, sm, md, lg, xl, '2xl') for
70
+ * responsive grid layouts.
36
71
  *
37
72
  * Uses pure Tailwind CSS classes for responsive behavior (no JS resize listeners).
38
73
  *