@proveanything/smartlinks-utils-ui 0.8.3 → 0.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.
@@ -675,285 +675,194 @@ interface RecordsAdminShellProps<TData = unknown> {
675
675
  */
676
676
  contextScopeMode?: 'strict' | 'expand';
677
677
  renderEditor: (ctx: EditorContext<TData>) => ReactNode;
678
- /**
679
- * Render the preview surface. Receives the editor's current value so the
680
- * preview tracks unsaved edits. When `previewScope` differs from the
681
- * editing scope (because the user picked something in `<PreviewScopePicker>`),
682
- * `previewScope` reflects the chosen target — apps can use it to load
683
- * resolved/collected/merged data for that scope instead of `resolved`.
684
- */
685
- renderPreview?: (ctx: {
686
- resolved: TData | null;
687
- previewScope: ParsedRef;
688
- }) => ReactNode;
689
- /**
690
- * How the preview surface attaches to the editor pane.
691
- * - `inline` (default): rendered below the form (current behaviour).
692
- * - `side`: resizable right pane next to the editor.
693
- * - `tab`: Editor / Preview siblings inside the right pane.
694
- * - `drawer`: slide-out from the right, toggled by a button.
695
- */
696
- previewMode?: 'inline' | 'side' | 'tab' | 'drawer';
697
- /**
698
- * When true, render a `<PreviewScopePicker>` in the preview chrome so admins
699
- * can preview as a different product/variant/batch than they're editing.
700
- * Default false.
701
- */
702
- previewScopePicker?: boolean;
703
- /**
704
- * Editor tab strip behaviour. See {@link RecordsAdminEditorTabsMode}.
705
- * Default `'off'` — the redundant single-tab strip is hidden when the
706
- * product has no variants/batches.
707
- */
708
- editorTabs?: RecordsAdminEditorTabsMode;
709
- /**
710
- * How to handle unsaved edits when the user navigates to a different
711
- * record/scope/tab.
712
- * - `prompt` (default): show a confirm dialog (Discard / Cancel / Save).
713
- * - `autosave`: silently save before navigating away.
714
- * - `keep`: hold edits in memory keyed by ref; coming back restores them.
715
- */
716
- dirtyStrategy?: 'prompt' | 'autosave' | 'keep';
717
- /**
718
- * Pre-delete hook. Return `false` to abort. Lets apps run app-specific
719
- * confirms (e.g. "this batch has 12k linked proofs"). When omitted the
720
- * inline two-step confirm is the only safeguard.
721
- */
722
- onBeforeDelete?: (scope: ParsedRef) => boolean | Promise<boolean>;
723
- /**
724
- * Disable the browser-level `beforeunload` prompt. Default false.
725
- * Set true when the host platform handles unload guarding itself.
726
- */
727
- disableBeforeUnload?: boolean;
728
- intro?: {
729
- title: string;
730
- body: ReactNode;
731
- /**
732
- * Where to surface the "show again" affordance after the user dismisses
733
- * the intro banner.
734
- *
735
- * - `'header'` *(default)* — small ghost icon-button (`?`) inline inside
736
- * the `ShellHeader`, right-aligned next to `headerActions`. Zero
737
- * vertical footprint. Auto-falls back to `'footer'` when no header
738
- * card is rendered (host hasn't enabled it).
739
- * - `'footer'` — render the `?` button in the quiet utility row that
740
- * sits above the rail/editor split.
741
- * - `'inline'` — legacy behaviour: full-width strip with a "How it
742
- * works" pill on the right.
743
- * - `'hidden'` — once dismissed, do not offer a way to bring it back
744
- * from this surface. The host can still re-enable via state reset.
745
- */
746
- reopenAffordance?: 'header' | 'footer' | 'inline' | 'hidden';
747
- /** Override the default "How it works" label / tooltip. */
748
- reopenLabel?: string;
749
- };
750
678
  csvSchema?: CsvSchema<TData>;
751
679
  classify?: (record: RecordSummary<TData>) => RecordStatus;
752
680
  defaultData?: () => TData;
753
681
  /**
754
- * Optional derivation for the label that represents an in-flight draft
755
- * in the unsaved-changes tray. Receives the editor's current value and
756
- * its scope; return a short, human-readable title (or `undefined` to
757
- * defer to the built-in heuristic). Useful when your record shape nests
758
- * the title under a custom key the heuristic doesn't know about.
759
- */
760
- deriveDraftLabel?: (value: TData, scope: ParsedRef) => string | undefined;
761
- /**
762
- * Which layouts the rail offers. Default `['list']`. When more than one is
763
- * supplied, a switcher appears above the list and the choice persists
764
- * per-app/recordType.
765
- */
766
- presentations?: RecordPresentation[];
767
- /** Initial presentation when nothing is persisted. Default first of `presentations`. */
768
- defaultPresentation?: RecordPresentation;
769
- /**
770
- * Optional custom row renderer for the rail. Applied to both `list` and
771
- * `compact` densities. Cards/galleries are not supported in the rail —
772
- * they belong on the right pane (see `renderItemList` / `itemView`).
773
- */
774
- renderListRow?: (record: RecordSummary<TData>, ctx: RecordSlotContext) => ReactNode;
775
- /**
776
- * Optional custom empty-state renderer for the rail. If omitted the shell
777
- * uses the default `<EmptyState>` with `i18n.emptyTitle` / `i18n.emptyBody`.
778
- */
779
- renderEmpty?: (ctx: {
780
- scope: ParsedRef | null;
781
- }) => ReactNode;
782
- /**
783
- * Whether each scope holds at most one record (`singleton`, default) or
784
- * many (`collection`, e.g. FAQs / recipes / SOPs). In collection mode the
785
- * shell treats scopes as folders containing items: when a scope is selected
786
- * with no item open, the right pane shows the multi-item view (default
787
- * table, or `renderItemList` / `itemView` overrides). Clicking an item
788
- * opens it in the editor with Back / prev / next nav, and the rail flips
789
- * to siblings (see `collectionRailMode`).
790
- */
791
- cardinality?: RecordCardinality;
792
- /** Display name for an item in collection mode (defaults to `'item'`). */
793
- itemNoun?: string;
794
- /**
795
- * Generate a host-local handle for a brand-new collection item draft when
796
- * "+ New" is clicked. The shell prefixes non-draft values so they remain
797
- * recognisably unsaved (`draft:…`) until the first save returns a real
798
- * record UUID. Use this when the host wants stable local draft identities
799
- * for analytics/UI bookkeeping — not to predeclare the eventual server id.
682
+ * Header card configuration. The card is off by default; set `show: true`
683
+ * (or pass any of `title` / `subtitle` / `icon` / `actions` / `stats`) to
684
+ * opt in.
800
685
  */
801
- generateItemId?: () => string;
686
+ header?: HeaderConfig;
802
687
  /**
803
- * Which built-in item views the right pane offers when a scope is selected
804
- * and no item is open. Default `['table']`. When more than one is supplied,
805
- * a switcher appears above the item view and the choice persists per
806
- * `appId` + `recordType`.
807
- *
808
- * Ignored when `renderItemList` is supplied (the host owns the entire view).
688
+ * Dismissable intro / "How it works" card shown above the rail+editor split.
689
+ * Only renders when supplied. The reopen affordance defaults to a small
690
+ * icon-button inside the header (auto-falls back to the utility row when
691
+ * no header is rendered).
809
692
  */
810
- itemViews?: ItemView[];
811
- /** Initial item view when nothing is persisted. Default first of `itemViews`. */
812
- defaultItemView?: ItemView;
693
+ intro?: IntroConfig;
694
+ rail?: RailConfig<TData>;
695
+ editor?: EditorConfig<TData>;
696
+ items?: ItemsConfig<TData>;
697
+ unsaved?: UnsavedConfig<TData>;
698
+ clipboard?: ClipboardConfig<TData>;
699
+ actions?: ActionsConfig;
813
700
  /**
814
- * Declarative columns for the built-in default `table` view. The shell
815
- * renders a styled table most apps with a few well-defined fields
816
- * (FAQ question + last-updated, recipe name + cuisine + servings) want
817
- * this rather than a custom renderer.
701
+ * Mirror the shell's runtime state into URL params. Off by default. The
702
+ * shell only owns `item`, `scope`, `view` host platform params are
703
+ * untouched.
818
704
  */
819
- itemColumns?: ItemColumn<TData>[];
820
- /**
821
- * Full custom item-view renderer. When supplied, replaces the built-in
822
- * table / cards / gallery entirely — `itemViews`, `itemColumns`, and
823
- * `renderItemCard` are ignored.
824
- */
825
- renderItemList?: (items: RecordSummary<TData>[], ctx: ItemViewContext) => ReactNode;
826
- /**
827
- * Custom card renderer for the `cards` and `gallery` item views. Falls
828
- * back to a styled built-in card when omitted.
829
- */
830
- renderItemCard?: (record: RecordSummary<TData>, ctx: ItemSlotContext) => ReactNode;
831
- /**
832
- * Visual density of the built-in `cards` / `gallery` item views.
833
- * - `'sm'` — tight: ~180px (cards) / ~240px (gallery) min column width.
834
- * - `'md'` *(default)* — comfortable: ~240px / ~320px.
835
- * - `'lg'` — spacious: ~320px / ~420px, suits hero-style imagery.
836
- *
837
- * Hosts that need exact pixel control can override the underlying
838
- * `--ra-item-card-min` / `--ra-item-gallery-min` CSS custom properties
839
- * on `.ra-shell` instead.
840
- */
841
- itemCardSize?: 'sm' | 'md' | 'lg';
842
- /**
843
- * Custom empty state when a scope has no items yet. Falls back to a
844
- * styled built-in empty state with a "+ New {noun}" CTA.
845
- */
846
- renderItemEmpty?: (ctx: ItemViewContext) => ReactNode;
847
- /**
848
- * What the rail shows once an item is open (collection cardinality only).
849
- * Default `'siblings'` — the rail flips to the items in the current scope
850
- * with a pinned `← All scopes` link. Set to `'scopes'` to keep the rail
851
- * on scope navigation (admin must use Back / prev / next instead).
852
- */
853
- collectionRailMode?: CollectionRailMode;
854
- /** Display title shown in the header card. Falls back to `label`, then `recordType`. */
855
- title?: string;
856
- /** Single-line muted subtitle under the header title. */
857
- subtitle?: string;
858
- /**
859
- * Render the branded header card above the rail. Off by default — most apps
860
- * don't need it (the surrounding host typically already shows a title). Set
861
- * `true` to opt in, or simply pass any of the header-customising props
862
- * (`title`, `subtitle`, `headerIcon`, `headerActions`, `showStats`,
863
- * `statsItems`) and the header will auto-show.
864
- */
865
- showHeader?: boolean;
866
- /** Icon shown in the header card. Falls back to `icons.header.byRecordType[recordType]`
867
- * or `icons.header.default`. Pass a Lucide icon component or any ReactNode. */
868
- headerIcon?: ReactNode;
869
- /** Right-aligned header content — typically a primary "+ New" button + secondary controls. */
870
- headerActions?: ReactNode;
871
- /** Show a counts strip in the header (e.g. Shared · Products). Off by default —
872
- * most apps don't need it. When `true`, the shell renders built-in counts unless
873
- * `statsItems` is also provided. */
874
- showStats?: boolean;
875
- /** Show the contextual icon to the left of the header title. Default true.
876
- * Set to `false` if the recordType icon (database, etc.) feels off-brand. */
877
- showHeaderIcon?: boolean;
878
- /** Fully custom stats strip content. When provided, replaces the built-in
879
- * Shared/Products counts. Each item renders as a value + uppercase label. */
880
- statsItems?: Array<{
705
+ deepLink?: DeepLinkOptions;
706
+ /** Override the default Lucide icons used by the shell. Deep-merged onto
707
+ * `DEFAULT_ICONS`. */
708
+ icons?: Partial<RecordsAdminIcons>;
709
+ i18n?: Partial<RecordsAdminI18n>;
710
+ onTelemetry?: (event: TelemetryEvent) => void;
711
+ className?: string;
712
+ }
713
+ interface HeaderStatsConfig {
714
+ /** Show a counts strip in the header. */
715
+ show?: boolean;
716
+ /** Fully custom stats items. When provided, replaces built-in counts. */
717
+ items?: Array<{
881
718
  label: string;
882
719
  value: string | number;
883
720
  }>;
884
721
  /** Optional title rendered above the stats strip (e.g. "At a glance"). */
885
- statsTitle?: string;
886
- /** Optional icon rendered next to `statsTitle`. Pass a Lucide icon element
887
- * or any ReactNode. */
888
- statsIcon?: ReactNode;
889
- /**
890
- * External help / documentation link for this admin surface. When set, the
891
- * shell renders a small icon-button in the header (right side) that opens
892
- * the URL in a new tab. Use this to point at an app-specific help doc in
893
- * your shared docs site.
894
- */
722
+ title?: string;
723
+ /** Optional icon rendered next to the stats title. */
724
+ icon?: ReactNode;
725
+ }
726
+ interface HeaderConfig {
727
+ /** Render the branded header card above the rail. Auto-true when any other
728
+ * header field is set. */
729
+ show?: boolean;
730
+ /** Display title shown in the header card. Falls back to `label`, then
731
+ * `recordType`. */
732
+ title?: string;
733
+ /** Single-line muted subtitle under the title. */
734
+ subtitle?: string;
735
+ /** Icon shown to the left of the header title. Falls back to
736
+ * `icons.header.byRecordType[recordType]` or `icons.header.default`. */
737
+ icon?: ReactNode;
738
+ /** Show the contextual icon. Default true. */
739
+ showIcon?: boolean;
740
+ /** Right-aligned header content — typically a primary "+ New" button. */
741
+ actions?: ReactNode;
742
+ /** External help / documentation link rendered as a small icon-button. */
895
743
  helpUrl?: string;
896
- /** Tooltip / aria-label for the help link. Defaults to "Help & documentation". */
744
+ /** Tooltip / aria-label for the help link. Defaults to
745
+ * "Help & documentation". */
897
746
  helpLabel?: string;
898
- /** Override the default Lucide icons used by the shell (scope tabs, statuses,
899
- * empty states, action menus, etc.). Deep-merged onto `DEFAULT_ICONS`. */
900
- icons?: Partial<RecordsAdminIcons>;
901
- /** When supplied, the records list is rendered as accordion-style groups.
902
- * Return `null` to put a record in the default "Other" bucket. */
747
+ /** Counts strip configuration. */
748
+ stats?: HeaderStatsConfig;
749
+ }
750
+ interface IntroConfig {
751
+ title: string;
752
+ body: ReactNode;
753
+ /**
754
+ * Where to surface the "show again" affordance after dismissal.
755
+ * Defaults to `'header'` (auto-falls back to `'footer'` when no header
756
+ * card is rendered).
757
+ */
758
+ reopenAffordance?: 'header' | 'footer' | 'inline' | 'hidden';
759
+ /** Override the default "How it works" label / tooltip. */
760
+ reopenLabel?: string;
761
+ }
762
+ interface RailConfig<TData = unknown> {
763
+ /** Which layouts the rail offers. Default `['list']`. */
764
+ presentations?: RecordPresentation[];
765
+ /** Initial presentation when nothing is persisted. */
766
+ defaultPresentation?: RecordPresentation;
767
+ /** Custom row renderer for `list` / `compact` densities. */
768
+ renderListRow?: (record: RecordSummary<TData>, ctx: RecordSlotContext) => ReactNode;
769
+ /**
770
+ * Custom empty-state renderer for the records rail. Receives the active
771
+ * scope (kind only). Falls back to the styled built-in empty state.
772
+ */
773
+ renderEmpty?: (ctx: {
774
+ scope: ScopeKind;
775
+ }) => ReactNode;
776
+ /** Group rail records into accordion-style buckets. Return `null` to put
777
+ * a record in the default "Other" bucket. */
903
778
  groupBy?: (record: RecordSummary) => {
904
779
  key: string;
905
780
  label: string;
906
781
  icon?: ReactNode;
907
782
  } | null;
908
- /** Custom empty-state renderer for the records rail. Falls back to the styled
909
- * built-in empty state (icon + title + body + optional CTAs). */
910
- renderEmptyState?: (ctx: {
911
- scope: ScopeKind;
912
- }) => ReactNode;
913
783
  /** Visual density. Default `comfortable`. */
914
784
  density?: 'comfortable' | 'compact';
915
- i18n?: Partial<RecordsAdminI18n>;
916
- onTelemetry?: (event: TelemetryEvent) => void;
917
- className?: string;
785
+ }
786
+ interface EditorPreviewConfig<TData = unknown> {
918
787
  /**
919
- * Override the resting label for footer / banner actions. When a key is
920
- * omitted the shell falls back to the i18n default ("Save", "Discard",
921
- * "Delete"). Loading / disabled states (e.g. "Saving…") remain owned by
922
- * the shell — only the resting label is customisable here.
923
- *
924
- * Hosts that need translated labels should pass already-translated
925
- * strings; the shell stays locale-agnostic.
788
+ * Render the preview surface. Receives the editor's current value so the
789
+ * preview tracks unsaved edits, plus the chosen `previewScope` (which can
790
+ * differ from the editing scope when `<PreviewScopePicker>` is used).
926
791
  */
927
- actionLabels?: Partial<Record<RecordsAdminActionKey, string>>;
792
+ render?: (ctx: {
793
+ resolved: TData | null;
794
+ previewScope: ParsedRef;
795
+ }) => ReactNode;
928
796
  /**
929
- * Optional icon component rendered before each action label (sized
930
- * `h-4 w-4`). Omit a key to preserve current look — the editor footer
931
- * renders Save/Discard without icons by default. Delete retains its
932
- * built-in `Trash2` unless overridden here.
797
+ * How the preview surface attaches to the editor pane.
798
+ * - `inline` (default): rendered below the form.
799
+ * - `side`: resizable right pane next to the editor.
800
+ * - `tab`: Editor / Preview siblings inside the right pane.
801
+ * - `drawer`: slide-out from the right, toggled by a button.
933
802
  */
934
- actionIcons?: Partial<Record<RecordsAdminActionKey, RecordsAdminActionIcon>>;
803
+ mode?: 'inline' | 'side' | 'tab' | 'drawer';
804
+ /** Render a `<PreviewScopePicker>` so admins can preview as a different
805
+ * product/variant/batch than they're editing. Default false. */
806
+ scopePicker?: boolean;
807
+ }
808
+ interface EditorConfig<TData = unknown> {
809
+ /** Editor tab strip behaviour. See {@link RecordsAdminEditorTabsMode}. */
810
+ tabs?: RecordsAdminEditorTabsMode;
811
+ /** Preview pane configuration. */
812
+ preview?: EditorPreviewConfig<TData>;
813
+ }
814
+ interface ItemsConfig<TData = unknown> {
935
815
  /**
936
- * Enable in-shell Copy / Paste between scopes for the current `recordType`.
937
- * Default `true`. The clipboard is scoped per `(appId, recordType)` and
938
- * persists in `sessionStorage` so it survives host route changes.
816
+ * Whether each scope holds at most one record (`singleton`, default) or
817
+ * many (`collection`).
939
818
  */
940
- enableClipboard?: boolean;
819
+ cardinality?: RecordCardinality;
820
+ /** Display name for an item in collection mode. Default `'item'`. */
821
+ noun?: string;
822
+ /** Generate a host-local handle for a brand-new draft. */
823
+ generateId?: () => string;
824
+ /** Which built-in item views the right pane offers. Default `['table']`. */
825
+ views?: ItemView[];
826
+ /** Initial item view when nothing is persisted. */
827
+ defaultView?: ItemView;
828
+ /** Declarative columns for the built-in `table` view. */
829
+ columns?: ItemColumn<TData>[];
830
+ /** Card renderer for `cards` / `gallery` views. */
831
+ renderCard?: (record: RecordSummary<TData>, ctx: ItemSlotContext) => ReactNode;
832
+ /** Full custom item-view renderer. Replaces built-in views entirely. */
833
+ renderList?: (items: RecordSummary<TData>[], ctx: ItemViewContext) => ReactNode;
834
+ /** Custom empty-state when a scope has no items yet. */
835
+ renderEmpty?: (ctx: ItemViewContext) => ReactNode;
836
+ /** Visual density of `cards` / `gallery` views. Default `'md'`. */
837
+ cardSize?: 'sm' | 'md' | 'lg';
838
+ /** What the rail shows once an item is open. Default `'siblings'`. */
839
+ railMode?: CollectionRailMode;
840
+ }
841
+ interface UnsavedConfig<TData = unknown> {
941
842
  /**
942
- * Optional transform on Copy. Receives the resolved value at the source
943
- * scope and returns what should be stored in the clipboard. Use this to
944
- * strip per-scope-only fields (e.g. country of origin) that shouldn't
945
- * propagate. Defaults to a deep clone of the resolved value.
843
+ * How to handle unsaved edits when navigating away.
844
+ * - `keep` (default): hold edits per editor mount; surface via the
845
+ * inline UnsavedBanner.
846
+ * - `prompt`: show a confirm dialog on navigation.
847
+ * - `autosave`: silently save before navigating.
946
848
  */
849
+ strategy?: 'prompt' | 'autosave' | 'keep';
850
+ /** Disable the browser-level `beforeunload` prompt. Default false. */
851
+ disableBeforeUnload?: boolean;
852
+ /** Pre-delete hook. Return `false` to abort. */
853
+ onBeforeDelete?: (scope: ParsedRef) => boolean | Promise<boolean>;
854
+ /** Derive a label for an in-flight draft in the unsaved-changes tray. */
855
+ deriveDraftLabel?: (value: TData, scope: ParsedRef) => string | undefined;
856
+ }
857
+ interface ClipboardConfig<TData = unknown> {
858
+ /** Enable in-shell Copy / Paste. Default `true`. */
859
+ enabled?: boolean;
860
+ /** Optional transform on Copy. Defaults to a deep clone. */
947
861
  onCopy?: (source: {
948
862
  value: TData;
949
863
  scope: ParsedRef;
950
864
  }) => TData;
951
- /**
952
- * Optional transform on Paste. Receives the clipboard payload and the
953
- * destination scope/current value. Return the value to apply, or `null`
954
- * to abort the paste. Defaults to replacing the destination's value with
955
- * the clipboard's.
956
- */
865
+ /** Optional transform on Paste. Return `null` to abort. */
957
866
  onPaste?: (clipboard: {
958
867
  value: TData;
959
868
  sourceScope: ParsedRef;
@@ -961,21 +870,12 @@ interface RecordsAdminShellProps<TData = unknown> {
961
870
  scope: ParsedRef;
962
871
  currentValue: TData | null;
963
872
  }) => TData | null;
964
- /**
965
- * Mirror the shell's runtime state (current scope, open item, right-pane
966
- * view) into URL parameters so links open to the right place and the
967
- * browser back/forward buttons feel native.
968
- *
969
- * Off by default. The shell only owns the params it knows about
970
- * (`item`, `scope`, `view`); platform context like `appId` /
971
- * `collectionId` lives in the host's URL and stays untouched.
972
- *
973
- * Pass an `adapter` to integrate with a host router (React Router,
974
- * SmartLinks `persistentQueryParams`, etc.) — without one the shell
975
- * uses a default `window.location` + `window.history` adapter that
976
- * also handles hash routing.
977
- */
978
- deepLink?: DeepLinkOptions;
873
+ }
874
+ interface ActionsConfig {
875
+ /** Override resting labels for `save` / `discard` / `delete` (etc.). */
876
+ labels?: Partial<Record<RecordsAdminActionKey, string>>;
877
+ /** Optional icon component rendered before each action label. */
878
+ icons?: Partial<Record<RecordsAdminActionKey, RecordsAdminActionIcon>>;
979
879
  }
980
880
  /**
981
881
  * Controls the small tab strip that appears above the editor body inside the