@asteby/metacore-runtime-react 6.0.0 → 6.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,15 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 6.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d7f1e55: Per-model extension registry, badge cell normalization, and auto-derived filter chips.
8
+ - `registerModelExtension(model, ext)` lets apps layer per-model UI on top of `<DynamicCRUDPage>` (header KPI strip, custom toolbar buttons, hidden create flow, title overrides) without forking the page or copy-pasting it.
9
+ - `defaultGetDynamicColumns` now accepts `type === 'badge'` (what the kernel emits) in addition to `cellStyle === 'badge'`. Columns marked `type: badge` previously rendered as plain text.
10
+ - `<DynamicTable>` derives a filter chip from every column flagged `filterable: true` plus either static options, a `searchEndpoint`, or boolean type, so apps no longer need to mirror the same options into a separate `filters` array on the metadata. Explicit `metadata.filters` still wins when present.
11
+ - Fixes the default `getDynamicColumns` fallback that previously read `col.name` instead of `col.key`, leaving cells blank for hosts that did not pass a custom factory.
12
+
3
13
  ## 6.0.0
4
14
 
5
15
  ### Patch Changes
@@ -20,7 +30,7 @@
20
30
 
21
31
  - e23eede: Publicación inicial a npm del ecosistema metacore.
22
32
 
23
- Propaga los 13 paquetes del SDK al registry público para que apps consumidoras (ops, link) migren de `file:` a semver y Renovate pueda propagar updates.
33
+ Propaga los 13 paquetes del SDK al registry público para que las host applications consumidoras migren de `file:` a semver y Renovate pueda propagar updates.
24
34
 
25
35
  ### Patch Changes
26
36
 
@@ -34,7 +44,7 @@
34
44
 
35
45
  - 6d243b0: Initial release of the metacore frontend ecosystem.
36
46
 
37
- 11 packages extracted from ops/link frontends into a publishable monorepo with auto-propagation via Changesets + Renovate.
47
+ 11 packages extracted from host application frontends into a publishable monorepo with auto-propagation via Changesets + Renovate.
38
48
 
39
49
  ### Patch Changes
40
50
 
package/README.md CHANGED
@@ -1,59 +1,82 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
- React runtime for [metacore](https://github.com/asteby/metacore-sdk) hosts. This
4
- package bundles the generic components a host (`ops`, `link`, `hub`) renders
5
- when showing addon contributionsdynamic tables, forms, action dispatchers,
6
- slot extension points and the federated addon loader.
7
-
8
- It is a *runtime*, not a UI kit: the actual visual primitives (buttons,
9
- dialogs, tables…) are resolved through the host's bundler aliases. The host
10
- must provide the following modules at build time:
11
-
12
- | Alias | Purpose |
13
- | ---------------------------------- | ------------------------------------- |
14
- | `@/components/ui/*` | shadcn primitives |
15
- | `@/components/data-table` | DataTableToolbar / Pagination / etc. |
16
- | `@/components/dynamic/dynamic-columns` | column renderers (`DynamicIcon`, etc) |
17
- | `@/components/dynamic/dynamic-record-dialog` | CRUD dialog (still host-owned) |
18
- | `@/components/dynamic/export-dialog` | Export dialog |
19
- | `@/components/dynamic/import-dialog` | Import dialog |
20
- | `@/lib/api` | axios instance |
21
- | `@/lib/utils` | `cn()` helper |
22
- | `@/stores/metadata-cache` | zustand store |
23
- | `@/stores/branch-store` | zustand store (optional) |
3
+ React runtime for [Metacore](https://github.com/asteby/metacore-sdk) hosts. The metadata-driven CRUD layer that turns a manifest declaration into a working UI: dynamic tables, forms, action dispatchers, slot extension points, capability gates, and the federated addon loader.
4
+
5
+ This is a *runtime*, not a UI kit visual primitives come from [`@asteby/metacore-ui`](../ui). Hosts inject their HTTP client and (optionally) tenant-branch context via React providers; no bundler aliases are required.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @asteby/metacore-runtime-react @asteby/metacore-sdk @asteby/metacore-ui
11
+ ```
12
+
13
+ Peers: `react`, `react-dom`, `react-i18next`, `i18next`, `@tanstack/react-router`, `@tanstack/react-table`, `date-fns`, `lucide-react`, `sonner`, `zustand`. They're declared as peers so React stays single-instance.
24
14
 
25
15
  ## Exports
26
16
 
27
- - `DynamicTable` CRUD-capable table driven by `manifest.model_definition`.
28
- - `DynamicForm` – renders a form from `fields[]`.
29
- - `ActionModalDispatcher` routes a custom action to its registered component
30
- (falls back to confirm dialog / generic form).
31
- - `AddonLoader` injects a federated `remoteEntry.js` and calls `register(api)`.
32
- - `Slot` / `slotRegistry` named extension points (`dashboard.widgets`, …).
33
- - `CapabilityGate` / `CapabilityProvider` conditional UI by capability.
34
- - `NavigationBuilder` / `mergeNavigation` merges host sidebar with addon nav.
35
- - `I18nProvider` injects `manifest.i18n` namespaces into the host's i18next.
17
+ | Export | What it does |
18
+ |---|---|
19
+ | `<DynamicTable model="…" />` | Metadata-driven CRUD table. Sortable, paginated, filterable, URL-syncable, with built-in dialogs. |
20
+ | `<DynamicForm fields={…} onSubmit={…} />` | Standalone form renderer over `ActionFieldDef[]`. |
21
+ | `<DynamicRecordDialog />` | Create / edit / view modal driven by `/metadata/modal/<model>`. |
22
+ | `<ActionModalDispatcher />` | Routes a custom action to its registered component, generic form, or confirm dialog. |
23
+ | `<AddonLoader />` | Injects a federated `remoteEntry.js` and calls the addon's `register(api)`. |
24
+ | `<Slot name="…" />` / `slotRegistry` | Named extension points contributed by addons. |
25
+ | `<CapabilityGate require="…" />` / `<CapabilityProvider />` | Conditional UI by capability. |
26
+ | `<NavigationBuilder />` / `useNavigation()` / `mergeNavigation()` | Merges host sidebar with addon `manifest.navigation`. |
27
+ | `<I18nProvider />` | Folds `manifest.i18n` namespaces into the host's i18next instance. |
28
+ | `<ApiProvider client={axios} />` / `useApi()` | Inject the host's HTTP client. Required by every dynamic component. |
29
+ | `<BranchProvider branch={…} />` / `useCurrentBranch()` | Optional tenant-branch context. |
30
+ | `useMetadataCache()` | Zustand store for table/modal metadata, persisted to LocalStorage. `prefetchAll(api)` warms it from `/metadata/all`. |
31
+ | `defaultGetDynamicColumns` / `makeDefaultGetDynamicColumns(helpers)` | The factory `<DynamicTable>` uses to convert metadata into TanStack column defs. The default reads from `col.key` (matching the kernel contract). |
32
+ | `DynamicIcon` | Lucide icon resolver by name. |
36
33
 
37
34
  ## Minimal usage
38
35
 
39
36
  ```tsx
40
- import { DynamicTable, Slot, CapabilityGate, AddonLoader } from '@asteby/metacore-runtime-react'
41
-
42
- <AddonLoader scope="billing" url="/addons/billing/remoteEntry.js" api={api}>
43
- <CapabilityGate require="invoice.read">
44
- <DynamicTable model="invoice" />
45
- <Slot name="invoice.footer" />
46
- </CapabilityGate>
47
- </AddonLoader>
37
+ import {
38
+ ApiProvider,
39
+ CapabilityProvider,
40
+ DynamicTable,
41
+ } from '@asteby/metacore-runtime-react'
42
+ import { api } from './lib/api'
43
+
44
+ export function App() {
45
+ return (
46
+ <ApiProvider client={api}>
47
+ <CapabilityProvider capabilities={session.capabilities}>
48
+ <DynamicTable model="tickets" />
49
+ </CapabilityProvider>
50
+ </ApiProvider>
51
+ )
52
+ }
48
53
  ```
49
54
 
50
- ## Installation
55
+ For props, response shapes, customization patterns and the full surface, see [`docs/dynamic-ui.md`](https://github.com/asteby/metacore-sdk/blob/main/docs/dynamic-ui.md).
51
56
 
52
- `@asteby/metacore-runtime-react` depends on `@asteby/metacore-sdk` for the
53
- canonical action registry and `AddonAPI` contract. Build order:
57
+ ## How it talks to the kernel
54
58
 
55
- ```
56
- pnpm --filter @asteby/metacore-sdk build
59
+ | Endpoint | Used by |
60
+ |---|---|
61
+ | `GET /metadata/table/<model>` | `<DynamicTable>` (cached). |
62
+ | `GET /metadata/modal/<model>` | `<DynamicRecordDialog>` (cached). |
63
+ | `GET /metadata/all` | `useMetadataCache().prefetchAll()`. |
64
+ | `GET /data/<model>` | `<DynamicTable>` list. |
65
+ | `GET /data/<model>/<id>` | `<DynamicRecordDialog>` view/edit. |
66
+ | `POST /data/<model>` | Create. |
67
+ | `PUT /data/<model>/<id>` | Update. |
68
+ | `DELETE /data/<model>/<id>` | Delete (single + bulk). |
69
+ | `POST /data/<model>/<id>/action/<key>` | `<ActionModalDispatcher>`. |
70
+ | `GET /options/<endpoint>` | Relation pickers + select prefetch. |
71
+
72
+ All endpoints can be overridden per-component via the `endpoint` prop.
73
+
74
+ ## Build
75
+
76
+ ```bash
57
77
  pnpm --filter @asteby/metacore-runtime-react build
58
- pnpm --filter <host> install
59
78
  ```
79
+
80
+ ## License
81
+
82
+ Apache-2.0
@@ -1,7 +1,7 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  // ExportDialog — lets users pick format (csv/json) + columns and kicks off
3
3
  // either a sync download or an async export job (polled via /exports/:id/status).
4
- // Ported from the ops starter. Axios-like client is provided by <ApiProvider>.
4
+ // Axios-like client is provided by <ApiProvider>.
5
5
  import { useState, useEffect, useCallback } from 'react';
6
6
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Button, Label, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@asteby/metacore-ui/primitives';
7
7
  import { Progress, RadioGroup, RadioGroupItem } from './_primitives';
@@ -1 +1 @@
1
- {"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/dialogs/import.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAG7C,UAAU,iBAAiB;IACvB,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,aAAa,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;CAC1B;AAoBD,wBAAgB,YAAY,CAAC,EACzB,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,UAAU,GACb,EAAE,iBAAiB,2CAwVnB"}
1
+ {"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/dialogs/import.tsx"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAG7C,UAAU,iBAAiB;IACvB,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,aAAa,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;CAC1B;AAoBD,wBAAgB,YAAY,CAAC,EACzB,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,UAAU,GACb,EAAE,iBAAiB,2CAwVnB"}
@@ -1,7 +1,6 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  // ImportDialog — three-step CSV/JSON import flow (upload → validate → import
3
- // with per-row error report). Ported from the ops starter. Axios-like client
4
- // is provided by <ApiProvider>.
3
+ // with per-row error report). Axios-like client is provided by <ApiProvider>.
5
4
  import { useState, useEffect, useRef } from 'react';
6
5
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Button, Input, Label, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@asteby/metacore-ui/primitives';
7
6
  import { Progress } from './_primitives';
@@ -0,0 +1,29 @@
1
+ import type { GetDynamicColumns } from './dynamic-columns-shim';
2
+ /** Host-supplied helpers consumed by avatar/image cell renderers. */
3
+ export interface DynamicColumnsHelpers {
4
+ /**
5
+ * Resolves a relative or absolute media path into a renderable URL. Hosts
6
+ * typically prepend their CDN/storage base. If omitted, paths are passed
7
+ * through verbatim.
8
+ */
9
+ getImageUrl?: (path: string) => string;
10
+ /**
11
+ * API origin used to build avatar URLs when the row carries a bare filename
12
+ * instead of an absolute URL or sibling `.avatar` field. Usually
13
+ * `import.meta.env.VITE_API_URL.replace('/api', '')`.
14
+ */
15
+ apiBaseUrl?: string;
16
+ }
17
+ /**
18
+ * Builds the canonical column factory used by `<DynamicTable>` when the host
19
+ * does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
20
+ * URL resolution.
21
+ */
22
+ export declare function makeDefaultGetDynamicColumns(helpers?: DynamicColumnsHelpers): GetDynamicColumns;
23
+ /**
24
+ * Eager-built variant — equivalent to `makeDefaultGetDynamicColumns()`. Use
25
+ * this when the host has no helpers to inject and a stable function reference
26
+ * suffices.
27
+ */
28
+ export declare const defaultGetDynamicColumns: GetDynamicColumns;
29
+ //# sourceMappingURL=dynamic-columns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAwHD;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAmVnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
@@ -0,0 +1,312 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Default `getDynamicColumns` factory used by hosts that don't need a custom
3
+ // renderer. Supports every cell type produced by kernel/dynamic metadata:
4
+ // badge (static + endpoint-loaded options), avatar, phone, date, boolean,
5
+ // relation-badge-list, media-gallery, image, plus a generic text fallback.
6
+ //
7
+ // The implementation was previously duplicated across multiple host apps
8
+ // (~550 LOC each, drifting). It now lives here so a single fix propagates
9
+ // to every host. Hosts inject app-specific URL helpers via the `helpers`
10
+ // argument so the SDK stays free of environment-bound code.
11
+ import * as React from 'react';
12
+ import { format } from 'date-fns';
13
+ import { es, enUS } from 'date-fns/locale';
14
+ import * as icons from 'lucide-react';
15
+ import { MoreHorizontal } from 'lucide-react';
16
+ import { Avatar, AvatarFallback, AvatarImage, Badge, Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@asteby/metacore-ui';
17
+ import { DataTableColumnHeader, FilterableColumnHeader, } from '@asteby/metacore-ui/data-table';
18
+ import { generateBadgeStyles } from '@asteby/metacore-ui/lib';
19
+ import { OptionsContext } from './options-context';
20
+ import { DynamicIcon } from './dynamic-icon';
21
+ const defaultGetImageUrl = (path) => path;
22
+ const getNestedValue = (obj, path) => path.split('.').reduce((acc, part) => acc && acc[part], obj);
23
+ const lowerFirst = (value) => {
24
+ if (!value)
25
+ return value;
26
+ return value.charAt(0).toLowerCase() + value.slice(1);
27
+ };
28
+ const getPathVariants = (path) => {
29
+ if (!path)
30
+ return [];
31
+ const normalized = path
32
+ .split('.')
33
+ .map((segment) => lowerFirst(segment) || segment)
34
+ .join('.');
35
+ return Array.from(new Set([path, normalized])).filter(Boolean);
36
+ };
37
+ const getValueFromPathVariants = (obj, path) => {
38
+ if (!path)
39
+ return undefined;
40
+ for (const candidate of getPathVariants(path)) {
41
+ const value = getNestedValue(obj, candidate);
42
+ if (value !== undefined && value !== null)
43
+ return value;
44
+ }
45
+ return undefined;
46
+ };
47
+ const useIsDarkTheme = () => {
48
+ const [isDark, setIsDark] = React.useState(() => typeof document !== 'undefined' &&
49
+ document.documentElement.classList.contains('dark'));
50
+ React.useEffect(() => {
51
+ if (typeof document === 'undefined')
52
+ return;
53
+ const sync = () => setIsDark(document.documentElement.classList.contains('dark'));
54
+ sync();
55
+ const observer = new MutationObserver(sync);
56
+ observer.observe(document.documentElement, {
57
+ attributes: true,
58
+ attributeFilter: ['class'],
59
+ });
60
+ return () => observer.disconnect();
61
+ }, []);
62
+ return isDark;
63
+ };
64
+ const renderRelationBadges = (items, col) => {
65
+ if (!Array.isArray(items) || items.length === 0) {
66
+ return _jsx("span", { className: "text-muted-foreground", children: "-" });
67
+ }
68
+ return (_jsx("div", { className: "flex flex-wrap gap-1", children: items.map((item, idx) => {
69
+ const relationTarget = col.relationPath
70
+ ? getValueFromPathVariants(item, col.relationPath) ?? item
71
+ : item;
72
+ const displaySource = relationTarget ?? item;
73
+ let displayValue = col.displayField !== undefined && col.displayField !== null
74
+ ? getValueFromPathVariants(displaySource, col.displayField)
75
+ : displaySource;
76
+ if (displayValue === undefined || displayValue === null) {
77
+ displayValue = displaySource;
78
+ }
79
+ const label = displayValue !== undefined && displayValue !== null
80
+ ? String(displayValue)
81
+ : '-';
82
+ let iconValue;
83
+ if (col.iconField) {
84
+ const rawIcon = getValueFromPathVariants(displaySource, col.iconField);
85
+ if (rawIcon !== undefined && rawIcon !== null) {
86
+ iconValue = String(rawIcon);
87
+ }
88
+ }
89
+ return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1", children: [iconValue && (_jsx(DynamicIcon, { name: iconValue, className: "h-3 w-3" })), _jsx("span", { children: label })] }, `${col.key}-${idx}`));
90
+ }) }));
91
+ };
92
+ const OptionBadge = ({ option }) => {
93
+ const isDark = useIsDarkTheme();
94
+ const colorStyles = option.color ? generateBadgeStyles(option.color, { isDark }) : {};
95
+ return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1 border-0", style: colorStyles, children: [option.icon && _jsx(DynamicIcon, { name: option.icon, className: "h-3.5 w-3.5" }), _jsx("span", { children: option.label })] }));
96
+ };
97
+ const BadgeWithEndpointOptions = ({ endpoint, value }) => {
98
+ const { optionsMap } = React.useContext(OptionsContext);
99
+ const options = optionsMap.get(endpoint) || [];
100
+ const option = options.find((opt) => opt.value === value);
101
+ if (option)
102
+ return _jsx(OptionBadge, { option: option, fallback: String(value) });
103
+ return _jsx(Badge, { variant: "outline", children: String(value) });
104
+ };
105
+ /**
106
+ * Builds the canonical column factory used by `<DynamicTable>` when the host
107
+ * does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
108
+ * URL resolution.
109
+ */
110
+ export function makeDefaultGetDynamicColumns(helpers = {}) {
111
+ const getImageUrl = helpers.getImageUrl ?? defaultGetImageUrl;
112
+ const apiBaseUrl = helpers.apiBaseUrl ?? '';
113
+ return function defaultGetDynamicColumns(metadata, onAction, t, currentLanguage, filterConfigs) {
114
+ const dateLocale = currentLanguage === 'en' ? enUS : es;
115
+ const columns = [
116
+ {
117
+ id: 'select',
118
+ header: ({ table }) => (_jsx(Checkbox, { checked: table.getIsAllPageRowsSelected() ||
119
+ (table.getIsSomePageRowsSelected() && 'indeterminate'), onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value), "aria-label": "Select all", className: "translate-y-[2px]" })),
120
+ cell: ({ row }) => (_jsx(Checkbox, { checked: row.getIsSelected(), onCheckedChange: (value) => row.toggleSelected(!!value), "aria-label": "Select row", className: "translate-y-[2px]" })),
121
+ enableSorting: false,
122
+ enableHiding: false,
123
+ },
124
+ ];
125
+ metadata.columns.forEach((col) => {
126
+ if (col.hidden)
127
+ return;
128
+ const translatedLabel = col.label;
129
+ const filterConfig = filterConfigs?.get(col.key);
130
+ const columnMeta = {
131
+ label: translatedLabel,
132
+ };
133
+ if (filterConfig) {
134
+ const fm = {
135
+ filterable: true,
136
+ filterType: filterConfig.filterType,
137
+ filterKey: filterConfig.filterKey,
138
+ filterOptions: filterConfig.options,
139
+ filterLoading: filterConfig.loading,
140
+ filterSearchEndpoint: filterConfig.searchEndpoint,
141
+ selectedValues: filterConfig.selectedValues,
142
+ onFilterChange: filterConfig.onFilterChange,
143
+ };
144
+ Object.assign(columnMeta, fm);
145
+ }
146
+ columns.push({
147
+ accessorKey: col.key,
148
+ id: col.key,
149
+ meta: columnMeta,
150
+ header: ({ column }) => filterConfig ? (_jsx(FilterableColumnHeader, { column: column, title: translatedLabel })) : (_jsx(DataTableColumnHeader, { column: column, title: translatedLabel })),
151
+ cell: ({ row }) => {
152
+ const value = getNestedValue(row.original, col.key);
153
+ // Kernel emits the renderer flag as `type`; older hosts used
154
+ // `cellStyle`. Accept both so a single backend works across
155
+ // SDK versions.
156
+ const renderAs = col.cellStyle ?? col.type;
157
+ // Endpoint-loaded badge options (preloaded into OptionsContext)
158
+ if (renderAs === 'badge' && col.useOptions && col.searchEndpoint) {
159
+ if (!value)
160
+ return _jsx("span", { className: "text-muted-foreground", children: "-" });
161
+ return _jsx(BadgeWithEndpointOptions, { endpoint: col.searchEndpoint, value: value });
162
+ }
163
+ // Static badge options — map value → label/icon/color
164
+ if (renderAs === 'badge' && col.options && col.options.length > 0) {
165
+ if (!value && value !== 0)
166
+ return _jsx("span", { className: "text-muted-foreground", children: "-" });
167
+ const option = col.options.find((o) => o.value === String(value));
168
+ if (option)
169
+ return _jsx(OptionBadge, { option: option, fallback: String(value) });
170
+ return _jsx(Badge, { variant: "outline", children: String(value) });
171
+ }
172
+ if (renderAs === 'relation-badge-list') {
173
+ return renderRelationBadges(value, col);
174
+ }
175
+ switch (col.type) {
176
+ case 'date': {
177
+ if (!value)
178
+ return _jsx("span", { className: "text-muted-foreground", children: "-" });
179
+ try {
180
+ const date = new Date(value);
181
+ if (isNaN(date.getTime()) || date.getFullYear() <= 1) {
182
+ return _jsx("span", { className: "text-muted-foreground", children: "-" });
183
+ }
184
+ return (_jsxs("div", { className: "flex items-center gap-1.5 text-muted-foreground", children: [_jsx(icons.Calendar, { className: "h-3.5 w-3.5 opacity-70" }), _jsx("span", { className: "text-sm font-medium", children: format(date, 'PPP', { locale: dateLocale }) })] }));
185
+ }
186
+ catch {
187
+ return _jsx("span", { children: String(value) });
188
+ }
189
+ }
190
+ case 'search':
191
+ case 'avatar': {
192
+ const namePath = col.tooltip || col.key;
193
+ const name = getNestedValue(row.original, namePath) || 'N/A';
194
+ const desc = getNestedValue(row.original, col.description || '');
195
+ let avatarSrc;
196
+ if (col.key.includes('.')) {
197
+ const parentPath = col.key.split('.').slice(0, -1).join('.');
198
+ const avatarPath = `${parentPath}.avatar`;
199
+ const possibleAvatar = getNestedValue(row.original, avatarPath);
200
+ if (possibleAvatar)
201
+ avatarSrc = String(possibleAvatar);
202
+ }
203
+ else if (value &&
204
+ (String(value).startsWith('http') || String(value).startsWith('https'))) {
205
+ avatarSrc = String(value);
206
+ }
207
+ else if (value) {
208
+ avatarSrc = `${apiBaseUrl}${col.basePath || ''}${value}`;
209
+ }
210
+ return (_jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg ring-1 ring-border/50", children: [_jsx(AvatarImage, { src: getImageUrl(avatarSrc || ''), alt: String(name), className: "object-cover" }), _jsx(AvatarFallback, { className: "text-[10px] font-bold bg-primary/5 text-primary rounded-lg", children: String(name)
211
+ .split(' ')
212
+ .map((n) => n[0])
213
+ .slice(0, 2)
214
+ .join('')
215
+ .toUpperCase() })] }), _jsxs("div", { className: "flex flex-col min-w-0 overflow-hidden", children: [_jsx("span", { className: "font-medium text-sm truncate leading-none mb-0.5 text-foreground/90", children: String(name) }), desc && (_jsx("span", { className: "text-[11px] text-muted-foreground truncate leading-none", children: String(desc) }))] })] }));
216
+ }
217
+ case 'relation-badge-list':
218
+ return renderRelationBadges(value, col);
219
+ case 'phone': {
220
+ if (!value)
221
+ return _jsx("span", { className: "text-muted-foreground", children: "-" });
222
+ return _jsx("span", { className: "font-medium text-sm", children: String(value) });
223
+ }
224
+ case 'boolean':
225
+ return value ? _jsx(Badge, { children: "S\u00ED" }) : _jsx(Badge, { variant: "secondary", children: "No" });
226
+ case 'media-gallery': {
227
+ if (!value || (Array.isArray(value) && value.length === 0)) {
228
+ return _jsx("span", { className: "text-muted-foreground", children: "-" });
229
+ }
230
+ const mediaItems = Array.isArray(value) ? value : [];
231
+ const visibleItems = mediaItems.slice(0, 3);
232
+ const remaining = mediaItems.length - 3;
233
+ return (_jsxs("div", { className: "flex -space-x-2 overflow-hidden", children: [visibleItems.map((item, i) => {
234
+ const src = item.url;
235
+ if (item.type === 'image') {
236
+ return (_jsxs(Avatar, { className: "inline-block h-8 w-8 rounded-full ring-2 ring-background", children: [_jsx(AvatarImage, { src: src, className: "object-cover" }), _jsx(AvatarFallback, { children: item.type?.[0] })] }, i));
237
+ }
238
+ return (_jsx("div", { className: "inline-flex h-8 w-8 items-center justify-center rounded-full bg-muted ring-2 ring-background", children: _jsx(DynamicIcon, { name: item.type === 'video'
239
+ ? 'Video'
240
+ : item.type === 'audio'
241
+ ? 'AudioLines'
242
+ : 'FileText', className: "h-4 w-4" }) }, i));
243
+ }), remaining > 0 && (_jsxs("div", { className: "flex h-8 w-8 items-center justify-center rounded-full bg-muted text-xs font-medium ring-2 ring-background", children: ["+", remaining] }))] }));
244
+ }
245
+ case 'image': {
246
+ const imageValue = value ||
247
+ (Array.isArray(row.original.media)
248
+ ? row.original.media.find((m) => m.type === 'image')?.url
249
+ : null);
250
+ if (!imageValue)
251
+ return _jsx("span", { className: "text-muted-foreground", children: "-" });
252
+ return (_jsx("div", { className: "h-10 w-10 relative rounded overflow-hidden bg-muted flex items-center justify-center", children: _jsx("img", { src: getImageUrl(String(imageValue)), alt: "Thumbnail", className: "h-full w-full object-contain", onError: (e) => {
253
+ ;
254
+ e.currentTarget.style.display = 'none';
255
+ } }) }));
256
+ }
257
+ default: {
258
+ if (typeof value === 'object' && value !== null) {
259
+ return (_jsx("span", { className: "text-muted-foreground text-xs", children: JSON.stringify(value) }));
260
+ }
261
+ if (col.key === 'description' ||
262
+ col.key === 'features' ||
263
+ col.key.includes('description')) {
264
+ return (_jsx("div", { className: "max-w-[350px]", title: String(value), children: _jsx("span", { className: "truncate font-medium block", children: value !== null && value !== undefined ? String(value) : '-' }) }));
265
+ }
266
+ return (_jsx("span", { className: "truncate font-medium", children: value !== null && value !== undefined ? String(value) : '-' }));
267
+ }
268
+ }
269
+ },
270
+ enableSorting: col.sortable,
271
+ enableHiding: true,
272
+ });
273
+ });
274
+ if (metadata.hasActions && metadata.actions.length > 0) {
275
+ columns.push({
276
+ id: 'actions',
277
+ header: () => _jsx("div", { className: "text-right", children: t ? t('common.actions') : 'Acciones' }),
278
+ size: 80,
279
+ maxSize: 80,
280
+ meta: {},
281
+ cell: ({ row }) => (_jsx("div", { className: "flex items-center justify-end", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-8 w-8 p-0", children: [_jsx("span", { className: "sr-only", children: "Abrir men\u00FA" }), _jsx(MoreHorizontal, { className: "h-4 w-4" })] }) }), _jsx(DropdownMenuContent, { align: "end", children: metadata.actions
282
+ .filter((action) => {
283
+ if (!action.condition)
284
+ return true;
285
+ const { field, operator, value } = action.condition;
286
+ const rowValue = String(row.original[field] ?? '');
287
+ const values = Array.isArray(value) ? value : [value];
288
+ switch (operator) {
289
+ case 'eq':
290
+ return rowValue === values[0];
291
+ case 'neq':
292
+ return rowValue !== values[0];
293
+ case 'in':
294
+ return values.includes(rowValue);
295
+ case 'not_in':
296
+ return !values.includes(rowValue);
297
+ default:
298
+ return true;
299
+ }
300
+ })
301
+ .map((action) => (_jsxs(DropdownMenuItem, { onClick: () => onAction && onAction(action.key, row.original), children: [_jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), action.label] }, action.key))) })] }) })),
302
+ });
303
+ }
304
+ return columns;
305
+ };
306
+ }
307
+ /**
308
+ * Eager-built variant — equivalent to `makeDefaultGetDynamicColumns()`. Use
309
+ * this when the host has no helpers to inject and a stable function reference
310
+ * suffices.
311
+ */
312
+ export const defaultGetDynamicColumns = makeDefaultGetDynamicColumns();
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ export interface DynamicCRUDPageStrings {
3
+ refresh?: string;
4
+ export?: string;
5
+ import?: string;
6
+ /** Used as the create button label when `newLabel` is not provided.
7
+ * Receives the singular form of the title. */
8
+ newPrefix?: string;
9
+ }
10
+ export interface DynamicCRUDPageClasses {
11
+ root?: string;
12
+ container?: string;
13
+ header?: string;
14
+ title?: string;
15
+ toolbar?: string;
16
+ tableWrapper?: string;
17
+ }
18
+ export interface DynamicCRUDPageProps {
19
+ /** Model key as registered on the backend (e.g. "customers"). */
20
+ model: string;
21
+ /** Override the data endpoint. Defaults to `/dynamic/<model>`. */
22
+ endpoint?: string;
23
+ /** Override the human title. Defaults to `metadata.title`. */
24
+ title?: string;
25
+ /** Override the create button label. Defaults to `${newPrefix} ${singular}`. */
26
+ newLabel?: string;
27
+ /** Strings used in default labels — pass when the host has its own i18n. */
28
+ i18n?: DynamicCRUDPageStrings;
29
+ /** Hide the create button + dialog even when metadata says CRUD is enabled. */
30
+ hideCreate?: boolean;
31
+ hideExport?: boolean;
32
+ hideImport?: boolean;
33
+ hideRefresh?: boolean;
34
+ /** Slot rendered above the title row (e.g. branch switcher, kpi strip). */
35
+ headerExtras?: React.ReactNode;
36
+ /** Slot rendered in the toolbar, before the create button. */
37
+ toolbarExtras?: React.ReactNode;
38
+ /** Tailwind class overrides for layout primitives. */
39
+ classes?: DynamicCRUDPageClasses;
40
+ /** Fired after a create/import/refresh successfully reloads the table. */
41
+ onChange?: () => void;
42
+ }
43
+ /**
44
+ * Page-level CRUD shell wired around <DynamicTable>. Hosts mount a single
45
+ * `<DynamicCRUDPage model="..." />` per route and the SDK takes care of
46
+ * metadata fetch, dialogs and toolbar.
47
+ */
48
+ export declare function DynamicCRUDPage(props: DynamicCRUDPageProps): import("react/jsx-runtime").JSX.Element;
49
+ //# sourceMappingURL=dynamic-crud-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic-crud-page.d.ts","sourceRoot":"","sources":["../src/dynamic-crud-page.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAKN,MAAM,OAAO,CAAA;AAWd,MAAM,WAAW,sBAAsB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;mDAC+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AASD,MAAM,WAAW,sBAAsB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,oBAAoB;IACjC,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,sBAAsB,CAAA;IAC7B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,8DAA8D;IAC9D,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC/B,sDAAsD;IACtD,OAAO,CAAC,EAAE,sBAAsB,CAAA;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CAkL1D"}