@asteby/metacore-runtime-react 23.2.0 → 23.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.
@@ -19,9 +19,11 @@ import * as icons from 'lucide-react';
19
19
  import { MoreHorizontal } from 'lucide-react';
20
20
  import { Avatar, AvatarFallback, AvatarImage, Badge, Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@asteby/metacore-ui';
21
21
  import { DataTableColumnHeader, FilterableColumnHeader, } from '@asteby/metacore-ui/data-table';
22
- import { generateBadgeStyles, getInitials, optionColor, relationChipStyles, } from '@asteby/metacore-ui/lib';
22
+ import { generateBadgeStyles, getInitials, relationChipStyles, } from '@asteby/metacore-ui/lib';
23
23
  import { Progress } from './dialogs/_primitives';
24
24
  import { humanizeToken } from './dynamic-columns-helpers';
25
+ import { OptionBadge, RelationThumbnail, statusColorFor, useIsDarkTheme, } from './display-value';
26
+ import { MediaValue } from './rich-url';
25
27
  import { OptionsContext } from './options-context';
26
28
  import { DynamicIcon, isLucideIconName } from './dynamic-icon';
27
29
  import { CollectionCell } from './collection-cell';
@@ -88,22 +90,6 @@ export const formatAggregateTotal = (col, value, currency, locale) => {
88
90
  ? { minimumFractionDigits: decimals, maximumFractionDigits: decimals }
89
91
  : {}, locale);
90
92
  };
91
- /**
92
- * Semantic status → badge color. Used by the `status` cell when no explicit
93
- * `options` color is declared. Generic, value-driven mapping.
94
- */
95
- const statusColorFor = (value) => {
96
- const v = value.toLowerCase();
97
- if (['active', 'enabled', 'paid', 'completed', 'done', 'success', 'approved', 'open']
98
- .includes(v))
99
- return '#22c55e';
100
- if (['pending', 'draft', 'processing', 'in_progress', 'review', 'waiting'].includes(v))
101
- return '#eab308';
102
- if (['inactive', 'disabled', 'cancelled', 'canceled', 'failed', 'rejected', 'error', 'closed']
103
- .includes(v))
104
- return '#ef4444';
105
- return '#6b7280';
106
- };
107
93
  /** Copyable monospaced text cell (code/IDs/hashes). */
108
94
  const CodeCell = ({ text, maxLength }) => {
109
95
  const [copied, setCopied] = React.useState(false);
@@ -195,23 +181,6 @@ const getValueFromPathVariants = (obj, path) => {
195
181
  }
196
182
  return undefined;
197
183
  };
198
- const useIsDarkTheme = () => {
199
- const [isDark, setIsDark] = React.useState(() => typeof document !== 'undefined' &&
200
- document.documentElement.classList.contains('dark'));
201
- React.useEffect(() => {
202
- if (typeof document === 'undefined')
203
- return;
204
- const sync = () => setIsDark(document.documentElement.classList.contains('dark'));
205
- sync();
206
- const observer = new MutationObserver(sync);
207
- observer.observe(document.documentElement, {
208
- attributes: true,
209
- attributeFilter: ['class'],
210
- });
211
- return () => observer.disconnect();
212
- }, []);
213
- return isDark;
214
- };
215
184
  const renderRelationBadges = (items, col) => {
216
185
  if (!Array.isArray(items) || items.length === 0) {
217
186
  return _jsx("span", { className: "text-muted-foreground", children: "-" });
@@ -240,25 +209,6 @@ const renderRelationBadges = (items, col) => {
240
209
  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}`));
241
210
  }) }));
242
211
  };
243
- /**
244
- * Tiny square thumbnail for a resolved relation/option that carries an `image`
245
- * (brand logo, product photo, customer/user avatar). Uses the same Avatar
246
- * primitive as the `avatar`/`creator` cells so a broken/loading image
247
- * gracefully falls back to the record's initials. Sized small (the box is an
248
- * inline style so an addon-arbitrary Tailwind class never gets dropped by a
249
- * consuming app's class scan). Rendered inline alongside a label — never alone.
250
- */
251
- const RelationThumbnail = ({ src, alt, getImageUrl, size = 18 }) => (_jsxs(Avatar, { className: "shrink-0 rounded-sm ring-1 ring-border/40", style: { width: size, height: size }, children: [_jsx(AvatarImage, { src: getImageUrl ? getImageUrl(src) : src, alt: alt, className: "object-cover" }), _jsx(AvatarFallback, { className: "rounded-sm bg-primary/10 text-[8px] font-bold text-primary", children: getInitials(alt) })] }));
252
- const OptionBadge = ({ option, getImageUrl }) => {
253
- const isDark = useIsDarkTheme();
254
- // Explicit backend color wins; otherwise derive a stable, cohesive color
255
- // from the option's value (fallback label) so "dead" gray badges come
256
- // alive. Inline style (hex-derived) so it works regardless of the host's
257
- // tailwind safelist — addon-arbitrary classes aren't in the host scan.
258
- const colorSource = option.color || optionColor(option.value || option.label);
259
- const colorStyles = generateBadgeStyles(colorSource, { isDark });
260
- return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1 border-0", style: colorStyles, children: [option.image ? (_jsx(RelationThumbnail, { src: option.image, alt: option.label, getImageUrl: getImageUrl, size: 16 })) : (option.icon && _jsx(DynamicIcon, { name: option.icon, className: "h-3.5 w-3.5" })), _jsx("span", { children: option.label })] }));
261
- };
262
212
  const BadgeWithEndpointOptions = ({ endpoint, value, getImageUrl }) => {
263
213
  const { optionsMap } = React.useContext(OptionsContext);
264
214
  const options = optionsMap.get(endpoint) || [];
@@ -635,25 +585,16 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
635
585
  if (!rawUrl)
636
586
  return _jsx(EmptyCell, {});
637
587
  const urlStr = String(rawUrl);
638
- const href = /^https?:\/\//i.test(urlStr) ? urlStr : `https://${urlStr}`;
639
- let label;
640
- if (labelField) {
641
- label = String(getNestedValue(row.original, labelField) ?? href);
642
- }
643
- else {
644
- try {
645
- label = new URL(href).hostname;
646
- }
647
- catch {
648
- label = urlStr;
649
- }
650
- }
651
- const isExternal = !/^https?:\/\/(localhost|127\.)/i.test(href);
652
- const newTab = styleCfg(col, 'new_tab', 'newTab') === true || isExternal;
653
- const iconName = styleCfg(col, 'icon') || 'ExternalLink';
654
- return (_jsxs("a", { href: href, ...(newTab
655
- ? { target: '_blank', rel: 'noopener noreferrer' }
656
- : {}), className: "inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline", onClick: (e) => e.stopPropagation(), children: [_jsx(DynamicIcon, { name: iconName, className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "truncate max-w-[260px]", children: label })] }));
588
+ // Explicit label from a `label_field` wins; otherwise
589
+ // the shared primitive derives a smart label (hostname
590
+ // / file name). Images render as a small (~h-8) inline
591
+ // thumbnail, files as a chip, else a link chip. Same
592
+ // renderer as the detail dialog.
593
+ const label = labelField
594
+ ? String(getNestedValue(row.original, labelField) ?? '')
595
+ : undefined;
596
+ const iconName = styleCfg(col, 'icon');
597
+ return (_jsx(MediaValue, { url: urlStr, getImageUrl: getImageUrl, label: label || undefined, icon: iconName || undefined, thumbHeight: 32, maxLabelWidth: 260 }));
657
598
  }
658
599
  case 'email': {
659
600
  if (!value)
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-row-actions.d.ts","sourceRoot":"","sources":["../src/dynamic-row-actions.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,SAAS,CAAA;AAE5D,MAAM,WAAW,0BAA0B;IACvC,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2EAA2E;IAC3E,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,4EAA4E;IAC5E,SAAS,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,iBAAiB;IAC9B,+CAA+C;IAC/C,oBAAoB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACjE;;;OAGG;IACH,OAAO,EAAE,KAAK,CAAC,YAAY,CAAA;CAC9B;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,SAAS,GACZ,EAAE,0BAA0B,GAAG,iBAAiB,CAmHhD"}
1
+ {"version":3,"file":"dynamic-row-actions.d.ts","sourceRoot":"","sources":["../src/dynamic-row-actions.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,SAAS,CAAA;AAE5D,MAAM,WAAW,0BAA0B;IACvC,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2EAA2E;IAC3E,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,4EAA4E;IAC5E,SAAS,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,iBAAiB;IAC9B,+CAA+C;IAC/C,oBAAoB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACjE;;;OAGG;IACH,OAAO,EAAE,KAAK,CAAC,YAAY,CAAA;CAC9B;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,SAAS,GACZ,EAAE,0BAA0B,GAAG,iBAAiB,CAqHhD"}
@@ -89,16 +89,18 @@ export function useDynamicRowActions({ model, endpoint, metadata, onAction, onRe
89
89
  try {
90
90
  const deleteEndpoint = endpoint ? `${endpoint}/${rowToDelete.id}` : `/data/${model}/${rowToDelete.id}`;
91
91
  const res = await api.delete(deleteEndpoint);
92
+ // CRUD estándar: no usar res.data.message (el endpoint dinámico
93
+ // devuelve texto en inglés que se filtraría al toast). String localizado.
92
94
  if (res.data.success) {
93
- toast.success(res.data.message || 'Eliminado correctamente');
95
+ toast.success(t('dynamic.delete_success', { defaultValue: 'Registro eliminado correctamente' }));
94
96
  onRefresh();
95
97
  }
96
98
  else
97
- toast.error(res.data.message || 'Error al eliminar');
99
+ toast.error(t('dynamic.delete_error', { defaultValue: 'No se pudo eliminar el registro' }));
98
100
  }
99
101
  catch (error) {
100
102
  console.error('Error al eliminar', error);
101
- toast.error('Error al eliminar el registro');
103
+ toast.error(t('dynamic.delete_error', { defaultValue: 'No se pudo eliminar el registro' }));
102
104
  }
103
105
  finally {
104
106
  setIsDeleting(false);
@@ -545,9 +545,9 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
545
545
  setBulkDeleteTotal(0);
546
546
  setRowSelection({});
547
547
  if (successCount > 0)
548
- toast.success(`${successCount} registro(s) eliminado(s) correctamente`);
548
+ toast.success(t('dynamic.bulk_delete_success', { count: successCount, defaultValue: '{{count}} registro(s) eliminado(s) correctamente' }));
549
549
  if (errorCount > 0)
550
- toast.error(`${errorCount} registro(s) no pudieron ser eliminados`);
550
+ toast.error(t('dynamic.bulk_delete_error', { count: errorCount, defaultValue: '{{count}} registro(s) no pudieron ser eliminados' }));
551
551
  handleRefresh();
552
552
  };
553
553
  const handleDynamicFilterChange = useCallback((filterKey, values) => {
package/dist/index.d.ts CHANGED
@@ -26,6 +26,7 @@ export * from './dynamic-icon';
26
26
  export type { ColumnFilterConfig, FilterOption as DynamicColumnFilterOption, GetDynamicColumns, DynamicIconComponent, } from './dynamic-columns-shim';
27
27
  export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, relationKeyFor, resolveRelationLabel, type DynamicColumnsHelpers, } from './dynamic-columns';
28
28
  export { humanizeToken } from './dynamic-columns-helpers';
29
+ export { UrlChip, FileChip, ImageThumbnail, MediaValue, RichText, linkifyText, classifyUrl, isImageUrl, isFileUrl, ensureHref, smartUrlLabel, fileNameFromUrl, splitTrailingPunct, type UrlKind, type LinkifyOptions, } from './rich-url';
29
30
  export { CollectionCell, formatScalar, prettifyKey, countLabel, type CollectionCellProps, type Translate as CollectionCellTranslate, } from './collection-cell';
30
31
  export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid';
31
32
  export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,OAAO,EACH,aAAa,EACb,KAAK,kBAAkB,EACvB,YAAY,EACZ,YAAY,EACZ,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,GAClB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACH,WAAW,EACX,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,KAAK,gBAAgB,GACxB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACH,iBAAiB,EACjB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC/B,MAAM,uBAAuB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,qBAAqB,EACrB,KAAK,gBAAgB,GACxB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACH,kBAAkB,EAClB,eAAe,EACf,KAAK,uBAAuB,EAC5B,KAAK,eAAe,GACvB,MAAM,wBAAwB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,WAAW,EAChB,KAAK,wBAAwB,GAChC,MAAM,wBAAwB,CAAA;AAC/B,cAAc,QAAQ,CAAA;AACtB,cAAc,mBAAmB,CAAA;AACjC,OAAO,EACH,mBAAmB,EACnB,MAAM,EACN,oBAAoB,EACpB,OAAO,EACP,sBAAsB,EACtB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,KAAK,EACV,KAAK,wBAAwB,GAChC,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACH,oBAAoB,EACpB,KAAK,0BAA0B,EAC/B,KAAK,iBAAiB,GACzB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACH,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,aAAa,EACb,kBAAkB,EAClB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,OAAO,EACZ,KAAK,SAAS,GACjB,MAAM,uBAAuB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,wBAAwB,CAAA;AACtC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,OAAO,EACH,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,KAAK,2BAA2B,EAChC,KAAK,qBAAqB,EAC1B,KAAK,8BAA8B,GACtC,MAAM,+BAA+B,CAAA;AACtC,OAAO,EACH,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,EACxB,WAAW,EACX,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,GAC9B,MAAM,yBAAyB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,YAAY,EACR,kBAAkB,EAClB,YAAY,IAAI,yBAAyB,EACzC,iBAAiB,EACjB,oBAAoB,GACvB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,wBAAwB,EACxB,4BAA4B,EAC5B,cAAc,EACd,oBAAoB,EACpB,KAAK,qBAAqB,GAC7B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AACzD,OAAO,EACH,OAAO,EACP,QAAQ,EACR,cAAc,EACd,UAAU,EACV,QAAQ,EACR,WAAW,EACX,WAAW,EACX,UAAU,EACV,SAAS,EACT,UAAU,EACV,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,KAAK,OAAO,EACZ,KAAK,cAAc,GACtB,MAAM,YAAY,CAAA;AACnB,OAAO,EACH,cAAc,EACd,YAAY,EACZ,WAAW,EACX,UAAU,EACV,KAAK,mBAAmB,EACxB,KAAK,SAAS,IAAI,uBAAuB,GAC5C,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAClE,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACzE,YAAY,EAAE,wBAAwB,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAC5G,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAC/D,YAAY,EACR,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,GACxB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC9B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACH,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,KAAK,qBAAqB,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GAC3B,MAAM,4BAA4B,CAAA;AACnC,OAAO,EACH,sBAAsB,EACtB,uBAAuB,GAC1B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,kBAAkB,EAClB,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,GAChC,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,eAAe,GACvB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACH,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,GACvB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACH,qBAAqB,EACrB,KAAK,0BAA0B,GAClC,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACH,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,iBAAiB,GACzB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACH,aAAa,EACb,KAAK,kBAAkB,GAC1B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACH,gBAAgB,EAChB,KAAK,qBAAqB,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,aAAa,EACb,eAAe,GAClB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACR,UAAU,EACV,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,aAAa,EACb,oBAAoB,EACpB,sBAAsB,EACtB,mBAAmB,EACnB,iBAAiB,EACjB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,oBAAoB,GACvB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACH,UAAU,EACV,SAAS,EACT,UAAU,EACV,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,cAAc,EACd,KAAK,iBAAiB,GACzB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,cAAc,EACd,cAAc,EACd,SAAS,EACT,UAAU,EACV,KAAK,mBAAmB,GAC3B,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACH,UAAU,EACV,SAAS,EACT,WAAW,EACX,WAAW,EACX,KAAK,eAAe,GACvB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACH,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,aAAa,EACb,YAAY,EACZ,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,eAAe,GACvB,MAAM,yBAAyB,CAAA"}
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ export { useHotSwapReload, applyHotSwapReload, withVersionParam, clearFederation
30
30
  export * from './dynamic-icon';
31
31
  export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, relationKeyFor, resolveRelationLabel, } from './dynamic-columns';
32
32
  export { humanizeToken } from './dynamic-columns-helpers';
33
+ export { UrlChip, FileChip, ImageThumbnail, MediaValue, RichText, linkifyText, classifyUrl, isImageUrl, isFileUrl, ensureHref, smartUrlLabel, fileNameFromUrl, splitTrailingPunct, } from './rich-url';
33
34
  export { CollectionCell, formatScalar, prettifyKey, countLabel, } from './collection-cell';
34
35
  export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid';
35
36
  export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record';
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Rich URL & media rendering primitives.
3
+ *
4
+ * A URL is never shown raw. Depending on what it points at we render:
5
+ * - an IMAGE (jpg/png/gif/webp/… or a `link.asteby.com/storage/media/` path)
6
+ * → an inline clickable THUMBNAIL that opens the full image in a new tab,
7
+ * - a FILE (pdf/zip/docx/…) → a chip with a file icon and the file name,
8
+ * - anything else → a compact link CHIP: an external-link glyph plus a short
9
+ * smart label (hostname, e.g. "github.com", or the last path segment for a
10
+ * file-like URL), with the full URL in a native tooltip.
11
+ *
12
+ * These live here (not inline in the table/dialog) so the dynamic TABLE cell
13
+ * (`dynamic-columns.tsx`) and the read-only DETAIL DIALOG (`dynamic-record.tsx`)
14
+ * share the EXACT same rendering. Ecosystem rule: shared primitives, zero copy.
15
+ *
16
+ * No external requests are made to render a link (no favicon service — CSP /
17
+ * privacy): the icon is a local lucide glyph.
18
+ */
19
+ import React from 'react';
20
+ export type UrlKind = 'image' | 'file' | 'link';
21
+ /** `true` when the string looks like an image URL/path. */
22
+ export declare function isImageUrl(url: string): boolean;
23
+ /** `true` when the string looks like a non-image downloadable file. */
24
+ export declare function isFileUrl(url: string): boolean;
25
+ /** Classify a URL so the right primitive (thumbnail / file chip / link) renders. */
26
+ export declare function classifyUrl(url: string): UrlKind;
27
+ /** Ensure a URL is absolute so `<a href>` and `new URL()` behave. */
28
+ export declare function ensureHref(url: string): string;
29
+ /**
30
+ * A short, human label for a URL. For a file-like URL (last segment carries an
31
+ * extension) the file name wins ("report.pdf"); otherwise the bare hostname
32
+ * ("github.com"), so a 120-char issue URL never shows raw. Falls back to the
33
+ * input when it can't be parsed.
34
+ */
35
+ export declare function smartUrlLabel(url: string): string;
36
+ /** Last path segment (decoded) — the file name for a file/image URL. */
37
+ export declare function fileNameFromUrl(url: string): string;
38
+ /**
39
+ * Compact link chip: external-link glyph + smart label, full URL in the
40
+ * tooltip, opens in a new tab. The canonical render for a plain (non-media)
41
+ * URL, matching the table's `url`/`link` cell.
42
+ */
43
+ export declare const UrlChip: React.FC<{
44
+ url: string;
45
+ label?: string;
46
+ icon?: string;
47
+ /** Smaller label cap for a table/kanban cell. */
48
+ maxLabelWidth?: number;
49
+ className?: string;
50
+ }>;
51
+ /**
52
+ * Chip for a downloadable non-image file: file-text glyph + the file name,
53
+ * opens the file in a new tab.
54
+ */
55
+ export declare const FileChip: React.FC<{
56
+ url: string;
57
+ maxLabelWidth?: number;
58
+ className?: string;
59
+ }>;
60
+ /**
61
+ * Inline image thumbnail — rounded, bordered, `object-cover` — that opens the
62
+ * full image in a new tab. On load error it degrades to a normal link chip so a
63
+ * dead image URL is still reachable (and never a broken-image icon). `maxHeight`
64
+ * keeps a cell thumbnail small (~h-8) while the dialog shows a larger preview.
65
+ */
66
+ export declare const ImageThumbnail: React.FC<{
67
+ url: string;
68
+ getImageUrl?: (path: string) => string;
69
+ /** Max rendered height in px. */
70
+ maxHeight?: number;
71
+ className?: string;
72
+ }>;
73
+ /**
74
+ * One URL value → the right primitive (thumbnail / file chip / link chip).
75
+ * Single entry point used by both the table cell and the detail dialog so a URL
76
+ * renders identically everywhere.
77
+ */
78
+ export declare const MediaValue: React.FC<{
79
+ url: string;
80
+ getImageUrl?: (path: string) => string;
81
+ /** Explicit label for the link chip (from a `label_field`). */
82
+ label?: string;
83
+ /** Explicit icon for the link chip. */
84
+ icon?: string;
85
+ /** Thumbnail max height (small in a cell, larger in the dialog). */
86
+ thumbHeight?: number;
87
+ /** Link/file label truncation cap. */
88
+ maxLabelWidth?: number;
89
+ className?: string;
90
+ }>;
91
+ /**
92
+ * Peel trailing punctuation a URL shouldn't swallow (sentence period, closing
93
+ * bracket, quote). A closing paren is kept only when the URL itself opened one
94
+ * (Wikipedia-style `(…)` paths), so `(see https://x.com/a)` links `https://x.com/a`
95
+ * but `https://en.wikipedia.org/wiki/Foo_(bar)` keeps its paren.
96
+ */
97
+ export declare function splitTrailingPunct(match: string): [string, string];
98
+ export interface LinkifyOptions {
99
+ getImageUrl?: (path: string) => string;
100
+ /** Max height for an inline image found in the text. */
101
+ imageHeight?: number;
102
+ }
103
+ /**
104
+ * Turn free text into React nodes, replacing every URL (bare or markdown
105
+ * `[label](url)`) with the matching rich primitive: an inline thumbnail for an
106
+ * image URL, a file chip for a file, otherwise a link chip. Plain text between
107
+ * matches is preserved verbatim (caller keeps `whitespace-pre-wrap`).
108
+ */
109
+ export declare function linkifyText(text: string, opts?: LinkifyOptions): React.ReactNode[];
110
+ /**
111
+ * Linkified free text. Renders the raw string with every URL turned into a rich
112
+ * chip/thumbnail — the read view for a long-text / textarea / body field.
113
+ */
114
+ export declare const RichText: React.FC<{
115
+ text: string;
116
+ getImageUrl?: (path: string) => string;
117
+ imageHeight?: number;
118
+ }>;
119
+ //# sourceMappingURL=rich-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rich-url.d.ts","sourceRoot":"","sources":["../src/rich-url.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,224 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Rich URL & media rendering primitives.
4
+ *
5
+ * A URL is never shown raw. Depending on what it points at we render:
6
+ * - an IMAGE (jpg/png/gif/webp/… or a `link.asteby.com/storage/media/` path)
7
+ * → an inline clickable THUMBNAIL that opens the full image in a new tab,
8
+ * - a FILE (pdf/zip/docx/…) → a chip with a file icon and the file name,
9
+ * - anything else → a compact link CHIP: an external-link glyph plus a short
10
+ * smart label (hostname, e.g. "github.com", or the last path segment for a
11
+ * file-like URL), with the full URL in a native tooltip.
12
+ *
13
+ * These live here (not inline in the table/dialog) so the dynamic TABLE cell
14
+ * (`dynamic-columns.tsx`) and the read-only DETAIL DIALOG (`dynamic-record.tsx`)
15
+ * share the EXACT same rendering. Ecosystem rule: shared primitives, zero copy.
16
+ *
17
+ * No external requests are made to render a link (no favicon service — CSP /
18
+ * privacy): the icon is a local lucide glyph.
19
+ */
20
+ import React from 'react';
21
+ import { cn } from '@asteby/metacore-ui/lib';
22
+ import { DynamicIcon } from './dynamic-icon';
23
+ /** Image file extensions we render as an inline thumbnail. */
24
+ const IMAGE_EXT_RE = /\.(jpe?g|png|gif|webp|avif|svg|bmp|ico)(?:[?#].*)?$/i;
25
+ /**
26
+ * Storage paths that serve images without a file extension (the link.asteby.com
27
+ * media CDN hands back opaque ids). Treated as images so uploaded photos still
28
+ * thumbnail.
29
+ */
30
+ const MEDIA_PATH_RE = /\/storage\/media\//i;
31
+ /** Non-image file extensions we render as a download-ish file chip. */
32
+ const FILE_EXT_RE = /\.(pdf|zip|rar|7z|tar|t?gz|docx?|xlsx?|pptx?|csv|txt|json|xml|md|rtf|odt|mp4|mov|avi|mkv|webm|mp3|wav|ogg|flac|apk|dmg|exe|pkg)(?:[?#].*)?$/i;
33
+ /** `true` when the string looks like an image URL/path. */
34
+ export function isImageUrl(url) {
35
+ return IMAGE_EXT_RE.test(url) || MEDIA_PATH_RE.test(url);
36
+ }
37
+ /** `true` when the string looks like a non-image downloadable file. */
38
+ export function isFileUrl(url) {
39
+ return FILE_EXT_RE.test(url);
40
+ }
41
+ /** Classify a URL so the right primitive (thumbnail / file chip / link) renders. */
42
+ export function classifyUrl(url) {
43
+ if (isImageUrl(url))
44
+ return 'image';
45
+ if (isFileUrl(url))
46
+ return 'file';
47
+ return 'link';
48
+ }
49
+ /** Ensure a URL is absolute so `<a href>` and `new URL()` behave. */
50
+ export function ensureHref(url) {
51
+ return /^(https?:)?\/\//i.test(url) || /^(mailto|tel):/i.test(url)
52
+ ? url
53
+ : `https://${url}`;
54
+ }
55
+ /**
56
+ * A short, human label for a URL. For a file-like URL (last segment carries an
57
+ * extension) the file name wins ("report.pdf"); otherwise the bare hostname
58
+ * ("github.com"), so a 120-char issue URL never shows raw. Falls back to the
59
+ * input when it can't be parsed.
60
+ */
61
+ export function smartUrlLabel(url) {
62
+ try {
63
+ const u = new URL(ensureHref(url));
64
+ const segs = u.pathname.split('/').filter(Boolean);
65
+ const last = segs[segs.length - 1];
66
+ if (last && /\.[a-z0-9]{1,8}$/i.test(last)) {
67
+ return decodeURIComponent(last);
68
+ }
69
+ return u.hostname.replace(/^www\./i, '');
70
+ }
71
+ catch {
72
+ return url;
73
+ }
74
+ }
75
+ /** Last path segment (decoded) — the file name for a file/image URL. */
76
+ export function fileNameFromUrl(url) {
77
+ try {
78
+ const u = new URL(ensureHref(url));
79
+ const segs = u.pathname.split('/').filter(Boolean);
80
+ return decodeURIComponent(segs[segs.length - 1] || u.hostname);
81
+ }
82
+ catch {
83
+ return url.split(/[?#]/)[0].split('/').pop() || url;
84
+ }
85
+ }
86
+ /** Stops row-click / dialog-close when a link inside them is clicked. */
87
+ function stop(e) {
88
+ e.stopPropagation();
89
+ }
90
+ /**
91
+ * Compact link chip: external-link glyph + smart label, full URL in the
92
+ * tooltip, opens in a new tab. The canonical render for a plain (non-media)
93
+ * URL, matching the table's `url`/`link` cell.
94
+ */
95
+ export const UrlChip = ({ url, label, icon, maxLabelWidth = 260, className }) => {
96
+ const href = ensureHref(url);
97
+ const text = label || smartUrlLabel(url);
98
+ return (_jsxs("a", { href: href, target: "_blank", rel: "noopener noreferrer", title: url, onClick: stop, className: cn('inline-flex max-w-full items-center gap-1.5 align-middle text-sm font-medium text-primary hover:underline', className), children: [_jsx(DynamicIcon, { name: icon || 'ExternalLink', className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "truncate", style: { maxWidth: maxLabelWidth }, children: text })] }));
99
+ };
100
+ /**
101
+ * Chip for a downloadable non-image file: file-text glyph + the file name,
102
+ * opens the file in a new tab.
103
+ */
104
+ export const FileChip = ({ url, maxLabelWidth = 240, className }) => {
105
+ const href = ensureHref(url);
106
+ return (_jsxs("a", { href: href, target: "_blank", rel: "noopener noreferrer", title: url, onClick: stop, className: cn('inline-flex max-w-full items-center gap-1.5 rounded-md border bg-muted/40 px-2 py-1 align-middle text-sm font-medium text-foreground hover:bg-muted', className), children: [_jsx(DynamicIcon, { name: "FileText", className: "h-3.5 w-3.5 shrink-0 opacity-70" }), _jsx("span", { className: "truncate", style: { maxWidth: maxLabelWidth }, children: fileNameFromUrl(url) })] }));
107
+ };
108
+ /**
109
+ * Inline image thumbnail — rounded, bordered, `object-cover` — that opens the
110
+ * full image in a new tab. On load error it degrades to a normal link chip so a
111
+ * dead image URL is still reachable (and never a broken-image icon). `maxHeight`
112
+ * keeps a cell thumbnail small (~h-8) while the dialog shows a larger preview.
113
+ */
114
+ export const ImageThumbnail = ({ url, getImageUrl, maxHeight = 160, className }) => {
115
+ const [failed, setFailed] = React.useState(false);
116
+ const href = ensureHref(url);
117
+ // Absolute URLs are used verbatim; only relative storage paths go through
118
+ // the host's image resolver (which prefixes the media base).
119
+ const src = /^(https?:)?\/\//i.test(url) ? url : getImageUrl ? getImageUrl(url) : url;
120
+ if (failed)
121
+ return _jsx(UrlChip, { url: url, icon: "Image" });
122
+ return (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", title: url, onClick: stop, className: "inline-block max-w-full align-middle", children: _jsx("img", { src: src, alt: smartUrlLabel(url), onError: () => setFailed(true), style: { maxHeight }, className: cn('max-w-full rounded-md border object-cover', className) }) }));
123
+ };
124
+ /**
125
+ * One URL value → the right primitive (thumbnail / file chip / link chip).
126
+ * Single entry point used by both the table cell and the detail dialog so a URL
127
+ * renders identically everywhere.
128
+ */
129
+ export const MediaValue = ({ url, getImageUrl, label, icon, thumbHeight, maxLabelWidth, className }) => {
130
+ switch (classifyUrl(url)) {
131
+ case 'image':
132
+ return (_jsx(ImageThumbnail, { url: url, getImageUrl: getImageUrl, maxHeight: thumbHeight, className: className }));
133
+ case 'file':
134
+ return _jsx(FileChip, { url: url, maxLabelWidth: maxLabelWidth, className: className });
135
+ default:
136
+ return (_jsx(UrlChip, { url: url, label: label, icon: icon, maxLabelWidth: maxLabelWidth, className: className }));
137
+ }
138
+ };
139
+ // URL / markdown-link matcher used to linkify free text. Captures either a
140
+ // markdown `[label](url)` or a bare http(s)/www URL. `[^\s<]+` is greedy on
141
+ // purpose — trailing punctuation is trimmed afterwards by `splitTrailingPunct`.
142
+ const LINK_IN_TEXT_RE = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<]+|www\.[^\s<]+)/gi;
143
+ /**
144
+ * Peel trailing punctuation a URL shouldn't swallow (sentence period, closing
145
+ * bracket, quote). A closing paren is kept only when the URL itself opened one
146
+ * (Wikipedia-style `(…)` paths), so `(see https://x.com/a)` links `https://x.com/a`
147
+ * but `https://en.wikipedia.org/wiki/Foo_(bar)` keeps its paren.
148
+ */
149
+ export function splitTrailingPunct(match) {
150
+ const m = match.match(/[).,;:!?\]}'"»›]+$/);
151
+ if (!m)
152
+ return [match, ''];
153
+ let trail = m[0];
154
+ let core = match.slice(0, match.length - trail.length);
155
+ if (trail.startsWith(')')) {
156
+ const opens = (core.match(/\(/g) || []).length;
157
+ const closes = (core.match(/\)/g) || []).length;
158
+ if (opens > closes) {
159
+ core += ')';
160
+ trail = trail.slice(1);
161
+ }
162
+ }
163
+ return [core, trail];
164
+ }
165
+ /**
166
+ * Turn free text into React nodes, replacing every URL (bare or markdown
167
+ * `[label](url)`) with the matching rich primitive: an inline thumbnail for an
168
+ * image URL, a file chip for a file, otherwise a link chip. Plain text between
169
+ * matches is preserved verbatim (caller keeps `whitespace-pre-wrap`).
170
+ */
171
+ export function linkifyText(text, opts = {}) {
172
+ if (!text)
173
+ return [];
174
+ const nodes = [];
175
+ const re = new RegExp(LINK_IN_TEXT_RE.source, 'gi');
176
+ let lastIndex = 0;
177
+ let key = 0;
178
+ let m;
179
+ while ((m = re.exec(text)) !== null) {
180
+ const [full, mdLabel, mdUrl, bareUrl] = m;
181
+ const start = m.index;
182
+ if (start > lastIndex)
183
+ nodes.push(text.slice(lastIndex, start));
184
+ if (mdUrl) {
185
+ // Markdown link: honor the author's label, keep media inline.
186
+ const kind = classifyUrl(mdUrl);
187
+ if (kind === 'image') {
188
+ nodes.push(_jsx(ImageThumbnail, { url: mdUrl, getImageUrl: opts.getImageUrl, maxHeight: opts.imageHeight ?? 200 }, key));
189
+ }
190
+ else if (kind === 'file') {
191
+ nodes.push(_jsx(FileChip, { url: mdUrl }, key));
192
+ }
193
+ else {
194
+ nodes.push(_jsx(UrlChip, { url: mdUrl, label: mdLabel }, key));
195
+ }
196
+ lastIndex = start + full.length;
197
+ }
198
+ else {
199
+ const [core, trail] = splitTrailingPunct(bareUrl);
200
+ const kind = classifyUrl(core);
201
+ if (kind === 'image') {
202
+ nodes.push(_jsx(ImageThumbnail, { url: core, getImageUrl: opts.getImageUrl, maxHeight: opts.imageHeight ?? 200 }, key));
203
+ }
204
+ else if (kind === 'file') {
205
+ nodes.push(_jsx(FileChip, { url: core }, key));
206
+ }
207
+ else {
208
+ nodes.push(_jsx(UrlChip, { url: core }, key));
209
+ }
210
+ if (trail)
211
+ nodes.push(trail);
212
+ lastIndex = start + bareUrl.length;
213
+ }
214
+ key++;
215
+ }
216
+ if (lastIndex < text.length)
217
+ nodes.push(text.slice(lastIndex));
218
+ return nodes;
219
+ }
220
+ /**
221
+ * Linkified free text. Renders the raw string with every URL turned into a rich
222
+ * chip/thumbnail — the read view for a long-text / textarea / body field.
223
+ */
224
+ export const RichText = ({ text, getImageUrl, imageHeight }) => (_jsx(_Fragment, { children: linkifyText(text, { getImageUrl, imageHeight }) }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "23.2.0",
3
+ "version": "23.4.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",