@knkcs/contenthub-ui 0.3.0 → 0.4.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
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0](https://github.com/knkCS/contenthub/compare/contenthub-ui-v0.3.1...contenthub-ui-v0.4.0) (2026-10-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * add editing settings for the workspace and per blueprint ([#271](https://github.com/knkCS/contenthub/issues/271)) ([f1d3cbb](https://github.com/knkCS/contenthub/commit/f1d3cbb8d4e5f3b355abb3740b770e9c565cea77))
9
+ * correct a release's dates in one request with change validity ([#274](https://github.com/knkCS/contenthub/issues/274)) ([a75dbdf](https://github.com/knkCS/contenthub/commit/a75dbdfc94c0aa969f8161e5904437ddd6136886))
10
+ * edit, save, publish and discard undated content directly ([#264](https://github.com/knkCS/contenthub/issues/264)) ([#276](https://github.com/knkCS/contenthub/issues/276)) ([6307d3c](https://github.com/knkCS/contenthub/commit/6307d3c7d4172c4ab461e3d1ddc82217f4609058))
11
+
12
+ ## [0.3.1](https://github.com/knkCS/contenthub/compare/contenthub-ui-v0.3.0...contenthub-ui-v0.3.1) (2026-10-13)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **ui:** read a change set without a status as the default status ([#257](https://github.com/knkCS/contenthub/issues/257)) ([b88b157](https://github.com/knkCS/contenthub/commit/b88b1571ec98d29ec78907ced7fbdacf93509f57))
18
+ * **ui:** show each Branch of a Chain at its own head in the Branch editor ([#258](https://github.com/knkCS/contenthub/issues/258)) ([cdae1bf](https://github.com/knkCS/contenthub/commit/cdae1bf1af7b6ceb9cfc281186e42341e39b9193)), closes [#252](https://github.com/knkCS/contenthub/issues/252)
19
+ * **ui:** stop calling a Branch up to date when Update from Main left a Pending Merge ([#256](https://github.com/knkCS/contenthub/issues/256)) ([396ea42](https://github.com/knkCS/contenthub/commit/396ea42c33af27721b8bdc176cd34acf1b5cfa60))
20
+
3
21
  ## [0.3.0](https://github.com/knkCS/contenthub/compare/contenthub-ui-v0.2.0...contenthub-ui-v0.3.0) (2026-10-13)
4
22
 
5
23
 
package/README.md CHANGED
@@ -4,15 +4,16 @@ contenthub's screens for knk hosts (contenthub ADR 0020, knkcms/core ADR 0003).
4
4
 
5
5
  The package exports screens only, never routes:
6
6
 
7
- - `ContenthubSettings`: the workspace's configuration on one settings page of three tabs, chosen by `?tab=` (`required-checks`, `block-reasons`, `collections`), so it needs a router. Required Checks are declared and edited in a Modal (name, scope, Blueprints by accessor or every one, independent); who records each is read from contenthub (every member, until narrowed) and narrowed from its row to named members from the host's members, adding oneself (break-glass) included, or opened to every member again; in `authz.mode: workspace` every member records every check, and the tab says so and offers no recorders editor. Block Reasons are listed, created, renamed and removed. Collections are the tree in order, created, renamed, moved and removed; restricted (making the admin its owner) and unrestricted, each confirmed, the unrestrict as an acknowledged disclosure; and a Restricted Collection's grants are managed from the host's members, granting oneself (break-glass) included. In `authz.mode: workspace` contenthub refuses restriction, and the tab then says it needs guardian mode.
7
+ - `ContenthubSettings`: the workspace's configuration on one settings page of four tabs, chosen by `?tab=` (`required-checks`, `block-reasons`, `editing`, `collections`), so it needs a router. Required Checks are declared and edited in a Modal (name, scope, Blueprints by accessor or every one, independent); who records each is read from contenthub (every member, until narrowed) and narrowed from its row to named members from the host's members, adding oneself (break-glass) included, or opened to every member again; in `authz.mode: workspace` every member records every check, and the tab says so and offers no recorders editor. Block Reasons are listed, created, renamed and removed. Editing shows the workspace's Editing Mode and Editing Block (ADR 0022), changed in a Modal, above a table of the per-Blueprint overrides, each Blueprint with the values its Content gets and a field left to the workspace marked as its default; overrides are added by accessor, changed and removed. Collections are the tree in order, created, renamed, moved and removed; restricted (making the admin its owner) and unrestricted, each confirmed, the unrestrict as an acknowledged disclosure; and a Restricted Collection's grants are managed from the host's members, granting oneself (break-glass) included. In `authz.mode: workspace` contenthub refuses restriction, and the tab then says it needs guardian mode.
8
8
  - `BlockReasonsSettings`: the Block Reasons on a page of their own, for a host that mounts them apart from `ContenthubSettings`.
9
9
  - `ContentLibrary`: the Collection tree in `SubNavLayout.Nav` beside the Content table, narrowing together with the toolbar's search, view (Main, In Force, a Change Set) and Blueprint filters; the selected Collection is the URL's `?collection=`, so it needs a router. Rows call `onOpenContent({ contentId, changeSetId? })`; New Content creates a Content in an open or new Change Set and calls `onOpenBranch`.
10
- - `ContentPage`: one Content, read on Main or (`changeSetId`) inside the Change Set it was born in, in tabs Data (read-only, through fieldkit's read mode, loaded lazily), Releases, Revisions (with Compare), References, Change Sets and Filing. The header's one Save sets Filing (Collections, assignees) and never writes a Revision. Its rail offers Start editing, Duplicate, Repeal or Withdraw, Block or Unblock, and Archive (only once nothing is In Force today or later) or Unarchive; Start editing and Duplicate end in `onOpenBranch`, and `onOpenContent` and `onOpenChangeSet` open the rows of its References and Change Sets tabs. It needs `@knkcs/fieldkit` as a peer.
11
- - `ChangeSetsIndex`: the workspace's Change Sets, narrowed by state and assignee, created in a Modal; `onOpenChangeSet(id)` opens one, a new one too.
12
- - `ChangeSetPage`: one Change Set, its General tab (name and Source while it is open, status label, assignees, dependencies) saved from the header's one Save; its Contents tab, whose rows call `onOpenBranch({ changeSetId, contentId, branchId })`; its Releases tab (Planned Releases, proposals read-only, the Releases they cover) and its Checks tab (each Required Check's latest result, stale or not, and Record). Its rail holds Publish, Update from Main and Discard, and an About section with the readiness tile (`ValidatePublish` at the current heads), the status label and the Source; Publish opens a review that publishes exactly the heads it shows. `onPublished` and `onDiscarded` tell the host. Its leave guard needs a data router (react-router's `useBlocker`).
13
- - `BranchEditor`: one Content inside a Change Set at one Branch (`changeSetId`, `contentId`, `branchId`), its header tabs chosen by `?tab=`. The Content tab renders the Blueprint Release's form through fieldkit's `SpecForm`, with knkeditor for rich text, both loaded lazily; reference fields search contenthub, media fields use the `MediaSource` port. The forms above the tabs are the drafts (fieldkit's "Consumer-owned Save across unmounting tabs"): the Content's, and the Release tab's Planned Release (Validity Period, supersedes, Release Spec values; the Change Set's proposals beside it, read-only). The header's one Save persists every changed tab: one `SaveRevision` on the head the editor loaded, with a field mask of what changed, then `PlanRelease` or `UpdatePlannedRelease`; a refused save sends no plan, and a refused plan leaves only the Release tab changed. A moved head is refused with Reload, never merged; the leave guard needs a data router. Compare shows the head against its fork or Main, References its outgoing edges and outdated pins. The rail offers Update from Main and the merge resolution it may leave, Compare to the fork, Preview, Append a Branch (`onOpenBranch` opens it) and Drop Chain (`onDeleted` leaves), and shows the fork Release, the effective date, the Chain position and the head. It needs `@knkcs/fieldkit` (0.18), `@knkcms/knkeditor-editor` and `@knkcms/knkeditor-vocabulary` (GitHub Packages) as peers.
10
+ - `ContentPage`: one Content, read on Main or (`changeSetId`) inside the Change Set it was born in, in tabs Data (read-only, through fieldkit's read mode, loaded lazily), Releases (on Dated Content each Release offers Change validity, which corrects its dates in one request, wherever the Blueprint's effective Editing Mode allows a Direct Edit), Revisions (with Compare), References, Change Sets and Filing. The header's one Save sets Filing (Collections, assignees) and never writes a Revision. Its rail offers Edit and Start editing as the Editing Mode in effect for the Content's Blueprint says (ADR 0022: Edit alone, both, or Start editing alone; Dated Content gets Start editing until a Direct Edit takes a date), Duplicate, Repeal or Withdraw, Block or Unblock, and Archive (only once nothing is In Force today or later) or Unarchive; Start editing and Duplicate end in `onOpenBranch`; Edit opens the caller's Direct Edit, or finds it open, and ends in `onOpenChangeSet`, so Edit is offered only with it. While the caller has an open Direct Edit of the Content the page says so and leads back to it. The Change Sets tab names each open Direct Edit's editor, and `onOpenContent` and `onOpenChangeSet` open the rows of its References and Change Sets tabs. It needs `@knkcs/fieldkit` as a peer.
11
+ - `ChangeSetsIndex`: the workspace's Change Sets, narrowed by kind (Direct Edits hidden until asked for), state and assignee, created in a Modal; `onOpenChangeSet(id)` opens one, a new one too.
12
+ - `ChangeSetPage`: one Change Set, its General tab (name and Source while it is open, status label, assignees, dependencies) saved from the header's one Save; its Contents tab, whose rows call `onOpenBranch({ changeSetId, contentId, branchId })`; its Releases tab (Planned Releases, proposals read-only, the Releases they cover) and its Checks tab (each Required Check's latest result, stale or not, and Record). Its rail holds Publish, Update from Main and Discard, and an About section with the readiness tile (`ValidatePublish` at the current heads), the status label and the Source; Publish opens a review that publishes exactly the heads it shows. `onPublished` and `onDiscarded` tell the host. Its leave guard needs a data router (react-router's `useBlocker`). A Change Set that is a Direct Edit shows as `DirectEditPage` instead.
13
+ - `DirectEditPage`: a Direct Edit (ADR 0022), as its editor sees it: the Content's fields in the Branch editor's form with the header's one Save, Publish and Discard in the rail (Publish asks for unsaved edits to be saved first, and lists every blocker of a refused Publish; Discard asks first), and no Change Set, Chain, Branch or Release tab. `onPublished` and `onDiscarded` tell the host. `ChangeSetPage` renders it for a Direct Edit, so a host needs no route of its own.
14
+ - `BranchEditor`: one Content inside a Change Set at one Branch (`changeSetId`, `contentId`, `branchId`), its header tabs chosen by `?tab=`. It reads the Content at that Branch's own head (`GetContent` with `branch_id`); only the Chain's last Branch is edited, and an earlier one shows read-only. The Content tab renders the Blueprint Release's form through fieldkit's `SpecForm`, with knkeditor for rich text, both loaded lazily; reference fields search contenthub, media fields use the `MediaSource` port. The forms above the tabs are the drafts (fieldkit's "Consumer-owned Save across unmounting tabs"): the Content's, and the Release tab's Planned Release (Validity Period, supersedes, Release Spec values; the Change Set's proposals beside it, read-only). The header's one Save persists every changed tab: one `SaveRevision` on the head the editor loaded, with a field mask of what changed, then `PlanRelease` or `UpdatePlannedRelease`; a refused save sends no plan, and a refused plan leaves only the Release tab changed. A moved head is refused with Reload, never merged; the leave guard needs a data router. Compare shows the head against its fork or Main, References its outgoing edges and outdated pins. The rail offers Update from Main and the merge resolution it may leave, Compare to the fork, Preview, Append a Branch (`onOpenBranch` opens it) and Drop Chain (`onDeleted` leaves), and shows the fork Release, the effective date, the Chain position and the head. It needs `@knkcs/fieldkit` (0.18), `@knkcms/knkeditor-editor` and `@knkcms/knkeditor-vocabulary` (GitHub Packages) as peers.
14
15
  - `MediaSource`, the port a host fills with mediahub, in fieldkit's media-adapter shape (ADR 0020). Without it a media field offers nothing to pick.
15
- - `StatusSource`, the port a host fills with statushub for Change Set status labels (ADR 0020). Without it the screens show the bare slug and offer no status picker.
16
+ - `StatusSource`, the port a host fills with statushub for Change Set status labels (ADR 0020). Without it the screens show the bare slug and offer no status picker. A status it answers with `isDefault` (statushub's default) is what a Change Set without a status reads as (ADR 0014 D9); a host should pass it on.
16
17
  - `BlueprintSource`: content Blueprints and their Releases, for New Content, Blueprint names and the Library's Blueprint filter, which offers the Blueprints of the listed Content too, archived ones included (#241). Not a port a host fills: contenthub serves them itself (`BlueprintCatalogService`), because blueprinthub is its direct dependency (ADR 0020 as amended, #238), so every screen reads them through its `ContenthubClient`. A host may pass its own, which only tests need. A Release contenthub refuses is offered disabled in New Content, with its reasons.
17
18
  - `ContenthubClient`, the port every screen takes, and `createConnectContenthubClient({ baseUrl, fetch })`, its Connect implementation. `baseUrl` is `""` when contenthub serves the page, the mount prefix (`/api/contenthub`) under core.
18
19
  - `ContenthubPathsProvider` and `ContenthubPaths`: where the host mounts the screens, for the breadcrumbs and links they draw. `contenthubSettings` is the settings page, whose tabs are `?tab=`; `contenthubSettingsPath(paths, tab)` builds a link to one.
@@ -1,4 +1,4 @@
1
- import { r as e } from "./content-library-v0mjVD4J.js";
1
+ import { r as e } from "./content-library-DFIk09bD.js";
2
2
  import { useMemo as t } from "react";
3
3
  import { jsx as n } from "react/jsx-runtime";
4
4
  import { FieldKitProvider as r, SpecForm as i } from "@knkcs/fieldkit/renderer";
@@ -83,4 +83,4 @@ function d({ client: e, changeSetId: a, branchId: o, fields: s, parts: c, labels
83
83
  //#endregion
84
84
  export { d as default };
85
85
 
86
- //# sourceMappingURL=branch-content-fields-LyF6sD2e.js.map
86
+ //# sourceMappingURL=branch-content-fields-BQG6U0yl.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"branch-content-fields-LyF6sD2e.js","names":["contentLabel","MAX_PAGE","itemOf","data","id","blueprintId","branchAdapters","wanted","page","page_size","list","client","query","blueprintIds","excluded","excludeIds","seen","items","c","start","total","ids","detail","contentId","mediaSource","file","filter","plugins","builtInFieldTypes","p","knkRichTextPlugin","BranchContentFields","adapters","useMemo","changeSetId","FieldKitProvider","SpecForm","fields","branchId"],"sources":["../src/screens/branch-adapters.ts","../src/screens/branch-content-fields.tsx"],"sourcesContent":["// fieldkit's adapters for the Branch editor (ADR 0020): references from\n// contenthub's own reads, media through the host's MediaSource port. Types\n// only from fieldkit, so this module adds nothing to the root bundle.\nimport type { FieldKitAdapters, PinTarget, ReferenceItem } from \"@knkcs/fieldkit/renderer\";\nimport type { ContenthubClient } from \"../client/contenthub-client\";\nimport type { MediaSource } from \"../types\";\nimport { contentLabel } from \"./content-library\";\n\n/** The most rows one search asks for: ListContents' own cap. */\nconst MAX_PAGE = 200;\n\n/** What a picker shows for a Content: its label, or its id when its data has none. */\nfunction itemOf(id: string, blueprintId: string, data: Record<string, unknown>): ReferenceItem {\n\treturn { id, display_name: contentLabel(data) || id, blueprint_id: blueprintId };\n}\n\n/**\n * The adapters a Branch's form gets: `reference` searches contenthub's\n * Editor Search on Main (which reads open Branch heads too, so Content born\n * in an open Change Set is found), names a referenced Content from its read\n * and offers its Releases to pin; `media` is the host's MediaSource, absent\n * when the host has none, which leaves a media field nothing to pick.\n */\nexport function branchAdapters({\n\tclient,\n\tchangeSetId,\n\tmediaSource,\n}: {\n\tclient: ContenthubClient;\n\tchangeSetId: string;\n\tmediaSource?: MediaSource;\n}): FieldKitAdapters {\n\treturn {\n\t\treference: {\n\t\t\tasync search({ query, blueprintIds, excludeIds, page, page_size }) {\n\t\t\t\tconst wanted = Math.min(MAX_PAGE, Math.max(1, page) * Math.max(1, page_size));\n\t\t\t\t// Every target Blueprint at once: the service narrows the page, so\n\t\t\t\t// it holds only what the Field may point at, and paging stays exact.\n\t\t\t\tconst list = await client.listContents({\n\t\t\t\t\tsearch: query.trim() || undefined,\n\t\t\t\t\tblueprintIds: blueprintIds.length > 0 ? blueprintIds : undefined,\n\t\t\t\t\tpageSize: wanted,\n\t\t\t\t});\n\t\t\t\tconst excluded = new Set(excludeIds ?? []);\n\t\t\t\tconst seen = new Set<string>();\n\t\t\t\tconst items: ReferenceItem[] = [];\n\t\t\t\t// A search lists a Content once per head that matched: one row each.\n\t\t\t\tfor (const c of list.contents) {\n\t\t\t\t\tif (seen.has(c.id) || excluded.has(c.id)) continue;\n\t\t\t\t\tseen.add(c.id);\n\t\t\t\t\titems.push(itemOf(c.id, c.blueprintId, c.data));\n\t\t\t\t}\n\t\t\t\tconst start = (Math.max(1, page) - 1) * page_size;\n\t\t\t\t// The total is exact on the last page; otherwise there is at least one more.\n\t\t\t\tconst total = list.nextPageToken ? items.length + 1 : items.length;\n\t\t\t\treturn { items: items.slice(start, start + page_size), total };\n\t\t\t},\n\t\t\tasync fetch(ids) {\n\t\t\t\treturn Promise.all(\n\t\t\t\t\tids.map(async (id) => {\n\t\t\t\t\t\t// Published Content reads on Main; Content born in this\n\t\t\t\t\t\t// Change Set exists only there.\n\t\t\t\t\t\tconst detail = await client\n\t\t\t\t\t\t\t.getContent({ id })\n\t\t\t\t\t\t\t.catch(() => client.getContent({ id, changeSetId }))\n\t\t\t\t\t\t\t.catch(() => undefined);\n\t\t\t\t\t\treturn detail\n\t\t\t\t\t\t\t? itemOf(id, detail.content.blueprintId, detail.content.data)\n\t\t\t\t\t\t\t: { id, display_name: id };\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t},\n\t\t\t// A Pin names a Release (the one pinning kind fieldkit has), newest first.\n\t\t\tasync listPinTargets(contentId) {\n\t\t\t\tconst timeline = await client.getTimeline(contentId);\n\t\t\t\treturn [...timeline.entries].reverse().map(\n\t\t\t\t\t(e): PinTarget => ({\n\t\t\t\t\t\tid: e.releaseId,\n\t\t\t\t\t\tlabel: `#${e.ordinal}`,\n\t\t\t\t\t\tdescription: e.validityPeriod\n\t\t\t\t\t\t\t? `${e.validityPeriod.firstDay} – ${e.validityPeriod.lastDay || \"…\"}`\n\t\t\t\t\t\t\t: e.publishTime.slice(0, 10),\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\tmedia: mediaSource\n\t\t\t? {\n\t\t\t\t\tupload: (file) => mediaSource.upload(file),\n\t\t\t\t\tbrowse: (filter) => mediaSource.browse(filter),\n\t\t\t\t}\n\t\t\t: undefined,\n\t};\n}\n","// fieldkit's renderer with knkeditor for rich text, behind a lazily loaded\n// module: both load the first time a Branch's Content tab is shown, never\n// with the rest of the package (spec #56; branch-editor-bundle.test.ts).\nimport { FieldKitProvider, SpecForm, type SpecFormLabels } from \"@knkcs/fieldkit/renderer\";\nimport { knkRichTextPlugin } from \"@knkcs/fieldkit/rich-text\";\nimport { builtInFieldTypes, type FieldTypePlugin, type Schema } from \"@knkcs/fieldkit/schema\";\nimport { useMemo } from \"react\";\nimport type { ContenthubClient } from \"../client/contenthub-client\";\nimport type { MediaSource, ResolvedField } from \"../types\";\nimport { branchAdapters } from \"./branch-adapters\";\n\n// rich_text through knkeditor (fieldkit ADR-0026); every other type as built in.\nconst plugins: FieldTypePlugin[] = builtInFieldTypes.map((p) =>\n\tp.id === \"rich_text\" ? (knkRichTextPlugin as FieldTypePlugin) : p,\n);\n\nexport interface BranchContentFieldsProps {\n\tclient: ContenthubClient;\n\tchangeSetId: string;\n\t/** The record the form holds, so SpecForm's remembered section is this Branch's. */\n\tbranchId: string;\n\tfields: ResolvedField[];\n\t/** The Resolved Release's parts: the Text Types its rich_text fields pin. */\n\tparts: Record<string, Record<string, unknown>>;\n\tlabels: SpecFormLabels;\n\treadOnly: boolean;\n\tmediaSource?: MediaSource;\n}\n\n/**\n * The Branch's data as the Blueprint Release lays it out, editable: its\n * sections, orientation and cards as fieldkit draws them, over the form the\n * editor holds above its tabs (fieldkit's spec-form.mdx, \"A Consumer-owned\n * Save across unmounting tabs\"). SpecForm keeps no draft of its own.\n */\nexport default function BranchContentFields({\n\tclient,\n\tchangeSetId,\n\tbranchId,\n\tfields,\n\tparts,\n\tlabels,\n\treadOnly,\n\tmediaSource,\n}: BranchContentFieldsProps) {\n\tconst adapters = useMemo(\n\t\t() => branchAdapters({ client, changeSetId, mediaSource }),\n\t\t[client, changeSetId, mediaSource],\n\t);\n\treturn (\n\t\t<FieldKitProvider plugins={plugins} adapters={adapters} parts={parts}>\n\t\t\t<SpecForm\n\t\t\t\tschema={fields as unknown as Schema}\n\t\t\t\tlabels={labels}\n\t\t\t\treadOnly={readOnly}\n\t\t\t\trecordKey={branchId}\n\t\t\t/>\n\t\t</FieldKitProvider>\n\t);\n}\n"],"mappings":";;;;;;;AASA,IAAMC,IAAW;AAGjB,SAASC,EAAO,GAAY,GAAqB,GAA8C;CAC9F,OAAO;EAAE;EAAI,cAAcF,EAAaG,CAAI,KAAKC;EAAI,cAAcC;CAAY;AAChF;AASA,SAAgBC,EAAe,EAC9B,WACA,gBACA,kBAKoB;CACpB,OAAO;EACN,WAAW;GACV,MAAM,OAAO,EAAE,UAAO,iBAAc,eAAY,SAAM,gBAAa;IAClE,IAAMC,IAAS,KAAK,IAAIN,GAAU,KAAK,IAAI,GAAGO,CAAI,IAAI,KAAK,IAAI,GAAGC,CAAS,CAAC,GAGtEC,IAAO,MAAMC,EAAO,aAAa;KACtC,QAAQC,EAAM,KAAK,KAAK,KAAA;KACxB,cAAcC,EAAa,SAAS,IAAIA,IAAe,KAAA;KACvD,UAAUN;IACX,CAAC,GACKO,IAAW,IAAI,IAAIC,KAAc,CAAC,CAAC,GACnCC,oBAAO,IAAI,IAAY,GACvBC,IAAyB,CAAC;IAEhC,KAAK,IAAMC,KAAKR,EAAK,UACpB,AAAIM,EAAK,IAAIE,EAAE,EAAE,KAAKJ,EAAS,IAAII,EAAE,EAAE,MACvCF,EAAK,IAAIE,EAAE,EAAE,GACbD,EAAM,KAAKf,EAAOgB,EAAE,IAAIA,EAAE,aAAaA,EAAE,IAAI,CAAC;IAE/C,IAAMC,KAAS,KAAK,IAAI,GAAGX,CAAI,IAAI,KAAKC,GAElCW,IAAQV,EAAK,gBAAgBO,EAAM,SAAS,IAAIA,EAAM;IAC5D,OAAO;KAAE,OAAOA,EAAM,MAAME,GAAOA,IAAQV,CAAS;KAAG;IAAM;GAC9D;GACA,MAAM,MAAM,GAAK;IAChB,OAAO,QAAQ,IACdY,EAAI,IAAI,OAAO,MAAO;KAGrB,IAAMC,IAAS,MAAMX,EACnB,WAAW,EAAE,MAAG,CAAC,CAAC,CAClB,YAAYA,EAAO,WAAW;MAAE;MAAI;KAAY,CAAC,CAAC,CAAC,CACnD,YAAY,KAAA,CAAS;KACvB,OAAOW,IACJpB,EAAOE,GAAIkB,EAAO,QAAQ,aAAaA,EAAO,QAAQ,IAAI,IAC1D;MAAE;MAAI,cAAclB;KAAG;IAC3B,CAAC,CACF;GACD;GAEA,MAAM,eAAe,GAAW;IAE/B,OAAO,CAAC,IAAG,MADYO,EAAO,YAAYY,CAAS,EAAA,CAC/B,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,KACrC,OAAkB;KAClB,IAAI,EAAE;KACN,OAAO,IAAI,EAAE;KACb,aAAa,EAAE,iBACZ,GAAG,EAAE,eAAe,SAAS,KAAK,EAAE,eAAe,WAAW,QAC9D,EAAE,YAAY,MAAM,GAAG,EAAE;IAC7B,EACD;GACD;EACD;EACA,OAAOC,IACJ;GACA,SAAS,MAASA,EAAY,OAAOC,CAAI;GACzC,SAAS,MAAWD,EAAY,OAAOE,CAAM;EAC9C,IACC,KAAA;CACJ;AACD;;;ACjFA,IAAMC,IAA6BC,EAAkB,KAAK,MACzDC,EAAE,OAAO,cAAeC,IAAwCD,CACjE;AAqBA,SAAwBE,EAAoB,EAC3C,WACA,gBACA,aACA,WACA,UACA,WACA,aACA,kBAC4B;CAC5B,IAAMC,IAAWC,QACV3B,EAAe;EAAE;EAAQ;EAAa;CAAY,CAAC,GACzD;EAACK;EAAQuB;EAAaV;CAAW,CAClC;CACA,OACC,kBAACW,GAAD;EAA2B;EAAmB;EAAiB;EAC9D,UAAA,kBAACC,GAAD;GACC,QAAQC;GACA;GACE;GACV,WAAWC;EACX,CAAA;CACgB,CAAA;AAEpB"}
1
+ {"version":3,"file":"branch-content-fields-BQG6U0yl.js","names":["contentLabel","MAX_PAGE","itemOf","data","id","blueprintId","branchAdapters","wanted","page","page_size","list","client","query","blueprintIds","excluded","excludeIds","seen","items","c","start","total","ids","detail","contentId","mediaSource","file","filter","plugins","builtInFieldTypes","p","knkRichTextPlugin","BranchContentFields","adapters","useMemo","changeSetId","FieldKitProvider","SpecForm","fields","branchId"],"sources":["../src/screens/branch-adapters.ts","../src/screens/branch-content-fields.tsx"],"sourcesContent":["// fieldkit's adapters for the Branch editor (ADR 0020): references from\n// contenthub's own reads, media through the host's MediaSource port. Types\n// only from fieldkit, so this module adds nothing to the root bundle.\nimport type { FieldKitAdapters, PinTarget, ReferenceItem } from \"@knkcs/fieldkit/renderer\";\nimport type { ContenthubClient } from \"../client/contenthub-client\";\nimport type { MediaSource } from \"../types\";\nimport { contentLabel } from \"./content-library\";\n\n/** The most rows one search asks for: ListContents' own cap. */\nconst MAX_PAGE = 200;\n\n/** What a picker shows for a Content: its label, or its id when its data has none. */\nfunction itemOf(id: string, blueprintId: string, data: Record<string, unknown>): ReferenceItem {\n\treturn { id, display_name: contentLabel(data) || id, blueprint_id: blueprintId };\n}\n\n/**\n * The adapters a Branch's form gets: `reference` searches contenthub's\n * Editor Search on Main (which reads open Branch heads too, so Content born\n * in an open Change Set is found), names a referenced Content from its read\n * and offers its Releases to pin; `media` is the host's MediaSource, absent\n * when the host has none, which leaves a media field nothing to pick.\n */\nexport function branchAdapters({\n\tclient,\n\tchangeSetId,\n\tmediaSource,\n}: {\n\tclient: ContenthubClient;\n\tchangeSetId: string;\n\tmediaSource?: MediaSource;\n}): FieldKitAdapters {\n\treturn {\n\t\treference: {\n\t\t\tasync search({ query, blueprintIds, excludeIds, page, page_size }) {\n\t\t\t\tconst wanted = Math.min(MAX_PAGE, Math.max(1, page) * Math.max(1, page_size));\n\t\t\t\t// Every target Blueprint at once: the service narrows the page, so\n\t\t\t\t// it holds only what the Field may point at, and paging stays exact.\n\t\t\t\tconst list = await client.listContents({\n\t\t\t\t\tsearch: query.trim() || undefined,\n\t\t\t\t\tblueprintIds: blueprintIds.length > 0 ? blueprintIds : undefined,\n\t\t\t\t\tpageSize: wanted,\n\t\t\t\t});\n\t\t\t\tconst excluded = new Set(excludeIds ?? []);\n\t\t\t\tconst seen = new Set<string>();\n\t\t\t\tconst items: ReferenceItem[] = [];\n\t\t\t\t// A search lists a Content once per head that matched: one row each.\n\t\t\t\tfor (const c of list.contents) {\n\t\t\t\t\tif (seen.has(c.id) || excluded.has(c.id)) continue;\n\t\t\t\t\tseen.add(c.id);\n\t\t\t\t\titems.push(itemOf(c.id, c.blueprintId, c.data));\n\t\t\t\t}\n\t\t\t\tconst start = (Math.max(1, page) - 1) * page_size;\n\t\t\t\t// The total is exact on the last page; otherwise there is at least one more.\n\t\t\t\tconst total = list.nextPageToken ? items.length + 1 : items.length;\n\t\t\t\treturn { items: items.slice(start, start + page_size), total };\n\t\t\t},\n\t\t\tasync fetch(ids) {\n\t\t\t\treturn Promise.all(\n\t\t\t\t\tids.map(async (id) => {\n\t\t\t\t\t\t// Published Content reads on Main; Content born in this\n\t\t\t\t\t\t// Change Set exists only there.\n\t\t\t\t\t\tconst detail = await client\n\t\t\t\t\t\t\t.getContent({ id })\n\t\t\t\t\t\t\t.catch(() => client.getContent({ id, changeSetId }))\n\t\t\t\t\t\t\t.catch(() => undefined);\n\t\t\t\t\t\treturn detail\n\t\t\t\t\t\t\t? itemOf(id, detail.content.blueprintId, detail.content.data)\n\t\t\t\t\t\t\t: { id, display_name: id };\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t},\n\t\t\t// A Pin names a Release (the one pinning kind fieldkit has), newest first.\n\t\t\tasync listPinTargets(contentId) {\n\t\t\t\tconst timeline = await client.getTimeline(contentId);\n\t\t\t\treturn [...timeline.entries].reverse().map(\n\t\t\t\t\t(e): PinTarget => ({\n\t\t\t\t\t\tid: e.releaseId,\n\t\t\t\t\t\tlabel: `#${e.ordinal}`,\n\t\t\t\t\t\tdescription: e.validityPeriod\n\t\t\t\t\t\t\t? `${e.validityPeriod.firstDay} – ${e.validityPeriod.lastDay || \"…\"}`\n\t\t\t\t\t\t\t: e.publishTime.slice(0, 10),\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t\tmedia: mediaSource\n\t\t\t? {\n\t\t\t\t\tupload: (file) => mediaSource.upload(file),\n\t\t\t\t\tbrowse: (filter) => mediaSource.browse(filter),\n\t\t\t\t}\n\t\t\t: undefined,\n\t};\n}\n","// fieldkit's renderer with knkeditor for rich text, behind a lazily loaded\n// module: both load the first time a Branch's Content tab is shown, never\n// with the rest of the package (spec #56; branch-editor-bundle.test.ts).\nimport { FieldKitProvider, SpecForm, type SpecFormLabels } from \"@knkcs/fieldkit/renderer\";\nimport { knkRichTextPlugin } from \"@knkcs/fieldkit/rich-text\";\nimport { builtInFieldTypes, type FieldTypePlugin, type Schema } from \"@knkcs/fieldkit/schema\";\nimport { useMemo } from \"react\";\nimport type { ContenthubClient } from \"../client/contenthub-client\";\nimport type { MediaSource, ResolvedField } from \"../types\";\nimport { branchAdapters } from \"./branch-adapters\";\n\n// rich_text through knkeditor (fieldkit ADR-0026); every other type as built in.\nconst plugins: FieldTypePlugin[] = builtInFieldTypes.map((p) =>\n\tp.id === \"rich_text\" ? (knkRichTextPlugin as FieldTypePlugin) : p,\n);\n\nexport interface BranchContentFieldsProps {\n\tclient: ContenthubClient;\n\tchangeSetId: string;\n\t/** The record the form holds, so SpecForm's remembered section is this Branch's. */\n\tbranchId: string;\n\tfields: ResolvedField[];\n\t/** The Resolved Release's parts: the Text Types its rich_text fields pin. */\n\tparts: Record<string, Record<string, unknown>>;\n\tlabels: SpecFormLabels;\n\treadOnly: boolean;\n\tmediaSource?: MediaSource;\n}\n\n/**\n * The Branch's data as the Blueprint Release lays it out, editable: its\n * sections, orientation and cards as fieldkit draws them, over the form the\n * editor holds above its tabs (fieldkit's spec-form.mdx, \"A Consumer-owned\n * Save across unmounting tabs\"). SpecForm keeps no draft of its own.\n */\nexport default function BranchContentFields({\n\tclient,\n\tchangeSetId,\n\tbranchId,\n\tfields,\n\tparts,\n\tlabels,\n\treadOnly,\n\tmediaSource,\n}: BranchContentFieldsProps) {\n\tconst adapters = useMemo(\n\t\t() => branchAdapters({ client, changeSetId, mediaSource }),\n\t\t[client, changeSetId, mediaSource],\n\t);\n\treturn (\n\t\t<FieldKitProvider plugins={plugins} adapters={adapters} parts={parts}>\n\t\t\t<SpecForm\n\t\t\t\tschema={fields as unknown as Schema}\n\t\t\t\tlabels={labels}\n\t\t\t\treadOnly={readOnly}\n\t\t\t\trecordKey={branchId}\n\t\t\t/>\n\t\t</FieldKitProvider>\n\t);\n}\n"],"mappings":";;;;;;;AASA,IAAMC,IAAW;AAGjB,SAASC,EAAO,GAAY,GAAqB,GAA8C;CAC9F,OAAO;EAAE;EAAI,cAAcF,EAAaG,CAAI,KAAKC;EAAI,cAAcC;CAAY;AAChF;AASA,SAAgBC,EAAe,EAC9B,WACA,gBACA,kBAKoB;CACpB,OAAO;EACN,WAAW;GACV,MAAM,OAAO,EAAE,UAAO,iBAAc,eAAY,SAAM,gBAAa;IAClE,IAAMC,IAAS,KAAK,IAAIN,GAAU,KAAK,IAAI,GAAGO,CAAI,IAAI,KAAK,IAAI,GAAGC,CAAS,CAAC,GAGtEC,IAAO,MAAMC,EAAO,aAAa;KACtC,QAAQC,EAAM,KAAK,KAAK,KAAA;KACxB,cAAcC,EAAa,SAAS,IAAIA,IAAe,KAAA;KACvD,UAAUN;IACX,CAAC,GACKO,IAAW,IAAI,IAAIC,KAAc,CAAC,CAAC,GACnCC,oBAAO,IAAI,IAAY,GACvBC,IAAyB,CAAC;IAEhC,KAAK,IAAMC,KAAKR,EAAK,UACpB,AAAIM,EAAK,IAAIE,EAAE,EAAE,KAAKJ,EAAS,IAAII,EAAE,EAAE,MACvCF,EAAK,IAAIE,EAAE,EAAE,GACbD,EAAM,KAAKf,EAAOgB,EAAE,IAAIA,EAAE,aAAaA,EAAE,IAAI,CAAC;IAE/C,IAAMC,KAAS,KAAK,IAAI,GAAGX,CAAI,IAAI,KAAKC,GAElCW,IAAQV,EAAK,gBAAgBO,EAAM,SAAS,IAAIA,EAAM;IAC5D,OAAO;KAAE,OAAOA,EAAM,MAAME,GAAOA,IAAQV,CAAS;KAAG;IAAM;GAC9D;GACA,MAAM,MAAM,GAAK;IAChB,OAAO,QAAQ,IACdY,EAAI,IAAI,OAAO,MAAO;KAGrB,IAAMC,IAAS,MAAMX,EACnB,WAAW,EAAE,MAAG,CAAC,CAAC,CAClB,YAAYA,EAAO,WAAW;MAAE;MAAI;KAAY,CAAC,CAAC,CAAC,CACnD,YAAY,KAAA,CAAS;KACvB,OAAOW,IACJpB,EAAOE,GAAIkB,EAAO,QAAQ,aAAaA,EAAO,QAAQ,IAAI,IAC1D;MAAE;MAAI,cAAclB;KAAG;IAC3B,CAAC,CACF;GACD;GAEA,MAAM,eAAe,GAAW;IAE/B,OAAO,CAAC,IAAG,MADYO,EAAO,YAAYY,CAAS,EAAA,CAC/B,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,KACrC,OAAkB;KAClB,IAAI,EAAE;KACN,OAAO,IAAI,EAAE;KACb,aAAa,EAAE,iBACZ,GAAG,EAAE,eAAe,SAAS,KAAK,EAAE,eAAe,WAAW,QAC9D,EAAE,YAAY,MAAM,GAAG,EAAE;IAC7B,EACD;GACD;EACD;EACA,OAAOC,IACJ;GACA,SAAS,MAASA,EAAY,OAAOC,CAAI;GACzC,SAAS,MAAWD,EAAY,OAAOE,CAAM;EAC9C,IACC,KAAA;CACJ;AACD;;;ACjFA,IAAMC,IAA6BC,EAAkB,KAAK,MACzDC,EAAE,OAAO,cAAeC,IAAwCD,CACjE;AAqBA,SAAwBE,EAAoB,EAC3C,WACA,gBACA,aACA,WACA,UACA,WACA,aACA,kBAC4B;CAC5B,IAAMC,IAAWC,QACV3B,EAAe;EAAE;EAAQ;EAAa;CAAY,CAAC,GACzD;EAACK;EAAQuB;EAAaV;CAAW,CAClC;CACA,OACC,kBAACW,GAAD;EAA2B;EAAmB;EAAiB;EAC9D,UAAA,kBAACC,GAAD;GACC,QAAQC;GACA;GACE;GACV,WAAWC;EACX,CAAA;CACgB,CAAA;AAEpB"}