@donotdev/ui 0.0.11 → 0.0.13

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.
Files changed (35) hide show
  1. package/dist/components/common/FeatureCard.js +1 -1
  2. package/dist/components/common/RedirectOverlay.js +1 -1
  3. package/dist/components/cookie-consent/CookieConsent.js +3 -3
  4. package/dist/components/layout/GameContainer.d.ts +26 -8
  5. package/dist/components/layout/GameContainer.d.ts.map +1 -1
  6. package/dist/components/layout/GameContainer.js +21 -2
  7. package/dist/components/layout/GameFlow.d.ts.map +1 -1
  8. package/dist/components/layout/GameFlow.js +27 -11
  9. package/dist/components/layout/PageContainer.d.ts +1 -1
  10. package/dist/components/layout/PageContainer.d.ts.map +1 -1
  11. package/dist/components/layout/components/header/CacheSettings.js +1 -1
  12. package/dist/crud/components/EntityCardList.d.ts.map +1 -1
  13. package/dist/crud/components/EntityCardList.js +16 -10
  14. package/dist/crud/components/EntityDisplayRenderer.js +2 -2
  15. package/dist/crud/components/EntityFormRenderer.d.ts.map +1 -1
  16. package/dist/crud/components/EntityFormRenderer.js +31 -20
  17. package/dist/crud/components/EntityList.d.ts +1 -1
  18. package/dist/crud/components/EntityList.d.ts.map +1 -1
  19. package/dist/crud/components/EntityList.js +11 -11
  20. package/dist/crud/components/Form.js +1 -1
  21. package/dist/dndev.css +128 -83
  22. package/dist/index.js +4 -4
  23. package/dist/internal/common/RouteErrorFallback.js +3 -3
  24. package/dist/internal/devtools/components/ConfigTab.js +1 -1
  25. package/dist/internal/devtools/components/CookieTab.js +1 -1
  26. package/dist/internal/devtools/components/DesignTab.js +2 -2
  27. package/dist/internal/devtools/components/StoresTab.js +2 -2
  28. package/dist/internal/layout/config/presets/game.d.ts.map +1 -1
  29. package/dist/internal/layout/config/presets/game.js +10 -5
  30. package/dist/internal/layout/zones/DnDevFooter.js +1 -1
  31. package/dist/internal/layout/zones/DnDevMergedBar.js +1 -1
  32. package/dist/routing/404.js +3 -3
  33. package/dist/routing/AuthGuardFallback.js +2 -2
  34. package/dist/styles/index.css +128 -83
  35. package/package.json +5 -5
@@ -56,7 +56,7 @@ const FeatureCard = ({ icon, title, subtitle, content, href, variant, elevated,
56
56
  margin: 0,
57
57
  };
58
58
  // Build title with icon if provided
59
- const titleContent = icon ? (_jsxs(Stack, { direction: "row", align: "center", gap: "medium", style: { width: '100%' }, children: [_jsx(IconBox, { icon: icon }), _jsx(Text, { as: "div", level: "h3", style: titleStyle, children: title })] })) : (_jsx(Text, { as: "div", level: "h3", style: titleStyle, children: title }));
59
+ const titleContent = icon ? (_jsxs(Stack, { direction: "row", align: "center", style: { width: '100%' }, children: [_jsx(IconBox, { icon: icon }), _jsx(Text, { as: "div", level: "h3", style: titleStyle, children: title })] })) : (_jsx(Text, { as: "div", level: "h3", style: titleStyle, children: title }));
60
60
  const card = (_jsx(Card, { variant: variant, elevated: elevated, onClick: onClick, className: className, "data-clickable": href || onClick ? 'true' : undefined, style: {
61
61
  paddingInlineStart: 'var(--gap-md)',
62
62
  paddingInlineEnd: 'var(--gap-md)',
@@ -219,7 +219,7 @@ export function RedirectOverlay() {
219
219
  textAlign: 'center',
220
220
  maxWidth: '400px',
221
221
  padding: 'var(--gap-lg)',
222
- }, children: [_jsxs(Stack, { direction: "row", gap: "medium", align: "center", children: [IconComponent && (_jsx(IconComponent, { style: {
222
+ }, children: [_jsxs(Stack, { direction: "row", align: "center", children: [IconComponent && (_jsx(IconComponent, { style: {
223
223
  width: '1.5rem',
224
224
  height: '1.5rem',
225
225
  color: 'var(--primary)',
@@ -141,14 +141,14 @@ function CookieConsent({ position = 'bottom', showBranding = true, brandingText
141
141
  declineAll();
142
142
  setShowBanner(false);
143
143
  };
144
- const bannerContent = (_jsxs(Stack, { gap: "medium", children: [_jsx(Text, { as: "p", variant: "muted", level: "small", children: t('cookieBannerDescription') }), hasOptionalCategories && (_jsx(Accordion, { type: "single", collapsible: true, value: showPreferences ? 'preferences' : '', onValueChange: (value) => setShowPreferences(value === 'preferences'), items: [
144
+ const bannerContent = (_jsxs(Stack, { children: [_jsx(Text, { as: "p", variant: "muted", level: "small", children: t('cookieBannerDescription') }), hasOptionalCategories && (_jsx(Accordion, { type: "single", collapsible: true, value: showPreferences ? 'preferences' : '', onValueChange: (value) => setShowPreferences(value === 'preferences'), items: [
145
145
  {
146
146
  value: 'preferences',
147
147
  trigger: (_jsxs(Stack, { direction: "row", align: "center", gap: "tight", children: [_jsx(Settings, { className: "dndev-size-md" }), t('customize')] })),
148
- content: (_jsx(Stack, { gap: "medium", children: categoryDefs.map((cat) => {
148
+ content: (_jsx(Stack, { children: categoryDefs.map((cat) => {
149
149
  const Icon = cat.icon;
150
150
  const enabled = preferenceCategories[cat.id] ?? false;
151
- return (_jsx(Card, { content: _jsxs(Stack, { direction: "row", align: "center", justify: "between", gap: "medium", children: [_jsxs(Stack, { direction: "row", align: "start", gap: "tight", style: { flex: 1 }, children: [_jsx(Icon, { className: "dndev-size-md", style: {
151
+ return (_jsx(Card, { content: _jsxs(Stack, { direction: "row", align: "center", justify: "between", children: [_jsxs(Stack, { direction: "row", align: "start", gap: "tight", style: { flex: 1 }, children: [_jsx(Icon, { className: "dndev-size-md", style: {
152
152
  color: 'var(--primary)',
153
153
  marginTop: '0.125rem',
154
154
  } }), _jsxs(Stack, { gap: "tight", children: [_jsx(Text, { as: "span", level: "body", style: { fontWeight: 600 }, children: cat.title }), _jsx(Text, { as: "span", variant: "muted", level: "small", children: cat.description }), cat.examples && (_jsx(Text, { as: "span", variant: "muted", level: "small", style: { fontStyle: 'italic' }, children: cat.examples }))] })] }), cat.locked ? (_jsx(Text, { as: "span", variant: "muted", level: "small", children: t('alwaysOn') })) : (_jsx(Switch, { checked: enabled, onCheckedChange: (val) => setPreferenceCategories((prev) => ({
@@ -1,23 +1,29 @@
1
1
  import type { ReactNode } from 'react';
2
+ /**
3
+ * CTA button definition
4
+ */
5
+ export interface CTAButton {
6
+ label: string;
7
+ onClick: () => void;
8
+ disabled?: boolean;
9
+ variant?: 'default' | 'outline' | 'ghost' | 'link';
10
+ }
2
11
  /**
3
12
  * GameContainer props
4
13
  */
5
14
  export interface GameContainerProps {
6
15
  /** Main content area (scrollable, centered by default) */
7
16
  content: ReactNode;
8
- /** Optional fixed CTA button at bottom */
9
- cta?: {
10
- label: string;
11
- onClick: () => void;
12
- disabled?: boolean;
13
- variant?: 'default' | 'outline' | 'ghost' | 'link';
14
- };
17
+ /** Optional fixed CTA button(s) at bottom - single button or array for multiple buttons */
18
+ cta?: CTAButton | CTAButton[];
15
19
  /** Alignment variant for content area */
16
20
  align?: 'center' | 'start' | 'stretch';
17
21
  /** Justify variant for content area */
18
22
  justify?: 'center' | 'start' | 'end' | 'between';
19
23
  /** Disable ScrollArea (use plain overflow) - for interactive content */
20
24
  disableScrollArea?: boolean;
25
+ /** Content width: 'narrow' (default, constrained) or 'full' */
26
+ contentVariant?: 'full' | 'narrow';
21
27
  /** Additional className for content wrapper */
22
28
  contentClassName?: string;
23
29
  /** Additional className for CTA button */
@@ -66,9 +72,21 @@ export interface GameContainerProps {
66
72
  * />
67
73
  * ```
68
74
  *
75
+ * @example
76
+ * Multiple CTA buttons (side by side)
77
+ * ```tsx
78
+ * <GameContainer
79
+ * content={<ChallengeContent />}
80
+ * cta={[
81
+ * { label: 'Accept', onClick: handleAccept, variant: 'default' },
82
+ * { label: 'Decline', onClick: handleDecline, variant: 'outline' }
83
+ * ]}
84
+ * />
85
+ * ```
86
+ *
69
87
  * @version 0.0.1
70
88
  * @since 0.0.1
71
89
  * @author AMBROISE PARK Consulting
72
90
  */
73
- export declare function GameContainer({ content, cta, align, justify, disableScrollArea, contentClassName, ctaClassName, }: GameContainerProps): import("react/jsx-runtime").JSX.Element;
91
+ export declare function GameContainer({ content, cta, align, justify, disableScrollArea, contentVariant, contentClassName, ctaClassName, }: GameContainerProps): import("react/jsx-runtime").JSX.Element;
74
92
  //# sourceMappingURL=GameContainer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"GameContainer.d.ts","sourceRoot":"","sources":["../../../src/components/layout/GameContainer.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,OAAO,EAAE,SAAS,CAAC;IAEnB,0CAA0C;IAC1C,GAAG,CAAC,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,IAAI,CAAC;QACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;KACpD,CAAC;IAEF,yCAAyC;IACzC,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;IAEvC,uCAAuC;IACvC,OAAO,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,CAAC;IAEjD,wEAAwE;IACxE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B,+CAA+C;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,GAAG,EACH,KAAgB,EAChB,OAAkB,EAClB,iBAAyB,EACzB,gBAAgB,EAChB,YAAY,GACb,EAAE,kBAAkB,2CAmCpB"}
1
+ {"version":3,"file":"GameContainer.d.ts","sourceRoot":"","sources":["../../../src/components/layout/GameContainer.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;CACpD;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,OAAO,EAAE,SAAS,CAAC;IAEnB,2FAA2F;IAC3F,GAAG,CAAC,EAAE,SAAS,GAAG,SAAS,EAAE,CAAC;IAE9B,yCAAyC;IACzC,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;IAEvC,uCAAuC;IACvC,OAAO,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,CAAC;IAEjD,wEAAwE;IACxE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAEnC,+CAA+C;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,GAAG,EACH,KAAgB,EAChB,OAAkB,EAClB,iBAAyB,EACzB,cAAyB,EACzB,gBAAgB,EAChB,YAAY,GACb,EAAE,kBAAkB,2CAoEpB"}
@@ -53,10 +53,29 @@ import { cn } from '@donotdev/components';
53
53
  * />
54
54
  * ```
55
55
  *
56
+ * @example
57
+ * Multiple CTA buttons (side by side)
58
+ * ```tsx
59
+ * <GameContainer
60
+ * content={<ChallengeContent />}
61
+ * cta={[
62
+ * { label: 'Accept', onClick: handleAccept, variant: 'default' },
63
+ * { label: 'Decline', onClick: handleDecline, variant: 'outline' }
64
+ * ]}
65
+ * />
66
+ * ```
67
+ *
56
68
  * @version 0.0.1
57
69
  * @since 0.0.1
58
70
  * @author AMBROISE PARK Consulting
59
71
  */
60
- export function GameContainer({ content, cta, align = 'center', justify = 'center', disableScrollArea = false, contentClassName, ctaClassName, }) {
61
- return (_jsxs("div", { className: "dndev-game-container", children: [_jsx("div", { className: cn('dndev-game-container__content', contentClassName), "data-align": align, "data-justify": justify, children: disableScrollArea ? (_jsx("div", { style: { overflow: 'auto', overscrollBehavior: 'contain' }, children: content })) : (_jsx(ScrollArea, { className: "dndev-game-container__scroll", children: content })) }), cta && (_jsx("div", { className: cn('dndev-game-container__cta', ctaClassName), children: _jsx(Button, { onClick: cta.onClick, disabled: cta.disabled ?? false, variant: cta.variant ?? 'default', className: "dndev-game-container__cta-button", children: cta.label }) }))] }));
72
+ export function GameContainer({ content, cta, align = 'center', justify = 'center', disableScrollArea = false, contentVariant = 'narrow', contentClassName, ctaClassName, }) {
73
+ const narrowWrapper = contentVariant === 'narrow' && (_jsx("div", { className: "dndev-game-container__content-narrow", children: content }));
74
+ const scrollContent = contentVariant === 'narrow' ? narrowWrapper : content;
75
+ const contentInner = disableScrollArea ? (_jsx("div", { style: {
76
+ overflow: 'auto',
77
+ overscrollBehavior: 'contain',
78
+ height: '100%',
79
+ }, children: scrollContent })) : (_jsx(ScrollArea, { className: "dndev-game-container__scroll", children: scrollContent }));
80
+ return (_jsxs("div", { className: "dndev-game-container", children: [_jsx("div", { className: cn('dndev-game-container__content', contentClassName), "data-align": align, "data-justify": justify, "data-content-variant": contentVariant, children: _jsx("div", { className: "dndev-game-container__scroll-wrapper", children: contentInner }) }), cta && (_jsx("div", { className: cn('dndev-game-container__cta', ctaClassName), children: Array.isArray(cta) ? (_jsx("div", { className: "dndev-game-container__cta-buttons", children: cta.map((button, index) => (_jsx(Button, { onClick: button.onClick, disabled: button.disabled ?? false, variant: button.variant ?? 'default', className: "dndev-game-container__cta-button", fullWidth: true, children: button.label }, index))) })) : (_jsx(Button, { onClick: cta.onClick, disabled: cta.disabled ?? false, variant: cta.variant ?? 'default', className: "dndev-game-container__cta-button", children: cta.label })) }))] }));
62
81
  }
@@ -1 +1 @@
1
- {"version":3,"file":"GameFlow.d.ts","sourceRoot":"","sources":["../../../src/components/layout/GameFlow.tsx"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CACjC,MAAM,EACN,MAAM,OAAO,CAAC;IAAE,OAAO,EAAE,aAAa,CAAC,GAAG,CAAC,CAAA;CAAE,CAAC,CAC/C,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,cAAc,EAAE,cAAc,CAAC;IAC/B,oCAAoC;IACpC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iDAAiD;IACjD,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAQD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,QAAQ,CAAC,EACvB,cAAc,EACd,aAAa,EACb,UAAU,EACV,QAAe,GAChB,EAAE,aAAa,kDAqCf"}
1
+ {"version":3,"file":"GameFlow.d.ts","sourceRoot":"","sources":["../../../src/components/layout/GameFlow.tsx"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CACjC,MAAM,EACN,MAAM,OAAO,CAAC;IAAE,OAAO,EAAE,aAAa,CAAC,GAAG,CAAC,CAAA;CAAE,CAAC,CAC/C,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,cAAc,EAAE,cAAc,CAAC;IAC/B,oCAAoC;IACpC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iDAAiD;IACjD,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAmCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,QAAQ,CAAC,EACvB,cAAc,EACd,aAAa,EACb,UAAU,EACV,QAAe,GAChB,EAAE,aAAa,kDAgCf"}
@@ -8,12 +8,34 @@ import { jsx as _jsx } from "react/jsx-runtime";
8
8
  * @since 0.0.1
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
- import { useEffect, useMemo, Suspense, lazy, } from 'react';
11
+ import { useEffect, Suspense, lazy, } from 'react';
12
12
  /**
13
13
  * Module-level cache for preloaded screens
14
14
  * Tracks which screens have been preloaded to avoid duplicate preloading
15
15
  */
16
16
  const preloadedScreens = new Set();
17
+ /**
18
+ * Module-level cache for lazy components.
19
+ * CRITICAL: lazy() must NOT be called inside a component (useMemo or otherwise).
20
+ * Doing so creates a new component TYPE on each call, which causes React to
21
+ * unmount/remount the entire subtree and can trigger Rules of Hooks violations.
22
+ * This cache ensures each screen name always resolves to the same lazy component ref.
23
+ */
24
+ const lazyComponentCache = new Map();
25
+ /**
26
+ * Get or create a cached lazy component for a screen name
27
+ */
28
+ function getLazyComponent(screenName, registry) {
29
+ const cached = lazyComponentCache.get(screenName);
30
+ if (cached)
31
+ return cached;
32
+ const importer = registry[screenName];
33
+ if (!importer)
34
+ return null;
35
+ const component = lazy(importer);
36
+ lazyComponentCache.set(screenName, component);
37
+ return component;
38
+ }
17
39
  /**
18
40
  * GameFlow - Screen router for game/session flows
19
41
  *
@@ -59,16 +81,10 @@ export function GameFlow({ screenRegistry, currentScreen, nextScreen, fallback =
59
81
  }
60
82
  }
61
83
  }, [nextScreen, screenRegistry]);
62
- // Get lazy component for currentScreen
63
- const ScreenComponent = useMemo(() => {
64
- if (!currentScreen)
65
- return null;
66
- const importer = screenRegistry[currentScreen];
67
- if (!importer)
68
- return null;
69
- // Create lazy component from registry
70
- return lazy(importer);
71
- }, [currentScreen, screenRegistry]);
84
+ // Get cached lazy component for currentScreen (stable reference per screen name)
85
+ const ScreenComponent = currentScreen
86
+ ? getLazyComponent(currentScreen, screenRegistry)
87
+ : null;
72
88
  // If no currentScreen or no component found, return null
73
89
  if (!currentScreen || !ScreenComponent) {
74
90
  return null;
@@ -3,7 +3,7 @@ import type { ComponentType, ReactNode } from 'react';
3
3
  /**
4
4
  * Page container variants - semantic layout patterns
5
5
  */
6
- export type PageContainerVariant = 'full' | 'standard' | 'docs' | 'fixed';
6
+ export type PageContainerVariant = 'full' | 'standard' | 'docs' | 'narrow' | 'fixed';
7
7
  /**
8
8
  * PageContainer component props
9
9
  */
@@ -1 +1 @@
1
- {"version":3,"file":"PageContainer.d.ts","sourceRoot":"","sources":["../../../src/components/layout/PageContainer.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAK9C,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEtD;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAC5B,MAAM,GACN,UAAU,GACV,MAAM,GACN,OAAO,CAAC;AAEZ;;GAEG;AACH,UAAU,kBAAkB;IAC1B,4BAA4B;IAC5B,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAE/B,uDAAuD;IACvD,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,oBAAoB;IACpB,QAAQ,EAAE,SAAS,CAAC;IAEpB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,aAAa,EAAE,aAAa,CAAC,kBAAkB,CA4B3D,CAAC"}
1
+ {"version":3,"file":"PageContainer.d.ts","sourceRoot":"","sources":["../../../src/components/layout/PageContainer.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAK9C,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEtD;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAC5B,MAAM,GACN,UAAU,GACV,MAAM,GACN,QAAQ,GACR,OAAO,CAAC;AAEZ;;GAEG;AACH,UAAU,kBAAkB;IAC1B,4BAA4B;IAC5B,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAE/B,uDAAuD;IACvD,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,oBAAoB;IACpB,QAAQ,EAAE,SAAS,CAAC;IAEpB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,aAAa,EAAE,aAAa,CAAC,kBAAkB,CA4B3D,CAAC"}
@@ -205,7 +205,7 @@ function CacheSettings() {
205
205
  setIsClearing(false);
206
206
  }
207
207
  };
208
- return (_jsxs(Stack, { gap: "medium", children: [_jsx(Stack, { gap: "medium", children: Object.entries(cacheOptions).map(([key, checked]) => (_jsxs(Stack, { direction: "row", align: "center", gap: "tight", children: [_jsx(Checkbox, { id: key, checked: checked, onCheckedChange: (checked) => setCacheOptions((prev) => ({ ...prev, [key]: !!checked })) }), _jsx("label", { htmlFor: key, style: {
208
+ return (_jsxs(Stack, { children: [_jsx(Stack, { children: Object.entries(cacheOptions).map(([key, checked]) => (_jsxs(Stack, { direction: "row", align: "center", gap: "tight", children: [_jsx(Checkbox, { id: key, checked: checked, onCheckedChange: (checked) => setCacheOptions((prev) => ({ ...prev, [key]: !!checked })) }), _jsx("label", { htmlFor: key, style: {
209
209
  fontSize: 'var(--font-size-sm)',
210
210
  fontWeight: 500,
211
211
  lineHeight: 1,
@@ -1 +1 @@
1
- {"version":3,"file":"EntityCardList.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityCardList.tsx"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAmB,EACnB,SAA0B,EAAE,2BAA2B;AACvD,MAAM,EACN,WAAmB,GACpB,EAAE,mBAAmB,2CA0RrB"}
1
+ {"version":3,"file":"EntityCardList.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityCardList.tsx"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAmB,EACnB,SAA0B,EAAE,2BAA2B;AACvD,MAAM,EACN,WAAmB,GACpB,EAAE,mBAAmB,2CA6RrB"}
@@ -13,12 +13,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
13
13
  * @since 0.0.1
14
14
  * @author AMBROISE PARK Consulting
15
15
  */
16
- import { useMemo, useCallback, useState } from 'react';
16
+ import { useMemo, useCallback } from 'react';
17
17
  import { Heart } from 'lucide-react';
18
18
  import { Grid, Card, Stack, Text, Spinner, Section, Button, } from '@donotdev/components';
19
19
  import { useTranslation } from '@donotdev/core';
20
20
  import { useNavigate } from '../../routing';
21
- import { translateFieldLabel, useCrudCardList, EntityFilters, useEntityFavorites, matchesFilter, formatValue, } from '@donotdev/crud';
21
+ import { translateFieldLabel, useCrudCardList, EntityFilters, useEntityFavorites, matchesFilter, formatValue, useCrudFilters, } from '@donotdev/crud';
22
22
  /**
23
23
  * Entity Card List Component - Card grid view for public/user-facing browsing
24
24
  *
@@ -41,9 +41,15 @@ filter, hideFilters = false, }) {
41
41
  const { isFavorite, toggleFavorite, favoritesFilter } = useEntityFavorites({
42
42
  collection: entity.collection,
43
43
  });
44
- const [showFavoritesOnly, setShowFavoritesOnly] = useState(false);
45
- // Filter state
46
- const [filters, setFilters] = useState({});
44
+ // Favorites toggle from CrudStore (persists across navigation)
45
+ // Note: EntityFilters now manages its own filters internally via useCrudFilters
46
+ const { showFavoritesOnly, setShowFavoritesOnly } = useCrudFilters({
47
+ collection: entity.collection,
48
+ });
49
+ // Get filters for applying to data (EntityFilters manages its own state)
50
+ const { filters } = useCrudFilters({
51
+ collection: entity.collection,
52
+ });
47
53
  // Apply filters from EntityFilters component
48
54
  const applyFilters = useCallback((item) => {
49
55
  if (Object.keys(filters).length === 0)
@@ -110,22 +116,22 @@ filter, hideFilters = false, }) {
110
116
  return (_jsxs(_Fragment, { children: [!hideFilters && (_jsx(Section, { title: tCrud('filters.title', {
111
117
  entity: entityName,
112
118
  defaultValue: `Browse ${entityName} - Filters`,
113
- }), collapsible: true, defaultOpen: true, children: _jsxs(Stack, { direction: "column", gap: "medium", children: [_jsx(Button, { variant: showFavoritesOnly ? 'primary' : 'outline', icon: _jsx(Heart, { size: 18 }), onClick: () => setShowFavoritesOnly(!showFavoritesOnly), children: showFavoritesOnly
119
+ }), collapsible: true, defaultOpen: true, children: _jsxs(Stack, { direction: "column", children: [_jsx(Button, { variant: showFavoritesOnly ? 'primary' : 'outline', icon: _jsx(Heart, { size: 18 }), onClick: () => setShowFavoritesOnly(!showFavoritesOnly), children: showFavoritesOnly
114
120
  ? tCrud('favorites.showAll', { defaultValue: 'Show All' })
115
121
  : tCrud('favorites.showFavorites', {
116
122
  defaultValue: 'Show Favorites',
117
- }) }), _jsx(EntityFilters, { entity: entity, data: rawData, filters: filters, onFiltersChange: setFilters, fieldsToFilter: entity.listCardFields })] }) })), _jsx(Section, { title: loading
123
+ }) }), _jsx(EntityFilters, { entity: entity, data: rawData, fieldsToFilter: entity.listCardFields })] }) })), _jsx(Section, { title: loading
118
124
  ? tCrud('results.title.fetching', { defaultValue: 'Fetching...' })
119
125
  : tCrud('results.title.count', {
120
126
  count: data.length,
121
127
  defaultValue: data.length === 1
122
128
  ? 'Found 1 occurrence'
123
129
  : `Found ${data.length} occurrences`,
124
- }), collapsible: true, defaultOpen: true, children: loading ? (_jsx(Stack, { align: "center", justify: "center", gap: "medium", style: { padding: 'var(--gap-3xl)' }, children: _jsx(Spinner, {}) })) : data.length === 0 ? (_jsxs(Stack, { align: "center", justify: "center", gap: "medium", style: { padding: 'var(--gap-3xl)', textAlign: 'center' }, children: [_jsx(Text, { level: "h3", style: { color: 'var(--muted-foreground)' }, children: tCrud('emptyState.title', {
130
+ }), collapsible: true, defaultOpen: true, children: loading ? (_jsx(Stack, { align: "center", justify: "center", style: { padding: 'var(--gap-3xl)' }, children: _jsx(Spinner, {}) })) : data.length === 0 ? (_jsxs(Stack, { align: "center", justify: "center", style: { padding: 'var(--gap-3xl)', textAlign: 'center' }, children: [_jsx(Text, { level: "h3", style: { color: 'var(--muted-foreground)' }, children: tCrud('emptyState.title', {
125
131
  defaultValue: `No ${entity.name.toLowerCase()} found`,
126
132
  }) }), _jsx(Text, { style: { color: 'var(--muted-foreground)' }, children: tCrud('emptyState.description', {
127
133
  defaultValue: `No ${entity.name.toLowerCase()} available at this time.`,
128
- }) })] })) : (_jsx(Grid, { cols: cols, gap: "medium", children: data.map((item) => {
134
+ }) })] })) : (_jsx(Grid, { cols: cols, children: data.map((item) => {
129
135
  const imageValue = imageField ? item[imageField] : null;
130
136
  // Backend optimizes picture fields for listCard: returns thumbUrl string directly
131
137
  // (or fullUrl if thumbUrl missing, or null if no picture)
@@ -152,7 +158,7 @@ filter, hideFilters = false, }) {
152
158
  })
153
159
  : tCrud('favorites.add', {
154
160
  defaultValue: 'Add to favorites',
155
- }) }), _jsxs(Stack, { direction: "column", gap: "medium", children: [imageUrl && (_jsx("div", { style: {
161
+ }) }), _jsxs(Stack, { direction: "column", children: [imageUrl && (_jsx("div", { style: {
156
162
  width: '100%',
157
163
  aspectRatio: '16/9',
158
164
  borderRadius: 'var(--radius-md)',
@@ -135,7 +135,7 @@ export function EntityDisplayRenderer({ entity, id, t, className = '', backend =
135
135
  }
136
136
  // Error or not found state
137
137
  if (displayError || !displayData) {
138
- return (_jsx(Stack, { align: "center", justify: "center", gap: "medium", style: {
138
+ return (_jsx(Stack, { align: "center", justify: "center", style: {
139
139
  padding: 'var(--gap-3xl)',
140
140
  textAlign: 'center',
141
141
  }, className: className, children: _jsxs(Stack, { direction: "column", gap: "tight", children: [_jsx("h3", { style: { color: 'var(--muted-foreground)' }, children: notFoundMessage ||
@@ -149,7 +149,7 @@ export function EntityDisplayRenderer({ entity, id, t, className = '', backend =
149
149
  : String(displayError) }))] }) }));
150
150
  }
151
151
  // Render all visible fields with values
152
- return (_jsx(Stack, { direction: "column", gap: "medium", className: className, children: visibleFields.map(([fieldName, fieldConfig]) => {
152
+ return (_jsx(Stack, { direction: "column", className: className, children: visibleFields.map(([fieldName, fieldConfig]) => {
153
153
  return (_jsx(DisplayFieldRenderer, { name: fieldName, config: fieldConfig, value: displayData[fieldName], t: translate }, fieldName));
154
154
  }) }));
155
155
  }
@@ -1 +1 @@
1
- {"version":3,"file":"EntityFormRenderer.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityFormRenderer.tsx"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EACV,uBAAuB,EACvB,YAAY,EAEb,MAAM,gBAAgB,CAAC;AAExB,YAAY,EAAE,uBAAuB,EAAE,CAAC;AAExC;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAAE,EACxE,MAAM,EACN,QAAQ,EACR,CAAC,EACD,SAAc,EACd,UAAU,EACV,OAAe,EACf,aAAa,EACb,aAAyB,EACzB,mBAAmB,EACnB,sBAAkC,EAClC,iBAAiB,EACjB,UAAU,EACV,SAA6C,EAC7C,QAAiC,EACjC,MAAM,EAAE,cAAc,EACtB,UAAU,EACV,UAAU,EACV,WAAW,EACX,QAAQ,EACR,oBAA2B,EAC3B,qBAAqB,EACrB,kBAA0B,GAC3B,EAAE,uBAAuB,CAAC,CAAC,CAAC,2CA+a5B;AAED,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"EntityFormRenderer.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityFormRenderer.tsx"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EACV,uBAAuB,EACvB,YAAY,EAEb,MAAM,gBAAgB,CAAC;AAExB,YAAY,EAAE,uBAAuB,EAAE,CAAC;AAExC;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,EAAE,EACxE,MAAM,EACN,QAAQ,EACR,CAAC,EACD,SAAc,EACd,UAAU,EACV,OAAe,EACf,aAAa,EACb,aAAyB,EACzB,mBAAmB,EACnB,sBAAkC,EAClC,iBAAiB,EACjB,UAAU,EACV,SAA6C,EAC7C,QAAiC,EACjC,MAAM,EAAE,cAAc,EACtB,UAAU,EACV,UAAU,EACV,WAAW,EACX,QAAQ,EACR,oBAA2B,EAC3B,qBAAqB,EACrB,kBAA0B,GAC3B,EAAE,uBAAuB,CAAC,CAAC,CAAC,2CAic5B;AAED,eAAe,kBAAkB,CAAC"}
@@ -13,7 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import { useEffect, useId, useMemo, useState } from 'react';
14
14
  import { FormProvider } from 'react-hook-form';
15
15
  import { Badge, Button, DropdownMenu, Grid, Stack, Spinner, } from '@donotdev/components';
16
- import { useTranslation } from '@donotdev/core';
16
+ import { hasRoleAccess, useTranslation } from '@donotdev/core';
17
17
  import { useNavigate } from '../../routing';
18
18
  import { DisplayFieldRenderer, FormFieldRenderer, UploadProvider, useEntityForm, useUnsavedChangesWarning, useConfirmNavigation, useFormStore, } from '@donotdev/crud';
19
19
  /**
@@ -34,12 +34,19 @@ export function EntityFormRenderer({ entity, onSubmit, t, className = '', submit
34
34
  // Generate stable form ID
35
35
  const generatedFormId = useId();
36
36
  const formId = externalFormId ?? `entity-form-${entity.name}-${generatedFormId}`;
37
- // Use entity name as i18n namespace
38
- const { t: translationFn } = useTranslation(entity.namespace);
37
+ // Entity + crud namespaces so translateLabel can resolve crud:status.* etc.
38
+ const { t: translationFn } = useTranslation([entity.namespace, 'crud']);
39
39
  const { t: tCrud } = useTranslation('crud');
40
40
  const translate = t || translationFn;
41
- // Preview role state for View As toggle (only used when visibility info is shown)
42
- const [previewRole, setPreviewRole] = useState('super');
41
+ // Visibility badges (developer-facing) are for admins/super only
42
+ const isAdminOrSuper = viewerRole === 'admin' || viewerRole === 'super';
43
+ const showVisibilityBadges = !hideVisibilityInfo && isAdminOrSuper;
44
+ // Preview dropdown (View As) is available for user/admin/super
45
+ const canPreviewRole = viewerRole === 'user' || viewerRole === 'admin' || viewerRole === 'super';
46
+ // Preview role state for View As toggle
47
+ const [previewRole, setPreviewRole] = useState(viewerRole === 'admin' || viewerRole === 'super' || viewerRole === 'user'
48
+ ? viewerRole
49
+ : 'guest');
43
50
  // Visibility → Badge variant mapping (inline, no separate file needed)
44
51
  const visibilityBadgeVariant = {
45
52
  guest: 'muted',
@@ -47,6 +54,7 @@ export function EntityFormRenderer({ entity, onSubmit, t, className = '', submit
47
54
  admin: 'warning',
48
55
  super: 'destructive',
49
56
  technical: 'accent',
57
+ owner: 'secondary',
50
58
  };
51
59
  // Role hierarchy for filtering (higher index = more access)
52
60
  const roleHierarchy = ['guest', 'user', 'admin', 'super'];
@@ -58,12 +66,14 @@ export function EntityFormRenderer({ entity, onSubmit, t, className = '', submit
58
66
  super: 'Super',
59
67
  technical: 'System',
60
68
  };
61
- // Preview dropdown menu items
62
- const previewMenuItems = useMemo(() => roleHierarchy.map((role) => ({
69
+ // Preview dropdown menu items (roles current viewer can assume)
70
+ const previewMenuItems = useMemo(() => roleHierarchy
71
+ .filter((role) => hasRoleAccess(viewerRole, role))
72
+ .map((role) => ({
63
73
  label: roleLabels[role],
64
74
  checked: previewRole === role,
65
75
  onClick: () => setPreviewRole(role),
66
- })), [previewRole]);
76
+ })), [previewRole, viewerRole]);
67
77
  // useEntityForm handles all orchestration
68
78
  const form = useEntityForm(entity, {
69
79
  formId,
@@ -73,10 +83,11 @@ export function EntityFormRenderer({ entity, onSubmit, t, className = '', submit
73
83
  t: translate,
74
84
  autoSave,
75
85
  });
76
- const { control, handleSubmit, formState: { errors }, fields: rawFields, formStatus, uploadProgress, cleanup, isDirty, resetForm, } = form;
86
+ const { control, handleSubmit, formState: { errors }, fields: rawFields, formStatus, uploadProgress, cleanup, isDirty, hasUserInteracted, resetForm, } = form;
87
+ const isDirtyForBlocking = isDirty && hasUserInteracted;
77
88
  // Filter fields based on previewRole when visibility info is shown
78
89
  const renderableFields = useMemo(() => {
79
- if (hideVisibilityInfo)
90
+ if (!canPreviewRole)
80
91
  return rawFields;
81
92
  const previewRoleIndex = roleHierarchy.indexOf(previewRole);
82
93
  return rawFields.filter(({ config }) => {
@@ -89,13 +100,13 @@ export function EntityFormRenderer({ entity, onSubmit, t, className = '', submit
89
100
  const fieldRoleIndex = roleHierarchy.indexOf(fieldVisibility);
90
101
  return fieldRoleIndex <= previewRoleIndex;
91
102
  });
92
- }, [rawFields, hideVisibilityInfo, previewRole]);
103
+ }, [rawFields, canPreviewRole, previewRole]);
93
104
  // Sync isDirty to FormStore (single source of truth)
94
105
  useEffect(() => {
95
106
  if (formId) {
96
- useFormStore.getState().setIsDirty(formId, isDirty);
107
+ useFormStore.getState().setIsDirty(formId, isDirtyForBlocking);
97
108
  }
98
- }, [formId, isDirty]);
109
+ }, [formId, isDirtyForBlocking]);
99
110
  // Cleanup on unmount
100
111
  useEffect(() => {
101
112
  return cleanup;
@@ -112,15 +123,15 @@ export function EntityFormRenderer({ entity, onSubmit, t, className = '', submit
112
123
  // Warn about unsaved changes when navigating away (browser refresh/close)
113
124
  // SPA navigation blocking is handled by router hooks via FormStore
114
125
  useUnsavedChangesWarning({
115
- isDirty,
126
+ isDirty: isDirtyForBlocking,
116
127
  enabled: warnOnUnsavedChanges,
117
128
  message: unsavedChangesLeaveMessage,
118
129
  });
119
130
  // Use framework helper for cancel confirmation (DRY, SSR-safe)
120
- const confirmNavigation = useConfirmNavigation(isDirty, unsavedChangesDiscardMessage);
131
+ const confirmNavigation = useConfirmNavigation(isDirtyForBlocking, unsavedChangesDiscardMessage);
121
132
  // Handle cancel with confirmation
122
133
  const handleCancel = async () => {
123
- if (isDirty) {
134
+ if (isDirtyForBlocking) {
124
135
  const confirmed = await confirmNavigation();
125
136
  if (!confirmed)
126
137
  return;
@@ -212,8 +223,8 @@ export function EntityFormRenderer({ entity, onSubmit, t, className = '', submit
212
223
  gridColumn: '1 / -1',
213
224
  display: 'contents',
214
225
  }, children: [isLoading && _jsx(Spinner, { overlay: true }), _jsxs("form", { onSubmit: handleSubmit(handleFormSubmit), noValidate: true, className: className, style: { width: '100%', gridColumn: '1 / -1', display: 'contents' }, children: [renderableFields.map(({ name, config, editable }) => {
215
- // Show visibility badge for non-guest fields when visibility info is enabled
216
- const showBadge = !hideVisibilityInfo && config.visibility !== 'guest';
226
+ // Show visibility badge for non-guest fields when visibility badges are enabled (admins/super only)
227
+ const showBadge = showVisibilityBadges && config.visibility !== 'guest';
217
228
  const badgeVariant = config.visibility !== 'hidden'
218
229
  ? visibilityBadgeVariant[config.visibility]
219
230
  : undefined;
@@ -239,8 +250,8 @@ export function EntityFormRenderer({ entity, onSubmit, t, className = '', submit
239
250
  if (showCancelButton && cancelButtonText) {
240
251
  buttons.push(_jsx(Button, { type: "button", onClick: handleCancel, disabled: buttonState.loading, variant: "outline", className: "dndev-w-full", children: cancelButtonText }, "cancel"));
241
252
  }
242
- // Preview dropdown
243
- if (!hideVisibilityInfo) {
253
+ // Preview dropdown (user/admin/super only)
254
+ if (canPreviewRole) {
244
255
  const previewVariant = visibilityBadgeVariant[previewRole] || 'muted';
245
256
  buttons.push(_jsx(DropdownMenu, { trigger: _jsxs(Button, { type: "button", variant: previewVariant, disabled: buttonState.loading, className: "dndev-w-full", children: [tCrud('visibility.preview', {
246
257
  defaultValue: 'Preview',
@@ -10,5 +10,5 @@ export type { EntityListProps };
10
10
  * - Edit and Delete actions (admin only)
11
11
  * - Auto-routing when handlers not provided
12
12
  */
13
- export declare function EntityList({ entity, userRole, basePath, onClick, hideFilters, pagination, pageSize: pageSizeProp, queryOptions, }: EntityListProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function EntityList({ entity, userRole, basePath, onClick, hideFilters, pagination, pageSize: pageSizeProp, queryOptions, exportable, }: EntityListProps): import("react/jsx-runtime").JSX.Element;
14
14
  //# sourceMappingURL=EntityList.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"EntityList.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityList.tsx"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,QAAkB,EAClB,QAAQ,EACR,OAAO,EACP,WAAmB,EACnB,UAAqB,EACrB,QAAQ,EAAE,YAAY,EACtB,YAAY,GACb,EAAE,eAAe,2CAsTjB"}
1
+ {"version":3,"file":"EntityList.d.ts","sourceRoot":"","sources":["../../../src/crud/components/EntityList.tsx"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,QAAkB,EAClB,QAAQ,EACR,OAAO,EACP,WAAmB,EACnB,UAAqB,EACrB,QAAQ,EAAE,YAAY,EACtB,YAAY,EACZ,UAAiB,GAClB,EAAE,eAAe,2CAkTjB"}
@@ -18,7 +18,7 @@ import { useMemo, useCallback, useState } from 'react';
18
18
  import { DataTable, Button, Stack, ActionButton, Section, Input, } from '@donotdev/components';
19
19
  import { useTranslation } from '@donotdev/core';
20
20
  import { useNavigate } from '../../routing';
21
- import { translateFieldLabel, useCrud, useCrudList, EntityFilters, matchesFilter, formatValue, } from '@donotdev/crud';
21
+ import { translateFieldLabel, useCrud, useCrudList, EntityFilters, matchesFilter, formatValue, useCrudFilters, } from '@donotdev/crud';
22
22
  /**
23
23
  * Entity List Component - Table view for admin/internal operations
24
24
  *
@@ -29,7 +29,7 @@ import { translateFieldLabel, useCrud, useCrudList, EntityFilters, matchesFilter
29
29
  * - Edit and Delete actions (admin only)
30
30
  * - Auto-routing when handlers not provided
31
31
  */
32
- export function EntityList({ entity, userRole = 'guest', basePath, onClick, hideFilters = false, pagination = 'client', pageSize: pageSizeProp, queryOptions, }) {
32
+ export function EntityList({ entity, userRole = 'guest', basePath, onClick, hideFilters = false, pagination = 'client', pageSize: pageSizeProp, queryOptions, exportable = true, }) {
33
33
  const navigate = useNavigate();
34
34
  const base = basePath ?? `/${entity.collection}`;
35
35
  // Server-side pagination state (only used when pagination='server')
@@ -54,8 +54,10 @@ export function EntityList({ entity, userRole = 'guest', basePath, onClick, hide
54
54
  const data = listData?.items || [];
55
55
  // Entity + crud namespaces so formatValue can resolve crud:price.* etc.
56
56
  const { t } = useTranslation([entity.namespace, 'crud']);
57
- // Filter state - supports string, number range {min, max}, date range {min, max}
58
- const [filters, setFilters] = useState({});
57
+ // Get filters for applying to data (EntityFilters manages its own state)
58
+ const { filters } = useCrudFilters({
59
+ collection: entity.collection,
60
+ });
59
61
  const [searchQuery, setSearchQuery] = useState('');
60
62
  // Refresh handler - triggers manual refetch in useList
61
63
  const handleRefresh = useCallback(async () => {
@@ -82,10 +84,6 @@ export function EntityList({ entity, userRole = 'guest', basePath, onClick, hide
82
84
  const handleDelete = useCallback(async (itemId) => {
83
85
  await deleteItem(itemId);
84
86
  }, [deleteItem]);
85
- // Update filters (for EntityFilters component)
86
- const handleFiltersChange = useCallback((newFilters) => {
87
- setFilters(newFilters);
88
- }, []);
89
87
  // Apply search and filters to data
90
88
  // @todo Server-side filtering: Currently only handles client-side filtering.
91
89
  // When pagination='server', filters should be sent to the server via useCrudList options.
@@ -170,9 +168,9 @@ export function EntityList({ entity, userRole = 'guest', basePath, onClick, hide
170
168
  return (_jsxs(_Fragment, { children: [_jsx(Section, { title: tCrud('filters.title', {
171
169
  entity: entityName,
172
170
  defaultValue: `Browse ${entityName} - Filters`,
173
- }), collapsible: true, defaultOpen: true, children: _jsxs(Stack, { gap: "medium", children: [_jsxs(Stack, { direction: "row", gap: "tight", align: "center", className: "dndev-w-full", style: { display: 'grid', gridTemplateColumns: '1fr auto auto' }, children: [_jsx(Input, { placeholder: tCrud('search.placeholder', {
171
+ }), collapsible: true, defaultOpen: true, children: _jsxs(Stack, { children: [_jsxs(Stack, { direction: "row", gap: "tight", align: "center", className: "dndev-w-full", style: { display: 'grid', gridTemplateColumns: '1fr auto auto' }, children: [_jsx(Input, { placeholder: tCrud('search.placeholder', {
174
172
  defaultValue: 'Search...',
175
- }), value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), icon: Search, className: "dndev-w-full" }), _jsx(Button, { icon: RefreshCw, variant: "outline", onClick: handleRefresh, disabled: loading, display: "compact", "aria-label": tCrud('refresh', { defaultValue: 'Refresh' }) }), _jsx(Button, { icon: Plus, onClick: handleCreate, display: "compact", children: tCrud('addNew', { defaultValue: 'Add New' }) })] }), !hideFilters && (_jsx(EntityFilters, { entity: entity, data: data, filters: filters, onFiltersChange: handleFiltersChange, fieldsToFilter: entity.listFields }))] }) }), _jsx(Section, { title: loading
173
+ }), value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), icon: Search, className: "dndev-w-full" }), _jsx(Button, { icon: RefreshCw, variant: "outline", onClick: handleRefresh, disabled: loading, display: "compact", "aria-label": tCrud('refresh', { defaultValue: 'Refresh' }) }), _jsx(Button, { icon: Plus, onClick: handleCreate, display: "compact", children: tCrud('addNew', { defaultValue: 'Add New' }) })] }), !hideFilters && (_jsx(EntityFilters, { entity: entity, data: data, fieldsToFilter: entity.listFields }))] }) }), _jsx(Section, { title: loading
176
174
  ? tCrud('results.title.fetching', {
177
175
  defaultValue: 'Fetching...',
178
176
  })
@@ -197,5 +195,7 @@ export function EntityList({ entity, userRole = 'guest', basePath, onClick, hide
197
195
  total: listData?.total,
198
196
  onPageChange: setCurrentPage,
199
197
  onPageSizeChange: setServerPageSize,
200
- }) }) })] }));
198
+ }),
199
+ // Export functionality
200
+ exportable: exportable, exportFilename: `${entity.collection}-export`, exportLabel: tCrud('export', { defaultValue: 'Export' }) }) })] }));
201
201
  }
@@ -64,7 +64,7 @@ const FormItemContext = createContext({});
64
64
  */
65
65
  const FormItem = ({ className, ...props }) => {
66
66
  const id = useId();
67
- return (_jsx(FormItemContext.Provider, { value: { id }, children: _jsx(Stack, { gap: "medium", className: className, ...props }) }));
67
+ return (_jsx(FormItemContext.Provider, { value: { id }, children: _jsx(Stack, { className: className, ...props }) }));
68
68
  };
69
69
  /**
70
70
  * Hook for accessing form field context