@pallasoft/ps-core-components 2.35.1 → 2.35.2

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 (60) hide show
  1. package/README.md +13 -0
  2. package/dist/components/ps-action-tile.js +1 -1
  3. package/dist/components/ps-badge.js +1 -1
  4. package/dist/components/ps-card-block-skeleton.js +1 -1
  5. package/dist/components/ps-card.js +1 -1
  6. package/dist/components/ps-form-fields-skeleton.js +1 -1
  7. package/dist/components/ps-intro-skeleton.js +1 -1
  8. package/dist/components/ps-page-skeleton.js +1 -1
  9. package/dist/components/ps-pill.js +1 -1
  10. package/dist/components/ps-section-header.js +1 -1
  11. package/dist/components/ps-skeleton-media.js +1 -1
  12. package/dist/components/ps-skeleton-table.js +2 -2
  13. package/dist/components/ps-tab-row-skeleton.js +3 -1
  14. package/dist/constants/ps-optimistic-config.d.ts +1 -0
  15. package/dist/constants/ps-optimistic-config.js +1 -0
  16. package/dist/constants/ps-skeleton-config.d.ts +4 -0
  17. package/dist/constants/ps-skeleton-config.js +22 -4
  18. package/dist/constants/ps-skeleton-shape.d.ts +5 -0
  19. package/dist/constants/ps-skeleton-shape.js +5 -0
  20. package/dist/esm/components/ps-action-tile.js +1 -1
  21. package/dist/esm/components/ps-badge.js +1 -1
  22. package/dist/esm/components/ps-card-block-skeleton.js +1 -1
  23. package/dist/esm/components/ps-card.js +1 -1
  24. package/dist/esm/components/ps-form-fields-skeleton.js +1 -1
  25. package/dist/esm/components/ps-intro-skeleton.js +1 -1
  26. package/dist/esm/components/ps-page-skeleton.js +1 -1
  27. package/dist/esm/components/ps-pill.js +1 -1
  28. package/dist/esm/components/ps-section-header.js +1 -1
  29. package/dist/esm/components/ps-skeleton-media.js +1 -1
  30. package/dist/esm/components/ps-skeleton-table.js +2 -2
  31. package/dist/esm/components/ps-tab-row-skeleton.js +3 -1
  32. package/dist/esm/constants/ps-optimistic-config.js +1 -0
  33. package/dist/esm/constants/ps-skeleton-config.js +22 -4
  34. package/dist/esm/constants/ps-skeleton-shape.js +5 -0
  35. package/dist/esm/hooks/use-ps-change-queue.js +7 -2
  36. package/dist/esm/hooks/use-ps-optimistic-fetcher.js +48 -10
  37. package/dist/esm/hooks/use-ps-optimistic-save.js +2 -1
  38. package/dist/esm/index.js +2 -0
  39. package/dist/esm/interfaces/ps-optimistic-failure-reason.js +1 -0
  40. package/dist/esm/interfaces/ps-optimistic-failure.js +1 -0
  41. package/dist/esm/interfaces/ps-optimistic-outcome.js +1 -0
  42. package/dist/esm/services/ps-optimistic-fetcher-service.js +12 -0
  43. package/dist/hooks/use-ps-change-queue.js +7 -2
  44. package/dist/hooks/use-ps-optimistic-fetcher.d.ts +1 -1
  45. package/dist/hooks/use-ps-optimistic-fetcher.js +48 -10
  46. package/dist/hooks/use-ps-optimistic-save.d.ts +1 -1
  47. package/dist/hooks/use-ps-optimistic-save.js +2 -1
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.js +2 -0
  50. package/dist/interfaces/ps-optimistic-failure-reason.d.ts +1 -0
  51. package/dist/interfaces/ps-optimistic-failure-reason.js +2 -0
  52. package/dist/interfaces/ps-optimistic-failure.d.ts +6 -0
  53. package/dist/interfaces/ps-optimistic-failure.js +2 -0
  54. package/dist/interfaces/ps-optimistic-fetcher-input.d.ts +2 -0
  55. package/dist/interfaces/ps-optimistic-outcome.d.ts +5 -0
  56. package/dist/interfaces/ps-optimistic-outcome.js +2 -0
  57. package/dist/interfaces/ps-optimistic-save-input.d.ts +2 -0
  58. package/dist/services/ps-optimistic-fetcher-service.d.ts +6 -0
  59. package/dist/services/ps-optimistic-fetcher-service.js +16 -0
  60. package/package.json +1 -1
package/README.md CHANGED
@@ -8,3 +8,16 @@ Shared Pallasoft core components package.
8
8
  - `npm run lint` runs ESLint using the shared package baseline.
9
9
  - `npm run format:check` checks Prettier formatting.
10
10
  - `npm run release` publishes through semantic-release.
11
+
12
+ ## Optimistic saves
13
+
14
+ Apps use `usePsOptimisticSave` for row lists and `usePsOptimisticFetcher` for custom submissions
15
+ (batches go through `merge`). `usePsChangeQueue` is internal plumbing; do not use it in apps.
16
+
17
+ A change shows at once, is sent in order, and either confirms or rolls back. A change counts as
18
+ failed when the server refuses it (`isSuccess` is false), when no new response arrives (network
19
+ failure or a thrown action), when the request takes longer than
20
+ `PsOptimisticConfig.submitTimeoutMs`, or when the component unmounts mid-request. Rows then roll
21
+ back, including changes queued behind the failed one. Pass the optional `onError` to tell the user:
22
+ `onError({ changes, data, reason })`, where `reason` is `rejected`, `no-response`, `timeout`,
23
+ `unmounted` or `error`.
@@ -9,5 +9,5 @@ const variantClassNames = {
9
9
  default: 'ps-action-tile',
10
10
  primary: 'ps-action-tile ps-action-tile-primary',
11
11
  };
12
- const PsActionTile = ({ className, description, icon, isLoading = false, label, type = 'button', variant = 'default', ...props }) => isLoading ? ((0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: ps_design_system_1.PallasoftClassNameService.merge('h-16 w-full rounded-ps-card', className), shape: ps_skeleton_shape_1.PsSkeletonShape.Block })) : ((0, jsx_runtime_1.jsxs)("button", { className: ps_design_system_1.PallasoftClassNameService.merge('ps-focus-ring w-full', variantClassNames[variant], className), type: type, ...props, children: [(0, jsx_runtime_1.jsxs)("span", { className: 'min-w-0', children: [(0, jsx_runtime_1.jsx)("span", { className: 'block break-words', children: label }), description ? ((0, jsx_runtime_1.jsx)("span", { className: ps_design_system_1.PallasoftClassNameService.merge('mt-0.5 block text-xs font-medium leading-5', variant === 'primary' ? 'text-white/75' : 'text-ink/50'), children: description })) : null] }), icon ? ((0, jsx_runtime_1.jsx)("span", { className: ps_design_system_1.PallasoftClassNameService.merge('flex h-9 w-9 shrink-0 items-center justify-center rounded-lg', variant === 'primary' ? 'bg-white/15 text-white' : 'ps-icon-surface'), children: icon })) : null] }));
12
+ const PsActionTile = ({ className, description, icon, isLoading = false, label, type = 'button', variant = 'default', ...props }) => isLoading ? ((0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: ps_design_system_1.PallasoftClassNameService.merge('h-16', className), shape: ps_skeleton_shape_1.PsSkeletonShape.Card })) : ((0, jsx_runtime_1.jsxs)("button", { className: ps_design_system_1.PallasoftClassNameService.merge('ps-focus-ring w-full', variantClassNames[variant], className), type: type, ...props, children: [(0, jsx_runtime_1.jsxs)("span", { className: 'min-w-0', children: [(0, jsx_runtime_1.jsx)("span", { className: 'block break-words', children: label }), description ? ((0, jsx_runtime_1.jsx)("span", { className: ps_design_system_1.PallasoftClassNameService.merge('mt-0.5 block text-xs font-medium leading-5', variant === 'primary' ? 'text-white/75' : 'text-ink/50'), children: description })) : null] }), icon ? ((0, jsx_runtime_1.jsx)("span", { className: ps_design_system_1.PallasoftClassNameService.merge('flex h-9 w-9 shrink-0 items-center justify-center rounded-lg', variant === 'primary' ? 'bg-white/15 text-white' : 'ps-icon-surface'), children: icon })) : null] }));
13
13
  exports.PsActionTile = PsActionTile;
@@ -12,5 +12,5 @@ const toneClassNames = {
12
12
  success: 'bg-success-soft text-emerald-700 dark:bg-emerald-400/18 dark:text-emerald-50',
13
13
  warning: 'bg-warning-soft text-amber-800 dark:bg-amber-400/18 dark:text-amber-50',
14
14
  };
15
- const PsBadge = ({ children, className, isLoading = false, tone = 'neutral', }) => isLoading ? ((0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: ps_design_system_1.PallasoftClassNameService.merge('h-7 w-20', className), shape: ps_skeleton_shape_1.PsSkeletonShape.Block })) : ((0, jsx_runtime_1.jsx)("span", { className: ps_design_system_1.PallasoftClassNameService.merge('inline-flex min-h-7 w-fit items-center justify-center whitespace-nowrap rounded-ps-control px-3 py-0.5 text-center text-xs font-semibold capitalize', toneClassNames[tone], className), children: children }));
15
+ const PsBadge = ({ children, className, isLoading = false, tone = 'neutral', }) => isLoading ? ((0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: className, shape: ps_skeleton_shape_1.PsSkeletonShape.Badge })) : ((0, jsx_runtime_1.jsx)("span", { className: ps_design_system_1.PallasoftClassNameService.merge('inline-flex min-h-7 w-fit items-center justify-center whitespace-nowrap rounded-ps-control px-3 py-0.5 text-center text-xs font-semibold capitalize', toneClassNames[tone], className), children: children }));
16
16
  exports.PsBadge = PsBadge;
@@ -4,5 +4,5 @@ exports.PsCardBlockSkeleton = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const ps_skeleton_shape_1 = require("../constants/ps-skeleton-shape");
6
6
  const ps_skeleton_1 = require("./ps-skeleton");
7
- const PsCardBlockSkeleton = ({ height }) => ((0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { height: height, radius: '1.25rem', shape: ps_skeleton_shape_1.PsSkeletonShape.Block, width: '100%' }));
7
+ const PsCardBlockSkeleton = ({ height }) => ((0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { height: height, shape: ps_skeleton_shape_1.PsSkeletonShape.Card }));
8
8
  exports.PsCardBlockSkeleton = PsCardBlockSkeleton;
@@ -17,5 +17,5 @@ const variantClassNames = {
17
17
  glass: 'ps-glass',
18
18
  muted: 'ps-card-muted',
19
19
  };
20
- const PsCard = ({ children, className, density = 'normal', eyebrow, isLoading = false, title, trailing, variant = 'default', }) => isLoading ? ((0, jsx_runtime_1.jsxs)(ps_skeleton_group_1.PsSkeletonGroup, { className: ps_design_system_1.PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3 rounded-ps-card', variantClassNames[variant], densityClassNames[density], className), label: title === undefined ? 'Loading' : title, children: [(0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'h-3 w-32' }), (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'h-20 w-full', shape: ps_skeleton_shape_1.PsSkeletonShape.Block })] })) : ((0, jsx_runtime_1.jsxs)("section", { className: ps_design_system_1.PallasoftClassNameService.merge('min-w-0 overflow-hidden rounded-ps-card', variantClassNames[variant], densityClassNames[density], className), children: [title ? ((0, jsx_runtime_1.jsxs)("div", { className: 'mb-3 flex min-w-0 items-start justify-between gap-3', children: [(0, jsx_runtime_1.jsxs)("div", { className: 'min-w-0', children: [eyebrow ? (0, jsx_runtime_1.jsx)("p", { className: 'ps-eyebrow break-words', children: eyebrow }) : null, (0, jsx_runtime_1.jsx)("h2", { className: 'mt-1 break-words text-base font-bold text-ink', children: title })] }), trailing ? (0, jsx_runtime_1.jsx)("div", { className: 'shrink-0', children: trailing }) : null] })) : null, children] }));
20
+ const PsCard = ({ children, className, density = 'normal', eyebrow, isLoading = false, title, trailing, variant = 'default', }) => isLoading ? ((0, jsx_runtime_1.jsxs)(ps_skeleton_group_1.PsSkeletonGroup, { className: ps_design_system_1.PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3 rounded-ps-card', variantClassNames[variant], densityClassNames[density], className), label: title === undefined ? 'Loading' : title, children: [(0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'h-3 w-32' }), (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { shape: ps_skeleton_shape_1.PsSkeletonShape.Control, className: 'h-20' })] })) : ((0, jsx_runtime_1.jsxs)("section", { className: ps_design_system_1.PallasoftClassNameService.merge('min-w-0 overflow-hidden rounded-ps-card', variantClassNames[variant], densityClassNames[density], className), children: [title ? ((0, jsx_runtime_1.jsxs)("div", { className: 'mb-3 flex min-w-0 items-start justify-between gap-3', children: [(0, jsx_runtime_1.jsxs)("div", { className: 'min-w-0', children: [eyebrow ? (0, jsx_runtime_1.jsx)("p", { className: 'ps-eyebrow break-words', children: eyebrow }) : null, (0, jsx_runtime_1.jsx)("h2", { className: 'mt-1 break-words text-base font-bold text-ink', children: title })] }), trailing ? (0, jsx_runtime_1.jsx)("div", { className: 'shrink-0', children: trailing }) : null] })) : null, children] }));
21
21
  exports.PsCard = PsCard;
@@ -6,5 +6,5 @@ const ps_design_system_1 = require("@pallasoft/ps-design-system");
6
6
  const ps_page_skeleton_config_1 = require("../constants/ps-page-skeleton-config");
7
7
  const ps_skeleton_shape_1 = require("../constants/ps-skeleton-shape");
8
8
  const ps_skeleton_1 = require("./ps-skeleton");
9
- const PsFormFieldsSkeleton = ({ columnsClassName = ps_page_skeleton_config_1.PsPageSkeletonConfig.formColumnsClassName, fields = ps_page_skeleton_config_1.PsPageSkeletonConfig.formFields, }) => ((0, jsx_runtime_1.jsx)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('grid gap-4', columnsClassName), children: fields.map((field) => ((0, jsx_runtime_1.jsxs)("div", { className: 'flex flex-col gap-2', children: [(0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: ps_page_skeleton_config_1.PsPageSkeletonConfig.formLabelClassName }), (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'h-ps-control w-full', shape: ps_skeleton_shape_1.PsSkeletonShape.Block })] }, field))) }));
9
+ const PsFormFieldsSkeleton = ({ columnsClassName = ps_page_skeleton_config_1.PsPageSkeletonConfig.formColumnsClassName, fields = ps_page_skeleton_config_1.PsPageSkeletonConfig.formFields, }) => ((0, jsx_runtime_1.jsx)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('grid gap-4', columnsClassName), children: fields.map((field) => ((0, jsx_runtime_1.jsxs)("div", { className: 'flex flex-col gap-2', children: [(0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: ps_page_skeleton_config_1.PsPageSkeletonConfig.formLabelClassName }), (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { shape: ps_skeleton_shape_1.PsSkeletonShape.Control })] }, field))) }));
10
10
  exports.PsFormFieldsSkeleton = PsFormFieldsSkeleton;
@@ -3,5 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsIntroSkeleton = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const ps_skeleton_1 = require("./ps-skeleton");
6
- const PsIntroSkeleton = () => ((0, jsx_runtime_1.jsxs)("div", { className: 'mx-auto flex w-full max-w-3xl flex-col items-center gap-4', children: [(0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { height: '0.875rem', radius: '999px', width: '6rem' }), (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { height: '2.5rem', radius: '0.75rem', width: '70%' }), (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { height: '1.125rem', radius: '0.5rem', width: '85%' })] }));
6
+ const PsIntroSkeleton = () => ((0, jsx_runtime_1.jsxs)("div", { className: 'mx-auto flex w-full max-w-3xl flex-col items-center gap-4', children: [(0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'h-3.5 w-24' }), (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'h-10 w-[70%]' }), (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'h-[1.125rem] w-[85%]' })] }));
7
7
  exports.PsIntroSkeleton = PsIntroSkeleton;
@@ -8,5 +8,5 @@ const ps_page_layout_1 = require("./ps-page-layout");
8
8
  const ps_page_stack_1 = require("./ps-page-stack");
9
9
  const ps_section_header_1 = require("./ps-section-header");
10
10
  const ps_skeleton_1 = require("./ps-skeleton");
11
- const PsPageSkeleton = ({ children, hasActions = false, isNarrow = false, loadingLabel, title, }) => ((0, jsx_runtime_1.jsx)(ps_page_layout_1.PsPageLayout, { isNarrow: isNarrow, children: (0, jsx_runtime_1.jsxs)(ps_page_stack_1.PsPageStack, { children: [title === undefined ? ((0, jsx_runtime_1.jsx)(ps_section_header_1.PsSectionHeader, { isLoading: true, title: loadingLabel, trailing: hasActions })) : ((0, jsx_runtime_1.jsx)(ps_page_header_1.PsPageHeader, { actions: hasActions ? ((0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { height: '2.5rem', radius: '9999px', shape: ps_skeleton_shape_1.PsSkeletonShape.Block, width: '9rem' })) : undefined, title: title })), children] }) }));
11
+ const PsPageSkeleton = ({ children, hasActions = false, isNarrow = false, loadingLabel, title, }) => ((0, jsx_runtime_1.jsx)(ps_page_layout_1.PsPageLayout, { isNarrow: isNarrow, children: (0, jsx_runtime_1.jsxs)(ps_page_stack_1.PsPageStack, { children: [title === undefined ? ((0, jsx_runtime_1.jsx)(ps_section_header_1.PsSectionHeader, { isLoading: true, title: loadingLabel, trailing: hasActions })) : ((0, jsx_runtime_1.jsx)(ps_page_header_1.PsPageHeader, { actions: hasActions ? (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { shape: ps_skeleton_shape_1.PsSkeletonShape.Button }) : undefined, title: title })), children] }) }));
12
12
  exports.PsPageSkeleton = PsPageSkeleton;
@@ -12,5 +12,5 @@ const toneClassNames = {
12
12
  success: 'bg-emerald-50 text-emerald-700 ring-emerald-100 dark:bg-emerald-400/18 dark:text-emerald-50 dark:ring-emerald-200/22',
13
13
  warning: 'bg-amber-50 text-amber-800 ring-amber-100 dark:bg-amber-400/18 dark:text-amber-50 dark:ring-amber-200/24',
14
14
  };
15
- const PsPill = ({ children, className, isLoading = false, tone = 'neutral', }) => isLoading ? ((0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: ps_design_system_1.PallasoftClassNameService.merge('h-8 w-24', className), shape: ps_skeleton_shape_1.PsSkeletonShape.Block })) : ((0, jsx_runtime_1.jsx)("span", { className: ps_design_system_1.PallasoftClassNameService.merge('inline-flex min-h-8 min-w-[5.5rem] max-w-full items-center justify-center rounded-ps-control px-5 py-1 text-center text-[0.72rem] font-black capitalize tracking-[0.01em] ring-1 ring-inset', toneClassNames[tone], className), children: (0, jsx_runtime_1.jsx)("span", { className: 'min-w-0 break-words', children: children }) }));
15
+ const PsPill = ({ children, className, isLoading = false, tone = 'neutral', }) => isLoading ? ((0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: className, shape: ps_skeleton_shape_1.PsSkeletonShape.Pill })) : ((0, jsx_runtime_1.jsx)("span", { className: ps_design_system_1.PallasoftClassNameService.merge('inline-flex min-h-8 min-w-[5.5rem] max-w-full items-center justify-center rounded-ps-control px-5 py-1 text-center text-[0.72rem] font-black capitalize tracking-[0.01em] ring-1 ring-inset', toneClassNames[tone], className), children: (0, jsx_runtime_1.jsx)("span", { className: 'min-w-0 break-words', children: children }) }));
16
16
  exports.PsPill = PsPill;
@@ -5,5 +5,5 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const ps_skeleton_shape_1 = require("../constants/ps-skeleton-shape");
6
6
  const ps_skeleton_1 = require("./ps-skeleton");
7
7
  const ps_skeleton_group_1 = require("./ps-skeleton-group");
8
- const PsSectionHeader = ({ description, eyebrow, isLoading = false, leading, title, trailing, }) => isLoading ? ((0, jsx_runtime_1.jsxs)(ps_skeleton_group_1.PsSkeletonGroup, { className: 'flex flex-wrap items-end justify-between gap-x-4 gap-y-3', label: title, children: [(0, jsx_runtime_1.jsxs)("div", { className: 'flex min-w-0 flex-1 basis-auto flex-col gap-2', children: [eyebrow ? (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'h-2.5 w-24' }) : null, (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'h-6 w-52' }), description ? (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'h-3 w-72' }) : null] }), trailing ? (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'h-ps-control w-32', shape: ps_skeleton_shape_1.PsSkeletonShape.Block }) : null] })) : ((0, jsx_runtime_1.jsxs)("div", { className: 'flex flex-wrap items-end justify-between gap-x-4 gap-y-3', children: [(0, jsx_runtime_1.jsxs)("div", { className: 'flex min-w-0 flex-1 basis-auto flex-col gap-2', children: [leading, eyebrow ? ((0, jsx_runtime_1.jsx)("p", { className: 'text-[0.68rem] font-semibold uppercase tracking-[0.22em] text-ink/50', children: eyebrow })) : null, (0, jsx_runtime_1.jsx)("h1", { className: 'break-words text-xl font-extrabold text-ink sm:text-2xl', children: title }), description ? ((0, jsx_runtime_1.jsx)("p", { className: 'max-w-3xl text-sm leading-6 text-ink/60', children: description })) : null] }), trailing ? ((0, jsx_runtime_1.jsx)("div", { className: 'flex w-full shrink-0 items-center justify-end sm:ml-auto sm:w-auto', children: trailing })) : null] }));
8
+ const PsSectionHeader = ({ description, eyebrow, isLoading = false, leading, title, trailing, }) => isLoading ? ((0, jsx_runtime_1.jsxs)(ps_skeleton_group_1.PsSkeletonGroup, { className: 'flex flex-wrap items-end justify-between gap-x-4 gap-y-3', label: title, children: [(0, jsx_runtime_1.jsxs)("div", { className: 'flex min-w-0 flex-1 basis-auto flex-col gap-2', children: [eyebrow ? (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'h-2.5 w-24' }) : null, (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'h-6 w-52' }), description ? (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'h-3 w-72' }) : null] }), trailing ? (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { shape: ps_skeleton_shape_1.PsSkeletonShape.Button }) : null] })) : ((0, jsx_runtime_1.jsxs)("div", { className: 'flex flex-wrap items-end justify-between gap-x-4 gap-y-3', children: [(0, jsx_runtime_1.jsxs)("div", { className: 'flex min-w-0 flex-1 basis-auto flex-col gap-2', children: [leading, eyebrow ? ((0, jsx_runtime_1.jsx)("p", { className: 'text-[0.68rem] font-semibold uppercase tracking-[0.22em] text-ink/50', children: eyebrow })) : null, (0, jsx_runtime_1.jsx)("h1", { className: 'break-words text-xl font-extrabold text-ink sm:text-2xl', children: title }), description ? ((0, jsx_runtime_1.jsx)("p", { className: 'max-w-3xl text-sm leading-6 text-ink/60', children: description })) : null] }), trailing ? ((0, jsx_runtime_1.jsx)("div", { className: 'flex w-full shrink-0 items-center justify-end sm:ml-auto sm:w-auto', children: trailing })) : null] }));
9
9
  exports.PsSectionHeader = PsSectionHeader;
@@ -6,5 +6,5 @@ const ps_design_system_1 = require("@pallasoft/ps-design-system");
6
6
  const ps_skeleton_shape_1 = require("../constants/ps-skeleton-shape");
7
7
  const ps_skeleton_1 = require("./ps-skeleton");
8
8
  const ps_skeleton_group_1 = require("./ps-skeleton-group");
9
- const PsSkeletonMedia = ({ aspect = '16 / 9', className, hasCaption = true, label, }) => ((0, jsx_runtime_1.jsxs)(ps_skeleton_group_1.PsSkeletonGroup, { className: ps_design_system_1.PallasoftClassNameService.merge('w-full', className), label: label, children: [(0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { aspect: aspect, radius: 'var(--ps-radius-card, 1rem)', shape: ps_skeleton_shape_1.PsSkeletonShape.Block }), hasCaption ? (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { lines: 2, shape: ps_skeleton_shape_1.PsSkeletonShape.Text }) : null] }));
9
+ const PsSkeletonMedia = ({ aspect = '16 / 9', className, hasCaption = true, label, }) => ((0, jsx_runtime_1.jsxs)(ps_skeleton_group_1.PsSkeletonGroup, { className: ps_design_system_1.PallasoftClassNameService.merge('w-full', className), label: label, children: [(0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { aspect: aspect, shape: ps_skeleton_shape_1.PsSkeletonShape.Card }), hasCaption ? (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { lines: 2, shape: ps_skeleton_shape_1.PsSkeletonShape.Text }) : null] }));
10
10
  exports.PsSkeletonMedia = PsSkeletonMedia;
@@ -15,8 +15,8 @@ const PsSkeletonTable = ({ alignments, className, columns = 4, hasFooter = false
15
15
  const cell = (rowIndex, columnIndex, isStrong) => ((0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: ps_design_system_1.PallasoftClassNameService.merge(isRight(columnIndex)
16
16
  ? 'ml-auto w-16'
17
17
  : ps_skeleton_config_1.PsSkeletonConfig.lineWidths[(rowIndex + columnIndex) % ps_skeleton_config_1.PsSkeletonConfig.lineWidths.length], isStrong ? 'h-3.5' : '') }, columnIndex));
18
- return ((0, jsx_runtime_1.jsxs)(ps_skeleton_group_1.PsSkeletonGroup, { className: ps_design_system_1.PallasoftClassNameService.merge('flex flex-col gap-3', className), children: [hasToolbar ? ((0, jsx_runtime_1.jsxs)("div", { className: 'flex items-center gap-3', children: [(0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'max-w-sm', shape: ps_skeleton_shape_1.PsSkeletonShape.Block }), (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'ml-auto w-24', shape: ps_skeleton_shape_1.PsSkeletonShape.Block })] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: isFramed
19
- ? 'overflow-hidden rounded-[1.25rem] border border-line bg-surface'
18
+ return ((0, jsx_runtime_1.jsxs)(ps_skeleton_group_1.PsSkeletonGroup, { className: ps_design_system_1.PallasoftClassNameService.merge('flex flex-col gap-3', className), children: [hasToolbar ? ((0, jsx_runtime_1.jsxs)("div", { className: 'flex items-center gap-3', children: [(0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'max-w-sm', shape: ps_skeleton_shape_1.PsSkeletonShape.Control }), (0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: 'ml-auto w-24', shape: ps_skeleton_shape_1.PsSkeletonShape.Button })] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: isFramed
19
+ ? ps_design_system_1.PallasoftClassNameService.merge('overflow-hidden border border-line bg-surface', ps_skeleton_config_1.PsSkeletonConfig.tableFrameClassName)
20
20
  : 'overflow-hidden', children: [(0, jsx_runtime_1.jsx)("div", { className: 'grid gap-4 bg-mist/70 px-5 py-4', style: { gridTemplateColumns }, children: Array.from({ length: columns }, (_, index) => ((0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: isRight(index) ? 'ml-auto h-2.5 w-12' : 'h-2.5 w-16' }, index))) }), Array.from({ length: rows }, (_, rowIndex) => ((0, jsx_runtime_1.jsx)("div", { className: 'grid items-center gap-4 border-t border-line px-5 py-4', style: { gridTemplateColumns }, children: Array.from({ length: columns }, (__, columnIndex) => cell(rowIndex, columnIndex, false)) }, rowIndex))), hasFooter ? ((0, jsx_runtime_1.jsx)("div", { className: 'grid items-center gap-4 border-t-2 border-line px-5 py-4', style: { gridTemplateColumns }, children: Array.from({ length: columns }, (__, columnIndex) => cell(rows, columnIndex, true)) })) : null] })] }));
21
21
  };
22
22
  exports.PsSkeletonTable = PsSkeletonTable;
@@ -2,7 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsTabRowSkeleton = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const ps_skeleton_config_1 = require("../constants/ps-skeleton-config");
5
6
  const ps_skeleton_shape_1 = require("../constants/ps-skeleton-shape");
6
7
  const ps_skeleton_1 = require("./ps-skeleton");
7
- const PsTabRowSkeleton = () => ((0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { height: '2.75rem', radius: '9999px', shape: ps_skeleton_shape_1.PsSkeletonShape.Block, width: '100%' }));
8
+ const ps_skeleton_group_1 = require("./ps-skeleton-group");
9
+ const PsTabRowSkeleton = () => ((0, jsx_runtime_1.jsx)(ps_skeleton_group_1.PsSkeletonGroup, { direction: 'row', gap: 'gap-2', children: Array.from({ length: ps_skeleton_config_1.PsSkeletonConfig.tabCount }, (_, index) => ((0, jsx_runtime_1.jsx)(ps_skeleton_1.PsSkeleton, { className: `${ps_skeleton_config_1.PsSkeletonConfig.compactControlHeightClassName} ${ps_skeleton_config_1.PsSkeletonConfig.tabWidthClassName}`, shape: ps_skeleton_shape_1.PsSkeletonShape.Control }, index))) }));
8
10
  exports.PsTabRowSkeleton = PsTabRowSkeleton;
@@ -1,3 +1,4 @@
1
1
  export declare const PsOptimisticConfig: {
2
2
  readonly pendingPrefix: "pending-";
3
+ readonly submitTimeoutMs: 30000;
3
4
  };
@@ -3,4 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsOptimisticConfig = void 0;
4
4
  exports.PsOptimisticConfig = {
5
5
  pendingPrefix: 'pending-',
6
+ submitTimeoutMs: 30000,
6
7
  };
@@ -10,6 +10,10 @@ export declare const PsSkeletonConfig: {
10
10
  readonly importantPrefix: "!";
11
11
  readonly widthUtilityPattern: RegExp;
12
12
  readonly heightUtilityPattern: RegExp;
13
+ readonly compactControlHeightClassName: "h-ps-control-compact";
14
+ readonly tabWidthClassName: "w-24";
15
+ readonly tabCount: 4;
16
+ readonly tableFrameClassName: "rounded-[0.875rem]";
13
17
  readonly shapeClassNames: Record<PsSkeletonShape, string>;
14
18
  readonly shapeSizes: Record<PsSkeletonShape, PsSkeletonSize>;
15
19
  };
@@ -2,6 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsSkeletonConfig = void 0;
4
4
  const ps_skeleton_shape_1 = require("./ps-skeleton-shape");
5
+ const controlRadius = 'rounded-ps-control';
6
+ const cardRadius = 'rounded-ps-card';
7
+ const textRadius = 'rounded-full';
8
+ const controlSize = { height: 'h-ps-control', width: 'w-full' };
5
9
  exports.PsSkeletonConfig = {
6
10
  defaultTextLines: 3,
7
11
  lastLineWidth: 'w-2/3',
@@ -12,16 +16,30 @@ exports.PsSkeletonConfig = {
12
16
  importantPrefix: '!',
13
17
  widthUtilityPattern: /^(w|size)-/,
14
18
  heightUtilityPattern: /^(h|size)-/,
19
+ compactControlHeightClassName: 'h-ps-control-compact',
20
+ tabWidthClassName: 'w-24',
21
+ tabCount: 4,
22
+ tableFrameClassName: 'rounded-[0.875rem]',
15
23
  shapeClassNames: {
16
- [ps_skeleton_shape_1.PsSkeletonShape.Block]: 'rounded-ps-control',
24
+ [ps_skeleton_shape_1.PsSkeletonShape.Badge]: controlRadius,
25
+ [ps_skeleton_shape_1.PsSkeletonShape.Block]: controlRadius,
26
+ [ps_skeleton_shape_1.PsSkeletonShape.Button]: controlRadius,
27
+ [ps_skeleton_shape_1.PsSkeletonShape.Card]: cardRadius,
17
28
  [ps_skeleton_shape_1.PsSkeletonShape.Circle]: 'shrink-0 rounded-full',
18
- [ps_skeleton_shape_1.PsSkeletonShape.Line]: 'rounded-full',
19
- [ps_skeleton_shape_1.PsSkeletonShape.Text]: 'rounded-full',
29
+ [ps_skeleton_shape_1.PsSkeletonShape.Control]: controlRadius,
30
+ [ps_skeleton_shape_1.PsSkeletonShape.Line]: textRadius,
31
+ [ps_skeleton_shape_1.PsSkeletonShape.Pill]: controlRadius,
32
+ [ps_skeleton_shape_1.PsSkeletonShape.Text]: textRadius,
20
33
  },
21
34
  shapeSizes: {
22
- [ps_skeleton_shape_1.PsSkeletonShape.Block]: { height: 'h-ps-control', width: 'w-full' },
35
+ [ps_skeleton_shape_1.PsSkeletonShape.Badge]: { height: 'h-7', width: 'w-20' },
36
+ [ps_skeleton_shape_1.PsSkeletonShape.Block]: controlSize,
37
+ [ps_skeleton_shape_1.PsSkeletonShape.Button]: { height: 'h-ps-control', width: 'w-32' },
38
+ [ps_skeleton_shape_1.PsSkeletonShape.Card]: { height: 'h-20', width: 'w-full' },
23
39
  [ps_skeleton_shape_1.PsSkeletonShape.Circle]: { height: 'h-9', width: 'w-9' },
40
+ [ps_skeleton_shape_1.PsSkeletonShape.Control]: controlSize,
24
41
  [ps_skeleton_shape_1.PsSkeletonShape.Line]: { height: 'h-3', width: 'w-full' },
42
+ [ps_skeleton_shape_1.PsSkeletonShape.Pill]: { height: 'h-8', width: 'w-24' },
25
43
  [ps_skeleton_shape_1.PsSkeletonShape.Text]: { height: 'h-3', width: 'w-full' },
26
44
  },
27
45
  };
@@ -1,6 +1,11 @@
1
1
  export declare enum PsSkeletonShape {
2
+ Badge = "badge",
2
3
  Block = "block",
4
+ Button = "button",
5
+ Card = "card",
3
6
  Circle = "circle",
7
+ Control = "control",
4
8
  Line = "line",
9
+ Pill = "pill",
5
10
  Text = "text"
6
11
  }
@@ -3,8 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsSkeletonShape = void 0;
4
4
  var PsSkeletonShape;
5
5
  (function (PsSkeletonShape) {
6
+ PsSkeletonShape["Badge"] = "badge";
6
7
  PsSkeletonShape["Block"] = "block";
8
+ PsSkeletonShape["Button"] = "button";
9
+ PsSkeletonShape["Card"] = "card";
7
10
  PsSkeletonShape["Circle"] = "circle";
11
+ PsSkeletonShape["Control"] = "control";
8
12
  PsSkeletonShape["Line"] = "line";
13
+ PsSkeletonShape["Pill"] = "pill";
9
14
  PsSkeletonShape["Text"] = "text";
10
15
  })(PsSkeletonShape || (exports.PsSkeletonShape = PsSkeletonShape = {}));
@@ -6,4 +6,4 @@ const variantClassNames = {
6
6
  default: 'ps-action-tile',
7
7
  primary: 'ps-action-tile ps-action-tile-primary',
8
8
  };
9
- export const PsActionTile = ({ className, description, icon, isLoading = false, label, type = 'button', variant = 'default', ...props }) => isLoading ? (_jsx(PsSkeleton, { className: PallasoftClassNameService.merge('h-16 w-full rounded-ps-card', className), shape: PsSkeletonShape.Block })) : (_jsxs("button", { className: PallasoftClassNameService.merge('ps-focus-ring w-full', variantClassNames[variant], className), type: type, ...props, children: [_jsxs("span", { className: 'min-w-0', children: [_jsx("span", { className: 'block break-words', children: label }), description ? (_jsx("span", { className: PallasoftClassNameService.merge('mt-0.5 block text-xs font-medium leading-5', variant === 'primary' ? 'text-white/75' : 'text-ink/50'), children: description })) : null] }), icon ? (_jsx("span", { className: PallasoftClassNameService.merge('flex h-9 w-9 shrink-0 items-center justify-center rounded-lg', variant === 'primary' ? 'bg-white/15 text-white' : 'ps-icon-surface'), children: icon })) : null] }));
9
+ export const PsActionTile = ({ className, description, icon, isLoading = false, label, type = 'button', variant = 'default', ...props }) => isLoading ? (_jsx(PsSkeleton, { className: PallasoftClassNameService.merge('h-16', className), shape: PsSkeletonShape.Card })) : (_jsxs("button", { className: PallasoftClassNameService.merge('ps-focus-ring w-full', variantClassNames[variant], className), type: type, ...props, children: [_jsxs("span", { className: 'min-w-0', children: [_jsx("span", { className: 'block break-words', children: label }), description ? (_jsx("span", { className: PallasoftClassNameService.merge('mt-0.5 block text-xs font-medium leading-5', variant === 'primary' ? 'text-white/75' : 'text-ink/50'), children: description })) : null] }), icon ? (_jsx("span", { className: PallasoftClassNameService.merge('flex h-9 w-9 shrink-0 items-center justify-center rounded-lg', variant === 'primary' ? 'bg-white/15 text-white' : 'ps-icon-surface'), children: icon })) : null] }));
@@ -9,4 +9,4 @@ const toneClassNames = {
9
9
  success: 'bg-success-soft text-emerald-700 dark:bg-emerald-400/18 dark:text-emerald-50',
10
10
  warning: 'bg-warning-soft text-amber-800 dark:bg-amber-400/18 dark:text-amber-50',
11
11
  };
12
- export const PsBadge = ({ children, className, isLoading = false, tone = 'neutral', }) => isLoading ? (_jsx(PsSkeleton, { className: PallasoftClassNameService.merge('h-7 w-20', className), shape: PsSkeletonShape.Block })) : (_jsx("span", { className: PallasoftClassNameService.merge('inline-flex min-h-7 w-fit items-center justify-center whitespace-nowrap rounded-ps-control px-3 py-0.5 text-center text-xs font-semibold capitalize', toneClassNames[tone], className), children: children }));
12
+ export const PsBadge = ({ children, className, isLoading = false, tone = 'neutral', }) => isLoading ? (_jsx(PsSkeleton, { className: className, shape: PsSkeletonShape.Badge })) : (_jsx("span", { className: PallasoftClassNameService.merge('inline-flex min-h-7 w-fit items-center justify-center whitespace-nowrap rounded-ps-control px-3 py-0.5 text-center text-xs font-semibold capitalize', toneClassNames[tone], className), children: children }));
@@ -1,4 +1,4 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { PsSkeletonShape } from '../constants/ps-skeleton-shape.js';
3
3
  import { PsSkeleton } from './ps-skeleton.js';
4
- export const PsCardBlockSkeleton = ({ height }) => (_jsx(PsSkeleton, { height: height, radius: '1.25rem', shape: PsSkeletonShape.Block, width: '100%' }));
4
+ export const PsCardBlockSkeleton = ({ height }) => (_jsx(PsSkeleton, { height: height, shape: PsSkeletonShape.Card }));
@@ -14,4 +14,4 @@ const variantClassNames = {
14
14
  glass: 'ps-glass',
15
15
  muted: 'ps-card-muted',
16
16
  };
17
- export const PsCard = ({ children, className, density = 'normal', eyebrow, isLoading = false, title, trailing, variant = 'default', }) => isLoading ? (_jsxs(PsSkeletonGroup, { className: PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3 rounded-ps-card', variantClassNames[variant], densityClassNames[density], className), label: title === undefined ? 'Loading' : title, children: [_jsx(PsSkeleton, { className: 'h-3 w-32' }), _jsx(PsSkeleton, { className: 'h-20 w-full', shape: PsSkeletonShape.Block })] })) : (_jsxs("section", { className: PallasoftClassNameService.merge('min-w-0 overflow-hidden rounded-ps-card', variantClassNames[variant], densityClassNames[density], className), children: [title ? (_jsxs("div", { className: 'mb-3 flex min-w-0 items-start justify-between gap-3', children: [_jsxs("div", { className: 'min-w-0', children: [eyebrow ? _jsx("p", { className: 'ps-eyebrow break-words', children: eyebrow }) : null, _jsx("h2", { className: 'mt-1 break-words text-base font-bold text-ink', children: title })] }), trailing ? _jsx("div", { className: 'shrink-0', children: trailing }) : null] })) : null, children] }));
17
+ export const PsCard = ({ children, className, density = 'normal', eyebrow, isLoading = false, title, trailing, variant = 'default', }) => isLoading ? (_jsxs(PsSkeletonGroup, { className: PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3 rounded-ps-card', variantClassNames[variant], densityClassNames[density], className), label: title === undefined ? 'Loading' : title, children: [_jsx(PsSkeleton, { className: 'h-3 w-32' }), _jsx(PsSkeleton, { shape: PsSkeletonShape.Control, className: 'h-20' })] })) : (_jsxs("section", { className: PallasoftClassNameService.merge('min-w-0 overflow-hidden rounded-ps-card', variantClassNames[variant], densityClassNames[density], className), children: [title ? (_jsxs("div", { className: 'mb-3 flex min-w-0 items-start justify-between gap-3', children: [_jsxs("div", { className: 'min-w-0', children: [eyebrow ? _jsx("p", { className: 'ps-eyebrow break-words', children: eyebrow }) : null, _jsx("h2", { className: 'mt-1 break-words text-base font-bold text-ink', children: title })] }), trailing ? _jsx("div", { className: 'shrink-0', children: trailing }) : null] })) : null, children] }));
@@ -3,4 +3,4 @@ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
3
  import { PsPageSkeletonConfig } from '../constants/ps-page-skeleton-config.js';
4
4
  import { PsSkeletonShape } from '../constants/ps-skeleton-shape.js';
5
5
  import { PsSkeleton } from './ps-skeleton.js';
6
- export const PsFormFieldsSkeleton = ({ columnsClassName = PsPageSkeletonConfig.formColumnsClassName, fields = PsPageSkeletonConfig.formFields, }) => (_jsx("div", { className: PallasoftClassNameService.merge('grid gap-4', columnsClassName), children: fields.map((field) => (_jsxs("div", { className: 'flex flex-col gap-2', children: [_jsx(PsSkeleton, { className: PsPageSkeletonConfig.formLabelClassName }), _jsx(PsSkeleton, { className: 'h-ps-control w-full', shape: PsSkeletonShape.Block })] }, field))) }));
6
+ export const PsFormFieldsSkeleton = ({ columnsClassName = PsPageSkeletonConfig.formColumnsClassName, fields = PsPageSkeletonConfig.formFields, }) => (_jsx("div", { className: PallasoftClassNameService.merge('grid gap-4', columnsClassName), children: fields.map((field) => (_jsxs("div", { className: 'flex flex-col gap-2', children: [_jsx(PsSkeleton, { className: PsPageSkeletonConfig.formLabelClassName }), _jsx(PsSkeleton, { shape: PsSkeletonShape.Control })] }, field))) }));
@@ -1,3 +1,3 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PsSkeleton } from './ps-skeleton.js';
3
- export const PsIntroSkeleton = () => (_jsxs("div", { className: 'mx-auto flex w-full max-w-3xl flex-col items-center gap-4', children: [_jsx(PsSkeleton, { height: '0.875rem', radius: '999px', width: '6rem' }), _jsx(PsSkeleton, { height: '2.5rem', radius: '0.75rem', width: '70%' }), _jsx(PsSkeleton, { height: '1.125rem', radius: '0.5rem', width: '85%' })] }));
3
+ export const PsIntroSkeleton = () => (_jsxs("div", { className: 'mx-auto flex w-full max-w-3xl flex-col items-center gap-4', children: [_jsx(PsSkeleton, { className: 'h-3.5 w-24' }), _jsx(PsSkeleton, { className: 'h-10 w-[70%]' }), _jsx(PsSkeleton, { className: 'h-[1.125rem] w-[85%]' })] }));
@@ -5,4 +5,4 @@ import { PsPageLayout } from './ps-page-layout.js';
5
5
  import { PsPageStack } from './ps-page-stack.js';
6
6
  import { PsSectionHeader } from './ps-section-header.js';
7
7
  import { PsSkeleton } from './ps-skeleton.js';
8
- export const PsPageSkeleton = ({ children, hasActions = false, isNarrow = false, loadingLabel, title, }) => (_jsx(PsPageLayout, { isNarrow: isNarrow, children: _jsxs(PsPageStack, { children: [title === undefined ? (_jsx(PsSectionHeader, { isLoading: true, title: loadingLabel, trailing: hasActions })) : (_jsx(PsPageHeader, { actions: hasActions ? (_jsx(PsSkeleton, { height: '2.5rem', radius: '9999px', shape: PsSkeletonShape.Block, width: '9rem' })) : undefined, title: title })), children] }) }));
8
+ export const PsPageSkeleton = ({ children, hasActions = false, isNarrow = false, loadingLabel, title, }) => (_jsx(PsPageLayout, { isNarrow: isNarrow, children: _jsxs(PsPageStack, { children: [title === undefined ? (_jsx(PsSectionHeader, { isLoading: true, title: loadingLabel, trailing: hasActions })) : (_jsx(PsPageHeader, { actions: hasActions ? _jsx(PsSkeleton, { shape: PsSkeletonShape.Button }) : undefined, title: title })), children] }) }));
@@ -9,4 +9,4 @@ const toneClassNames = {
9
9
  success: 'bg-emerald-50 text-emerald-700 ring-emerald-100 dark:bg-emerald-400/18 dark:text-emerald-50 dark:ring-emerald-200/22',
10
10
  warning: 'bg-amber-50 text-amber-800 ring-amber-100 dark:bg-amber-400/18 dark:text-amber-50 dark:ring-amber-200/24',
11
11
  };
12
- export const PsPill = ({ children, className, isLoading = false, tone = 'neutral', }) => isLoading ? (_jsx(PsSkeleton, { className: PallasoftClassNameService.merge('h-8 w-24', className), shape: PsSkeletonShape.Block })) : (_jsx("span", { className: PallasoftClassNameService.merge('inline-flex min-h-8 min-w-[5.5rem] max-w-full items-center justify-center rounded-ps-control px-5 py-1 text-center text-[0.72rem] font-black capitalize tracking-[0.01em] ring-1 ring-inset', toneClassNames[tone], className), children: _jsx("span", { className: 'min-w-0 break-words', children: children }) }));
12
+ export const PsPill = ({ children, className, isLoading = false, tone = 'neutral', }) => isLoading ? (_jsx(PsSkeleton, { className: className, shape: PsSkeletonShape.Pill })) : (_jsx("span", { className: PallasoftClassNameService.merge('inline-flex min-h-8 min-w-[5.5rem] max-w-full items-center justify-center rounded-ps-control px-5 py-1 text-center text-[0.72rem] font-black capitalize tracking-[0.01em] ring-1 ring-inset', toneClassNames[tone], className), children: _jsx("span", { className: 'min-w-0 break-words', children: children }) }));
@@ -2,4 +2,4 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PsSkeletonShape } from '../constants/ps-skeleton-shape.js';
3
3
  import { PsSkeleton } from './ps-skeleton.js';
4
4
  import { PsSkeletonGroup } from './ps-skeleton-group.js';
5
- export const PsSectionHeader = ({ description, eyebrow, isLoading = false, leading, title, trailing, }) => isLoading ? (_jsxs(PsSkeletonGroup, { className: 'flex flex-wrap items-end justify-between gap-x-4 gap-y-3', label: title, children: [_jsxs("div", { className: 'flex min-w-0 flex-1 basis-auto flex-col gap-2', children: [eyebrow ? _jsx(PsSkeleton, { className: 'h-2.5 w-24' }) : null, _jsx(PsSkeleton, { className: 'h-6 w-52' }), description ? _jsx(PsSkeleton, { className: 'h-3 w-72' }) : null] }), trailing ? _jsx(PsSkeleton, { className: 'h-ps-control w-32', shape: PsSkeletonShape.Block }) : null] })) : (_jsxs("div", { className: 'flex flex-wrap items-end justify-between gap-x-4 gap-y-3', children: [_jsxs("div", { className: 'flex min-w-0 flex-1 basis-auto flex-col gap-2', children: [leading, eyebrow ? (_jsx("p", { className: 'text-[0.68rem] font-semibold uppercase tracking-[0.22em] text-ink/50', children: eyebrow })) : null, _jsx("h1", { className: 'break-words text-xl font-extrabold text-ink sm:text-2xl', children: title }), description ? (_jsx("p", { className: 'max-w-3xl text-sm leading-6 text-ink/60', children: description })) : null] }), trailing ? (_jsx("div", { className: 'flex w-full shrink-0 items-center justify-end sm:ml-auto sm:w-auto', children: trailing })) : null] }));
5
+ export const PsSectionHeader = ({ description, eyebrow, isLoading = false, leading, title, trailing, }) => isLoading ? (_jsxs(PsSkeletonGroup, { className: 'flex flex-wrap items-end justify-between gap-x-4 gap-y-3', label: title, children: [_jsxs("div", { className: 'flex min-w-0 flex-1 basis-auto flex-col gap-2', children: [eyebrow ? _jsx(PsSkeleton, { className: 'h-2.5 w-24' }) : null, _jsx(PsSkeleton, { className: 'h-6 w-52' }), description ? _jsx(PsSkeleton, { className: 'h-3 w-72' }) : null] }), trailing ? _jsx(PsSkeleton, { shape: PsSkeletonShape.Button }) : null] })) : (_jsxs("div", { className: 'flex flex-wrap items-end justify-between gap-x-4 gap-y-3', children: [_jsxs("div", { className: 'flex min-w-0 flex-1 basis-auto flex-col gap-2', children: [leading, eyebrow ? (_jsx("p", { className: 'text-[0.68rem] font-semibold uppercase tracking-[0.22em] text-ink/50', children: eyebrow })) : null, _jsx("h1", { className: 'break-words text-xl font-extrabold text-ink sm:text-2xl', children: title }), description ? (_jsx("p", { className: 'max-w-3xl text-sm leading-6 text-ink/60', children: description })) : null] }), trailing ? (_jsx("div", { className: 'flex w-full shrink-0 items-center justify-end sm:ml-auto sm:w-auto', children: trailing })) : null] }));
@@ -3,4 +3,4 @@ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
3
  import { PsSkeletonShape } from '../constants/ps-skeleton-shape.js';
4
4
  import { PsSkeleton } from './ps-skeleton.js';
5
5
  import { PsSkeletonGroup } from './ps-skeleton-group.js';
6
- export const PsSkeletonMedia = ({ aspect = '16 / 9', className, hasCaption = true, label, }) => (_jsxs(PsSkeletonGroup, { className: PallasoftClassNameService.merge('w-full', className), label: label, children: [_jsx(PsSkeleton, { aspect: aspect, radius: 'var(--ps-radius-card, 1rem)', shape: PsSkeletonShape.Block }), hasCaption ? _jsx(PsSkeleton, { lines: 2, shape: PsSkeletonShape.Text }) : null] }));
6
+ export const PsSkeletonMedia = ({ aspect = '16 / 9', className, hasCaption = true, label, }) => (_jsxs(PsSkeletonGroup, { className: PallasoftClassNameService.merge('w-full', className), label: label, children: [_jsx(PsSkeleton, { aspect: aspect, shape: PsSkeletonShape.Card }), hasCaption ? _jsx(PsSkeleton, { lines: 2, shape: PsSkeletonShape.Text }) : null] }));
@@ -12,7 +12,7 @@ export const PsSkeletonTable = ({ alignments, className, columns = 4, hasFooter
12
12
  const cell = (rowIndex, columnIndex, isStrong) => (_jsx(PsSkeleton, { className: PallasoftClassNameService.merge(isRight(columnIndex)
13
13
  ? 'ml-auto w-16'
14
14
  : PsSkeletonConfig.lineWidths[(rowIndex + columnIndex) % PsSkeletonConfig.lineWidths.length], isStrong ? 'h-3.5' : '') }, columnIndex));
15
- return (_jsxs(PsSkeletonGroup, { className: PallasoftClassNameService.merge('flex flex-col gap-3', className), children: [hasToolbar ? (_jsxs("div", { className: 'flex items-center gap-3', children: [_jsx(PsSkeleton, { className: 'max-w-sm', shape: PsSkeletonShape.Block }), _jsx(PsSkeleton, { className: 'ml-auto w-24', shape: PsSkeletonShape.Block })] })) : null, _jsxs("div", { className: isFramed
16
- ? 'overflow-hidden rounded-[1.25rem] border border-line bg-surface'
15
+ return (_jsxs(PsSkeletonGroup, { className: PallasoftClassNameService.merge('flex flex-col gap-3', className), children: [hasToolbar ? (_jsxs("div", { className: 'flex items-center gap-3', children: [_jsx(PsSkeleton, { className: 'max-w-sm', shape: PsSkeletonShape.Control }), _jsx(PsSkeleton, { className: 'ml-auto w-24', shape: PsSkeletonShape.Button })] })) : null, _jsxs("div", { className: isFramed
16
+ ? PallasoftClassNameService.merge('overflow-hidden border border-line bg-surface', PsSkeletonConfig.tableFrameClassName)
17
17
  : 'overflow-hidden', children: [_jsx("div", { className: 'grid gap-4 bg-mist/70 px-5 py-4', style: { gridTemplateColumns }, children: Array.from({ length: columns }, (_, index) => (_jsx(PsSkeleton, { className: isRight(index) ? 'ml-auto h-2.5 w-12' : 'h-2.5 w-16' }, index))) }), Array.from({ length: rows }, (_, rowIndex) => (_jsx("div", { className: 'grid items-center gap-4 border-t border-line px-5 py-4', style: { gridTemplateColumns }, children: Array.from({ length: columns }, (__, columnIndex) => cell(rowIndex, columnIndex, false)) }, rowIndex))), hasFooter ? (_jsx("div", { className: 'grid items-center gap-4 border-t-2 border-line px-5 py-4', style: { gridTemplateColumns }, children: Array.from({ length: columns }, (__, columnIndex) => cell(rows, columnIndex, true)) })) : null] })] }));
18
18
  };
@@ -1,4 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PsSkeletonConfig } from '../constants/ps-skeleton-config.js';
2
3
  import { PsSkeletonShape } from '../constants/ps-skeleton-shape.js';
3
4
  import { PsSkeleton } from './ps-skeleton.js';
4
- export const PsTabRowSkeleton = () => (_jsx(PsSkeleton, { height: '2.75rem', radius: '9999px', shape: PsSkeletonShape.Block, width: '100%' }));
5
+ import { PsSkeletonGroup } from './ps-skeleton-group.js';
6
+ export const PsTabRowSkeleton = () => (_jsx(PsSkeletonGroup, { direction: 'row', gap: 'gap-2', children: Array.from({ length: PsSkeletonConfig.tabCount }, (_, index) => (_jsx(PsSkeleton, { className: `${PsSkeletonConfig.compactControlHeightClassName} ${PsSkeletonConfig.tabWidthClassName}`, shape: PsSkeletonShape.Control }, index))) }));
@@ -1,3 +1,4 @@
1
1
  export const PsOptimisticConfig = {
2
2
  pendingPrefix: 'pending-',
3
+ submitTimeoutMs: 30000,
3
4
  };
@@ -1,4 +1,8 @@
1
1
  import { PsSkeletonShape } from './ps-skeleton-shape.js';
2
+ const controlRadius = 'rounded-ps-control';
3
+ const cardRadius = 'rounded-ps-card';
4
+ const textRadius = 'rounded-full';
5
+ const controlSize = { height: 'h-ps-control', width: 'w-full' };
2
6
  export const PsSkeletonConfig = {
3
7
  defaultTextLines: 3,
4
8
  lastLineWidth: 'w-2/3',
@@ -9,16 +13,30 @@ export const PsSkeletonConfig = {
9
13
  importantPrefix: '!',
10
14
  widthUtilityPattern: /^(w|size)-/,
11
15
  heightUtilityPattern: /^(h|size)-/,
16
+ compactControlHeightClassName: 'h-ps-control-compact',
17
+ tabWidthClassName: 'w-24',
18
+ tabCount: 4,
19
+ tableFrameClassName: 'rounded-[0.875rem]',
12
20
  shapeClassNames: {
13
- [PsSkeletonShape.Block]: 'rounded-ps-control',
21
+ [PsSkeletonShape.Badge]: controlRadius,
22
+ [PsSkeletonShape.Block]: controlRadius,
23
+ [PsSkeletonShape.Button]: controlRadius,
24
+ [PsSkeletonShape.Card]: cardRadius,
14
25
  [PsSkeletonShape.Circle]: 'shrink-0 rounded-full',
15
- [PsSkeletonShape.Line]: 'rounded-full',
16
- [PsSkeletonShape.Text]: 'rounded-full',
26
+ [PsSkeletonShape.Control]: controlRadius,
27
+ [PsSkeletonShape.Line]: textRadius,
28
+ [PsSkeletonShape.Pill]: controlRadius,
29
+ [PsSkeletonShape.Text]: textRadius,
17
30
  },
18
31
  shapeSizes: {
19
- [PsSkeletonShape.Block]: { height: 'h-ps-control', width: 'w-full' },
32
+ [PsSkeletonShape.Badge]: { height: 'h-7', width: 'w-20' },
33
+ [PsSkeletonShape.Block]: controlSize,
34
+ [PsSkeletonShape.Button]: { height: 'h-ps-control', width: 'w-32' },
35
+ [PsSkeletonShape.Card]: { height: 'h-20', width: 'w-full' },
20
36
  [PsSkeletonShape.Circle]: { height: 'h-9', width: 'w-9' },
37
+ [PsSkeletonShape.Control]: controlSize,
21
38
  [PsSkeletonShape.Line]: { height: 'h-3', width: 'w-full' },
39
+ [PsSkeletonShape.Pill]: { height: 'h-8', width: 'w-24' },
22
40
  [PsSkeletonShape.Text]: { height: 'h-3', width: 'w-full' },
23
41
  },
24
42
  };
@@ -1,7 +1,12 @@
1
1
  export var PsSkeletonShape;
2
2
  (function (PsSkeletonShape) {
3
+ PsSkeletonShape["Badge"] = "badge";
3
4
  PsSkeletonShape["Block"] = "block";
5
+ PsSkeletonShape["Button"] = "button";
6
+ PsSkeletonShape["Card"] = "card";
4
7
  PsSkeletonShape["Circle"] = "circle";
8
+ PsSkeletonShape["Control"] = "control";
5
9
  PsSkeletonShape["Line"] = "line";
10
+ PsSkeletonShape["Pill"] = "pill";
6
11
  PsSkeletonShape["Text"] = "text";
7
12
  })(PsSkeletonShape || (PsSkeletonShape = {}));
@@ -14,10 +14,15 @@ export const usePsChangeQueue = ({ onDrained, onFailed, send, }) => {
14
14
  while (pending.current.length > 0) {
15
15
  const batch = pending.current;
16
16
  pending.current = [];
17
- await latest.current.send(batch).catch(() => {
17
+ await new Promise((resolve) => resolve(latest.current.send(batch))).catch(() => {
18
18
  const dropped = pending.current;
19
19
  pending.current = [];
20
- latest.current.onFailed(batch, dropped);
20
+ try {
21
+ latest.current.onFailed(batch, dropped);
22
+ }
23
+ catch {
24
+ return;
25
+ }
21
26
  });
22
27
  }
23
28
  isRunning.current = false;
@@ -1,13 +1,16 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { PsOptimisticConfig } from '../constants/ps-optimistic-config.js';
3
+ import { PsOptimisticFetcherService } from '../services/ps-optimistic-fetcher-service.js';
2
4
  import { usePsChangeQueue } from './use-ps-change-queue.js';
3
5
  import { usePsOptimisticRows } from './use-ps-optimistic-rows.js';
4
- export const usePsOptimisticFetcher = ({ apply, fetcher, isHolding = false, isSuccess, merge, revalidator, rows, }) => {
6
+ export const usePsOptimisticFetcher = ({ apply, fetcher, isHolding = false, isSuccess, merge, onError, revalidator, rows, }) => {
5
7
  const [isUnsettled, setIsUnsettled] = useState(false);
6
8
  const submitted = useRef(0);
7
9
  const settle = useRef(null);
8
10
  const sawBusy = useRef(false);
9
11
  const dataBefore = useRef(fetcher.data);
10
12
  const dataSeen = useRef(fetcher.data);
13
+ const failure = useRef(null);
11
14
  const { forgetLast, remember, rows: shown, } = usePsOptimisticRows({
12
15
  apply,
13
16
  isHolding,
@@ -27,32 +30,67 @@ export const usePsOptimisticFetcher = ({ apply, fetcher, isHolding = false, isSu
27
30
  const done = settle.current;
28
31
  settle.current = null;
29
32
  sawBusy.current = false;
30
- done(fetcher.data);
33
+ done(PsOptimisticFetcherService.outcome(fetcher.data, dataBefore.current));
31
34
  }, [fetcher.data, fetcher.state]);
35
+ useEffect(() => () => {
36
+ if (settle.current === null)
37
+ return;
38
+ const done = settle.current;
39
+ settle.current = null;
40
+ done({ data: undefined, reason: 'unmounted' });
41
+ }, []);
32
42
  const submitOne = useCallback((values) => new Promise((resolve) => {
33
- settle.current = resolve;
43
+ const finish = (outcome) => {
44
+ clearTimeout(timer);
45
+ if (settle.current === finish)
46
+ settle.current = null;
47
+ resolve(outcome);
48
+ };
49
+ const timer = setTimeout(() => finish({ data: undefined, reason: 'timeout' }), PsOptimisticConfig.submitTimeoutMs);
50
+ settle.current = finish;
34
51
  sawBusy.current = false;
35
52
  dataBefore.current = dataSeen.current;
36
- void fetcher.submit(values, { defaultShouldRevalidate: false, method: 'post' });
53
+ try {
54
+ void Promise.resolve(fetcher.submit(values, { defaultShouldRevalidate: false, method: 'post' })).catch(() => finish({ data: undefined, reason: 'no-response' }));
55
+ }
56
+ catch {
57
+ finish({ data: undefined, reason: 'no-response' });
58
+ }
37
59
  }), [fetcher]);
38
60
  const send = useCallback(async (queued) => {
61
+ failure.current = null;
39
62
  for (const submission of merge === undefined ? queued : merge(queued)) {
40
- const result = await submitOne(submission);
41
- if (!isSuccess(result))
42
- throw result;
63
+ const outcome = await submitOne(submission);
64
+ const reason = PsOptimisticFetcherService.failureReason(outcome, isSuccess);
65
+ if (reason !== null) {
66
+ failure.current = { data: outcome.data, reason };
67
+ throw new Error(reason);
68
+ }
43
69
  }
44
70
  }, [isSuccess, merge, submitOne]);
45
71
  const revalidate = useCallback(() => {
46
72
  const through = submitted.current;
47
- void Promise.resolve(revalidator.revalidate()).finally(() => {
73
+ const release = () => {
48
74
  if (submitted.current === through)
49
75
  setIsUnsettled(false);
50
- });
76
+ };
77
+ void new Promise((resolve) => resolve(revalidator.revalidate()))
78
+ .catch(() => undefined)
79
+ .then(release);
51
80
  }, [revalidator]);
52
81
  const forgetFailed = useCallback((failed, dropped) => {
53
82
  for (let index = 0; index < failed.length + dropped.length; index += 1)
54
83
  forgetLast();
55
- }, [forgetLast]);
84
+ const cause = failure.current;
85
+ failure.current = null;
86
+ if (onError === undefined)
87
+ return;
88
+ if (cause === null) {
89
+ onError({ changes: [...failed, ...dropped], data: undefined, reason: 'error' });
90
+ return;
91
+ }
92
+ onError({ ...cause, changes: [...failed, ...dropped] });
93
+ }, [forgetLast, onError]);
56
94
  const queue = usePsChangeQueue({
57
95
  onDrained: revalidate,
58
96
  onFailed: forgetFailed,
@@ -1,12 +1,13 @@
1
1
  import { useCallback } from 'react';
2
2
  import { PsOptimisticSaveService } from '../services/ps-optimistic-save-service.js';
3
3
  import { usePsOptimisticFetcher } from './use-ps-optimistic-fetcher.js';
4
- export const usePsOptimisticSave = ({ fetcher, isSuccess, revalidator, rows, scopeId, spec, }) => {
4
+ export const usePsOptimisticSave = ({ fetcher, isSuccess, onError, revalidator, rows, scopeId, spec, }) => {
5
5
  const apply = useCallback((base, changes) => PsOptimisticSaveService.apply(base, changes, spec), [spec]);
6
6
  const optimistic = usePsOptimisticFetcher({
7
7
  apply,
8
8
  fetcher,
9
9
  isSuccess,
10
+ onError,
10
11
  revalidator,
11
12
  rows,
12
13
  });
package/dist/esm/index.js CHANGED
@@ -95,6 +95,8 @@ export * from './interfaces/ps-live-input.js';
95
95
  export * from './interfaces/ps-live-open-options.js';
96
96
  export * from './interfaces/ps-optimistic-fetcher-input.js';
97
97
  export * from './interfaces/ps-optimistic-fetcher-result.js';
98
+ export * from './interfaces/ps-optimistic-failure.js';
99
+ export * from './interfaces/ps-optimistic-failure-reason.js';
98
100
  export * from './interfaces/ps-revalidator-like.js';
99
101
  export * from './interfaces/ps-optimistic-rows-input.js';
100
102
  export * from './interfaces/ps-optimistic-rows-result.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ export class PsOptimisticFetcherService {
2
+ static outcome(data, before) {
3
+ if (data === before)
4
+ return { data: undefined, reason: 'no-response' };
5
+ return { data, reason: 'settled' };
6
+ }
7
+ static failureReason(outcome, isSuccess) {
8
+ if (outcome.reason !== 'settled')
9
+ return outcome.reason;
10
+ return isSuccess(outcome.data) ? null : 'rejected';
11
+ }
12
+ }
@@ -17,10 +17,15 @@ const usePsChangeQueue = ({ onDrained, onFailed, send, }) => {
17
17
  while (pending.current.length > 0) {
18
18
  const batch = pending.current;
19
19
  pending.current = [];
20
- await latest.current.send(batch).catch(() => {
20
+ await new Promise((resolve) => resolve(latest.current.send(batch))).catch(() => {
21
21
  const dropped = pending.current;
22
22
  pending.current = [];
23
- latest.current.onFailed(batch, dropped);
23
+ try {
24
+ latest.current.onFailed(batch, dropped);
25
+ }
26
+ catch {
27
+ return;
28
+ }
24
29
  });
25
30
  }
26
31
  isRunning.current = false;
@@ -1,3 +1,3 @@
1
1
  import type { PsOptimisticFetcherInput } from '../interfaces/ps-optimistic-fetcher-input';
2
2
  import type { PsOptimisticFetcherResult } from '../interfaces/ps-optimistic-fetcher-result';
3
- export declare const usePsOptimisticFetcher: <Row, Data>({ apply, fetcher, isHolding, isSuccess, merge, revalidator, rows, }: PsOptimisticFetcherInput<Row, Data>) => PsOptimisticFetcherResult<Row>;
3
+ export declare const usePsOptimisticFetcher: <Row, Data>({ apply, fetcher, isHolding, isSuccess, merge, onError, revalidator, rows, }: PsOptimisticFetcherInput<Row, Data>) => PsOptimisticFetcherResult<Row>;
@@ -2,15 +2,18 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.usePsOptimisticFetcher = void 0;
4
4
  const react_1 = require("react");
5
+ const ps_optimistic_config_1 = require("../constants/ps-optimistic-config");
6
+ const ps_optimistic_fetcher_service_1 = require("../services/ps-optimistic-fetcher-service");
5
7
  const use_ps_change_queue_1 = require("./use-ps-change-queue");
6
8
  const use_ps_optimistic_rows_1 = require("./use-ps-optimistic-rows");
7
- const usePsOptimisticFetcher = ({ apply, fetcher, isHolding = false, isSuccess, merge, revalidator, rows, }) => {
9
+ const usePsOptimisticFetcher = ({ apply, fetcher, isHolding = false, isSuccess, merge, onError, revalidator, rows, }) => {
8
10
  const [isUnsettled, setIsUnsettled] = (0, react_1.useState)(false);
9
11
  const submitted = (0, react_1.useRef)(0);
10
12
  const settle = (0, react_1.useRef)(null);
11
13
  const sawBusy = (0, react_1.useRef)(false);
12
14
  const dataBefore = (0, react_1.useRef)(fetcher.data);
13
15
  const dataSeen = (0, react_1.useRef)(fetcher.data);
16
+ const failure = (0, react_1.useRef)(null);
14
17
  const { forgetLast, remember, rows: shown, } = (0, use_ps_optimistic_rows_1.usePsOptimisticRows)({
15
18
  apply,
16
19
  isHolding,
@@ -30,32 +33,67 @@ const usePsOptimisticFetcher = ({ apply, fetcher, isHolding = false, isSuccess,
30
33
  const done = settle.current;
31
34
  settle.current = null;
32
35
  sawBusy.current = false;
33
- done(fetcher.data);
36
+ done(ps_optimistic_fetcher_service_1.PsOptimisticFetcherService.outcome(fetcher.data, dataBefore.current));
34
37
  }, [fetcher.data, fetcher.state]);
38
+ (0, react_1.useEffect)(() => () => {
39
+ if (settle.current === null)
40
+ return;
41
+ const done = settle.current;
42
+ settle.current = null;
43
+ done({ data: undefined, reason: 'unmounted' });
44
+ }, []);
35
45
  const submitOne = (0, react_1.useCallback)((values) => new Promise((resolve) => {
36
- settle.current = resolve;
46
+ const finish = (outcome) => {
47
+ clearTimeout(timer);
48
+ if (settle.current === finish)
49
+ settle.current = null;
50
+ resolve(outcome);
51
+ };
52
+ const timer = setTimeout(() => finish({ data: undefined, reason: 'timeout' }), ps_optimistic_config_1.PsOptimisticConfig.submitTimeoutMs);
53
+ settle.current = finish;
37
54
  sawBusy.current = false;
38
55
  dataBefore.current = dataSeen.current;
39
- void fetcher.submit(values, { defaultShouldRevalidate: false, method: 'post' });
56
+ try {
57
+ void Promise.resolve(fetcher.submit(values, { defaultShouldRevalidate: false, method: 'post' })).catch(() => finish({ data: undefined, reason: 'no-response' }));
58
+ }
59
+ catch {
60
+ finish({ data: undefined, reason: 'no-response' });
61
+ }
40
62
  }), [fetcher]);
41
63
  const send = (0, react_1.useCallback)(async (queued) => {
64
+ failure.current = null;
42
65
  for (const submission of merge === undefined ? queued : merge(queued)) {
43
- const result = await submitOne(submission);
44
- if (!isSuccess(result))
45
- throw result;
66
+ const outcome = await submitOne(submission);
67
+ const reason = ps_optimistic_fetcher_service_1.PsOptimisticFetcherService.failureReason(outcome, isSuccess);
68
+ if (reason !== null) {
69
+ failure.current = { data: outcome.data, reason };
70
+ throw new Error(reason);
71
+ }
46
72
  }
47
73
  }, [isSuccess, merge, submitOne]);
48
74
  const revalidate = (0, react_1.useCallback)(() => {
49
75
  const through = submitted.current;
50
- void Promise.resolve(revalidator.revalidate()).finally(() => {
76
+ const release = () => {
51
77
  if (submitted.current === through)
52
78
  setIsUnsettled(false);
53
- });
79
+ };
80
+ void new Promise((resolve) => resolve(revalidator.revalidate()))
81
+ .catch(() => undefined)
82
+ .then(release);
54
83
  }, [revalidator]);
55
84
  const forgetFailed = (0, react_1.useCallback)((failed, dropped) => {
56
85
  for (let index = 0; index < failed.length + dropped.length; index += 1)
57
86
  forgetLast();
58
- }, [forgetLast]);
87
+ const cause = failure.current;
88
+ failure.current = null;
89
+ if (onError === undefined)
90
+ return;
91
+ if (cause === null) {
92
+ onError({ changes: [...failed, ...dropped], data: undefined, reason: 'error' });
93
+ return;
94
+ }
95
+ onError({ ...cause, changes: [...failed, ...dropped] });
96
+ }, [forgetLast, onError]);
59
97
  const queue = (0, use_ps_change_queue_1.usePsChangeQueue)({
60
98
  onDrained: revalidate,
61
99
  onFailed: forgetFailed,
@@ -1,3 +1,3 @@
1
1
  import type { PsOptimisticSaveInput } from '../interfaces/ps-optimistic-save-input';
2
2
  import type { PsOptimisticSaveResult } from '../interfaces/ps-optimistic-save-result';
3
- export declare const usePsOptimisticSave: <Row, Values, Data>({ fetcher, isSuccess, revalidator, rows, scopeId, spec, }: PsOptimisticSaveInput<Row, Values, Data>) => PsOptimisticSaveResult<Row, Values>;
3
+ export declare const usePsOptimisticSave: <Row, Values, Data>({ fetcher, isSuccess, onError, revalidator, rows, scopeId, spec, }: PsOptimisticSaveInput<Row, Values, Data>) => PsOptimisticSaveResult<Row, Values>;
@@ -4,12 +4,13 @@ exports.usePsOptimisticSave = void 0;
4
4
  const react_1 = require("react");
5
5
  const ps_optimistic_save_service_1 = require("../services/ps-optimistic-save-service");
6
6
  const use_ps_optimistic_fetcher_1 = require("./use-ps-optimistic-fetcher");
7
- const usePsOptimisticSave = ({ fetcher, isSuccess, revalidator, rows, scopeId, spec, }) => {
7
+ const usePsOptimisticSave = ({ fetcher, isSuccess, onError, revalidator, rows, scopeId, spec, }) => {
8
8
  const apply = (0, react_1.useCallback)((base, changes) => ps_optimistic_save_service_1.PsOptimisticSaveService.apply(base, changes, spec), [spec]);
9
9
  const optimistic = (0, use_ps_optimistic_fetcher_1.usePsOptimisticFetcher)({
10
10
  apply,
11
11
  fetcher,
12
12
  isSuccess,
13
+ onError,
13
14
  revalidator,
14
15
  rows,
15
16
  });
package/dist/index.d.ts CHANGED
@@ -95,6 +95,8 @@ export * from './interfaces/ps-live-input';
95
95
  export * from './interfaces/ps-live-open-options';
96
96
  export * from './interfaces/ps-optimistic-fetcher-input';
97
97
  export * from './interfaces/ps-optimistic-fetcher-result';
98
+ export * from './interfaces/ps-optimistic-failure';
99
+ export * from './interfaces/ps-optimistic-failure-reason';
98
100
  export * from './interfaces/ps-revalidator-like';
99
101
  export * from './interfaces/ps-optimistic-rows-input';
100
102
  export * from './interfaces/ps-optimistic-rows-result';
package/dist/index.js CHANGED
@@ -111,6 +111,8 @@ __exportStar(require("./interfaces/ps-live-input"), exports);
111
111
  __exportStar(require("./interfaces/ps-live-open-options"), exports);
112
112
  __exportStar(require("./interfaces/ps-optimistic-fetcher-input"), exports);
113
113
  __exportStar(require("./interfaces/ps-optimistic-fetcher-result"), exports);
114
+ __exportStar(require("./interfaces/ps-optimistic-failure"), exports);
115
+ __exportStar(require("./interfaces/ps-optimistic-failure-reason"), exports);
114
116
  __exportStar(require("./interfaces/ps-revalidator-like"), exports);
115
117
  __exportStar(require("./interfaces/ps-optimistic-rows-input"), exports);
116
118
  __exportStar(require("./interfaces/ps-optimistic-rows-result"), exports);
@@ -0,0 +1 @@
1
+ export type PsOptimisticFailureReason = 'error' | 'no-response' | 'rejected' | 'timeout' | 'unmounted';
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ import type { PsOptimisticFailureReason } from './ps-optimistic-failure-reason';
2
+ export interface PsOptimisticFailure<Data> {
3
+ changes: Record<string, string>[];
4
+ data: Data | undefined;
5
+ reason: PsOptimisticFailureReason;
6
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,4 +1,5 @@
1
1
  import type { PsFetcherLike } from './ps-fetcher-like';
2
+ import type { PsOptimisticFailure } from './ps-optimistic-failure';
2
3
  import type { PsRevalidatorLike } from './ps-revalidator-like';
3
4
  export interface PsOptimisticFetcherInput<Row, Data> {
4
5
  apply: (rows: Row[], changes: Record<string, string>[]) => Row[];
@@ -6,6 +7,7 @@ export interface PsOptimisticFetcherInput<Row, Data> {
6
7
  isHolding?: boolean;
7
8
  isSuccess: (data: Data | undefined) => boolean;
8
9
  merge?: (changes: Record<string, string>[]) => Record<string, string>[];
10
+ onError?: (failure: PsOptimisticFailure<Data>) => void;
9
11
  revalidator: PsRevalidatorLike;
10
12
  rows: Row[];
11
13
  }
@@ -0,0 +1,5 @@
1
+ import type { PsOptimisticFailureReason } from './ps-optimistic-failure-reason';
2
+ export interface PsOptimisticOutcome<Data> {
3
+ data: Data | undefined;
4
+ reason: PsOptimisticFailureReason | 'settled';
5
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,9 +1,11 @@
1
1
  import type { PsFetcherLike } from './ps-fetcher-like';
2
+ import type { PsOptimisticFailure } from './ps-optimistic-failure';
2
3
  import type { PsOptimisticSaveSpec } from './ps-optimistic-save-spec';
3
4
  import type { PsRevalidatorLike } from './ps-revalidator-like';
4
5
  export interface PsOptimisticSaveInput<Row, Values, Data> {
5
6
  fetcher: PsFetcherLike<Data>;
6
7
  isSuccess: (data: Data | undefined) => boolean;
8
+ onError?: (failure: PsOptimisticFailure<Data>) => void;
7
9
  revalidator: PsRevalidatorLike;
8
10
  rows: Row[];
9
11
  scopeId: string;
@@ -0,0 +1,6 @@
1
+ import type { PsOptimisticFailureReason } from '../interfaces/ps-optimistic-failure-reason';
2
+ import type { PsOptimisticOutcome } from '../interfaces/ps-optimistic-outcome';
3
+ export declare class PsOptimisticFetcherService {
4
+ static outcome<Data>(data: Data | undefined, before: Data | undefined): PsOptimisticOutcome<Data>;
5
+ static failureReason<Data>(outcome: PsOptimisticOutcome<Data>, isSuccess: (data: Data | undefined) => boolean): PsOptimisticFailureReason | null;
6
+ }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsOptimisticFetcherService = void 0;
4
+ class PsOptimisticFetcherService {
5
+ static outcome(data, before) {
6
+ if (data === before)
7
+ return { data: undefined, reason: 'no-response' };
8
+ return { data, reason: 'settled' };
9
+ }
10
+ static failureReason(outcome, isSuccess) {
11
+ if (outcome.reason !== 'settled')
12
+ return outcome.reason;
13
+ return isSuccess(outcome.data) ? null : 'rejected';
14
+ }
15
+ }
16
+ exports.PsOptimisticFetcherService = PsOptimisticFetcherService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pallasoft/ps-core-components",
3
- "version": "2.35.1",
3
+ "version": "2.35.2",
4
4
  "private": false,
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/esm/index.js",