@knkcs/mediahub-ui 0.4.0 → 0.5.1

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 ADDED
@@ -0,0 +1,62 @@
1
+ # @knkcs/mediahub-ui
2
+
3
+ Mediahub's reusable UI, published as a versioned npm package so that every host renders the same
4
+ media surfaces instead of re-implementing them.
5
+
6
+ Two hosts consume it today: knkCMS core, which embeds it as a build-time dependency, and mediahub's
7
+ own standalone app, which consumes it through the workspace.
8
+
9
+ ## Screens, not routes
10
+
11
+ A screen takes **typed paths** and emits **callbacks**; the host owns routing and draws the page
12
+ frame. No screen builds a URL, and no host builds UI a screen already owns.
13
+
14
+ That contract is the point of the package. When a host re-implements a surface instead of composing
15
+ it, the two copies diverge and bugs are fixed in one of them — see
16
+ [knkCS/mediahub#138](https://github.com/knkCS/mediahub/issues/138), which consolidated the asset-type
17
+ screens after four such bugs, and
18
+ [mediahub ADR 0008](../../docs/adr/0008-narrowing-composes-folder-collection-and-tag.md), which
19
+ removed a host-built browse switcher for the same reason.
20
+
21
+ ## What it exports
22
+
23
+ | Surface | What it is |
24
+ |---|---|
25
+ | `AssetLibrary` | The media browser: folder tree, filters, upload, and the context rail carrying Tags and Collections |
26
+ | `AssetDetail` | One asset, its versions and its metadata |
27
+ | `AssetPicker`, `CollectionPicker` | Embeddable pickers for a host's own forms |
28
+ | `AssetTypeIndex`, `AssetTypeCreate`, `AssetTypeDetail` | Asset-type administration, including the AI-processes tab |
29
+ | `CollectionIndex` | The Collections the user can see, marking the Restricted ones. Listing is not gated by authority — per [ADR 0001](../../docs/adr/0001-grant-authority-owner-or-workspace-admin.md) seeing a Collection and managing it are separate privileges |
30
+ | `CollectionCreate` | Create a Collection from a name, slug and description. **Static only** — a smart Collection's `rules` are stored and never evaluated, so no type selector and no rules editor appear ([#173](https://github.com/knkCS/mediahub/issues/173)) |
31
+ | `CollectionDetail` | One Collection's identity — name, description, delete — **and its access control**: the Restricted toggle (with the leak-guard refusal naming what it would expose) and the roster, offered only where the can-manage probe says the reader is an authority ([ADR 0001](../../docs/adr/0001-grant-authority-owner-or-workspace-admin.md): owner-on-the-Collection or admin-on-its-workspace, and grantees never see the roster). Not Curation: adding and removing assets happens in the library, where the assets are |
32
+ | `MediahubProvider` | The provider a host mounts once, holding the clients and query client |
33
+ | Clients | `createConnectMediahubClients` adapts per-service Connect clients into the domain clients the screens' hooks read, and owns the mapping — a host supplies transport only |
34
+ | Hooks and stores | The queries and mutations behind the screens, for a host composing its own view |
35
+
36
+ Everything lands at the package root (`.`). A second entry point, `./testing`, ships a stub client
37
+ and fixtures so a host can test its own composition without a server.
38
+
39
+ ## Narrowing composes
40
+
41
+ Folder, Collection and Tag narrow the library **together** — one folder, many collections, many tags.
42
+ They are not alternative modes, and a UI that makes them mutually exclusive cannot express a question
43
+ the data model permits. `AssetLibrary` accounts for what is applied with a row of dismissible filter
44
+ chips. See ADR 0008 and the `Narrowing` entry in [`CONTEXT.md`](../../CONTEXT.md).
45
+
46
+ ## Layout comes from anker
47
+
48
+ Screens use anker's templates and primitives — `IndexPageTemplate`, `DetailPageTemplate`,
49
+ `SubNavLayout`, `ContextRail` — and report their frame through anker's host contract. anker's own
50
+ guidance is explicit: if a template does not fit, file an issue rather than hand-rolling a layout.
51
+
52
+ ## Peers
53
+
54
+ Every non-trivial dependency is a **peer** the host supplies once, so a host and the package can
55
+ never load two copies of React, the query client or anker. The Rollup external list is derived from
56
+ `peerDependencies`, so declaring a peer is the whole change.
57
+
58
+ ## Releases
59
+
60
+ Versioned with release-please from conventional commits, and published to npm by CI. A commit whose
61
+ subject loses its `feat:`/`fix:` prefix — which a squash merge can do — produces no release at all,
62
+ silently ([knkCS/mediahub#185](https://github.com/knkCS/mediahub/issues/185)).
package/dist/index.d.ts CHANGED
@@ -13,6 +13,46 @@ import { UseMutateFunction } from '@tanstack/react-query';
13
13
  import { UseMutationResult } from '@tanstack/react-query';
14
14
  import { UseQueryResult } from '@tanstack/react-query';
15
15
 
16
+ /** One narrowing currently applied to the library. */
17
+ export declare interface ActiveFilterChip {
18
+ /** Stable identity for the row — never rendered. */
19
+ id: string;
20
+ /**
21
+ * What the chip says, already translated and already resolved to a name.
22
+ * A chip never carries a raw id: the caller resolves Folder, Collection
23
+ * and Tag through the data the panels load, and falls back to the kind's
24
+ * own word when a row cannot be resolved.
25
+ */
26
+ label: string;
27
+ /** Removes exactly this filter, leaving the others applied. */
28
+ onRemove: () => void;
29
+ }
30
+
31
+ /**
32
+ * The library's account of what is narrowing it (ADR 0008).
33
+ *
34
+ * Presentation only: it owns no filter state and resolves no names — it
35
+ * renders the chips it is handed and reports the dismissals, so the caller's
36
+ * single filter state stays the one source the panels also write.
37
+ *
38
+ * With nothing applied it renders `null` rather than an empty band, so the
39
+ * row costs no vertical space until there is something to account for.
40
+ *
41
+ * Each chip is anker's `Toolbar.FilterChip`, a native button — reachable by
42
+ * Tab and activated by Enter or Space with no key handling of our own.
43
+ */
44
+ export declare const ActiveFilterChips: React.FC<ActiveFilterChipsProps>;
45
+
46
+ export declare interface ActiveFilterChipsProps {
47
+ chips: ActiveFilterChip[];
48
+ onClearAll: () => void;
49
+ /** Names the group for assistive technology, e.g. "Active filters". */
50
+ groupLabel: string;
51
+ /** Said before each chip's label, so the button announces what it does. */
52
+ removeLabel: string;
53
+ clearAllLabel: string;
54
+ }
55
+
16
56
  export declare interface AddVersionVars {
17
57
  assetId: string;
18
58
  file: File;
@@ -564,6 +604,146 @@ export declare interface CollectionClient {
564
604
  setRestricted(id: string, restricted: boolean): Promise<CollectionInfo>;
565
605
  }
566
606
 
607
+ /**
608
+ * Create a Collection. The package owns the screen; both hosts compose it and
609
+ * keep their routing to themselves (#176, parent #174).
610
+ *
611
+ * **Static Collections only.** There is deliberately no Smart/Static selector
612
+ * and no `rules` editor, though the standalone app's form dialog offered both.
613
+ * A smart Collection's `rules` are validated as JSON, stored and returned —
614
+ * and never evaluated anywhere; membership resolves exclusively through
615
+ * explicit edges. The control would therefore do nothing at all, and this
616
+ * project does not ship a known-inert control into a second host
617
+ * (knkCS/mediahub#173). Every Collection created here is `static` with empty
618
+ * rules.
619
+ *
620
+ * The slug follows the name until it is edited by hand and then stops, and the
621
+ * server's rule (lowercase alphanumeric with hyphens, at least two characters)
622
+ * is both stated on the form and enforced before submit — without both, a name
623
+ * like `Bild Anhang` produced a bare 400 (knkcms/core#861).
624
+ */
625
+ export declare function CollectionCreate({ collectionsPath, onCreated, onCancel, }: CollectionCreateProps): default_2.JSX.Element;
626
+
627
+ export declare namespace CollectionCreate {
628
+ var displayName: string;
629
+ }
630
+
631
+ export declare interface CollectionCreateProps {
632
+ /**
633
+ * The host's Collection index path — the breadcrumb's target. A path, not a
634
+ * route: the screen never navigates, it only says where the list it belongs
635
+ * to lives (core's `docs/service-integration.md`; spec #174).
636
+ */
637
+ collectionsPath: string;
638
+ /**
639
+ * The Collection was created; the host receives its id and decides where to
640
+ * go. Id only, like `CollectionIndex`'s `onOpen` — the host needs it to
641
+ * build a detail URL, and nothing here knows what that URL looks like.
642
+ */
643
+ onCreated?: (collectionId: string) => void;
644
+ /** Called when the administrator abandons the form. */
645
+ onCancel?: () => void;
646
+ }
647
+
648
+ /**
649
+ * The packaged collection detail screen: view one Collection, correct its name
650
+ * and description, delete it behind a confirmation, and — for an authority —
651
+ * decide who may see it.
652
+ *
653
+ * **Authority, per ADR 0001**, is owner-on-the-Collection *or* admin-on-its-
654
+ * workspace, and the package already answers it: `useCanManage` probes it by
655
+ * attempting the roster read the server gates on exactly that authority. Only
656
+ * `data === true` unlocks anything — a pending probe, a denial and a failure
657
+ * all render the same screen without management controls (fail closed).
658
+ *
659
+ * **A denial is an answer, not a failure.** `canManage` reads Connect's
660
+ * *numeric* code 7 as "not an authority" and returns false. A host transport
661
+ * that flattens errors to a bare message (core's did until
662
+ * knkboor/general#814) turns that answer into a rejection — which this screen
663
+ * survives, because a rejected probe is not `true` either, so the controls
664
+ * stay hidden instead of the screen breaking for the users with least access.
665
+ * Both shapes are pinned by test.
666
+ *
667
+ * **Grantees never see the roster** (ADR 0001 again): content access and
668
+ * roster access are separate privileges, so a viewer gets the Collection's
669
+ * identity and no Manage Access affordance at all — the roster is never
670
+ * fetched for them, not merely hidden after the fact. Authority withdrawn
671
+ * *mid-session* takes the controls away at once and closes an open dialog
672
+ * through its `open` prop — the machine itself stays mounted, because
673
+ * unmounting a dialog while it is open leaks the body scroll lock over the
674
+ * whole application (CLAUDE-ANKER's Modal mount pattern). The Restricted state
675
+ * itself is on the record they already read and is shown to everyone, as the
676
+ * index and the folder tree show it.
677
+ *
678
+ * **Curation** — adding and removing assets — is not this screen's job: per
679
+ * ADR 0003 it happens in the library, where the assets are.
680
+ *
681
+ * **The patch is the whole record this screen owns, every time.**
682
+ * `UpdateCollectionRequest` carries plain proto3 scalars and the service applies
683
+ * `description` unconditionally, so a patch naming only `name` clears the
684
+ * description (knkCS/mediahub#159, the same trap as knkcms/core#863). Saving
685
+ * therefore sends both fields, and deliberately does *not* name `rules` — the
686
+ * smart-collection field this surface does not edit (knkCS/mediahub#173) — so
687
+ * the client's read-modify-write keeps whatever is stored there.
688
+ *
689
+ * **Correct against the wire, not against the declared type.** Connect omits
690
+ * empty strings from its JSON, so a description-less collection arrives with
691
+ * the key absent. Every field is defaulted where it is read, so an absent value
692
+ * renders as empty rather than flipping a controlled input to uncontrolled.
693
+ */
694
+ export declare function CollectionDetail({ collectionId, collectionsPath, onBack, onDeleted, }: CollectionDetailProps): default_2.JSX.Element;
695
+
696
+ export declare namespace CollectionDetail {
697
+ var displayName: string;
698
+ }
699
+
700
+ export declare interface CollectionDetailProps {
701
+ /** The collection to show. */
702
+ collectionId: string;
703
+ /**
704
+ * Typed path of the host's collection index, used for the breadcrumb back
705
+ * link. The screen never routes itself — the host owns its URLs
706
+ * (core's `docs/service-integration.md`).
707
+ */
708
+ collectionsPath?: string;
709
+ /**
710
+ * Navigate back to the collection index. Given one, the screen renders a
711
+ * back affordance that confirms before discarding unsaved edits — the
712
+ * breadcrumb link above is the host's own navigation and the screen cannot
713
+ * intercept it.
714
+ */
715
+ onBack?: () => void;
716
+ /** Called after a successful delete; the host decides where to go next. */
717
+ onDeleted?: () => void;
718
+ }
719
+
720
+ /**
721
+ * The workspace's Collections, listed. The package owns the screen; both hosts
722
+ * compose it and keep their own routing to themselves (#175, parent #174).
723
+ *
724
+ * Listing is deliberately NOT gated by authority: a Collection the user can see
725
+ * is listed, because per ADR 0001 seeing a Collection and managing it are
726
+ * separate privileges. Management controls arrive in later tickets and are
727
+ * gated there, on the can-manage probe — never here, on the listing.
728
+ */
729
+ export declare function CollectionIndex(props: CollectionIndexProps): React.JSX.Element;
730
+
731
+ export declare namespace CollectionIndex {
732
+ var displayName: string;
733
+ }
734
+
735
+ export declare interface CollectionIndexProps {
736
+ /**
737
+ * A collection was opened. The host decides what that means — it receives
738
+ * the id and nothing else, because this package must not know either host's
739
+ * URL structure (core's `docs/service-integration.md`; spec #138, #174).
740
+ * Screens, not routes: nothing here navigates and nothing here builds a path.
741
+ */
742
+ onOpen?: (collectionId: string) => void;
743
+ /** The reader asked for a new collection. Same contract as `onOpen`. */
744
+ onCreate?: () => void;
745
+ }
746
+
567
747
  export declare interface CollectionInfo {
568
748
  id: string;
569
749
  workspaceId: string;
@@ -1124,6 +1304,41 @@ export declare const i18nResources: {
1124
1304
  discardConfirmMessage: string;
1125
1305
  discardConfirmLabel: string;
1126
1306
  };
1307
+ collectionDetail: {
1308
+ breadcrumbCollections: string;
1309
+ notFound: string;
1310
+ untitled: string;
1311
+ backButtonAria: string;
1312
+ nameLabel: string;
1313
+ nameRequired: string;
1314
+ slugLabel: string;
1315
+ descriptionLabel: string;
1316
+ descriptionPlaceholder: string;
1317
+ save: string;
1318
+ delete: string;
1319
+ saveSuccessToast: string;
1320
+ saveFailedToast: string;
1321
+ deleteConfirmTitle: string;
1322
+ deleteConfirmMessage: string;
1323
+ deleteConfirmLabel: string;
1324
+ deleteSuccessToast: string;
1325
+ deleteFailedToast: string;
1326
+ discardConfirmTitle: string;
1327
+ discardConfirmMessage: string;
1328
+ discardConfirmLabel: string;
1329
+ accessHeading: string;
1330
+ accessOpenNote: string;
1331
+ accessRestrictedNote: string;
1332
+ restrict: string;
1333
+ unrestrict: string;
1334
+ manageAccess: string;
1335
+ unrestrictConfirmTitle: string;
1336
+ unrestrictConfirmLabel: string;
1337
+ restrictSuccessToast: string;
1338
+ unrestrictSuccessToast: string;
1339
+ restrictFailedToast: string;
1340
+ unrestrictFailedToast: string;
1341
+ };
1127
1342
  assetLibrary: {
1128
1343
  pageTitle: string;
1129
1344
  uploadButton: string;
@@ -1158,6 +1373,20 @@ export declare const i18nResources: {
1158
1373
  clearStatusGroup: string;
1159
1374
  assetsLoadFailedHeader: string;
1160
1375
  noAssetsFound: string;
1376
+ activeFiltersLabel: string;
1377
+ removeFilter: string;
1378
+ clearAllFilters: string;
1379
+ chipFolder: string;
1380
+ chipFolderUnresolved: string;
1381
+ chipCollection: string;
1382
+ chipCollectionUnresolved: string;
1383
+ chipTag: string;
1384
+ chipTagUnresolved: string;
1385
+ chipStatus: string;
1386
+ chipStatusGroup: string;
1387
+ chipSearch: string;
1388
+ noAssetsMatchFilters: string;
1389
+ noAssetsMatchFiltersDescription: string;
1161
1390
  };
1162
1391
  assetPicker: {
1163
1392
  headerSingle: string;
@@ -1401,6 +1630,36 @@ export declare const i18nResources: {
1401
1630
  noMatchesDescription: string;
1402
1631
  loadFailedHeader: string;
1403
1632
  };
1633
+ collectionCreate: {
1634
+ title: string;
1635
+ breadcrumbCollections: string;
1636
+ nameLabel: string;
1637
+ namePlaceholder: string;
1638
+ nameRequired: string;
1639
+ slugLabel: string;
1640
+ slugHint: string;
1641
+ slugInvalid: string;
1642
+ descriptionLabel: string;
1643
+ descriptionPlaceholder: string;
1644
+ submit: string;
1645
+ cancel: string;
1646
+ createdToast: string;
1647
+ createFailedToast: string;
1648
+ };
1649
+ collectionIndex: {
1650
+ pageTitle: string;
1651
+ pageSubtitle: string;
1652
+ newButton: string;
1653
+ searchPlaceholder: string;
1654
+ collectionsCount: string;
1655
+ columnName: string;
1656
+ columnDescription: string;
1657
+ noCollectionsHeader: string;
1658
+ noCollectionsDescription: string;
1659
+ noMatchesHeader: string;
1660
+ noMatchesDescription: string;
1661
+ loadFailedHeader: string;
1662
+ };
1404
1663
  aiProcesses: {
1405
1664
  intro: string;
1406
1665
  newProcess: string;
@@ -1552,6 +1811,41 @@ export declare const i18nResources: {
1552
1811
  discardConfirmMessage: string;
1553
1812
  discardConfirmLabel: string;
1554
1813
  };
1814
+ collectionDetail: {
1815
+ breadcrumbCollections: string;
1816
+ notFound: string;
1817
+ untitled: string;
1818
+ backButtonAria: string;
1819
+ nameLabel: string;
1820
+ nameRequired: string;
1821
+ slugLabel: string;
1822
+ descriptionLabel: string;
1823
+ descriptionPlaceholder: string;
1824
+ save: string;
1825
+ delete: string;
1826
+ saveSuccessToast: string;
1827
+ saveFailedToast: string;
1828
+ deleteConfirmTitle: string;
1829
+ deleteConfirmMessage: string;
1830
+ deleteConfirmLabel: string;
1831
+ deleteSuccessToast: string;
1832
+ deleteFailedToast: string;
1833
+ discardConfirmTitle: string;
1834
+ discardConfirmMessage: string;
1835
+ discardConfirmLabel: string;
1836
+ accessHeading: string;
1837
+ accessOpenNote: string;
1838
+ accessRestrictedNote: string;
1839
+ restrict: string;
1840
+ unrestrict: string;
1841
+ manageAccess: string;
1842
+ unrestrictConfirmTitle: string;
1843
+ unrestrictConfirmLabel: string;
1844
+ restrictSuccessToast: string;
1845
+ unrestrictSuccessToast: string;
1846
+ restrictFailedToast: string;
1847
+ unrestrictFailedToast: string;
1848
+ };
1555
1849
  assetLibrary: {
1556
1850
  pageTitle: string;
1557
1851
  uploadButton: string;
@@ -1586,6 +1880,20 @@ export declare const i18nResources: {
1586
1880
  clearStatusGroup: string;
1587
1881
  assetsLoadFailedHeader: string;
1588
1882
  noAssetsFound: string;
1883
+ activeFiltersLabel: string;
1884
+ removeFilter: string;
1885
+ clearAllFilters: string;
1886
+ chipFolder: string;
1887
+ chipFolderUnresolved: string;
1888
+ chipCollection: string;
1889
+ chipCollectionUnresolved: string;
1890
+ chipTag: string;
1891
+ chipTagUnresolved: string;
1892
+ chipStatus: string;
1893
+ chipStatusGroup: string;
1894
+ chipSearch: string;
1895
+ noAssetsMatchFilters: string;
1896
+ noAssetsMatchFiltersDescription: string;
1589
1897
  };
1590
1898
  assetPicker: {
1591
1899
  headerSingle: string;
@@ -1829,6 +2137,36 @@ export declare const i18nResources: {
1829
2137
  noMatchesDescription: string;
1830
2138
  loadFailedHeader: string;
1831
2139
  };
2140
+ collectionCreate: {
2141
+ title: string;
2142
+ breadcrumbCollections: string;
2143
+ nameLabel: string;
2144
+ namePlaceholder: string;
2145
+ nameRequired: string;
2146
+ slugLabel: string;
2147
+ slugHint: string;
2148
+ slugInvalid: string;
2149
+ descriptionLabel: string;
2150
+ descriptionPlaceholder: string;
2151
+ submit: string;
2152
+ cancel: string;
2153
+ createdToast: string;
2154
+ createFailedToast: string;
2155
+ };
2156
+ collectionIndex: {
2157
+ pageTitle: string;
2158
+ pageSubtitle: string;
2159
+ newButton: string;
2160
+ searchPlaceholder: string;
2161
+ collectionsCount: string;
2162
+ columnName: string;
2163
+ columnDescription: string;
2164
+ noCollectionsHeader: string;
2165
+ noCollectionsDescription: string;
2166
+ noMatchesHeader: string;
2167
+ noMatchesDescription: string;
2168
+ loadFailedHeader: string;
2169
+ };
1832
2170
  aiProcesses: {
1833
2171
  intro: string;
1834
2172
  newProcess: string;