@wheelhouse/ui 0.2.6 → 0.2.7

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 (73) hide show
  1. package/dist/blocks/columns/column-popover-panel-header.d.ts +10 -0
  2. package/dist/blocks/columns/column-popover-panel-header.d.ts.map +1 -0
  3. package/dist/blocks/columns/column-popover-panel-header.js +9 -0
  4. package/dist/blocks/columns/columns-add-view.d.ts +12 -0
  5. package/dist/blocks/columns/columns-add-view.d.ts.map +1 -0
  6. package/dist/blocks/columns/columns-add-view.js +34 -0
  7. package/dist/blocks/columns/columns-types.d.ts +26 -1
  8. package/dist/blocks/columns/columns-types.d.ts.map +1 -1
  9. package/dist/blocks/columns/columns-types.js +9 -2
  10. package/dist/blocks/columns/columns-utils.d.ts +7 -3
  11. package/dist/blocks/columns/columns-utils.d.ts.map +1 -1
  12. package/dist/blocks/columns/columns-utils.js +28 -6
  13. package/dist/blocks/columns/columns.d.ts.map +1 -1
  14. package/dist/blocks/columns/columns.js +106 -68
  15. package/dist/blocks/columns/columns.stories.d.ts +1 -0
  16. package/dist/blocks/columns/columns.stories.d.ts.map +1 -1
  17. package/dist/blocks/columns/columns.stories.js +19 -4
  18. package/dist/blocks/columns/index.d.ts +1 -1
  19. package/dist/blocks/columns/index.d.ts.map +1 -1
  20. package/dist/blocks/columns/index.js +1 -1
  21. package/dist/components/avatar/avatar.d.ts +3 -2
  22. package/dist/components/avatar/avatar.d.ts.map +1 -1
  23. package/dist/components/avatar/avatar.js +3 -2
  24. package/dist/components/avatar/avatar.stories.d.ts.map +1 -1
  25. package/dist/components/avatar/avatar.stories.js +7 -0
  26. package/dist/components/button/button.d.ts +3 -3
  27. package/dist/components/button/button.js +7 -7
  28. package/dist/components/button-group/button-group.js +2 -2
  29. package/dist/components/calendar/calendar.js +2 -2
  30. package/dist/components/data-grid/data-grid-column-filter.js +1 -1
  31. package/dist/components/field/field.d.ts +1 -1
  32. package/dist/components/field/field.d.ts.map +1 -1
  33. package/dist/components/filters/filter-date-metric-value.js +1 -1
  34. package/dist/components/filters/filters.d.ts.map +1 -1
  35. package/dist/components/filters/filters.js +3 -2
  36. package/dist/components/index.d.ts +2 -0
  37. package/dist/components/index.d.ts.map +1 -1
  38. package/dist/components/index.js +2 -0
  39. package/dist/components/input/input.d.ts +2 -2
  40. package/dist/components/input/input.d.ts.map +1 -1
  41. package/dist/components/input/input.js +4 -3
  42. package/dist/components/input/input.stories.d.ts +1 -1
  43. package/dist/components/input/input.stories.d.ts.map +1 -1
  44. package/dist/components/input/input.stories.js +1 -1
  45. package/dist/components/input-group/input-group.d.ts +1 -1
  46. package/dist/components/number-field/index.d.ts +2 -0
  47. package/dist/components/number-field/index.d.ts.map +1 -0
  48. package/dist/components/number-field/index.js +1 -0
  49. package/dist/components/number-field/number-field.d.ts +59 -0
  50. package/dist/components/number-field/number-field.d.ts.map +1 -0
  51. package/dist/components/number-field/number-field.js +49 -0
  52. package/dist/components/number-field/number-field.stories.d.ts +25 -0
  53. package/dist/components/number-field/number-field.stories.d.ts.map +1 -0
  54. package/dist/components/number-field/number-field.stories.js +225 -0
  55. package/dist/components/overlapping-stack/index.d.ts +3 -0
  56. package/dist/components/overlapping-stack/index.d.ts.map +1 -0
  57. package/dist/components/overlapping-stack/index.js +2 -0
  58. package/dist/components/overlapping-stack/overlapping-stack.d.ts +12 -0
  59. package/dist/components/overlapping-stack/overlapping-stack.d.ts.map +1 -0
  60. package/dist/components/overlapping-stack/overlapping-stack.js +45 -0
  61. package/dist/components/overlapping-stack/overlapping-stack.stories.d.ts +78 -0
  62. package/dist/components/overlapping-stack/overlapping-stack.stories.d.ts.map +1 -0
  63. package/dist/components/overlapping-stack/overlapping-stack.stories.js +120 -0
  64. package/dist/components/overlapping-stack/use-overlapping-stack.d.ts +47 -0
  65. package/dist/components/overlapping-stack/use-overlapping-stack.d.ts.map +1 -0
  66. package/dist/components/overlapping-stack/use-overlapping-stack.js +47 -0
  67. package/dist/components/sidebar/sidebar.js +1 -1
  68. package/dist/components/textarea/textarea.js +1 -1
  69. package/dist/components/toggle/toggle.d.ts +3 -3
  70. package/dist/components/toggle/toggle.js +4 -4
  71. package/dist/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +2 -1
  73. package/src/styles/globals.css +6 -24
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function ColumnPopoverPanelHeader({ title, explainer, helpAriaLabel, trailing, className, }: {
3
+ title: string;
4
+ /** When non-empty, shows a help icon with this text in a tooltip. */
5
+ explainer?: string;
6
+ helpAriaLabel?: string;
7
+ trailing?: ReactNode;
8
+ className?: string;
9
+ }): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=column-popover-panel-header.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"column-popover-panel-header.d.ts","sourceRoot":"","sources":["../../../src/blocks/columns/column-popover-panel-header.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAMvC,wBAAgB,wBAAwB,CAAC,EACrC,KAAK,EACL,SAAS,EACT,aAAsB,EACtB,QAAQ,EACR,SAAS,GACZ,EAAE;IACC,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,2CA6BA"}
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { CircleHelp } from 'lucide-react';
4
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../../components/tooltip';
5
+ import { cn } from '../../lib/utils';
6
+ export function ColumnPopoverPanelHeader({ title, explainer, helpAriaLabel = 'Help', trailing, className, }) {
7
+ const helpText = explainer?.trim();
8
+ return (_jsxs("div", { className: cn('flex shrink-0 items-center justify-between gap-2 border-b border-border px-3 py-2.5', className), children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1", children: [_jsx("p", { className: "text-sm font-medium", children: title }), helpText ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx("button", { type: "button", className: "inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", "aria-label": helpAriaLabel }), children: _jsx(CircleHelp, { className: "size-3.5", "aria-hidden": true }) }), _jsx(TooltipContent, { side: "bottom", align: "start", className: "max-w-[min(100vw-2rem,280px)] text-pretty", children: helpText })] })) : null] }), trailing] }));
9
+ }
@@ -0,0 +1,12 @@
1
+ import type { ColumnsI18n, SelectedColumn } from './columns-types';
2
+ import { type ColumnsMenuSection } from './columns-utils';
3
+ export declare function ColumnsAddView({ i18n, sections, value, pendingAddIds, onTogglePendingAdd, onCancel, onConfirm, }: {
4
+ i18n: ColumnsI18n;
5
+ sections: ColumnsMenuSection[];
6
+ value: SelectedColumn[];
7
+ pendingAddIds: Set<string>;
8
+ onTogglePendingAdd: (id: string, checked: boolean) => void;
9
+ onCancel: () => void;
10
+ onConfirm: () => void;
11
+ }): import("react/jsx-runtime").JSX.Element;
12
+ //# sourceMappingURL=columns-add-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"columns-add-view.d.ts","sourceRoot":"","sources":["../../../src/blocks/columns/columns-add-view.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAoB,WAAW,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACrF,OAAO,EAAiC,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAgCzF,wBAAgB,cAAc,CAAC,EAC3B,IAAI,EACJ,QAAQ,EACR,KAAK,EACL,aAAa,EACb,kBAAkB,EAClB,QAAQ,EACR,SAAS,GACZ,EAAE;IACC,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,kBAAkB,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3D,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;CACzB,2CA0DA"}
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronRight } from 'lucide-react';
4
+ import { Button } from '../../components/button';
5
+ import { Checkbox } from '../../components/checkbox/checkbox';
6
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../components/collapsible';
7
+ import { ScrollArea } from '../../components/scroll-area';
8
+ import { ColumnPopoverPanelHeader } from './column-popover-panel-header';
9
+ import { columnAllowsMultipleInstances } from './columns-utils';
10
+ function isFieldAvailableToAddFromCatalog(field, currentValue) {
11
+ if (columnAllowsMultipleInstances(field))
12
+ return true;
13
+ return !currentValue.some((v) => v.id === field.id);
14
+ }
15
+ const addCatalogFieldRowClassName = 'flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1 text-start text-sm text-foreground hover:bg-accent hover:text-accent-foreground';
16
+ function AddCatalogFieldRow({ field, checked, onToggle, showIcon = false, }) {
17
+ const checkboxId = `columns-add-${field.id}`;
18
+ return (_jsxs("label", { htmlFor: checkboxId, className: addCatalogFieldRowClassName, children: [_jsx(Checkbox, { id: checkboxId, checked: checked, onCheckedChange: (next) => onToggle(field.id, next === true) }), showIcon ? field.icon : null, _jsx("span", { className: "min-w-0 flex-1 truncate", children: field.name })] }));
19
+ }
20
+ export function ColumnsAddView({ i18n, sections, value, pendingAddIds, onTogglePendingAdd, onCancel, onConfirm, }) {
21
+ return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [_jsx(ColumnPopoverPanelHeader, { title: i18n.addColumnsAction, explainer: i18n.addColumnsStagedExplainer }), _jsxs(ScrollArea, { className: "min-h-0 flex-1", children: [_jsxs("div", { className: "sticky top-0 z-10 border-b border-border bg-popover px-3 py-2 text-xs text-muted-foreground", children: [_jsx("span", { className: "font-medium text-primary tabular-nums", children: value.length }), " ", i18n.columnsSelectedSuffix(value.length)] }), _jsx("div", { className: "flex flex-col gap-2 px-3 py-2 pb-2", children: sections.map((section, si) => {
22
+ const available = section.fields.filter((f) => isFieldAvailableToAddFromCatalog(f, value));
23
+ if (available.length === 0)
24
+ return null;
25
+ const flatUngrouped = !section.groupLabel && available.length === 1;
26
+ if (flatUngrouped) {
27
+ const field = available[0];
28
+ return (_jsx(AddCatalogFieldRow, { field: field, checked: pendingAddIds.has(field.id), onToggle: onTogglePendingAdd, showIcon: true }, field.id));
29
+ }
30
+ const heading = section.groupLabel ??
31
+ (available.length > 1 ? i18n.addColumnsSectionFallbackTitle : (available[0]?.name ?? i18n.addColumnsSectionFallbackTitle));
32
+ return (_jsxs(Collapsible, { defaultOpen: true, className: "flex flex-col gap-1", children: [_jsxs(CollapsibleTrigger, { className: "group flex w-full items-center gap-2 rounded-md px-1 py-1 text-start text-sm font-medium hover:bg-muted/80", type: "button", children: [available[0]?.icon, _jsx("span", { className: "min-w-0 flex-1 truncate", children: heading }), _jsx(ChevronRight, { className: "size-4 shrink-0 text-muted-foreground transition-transform group-data-[panel-open]:rotate-90" })] }), _jsx(CollapsibleContent, { className: "flex flex-col", children: available.map((field) => (_jsx(AddCatalogFieldRow, { field: field, checked: pendingAddIds.has(field.id), onToggle: onTogglePendingAdd }, field.id))) })] }, si));
33
+ }) })] }), _jsxs("div", { className: "flex shrink-0 items-center justify-end gap-2 border-t border-border px-3 py-2.5", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onCancel, children: i18n.cancel }), _jsx(Button, { type: "button", size: "sm", disabled: pendingAddIds.size === 0, onClick: onConfirm, children: i18n.addSelectedColumns })] })] }));
34
+ }
@@ -6,6 +6,11 @@ export interface ColumnDefinition {
6
6
  id: string;
7
7
  name: string;
8
8
  description?: string;
9
+ /**
10
+ * When true, the field can appear multiple times in the selection (e.g. different date windows).
11
+ * Default: same as `supportsDateRange` (date-metric columns allow duplicates unless set to `false`).
12
+ */
13
+ allowMultipleInstances?: boolean;
9
14
  /** When true, selecting the row opens the date configuration pane. */
10
15
  supportsDateRange?: boolean;
11
16
  /** Merged into DateSelector for this column; overrides `Columns` dateMetricOptions. */
@@ -19,6 +24,8 @@ export interface ColumnFieldGroup {
19
24
  export type ColumnsFieldsConfig = (ColumnDefinition | ColumnFieldGroup)[];
20
25
  export interface SelectedColumn {
21
26
  id: string;
27
+ /** Stable row identity for DnD and updates when multiple rows share the same catalog `id`. */
28
+ instanceId?: string;
22
29
  dateRange?: DateSelectorValue;
23
30
  /**
24
31
  * Pin state when pin controls are shown (`false` unpinned, `true` pinned).
@@ -29,13 +36,31 @@ export interface SelectedColumn {
29
36
  export interface ColumnsI18n {
30
37
  triggerLabel: string;
31
38
  triggerCount: (n: number) => string;
39
+ /** Main popover: title above the selected-column list (typically “Columns”). */
32
40
  addColumnsTitle: string;
33
- addColumnsSubtitle: string;
41
+ /** Opens the staged add-columns view; also used as the add-step panel title (typically “Add columns”). */
42
+ addColumnsAction: string;
43
+ /** Optional tooltip copy for the manage view header (help icon only when non-empty). */
44
+ manageColumnsExplainer?: string;
45
+ /** Optional tooltip copy for the staged add view header (help icon only when non-empty). */
46
+ addColumnsStagedExplainer?: string;
34
47
  clearAll: string;
35
48
  /** Shown after the numeric count in the footer (e.g. "columns selected"). */
36
49
  columnsSelectedSuffix: (n: number) => string;
37
50
  configureDateRangeHint: string;
38
51
  done: string;
52
+ /** Shown in the manage view when `value` is empty. */
53
+ noColumnsSelected: string;
54
+ /** Staged add view: confirm (appends checked columns, then returns to manage). */
55
+ addSelectedColumns: string;
56
+ /** Staged add view: discard staged selection and return to manage. */
57
+ cancel: string;
58
+ /** `aria-label` for the header help control that reveals extra guidance in a tooltip. */
59
+ headerSectionHelpAriaLabel: string;
60
+ /**
61
+ * Staged add view: section heading when the catalog section has no `group` label and multiple fields are shown together.
62
+ */
63
+ addColumnsSectionFallbackTitle: string;
39
64
  /** Accessible label for the pin control when the column is unpinned. */
40
65
  pinColumn: string;
41
66
  /** Accessible label for the pin control when the column is pinned. */
@@ -1 +1 @@
1
- {"version":3,"file":"columns-types.d.ts","sourceRoot":"","sources":["../../../src/blocks/columns/columns-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE7E,uHAAuH;AACvH,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,iBAAiB,EAAE,OAAO,GAAG,UAAU,CAAC,CAAC;AAErF,MAAM,WAAW,gBAAgB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,uFAAuF;IACvF,iBAAiB,CAAC,EAAE,OAAO,CAAC,wBAAwB,CAAC,CAAC;IACtD,IAAI,CAAC,EAAE,SAAS,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AAED,MAAM,MAAM,mBAAmB,GAAG,CAAC,gBAAgB,GAAG,gBAAgB,CAAC,EAAE,CAAC;AAE1E,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,qBAAqB,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,sBAAsB,EAAE,MAAM,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,oBAAoB,EAAE,WAalC,CAAC;AAEF,MAAM,WAAW,YAAY;IACzB,OAAO,EAAE,mBAAmB,CAAC;IAC7B,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC5B,kHAAkH;IAClH,iBAAiB,CAAC,EAAE,OAAO,CAAC,wBAAwB,CAAC,CAAC;IACtD;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB"}
1
+ {"version":3,"file":"columns-types.d.ts","sourceRoot":"","sources":["../../../src/blocks/columns/columns-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE7E,uHAAuH;AACvH,MAAM,MAAM,wBAAwB,GAAG,IAAI,CAAC,iBAAiB,EAAE,OAAO,GAAG,UAAU,CAAC,CAAC;AAErF,MAAM,WAAW,gBAAgB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,uFAAuF;IACvF,iBAAiB,CAAC,EAAE,OAAO,CAAC,wBAAwB,CAAC,CAAC;IACtD,IAAI,CAAC,EAAE,SAAS,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AAED,MAAM,MAAM,mBAAmB,GAAG,CAAC,gBAAgB,GAAG,gBAAgB,CAAC,EAAE,CAAC;AAE1E,MAAM,WAAW,cAAc;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,8FAA8F;IAC9F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,gFAAgF;IAChF,eAAe,EAAE,MAAM,CAAC;IACxB,0GAA0G;IAC1G,gBAAgB,EAAE,MAAM,CAAC;IACzB,wFAAwF;IACxF,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,4FAA4F;IAC5F,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,qBAAqB,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,sBAAsB,EAAE,MAAM,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,kBAAkB,EAAE,MAAM,CAAC;IAC3B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,yFAAyF;IACzF,0BAA0B,EAAE,MAAM,CAAC;IACnC;;OAEG;IACH,8BAA8B,EAAE,MAAM,CAAC;IACvC,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,oBAAoB,EAAE,WAoBlC,CAAC;AAEF,MAAM,WAAW,YAAY;IACzB,OAAO,EAAE,mBAAmB,CAAC;IAC7B,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC5B,kHAAkH;IAClH,iBAAiB,CAAC,EAAE,OAAO,CAAC,wBAAwB,CAAC,CAAC;IACtD;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB"}
@@ -1,12 +1,19 @@
1
1
  export const DEFAULT_COLUMNS_I18N = {
2
2
  triggerLabel: 'Columns',
3
3
  triggerCount: (n) => `${n} column${n === 1 ? '' : 's'}`,
4
- addColumnsTitle: 'Add Columns',
5
- addColumnsSubtitle: 'Select columns to add to the table',
4
+ addColumnsTitle: 'Columns',
5
+ addColumnsAction: 'Add columns',
6
+ manageColumnsExplainer: 'Reorder, pin, and remove columns. Configure date ranges where available.',
7
+ addColumnsStagedExplainer: 'Pick columns, then Add.',
6
8
  clearAll: 'Clear all',
7
9
  columnsSelectedSuffix: (n) => (n === 1 ? 'column selected' : 'columns selected'),
8
10
  configureDateRangeHint: 'Configure date range for this column',
9
11
  done: 'Done',
12
+ noColumnsSelected: 'No columns selected.',
13
+ addSelectedColumns: 'Add',
14
+ cancel: 'Cancel',
15
+ headerSectionHelpAriaLabel: 'Help',
16
+ addColumnsSectionFallbackTitle: 'Columns',
10
17
  pinColumn: 'Pin column',
11
18
  unpinColumn: 'Unpin column',
12
19
  pinnedHeading: 'Pinned',
@@ -1,5 +1,9 @@
1
1
  import type { DateSelectorValue } from '../date-selector';
2
2
  import type { ColumnDefinition, ColumnsFieldsConfig, SelectedColumn } from './columns-types';
3
+ export declare function getSelectedColumnRowKey(row: SelectedColumn): string;
4
+ export declare function newSelectedColumn(definitionId: string): SelectedColumn;
5
+ /** When `allowMultipleInstances` is unset, date-metric columns default to allowing duplicates. */
6
+ export declare function columnAllowsMultipleInstances(def: ColumnDefinition): boolean;
3
7
  /** Result of splitting `value` into pinned (prefix) and unpinned (suffix) partitions. */
4
8
  export type SplitPinnedPrefixResult = {
5
9
  pinned: SelectedColumn[];
@@ -20,11 +24,11 @@ export declare function mergePinnedPartitions(pinned: SelectedColumn[], unpinned
20
24
  */
21
25
  export declare function normalizePinnedColumnOrder(value: SelectedColumn[]): SelectedColumn[];
22
26
  /**
23
- * Toggles `pinned` for the column with `id`, moving it to the end of the pinned block
27
+ * Toggles `pinned` for the row with `rowKey` (see {@link getSelectedColumnRowKey}), moving it to the end of the pinned block
24
28
  * when pinning or to the start of the unpinned block when unpinning.
25
- * Unknown ids return a shallow copy of `value` unchanged.
29
+ * Unknown keys return a shallow copy of `value` unchanged.
26
30
  */
27
- export declare function toggleColumnPinned(value: SelectedColumn[], id: string): SelectedColumn[];
31
+ export declare function toggleColumnPinned(value: SelectedColumn[], rowKey: string): SelectedColumn[];
28
32
  /**
29
33
  * Applies a reorder from sortable indices; returns `null` when the move is invalid or a no-op.
30
34
  * With `lockPinning`, moves across the contiguous pinned / unpinned boundary are rejected.
@@ -1 +1 @@
1
- {"version":3,"file":"columns-utils.d.ts","sourceRoot":"","sources":["../../../src/blocks/columns/columns-utils.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAoB,mBAAmB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAE/G,yFAAyF;AACzF,MAAM,MAAM,uBAAuB,GAAG;IAClC,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC9B,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,uBAAuB,CAWlF;AAED,6GAA6G;AAC7G,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,MAAM,CAOlE;AAED,qDAAqD;AACrD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CAE5G;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CAGpF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,EAAE,MAAM,GAAG,cAAc,EAAE,CAoBxF;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,cAAc,EAAE,GAAG,IAAI,CAatJ;AAED,uGAAuG;AACvG,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAoDtF;AAMD,wBAAgB,cAAc,CAAC,MAAM,EAAE,mBAAmB,GAAG,gBAAgB,EAAE,CAO9E;AAED,MAAM,MAAM,kBAAkB,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAAC;AAErF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,kBAAkB,EAAE,CAWrF;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAE5E;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAS5F"}
1
+ {"version":3,"file":"columns-utils.d.ts","sourceRoot":"","sources":["../../../src/blocks/columns/columns-utils.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAoB,mBAAmB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAE/G,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,CAEnE;AAED,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,cAAc,CAEtE;AAED,kGAAkG;AAClG,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAK5E;AAaD,yFAAyF;AACzF,MAAM,MAAM,uBAAuB,GAAG;IAClC,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC9B,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,uBAAuB,CAWlF;AAED,6GAA6G;AAC7G,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,MAAM,CAOlE;AAED,qDAAqD;AACrD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CAE5G;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CAGpF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE,CAoB5F;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,cAAc,EAAE,GAAG,IAAI,CAatJ;AAED,uGAAuG;AACvG,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAoDtF;AAMD,wBAAgB,cAAc,CAAC,MAAM,EAAE,mBAAmB,GAAG,gBAAgB,EAAE,CAO9E;AAED,MAAM,MAAM,kBAAkB,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAAC;AAErF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,kBAAkB,EAAE,CAWrF;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAE5E;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAS5F"}
@@ -1,7 +1,29 @@
1
- import { arrayMove } from '@dnd-kit/sortable';
2
1
  import { format } from 'date-fns';
3
2
  import { DEFAULT_DATE_SELECTOR_I18N } from '../date-selector/date-selector-types';
4
3
  import { getRollingSummaryLabel } from '../date-selector/date-selector-value';
4
+ export function getSelectedColumnRowKey(row) {
5
+ return row.instanceId ?? row.id;
6
+ }
7
+ export function newSelectedColumn(definitionId) {
8
+ return { id: definitionId, instanceId: crypto.randomUUID() };
9
+ }
10
+ /** When `allowMultipleInstances` is unset, date-metric columns default to allowing duplicates. */
11
+ export function columnAllowsMultipleInstances(def) {
12
+ if (def.allowMultipleInstances !== undefined) {
13
+ return def.allowMultipleInstances;
14
+ }
15
+ return def.supportsDateRange === true;
16
+ }
17
+ /**
18
+ * Move an array item to a new index (inclusive semantics match `@dnd-kit/sortable` `arrayMove`).
19
+ * Implemented locally so this module stays free of `@dnd-kit/sortable`, whose entry re-exports
20
+ * hooks and cannot run under the React Server Components `react` build.
21
+ */
22
+ function arrayMove(array, from, to) {
23
+ const next = array.slice();
24
+ next.splice(to < 0 ? next.length + to : to, 0, next.splice(from, 1)[0]);
25
+ return next;
26
+ }
5
27
  /**
6
28
  * Splits selected columns into pinned vs unpinned rows in display order.
7
29
  * Uses `pinned === true` for the pinned partition; all other rows go to unpinned.
@@ -43,21 +65,21 @@ export function normalizePinnedColumnOrder(value) {
43
65
  return [...pinned.map((c) => ({ ...c, pinned: true })), ...unpinned.map((c) => ({ ...c, pinned: false }))];
44
66
  }
45
67
  /**
46
- * Toggles `pinned` for the column with `id`, moving it to the end of the pinned block
68
+ * Toggles `pinned` for the row with `rowKey` (see {@link getSelectedColumnRowKey}), moving it to the end of the pinned block
47
69
  * when pinning or to the start of the unpinned block when unpinning.
48
- * Unknown ids return a shallow copy of `value` unchanged.
70
+ * Unknown keys return a shallow copy of `value` unchanged.
49
71
  */
50
- export function toggleColumnPinned(value, id) {
72
+ export function toggleColumnPinned(value, rowKey) {
51
73
  const { pinned: p0, unpinned: u0 } = splitPinnedPrefix(value);
52
74
  const pinned = [...p0];
53
75
  const unpinned = [...u0];
54
- const pi = pinned.findIndex((c) => c.id === id);
76
+ const pi = pinned.findIndex((c) => getSelectedColumnRowKey(c) === rowKey);
55
77
  if (pi >= 0) {
56
78
  const [row] = pinned.splice(pi, 1);
57
79
  unpinned.unshift({ ...row, pinned: false });
58
80
  return mergePinnedPartitions(pinned, unpinned);
59
81
  }
60
- const ui = unpinned.findIndex((c) => c.id === id);
82
+ const ui = unpinned.findIndex((c) => getSelectedColumnRowKey(c) === rowKey);
61
83
  if (ui >= 0) {
62
84
  const [row] = unpinned.splice(ui, 1);
63
85
  pinned.push({ ...row, pinned: true });
@@ -1 +1 @@
1
- {"version":3,"file":"columns.d.ts","sourceRoot":"","sources":["../../../src/blocks/columns/columns.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAA4B,YAAY,EAAkB,MAAM,iBAAiB,CAAC;AAsB9F,wBAAgB,OAAO,CAAC,EACpB,OAAO,EACP,KAAK,EACL,QAAQ,EACR,SAAS,EACT,uBAAuB,EACvB,IAAI,EAAE,WAAW,EACjB,iBAAiB,EAAE,qBAAqB,EACxC,WAAmB,GACtB,EAAE,YAAY,2CA+Wd"}
1
+ {"version":3,"file":"columns.d.ts","sourceRoot":"","sources":["../../../src/blocks/columns/columns.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAA4B,YAAY,EAAkB,MAAM,iBAAiB,CAAC;AA6B9F,wBAAgB,OAAO,CAAC,EACpB,OAAO,EACP,KAAK,EACL,QAAQ,EACR,SAAS,EACT,uBAAuB,EACvB,IAAI,EAAE,WAAW,EACjB,iBAAiB,EAAE,qBAAqB,EACxC,WAAmB,GACtB,EAAE,YAAY,2CAwWd"}
@@ -1,18 +1,19 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useCallback, useMemo, useState } from 'react';
4
4
  import { closestCenter } from '@dnd-kit/core';
5
- import { ChevronDown, ChevronRight, Columns3, GripVertical, Pin, PinOff, X } from 'lucide-react';
5
+ import { ChevronDown, Columns3, GripVertical, Pin, PinOff, X } from 'lucide-react';
6
6
  import { Button } from '../../components/button';
7
- import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../components/collapsible';
8
7
  import { Popover, PopoverContent, PopoverTrigger } from '../../components/popover';
8
+ import { TooltipProvider } from '../../components/tooltip';
9
9
  import { ScrollArea } from '../../components/scroll-area';
10
- import { Separator } from '../../components/separator';
11
10
  import { Sortable, SortableItem, SortableItemHandle } from '../../components/sortable';
12
11
  import { cn } from '../../lib/utils';
13
12
  import { DateSelector } from '../date-selector';
14
13
  import { DEFAULT_COLUMNS_I18N } from './columns-types';
15
- import { buildColumnMap, formatColumnDateRangeSecondary, parseColumnSections, pinnedPrefixLength, reorderColumnsSelection, toggleColumnPinned, } from './columns-utils';
14
+ import { ColumnPopoverPanelHeader } from './column-popover-panel-header';
15
+ import { ColumnsAddView } from './columns-add-view';
16
+ import { buildColumnMap, columnAllowsMultipleInstances, formatColumnDateRangeSecondary, getSelectedColumnRowKey, newSelectedColumn, parseColumnSections, pinnedPrefixLength, reorderColumnsSelection, toggleColumnPinned, } from './columns-utils';
16
17
  const DEFAULT_DATE_METRIC_OPTIONS = {
17
18
  showRollingPresets: true,
18
19
  periodTypes: ['day', 'month', 'quarter', 'year'],
@@ -28,10 +29,12 @@ export function Columns({ columns, value, onChange, className, popoverContentCla
28
29
  const columnMap = useMemo(() => buildColumnMap(columns), [columns]);
29
30
  const sections = useMemo(() => parseColumnSections(columns), [columns]);
30
31
  const [open, setOpen] = useState(false);
31
- const [activeColumnId, setActiveColumnId] = useState(null);
32
- const selectedIdSet = useMemo(() => new Set(value.map((v) => v.id)), [value]);
33
- const activeDef = activeColumnId ? columnMap[activeColumnId] : undefined;
34
- const showRightPanel = Boolean(activeDef?.supportsDateRange && activeColumnId && selectedIdSet.has(activeColumnId));
32
+ const [popoverView, setPopoverView] = useState('manage');
33
+ const [pendingAddIds, setPendingAddIds] = useState(() => new Set());
34
+ const [activeRowKey, setActiveRowKey] = useState(null);
35
+ const activeSelected = activeRowKey ? value.find((s) => getSelectedColumnRowKey(s) === activeRowKey) : undefined;
36
+ const activeDef = activeSelected ? columnMap[activeSelected.id] : undefined;
37
+ const showRightPanel = popoverView === 'manage' && Boolean(activeDef?.supportsDateRange && activeRowKey);
35
38
  const mergedDateMetricOptions = useMemo(() => {
36
39
  const colOpts = activeDef?.supportsDateRange ? activeDef.dateMetricOptions : undefined;
37
40
  return {
@@ -41,33 +44,73 @@ export function Columns({ columns, value, onChange, className, popoverContentCla
41
44
  };
42
45
  }, [activeDef, dateMetricOptionsProp]);
43
46
  const { className: dateMetricClassName, ...dateSelectorProps } = mergedDateMetricOptions;
44
- const activeSelected = activeColumnId ? value.find((s) => s.id === activeColumnId) : undefined;
45
47
  const activeDateRange = activeSelected?.dateRange;
48
+ const resetPopoverUi = useCallback(() => {
49
+ setPopoverView('manage');
50
+ setPendingAddIds(new Set());
51
+ setActiveRowKey(null);
52
+ }, []);
46
53
  const handleOpenChange = useCallback((next) => {
47
54
  setOpen(next);
48
55
  if (!next) {
49
- setActiveColumnId(null);
56
+ resetPopoverUi();
50
57
  }
51
- }, []);
52
- const updateSelectedDateRange = useCallback((columnId, dateRange) => {
53
- onChange(value.map((row) => (row.id === columnId ? { ...row, dateRange } : row)));
58
+ }, [resetPopoverUi]);
59
+ const updateSelectedDateRange = useCallback((rowKey, dateRange) => {
60
+ onChange(value.map((row) => (getSelectedColumnRowKey(row) === rowKey ? { ...row, dateRange } : row)));
54
61
  }, [onChange, value]);
55
- const removeColumn = useCallback((id) => {
56
- onChange(value.filter((v) => v.id !== id));
57
- setActiveColumnId((prev) => (prev === id ? null : prev));
62
+ const removeColumn = useCallback((rowKey) => {
63
+ onChange(value.filter((v) => getSelectedColumnRowKey(v) !== rowKey));
64
+ setActiveRowKey((prev) => (prev === rowKey ? null : prev));
58
65
  }, [onChange, value]);
59
66
  const clearAll = useCallback(() => {
60
67
  onChange([]);
61
- setActiveColumnId(null);
62
- }, [onChange]);
63
- const addColumn = useCallback((id) => {
64
- if (selectedIdSet.has(id))
68
+ resetPopoverUi();
69
+ }, [onChange, resetPopoverUi]);
70
+ const goToAddView = useCallback(() => {
71
+ setActiveRowKey(null);
72
+ setPendingAddIds(new Set());
73
+ setPopoverView('add');
74
+ }, []);
75
+ const cancelAddView = useCallback(() => {
76
+ setPendingAddIds(new Set());
77
+ setPopoverView('manage');
78
+ }, []);
79
+ const confirmAddColumns = useCallback(() => {
80
+ if (pendingAddIds.size === 0)
65
81
  return;
66
- onChange([...value, { id }]);
67
- }, [onChange, selectedIdSet, value]);
68
- const handleRowActivate = useCallback((id) => {
69
- setActiveColumnId(id);
82
+ const idsToAdd = Array.from(pendingAddIds);
83
+ let next = [...value];
84
+ for (const defId of idsToAdd) {
85
+ const def = columnMap[defId];
86
+ if (!def || !columnAllowsMultipleInstances(def))
87
+ continue;
88
+ if (next.some((r) => r.id === defId)) {
89
+ next = next.map((r) => (r.id === defId && r.instanceId === undefined ? { ...r, instanceId: crypto.randomUUID() } : r));
90
+ }
91
+ }
92
+ const additions = idsToAdd.map((id) => newSelectedColumn(id));
93
+ onChange([...next, ...additions]);
94
+ setPendingAddIds(new Set());
95
+ setPopoverView('manage');
96
+ }, [onChange, pendingAddIds, value, columnMap]);
97
+ const togglePendingAdd = useCallback((id, checked) => {
98
+ setPendingAddIds((prev) => {
99
+ const next = new Set(prev);
100
+ if (checked)
101
+ next.add(id);
102
+ else
103
+ next.delete(id);
104
+ return next;
105
+ });
70
106
  }, []);
107
+ const handleRowActivate = useCallback((rowKey) => {
108
+ const row = value.find((s) => getSelectedColumnRowKey(s) === rowKey);
109
+ const def = row ? columnMap[row.id] : undefined;
110
+ if (!def?.supportsDateRange)
111
+ return;
112
+ setActiveRowKey(rowKey);
113
+ }, [value, columnMap]);
71
114
  const pinnedPrefixLen = useMemo(() => pinnedPrefixLength(value), [value]);
72
115
  const unpinnedCount = value.length - pinnedPrefixLen;
73
116
  const lockedPartitionCollision = useMemo(() => {
@@ -76,12 +119,12 @@ export function Columns({ columns, value, onChange, className, popoverContentCla
76
119
  return (args) => {
77
120
  const collisions = closestCenter(args);
78
121
  const pc = pinnedPrefixLength(value);
79
- const activeIdx = value.findIndex((item) => item.id === String(args.active.id));
122
+ const activeIdx = value.findIndex((item) => getSelectedColumnRowKey(item) === String(args.active.id));
80
123
  if (activeIdx < 0 || !collisions?.length)
81
124
  return collisions;
82
125
  const activeInPinned = activeIdx < pc;
83
126
  const filtered = collisions.filter((c) => {
84
- const overIdx = value.findIndex((item) => item.id === String(c.id));
127
+ const overIdx = value.findIndex((item) => getSelectedColumnRowKey(item) === String(c.id));
85
128
  if (overIdx < 0)
86
129
  return false;
87
130
  return overIdx < pc === activeInPinned;
@@ -89,8 +132,8 @@ export function Columns({ columns, value, onChange, className, popoverContentCla
89
132
  return filtered;
90
133
  };
91
134
  }, [lockPinning, value]);
92
- const handleTogglePin = useCallback((id) => {
93
- onChange(toggleColumnPinned(value, id));
135
+ const handleTogglePin = useCallback((rowKey) => {
136
+ onChange(toggleColumnPinned(value, rowKey));
94
137
  }, [onChange, value]);
95
138
  const handleColumnsMove = useCallback(({ activeIndex, overIndex }) => {
96
139
  const next = reorderColumnsSelection(value, lockPinning, activeIndex, overIndex);
@@ -98,43 +141,38 @@ export function Columns({ columns, value, onChange, className, popoverContentCla
98
141
  onChange(next);
99
142
  }, [value, onChange, lockPinning]);
100
143
  const count = value.length;
101
- return (_jsx("div", { className: cn(className), children: _jsxs(Popover, { open: open, onOpenChange: handleOpenChange, children: [_jsxs(PopoverTrigger, { render: _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "gap-2 font-normal", "aria-expanded": open, "aria-haspopup": "dialog" }), children: [_jsx(Columns3, { className: "size-4 text-primary", "aria-hidden": true }), _jsxs("span", { className: "text-sm", children: [i18n.triggerLabel, " \u00B7 ", i18n.triggerCount(count)] }), _jsx(ChevronDown, { className: "size-4 opacity-60", "aria-hidden": true })] }), _jsx(PopoverContent, { align: "start", className: cn('w-auto max-w-[min(100vw-2rem,720px)] gap-0 overflow-hidden p-0 shadow-lg ring-1 ring-foreground/10', showRightPanel ? 'min-w-[min(100vw-2rem,720px)]' : 'min-w-[min(100vw-2rem,340px)]', popoverContentClassName), children: _jsxs("div", { className: "flex max-h-[min(85vh,560px)] min-h-0", children: [_jsxs("div", { className: cn('flex min-h-0 min-w-0 flex-1 flex-col border-e border-border', showRightPanel ? 'max-w-[340px]' : 'w-full max-w-[360px]'), children: [_jsx("div", { className: "shrink-0 space-y-1 border-b border-border px-3 py-2.5", children: _jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm font-medium", children: i18n.addColumnsTitle }), _jsx("p", { className: "text-xs text-muted-foreground", children: i18n.addColumnsSubtitle })] }), _jsx("button", { type: "button", onClick: clearAll, disabled: count === 0, className: "shrink-0 text-xs font-medium text-primary hover:underline disabled:pointer-events-none disabled:opacity-40", children: i18n.clearAll })] }) }), _jsx(ScrollArea, { className: "min-h-0 flex-1", children: _jsxs("div", { className: "flex flex-col gap-2 px-3 py-2", children: [value.length > 0 ? (_jsxs("div", { className: "flex flex-col gap-2", children: [lockPinning && pinnedPrefixLen > 0 ? (_jsx("p", { className: "px-0.5 text-xs font-medium text-muted-foreground", children: i18n.pinnedHeading })) : null, _jsx(Sortable, { value: value, onValueChange: onChange, getItemValue: (item) => item.id, onMove: handleColumnsMove, collisionDetection: lockedPartitionCollision, className: "flex flex-col gap-2", children: value.flatMap((row, index) => {
102
- const def = columnMap[row.id];
103
- if (!def)
104
- return [];
105
- const isActive = activeColumnId === row.id;
106
- const secondary = row.dateRange && def.supportsDateRange ? formatColumnDateRangeSecondary(row.dateRange) : null;
107
- const showHighlight = isActive && def.supportsDateRange;
108
- const isPinned = row.pinned === true;
109
- const showSectionDivider = lockPinning && pinnedPrefixLen > 0 && unpinnedCount > 0 && index === pinnedPrefixLen;
110
- const item = (_jsxs(SortableItem, { value: row.id, className: cn('flex min-w-0 items-center gap-2 rounded-md border px-2 py-1.5 text-sm shadow-xs transition-colors', showHighlight
111
- ? 'border-primary bg-primary text-primary-foreground'
112
- : isActive
113
- ? 'border-primary/40 bg-primary/5'
114
- : 'border-border bg-card text-card-foreground'), children: [_jsx(SortableItemHandle, { className: cn('shrink-0 touch-none', showHighlight ? 'text-primary-foreground/80' : 'text-muted-foreground'), "aria-label": `Reorder ${def.name}`, children: _jsx(GripVertical, { className: "size-4", strokeWidth: 2.5 }) }), !lockPinning && row.pinned !== undefined ? (_jsx("button", { type: "button", className: cn('shrink-0 rounded-sm p-0.5 outline-none hover:bg-black/10 focus-visible:ring-2 focus-visible:ring-ring', showHighlight && 'hover:bg-white/15'), "aria-pressed": isPinned, "aria-label": isPinned ? i18n.unpinColumn : i18n.pinColumn, onClick: (e) => {
115
- e.stopPropagation();
116
- handleTogglePin(row.id);
117
- }, children: isPinned ? (_jsx(PinOff, { className: "size-4", "aria-hidden": true })) : (_jsx(Pin, { className: "size-4 opacity-70", "aria-hidden": true })) })) : null, _jsxs("button", { type: "button", className: "flex min-w-0 flex-1 items-center gap-1.5 text-start", onClick: () => handleRowActivate(row.id), children: [_jsx("span", { className: cn('truncate font-medium', !secondary && 'flex-1'), children: def.name }), secondary ? (_jsxs("span", { className: cn('truncate text-xs', showHighlight ? 'text-primary-foreground/85' : 'text-muted-foreground'), children: ["\u00B7 ", secondary] })) : null] }), _jsx("button", { type: "button", className: cn('shrink-0 rounded-sm p-0.5 outline-none hover:bg-black/10 focus-visible:ring-2 focus-visible:ring-ring', showHighlight && 'hover:bg-white/15'), "aria-label": `Remove ${def.name}`, onClick: (e) => {
118
- e.stopPropagation();
119
- removeColumn(row.id);
120
- }, children: _jsx(X, { className: "size-4" }) })] }, row.id));
121
- if (showSectionDivider) {
122
- return [
123
- _jsxs("div", { className: "flex flex-col gap-2 px-0.5 pt-0.5", children: [_jsx(Separator, { className: "bg-border" }), _jsx("p", { className: "text-xs font-medium text-muted-foreground", children: i18n.unpinnedHeading })] }, `columns-unpinned-heading-${row.id}`),
124
- item,
125
- ];
126
- }
127
- return [item];
128
- }) })] })) : (_jsx("p", { className: "text-xs text-muted-foreground", children: "No columns selected." })), _jsx(Separator, { className: "my-1" }), _jsx("div", { className: "flex flex-col gap-2 pb-2", children: sections.map((section, si) => {
129
- const available = section.fields.filter((f) => !selectedIdSet.has(f.id));
130
- if (available.length === 0)
131
- return null;
132
- const flatUngrouped = !section.groupLabel && available.length === 1;
133
- if (flatUngrouped) {
134
- const field = available[0];
135
- return (_jsxs("button", { type: "button", className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm text-foreground hover:bg-accent hover:text-accent-foreground", onClick: () => addColumn(field.id), children: [field.icon, _jsx("span", { className: "min-w-0 flex-1 truncate", children: field.name })] }, field.id));
136
- }
137
- const heading = section.groupLabel ?? (available.length > 1 ? 'Columns' : (available[0]?.name ?? 'Columns'));
138
- return (_jsxs(Collapsible, { defaultOpen: true, className: "flex flex-col gap-1", children: [_jsxs(CollapsibleTrigger, { className: "group flex w-full items-center gap-2 rounded-md px-1 py-1.5 text-start text-sm font-medium hover:bg-muted/80", type: "button", children: [available[0]?.icon, _jsx("span", { className: "min-w-0 flex-1 truncate", children: heading }), _jsx(ChevronRight, { className: "size-4 shrink-0 text-muted-foreground transition-transform group-data-[panel-open]:rotate-90" })] }), _jsx(CollapsibleContent, { className: "flex flex-col gap-0.5 ps-1", children: available.map((field) => (_jsx("button", { type: "button", className: "rounded-md px-2 py-1.5 text-start text-sm text-foreground hover:bg-accent hover:text-accent-foreground", onClick: () => addColumn(field.id), children: field.name }, field.id))) })] }, si));
139
- }) })] }) }), _jsxs("div", { className: "shrink-0 border-t border-border px-3 py-2 text-xs text-muted-foreground", children: [_jsx("span", { className: "font-medium text-primary tabular-nums", children: count }), " ", i18n.columnsSelectedSuffix(count)] })] }), showRightPanel && activeColumnId && activeDef?.supportsDateRange ? (_jsxs("div", { className: "flex min-h-0 w-[min(100%,380px)] min-w-[280px] flex-1 flex-col border-border bg-popover", children: [_jsxs("div", { className: "flex shrink-0 items-start justify-between gap-2 border-b border-border px-3 py-2.5", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm font-medium", children: activeDef.name }), _jsx("p", { className: "text-xs text-muted-foreground", children: activeDef.description ?? i18n.configureDateRangeHint })] }), _jsx("button", { type: "button", className: "shrink-0 rounded-sm p-1 text-muted-foreground hover:bg-muted hover:text-foreground", "aria-label": "Close configuration", onClick: () => setActiveColumnId(null), children: _jsx(X, { className: "size-4" }) })] }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden", children: _jsx("div", { className: "flex min-h-0 flex-1 flex-col p-3", children: _jsx(DateSelector, { ...dateSelectorProps, value: activeDateRange, onChange: (next) => updateSelectedDateRange(activeColumnId, next), className: cn('min-h-0 max-w-none flex-1', dateMetricClassName) }) }) }), _jsx("div", { className: "flex shrink-0 items-center justify-end gap-2 border-t border-border px-3 py-2.5", children: _jsx(Button, { type: "button", size: "sm", onClick: () => setActiveColumnId(null), children: i18n.done }) })] })) : null] }) })] }) }));
144
+ return (_jsx("div", { className: cn(className), children: _jsxs(Popover, { open: open, onOpenChange: handleOpenChange, children: [_jsxs(PopoverTrigger, { render: _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "gap-2 font-normal", "aria-expanded": open, "aria-haspopup": "dialog" }), children: [_jsx(Columns3, { className: "text-primary", "aria-hidden": true }), _jsxs("span", { className: "text-sm", children: [i18n.triggerLabel, " \u00B7 ", i18n.triggerCount(count)] }), _jsx(ChevronDown, { className: "opacity-60", "aria-hidden": true })] }), _jsx(PopoverContent, { align: "start", className: cn('w-auto max-w-[min(100vw-2rem,640px)] gap-0 overflow-hidden p-0 shadow-lg ring-1 ring-foreground/10', showRightPanel ? 'min-w-[min(100vw-2rem,640px)]' : 'min-w-[min(100vw-2rem,320px)]', popoverContentClassName), children: _jsx(TooltipProvider, { delay: 200, children: _jsxs("div", { className: "flex max-h-[min(85vh,560px)] min-h-0", children: [_jsx("div", { className: "flex min-h-0 w-[320px] shrink-0 flex-col", children: popoverView === 'manage' ? (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [_jsx(ColumnPopoverPanelHeader, { title: i18n.addColumnsTitle, explainer: i18n.manageColumnsExplainer, trailing: _jsx("button", { type: "button", onClick: clearAll, disabled: count === 0, className: "shrink-0 text-xs font-medium text-primary hover:underline disabled:pointer-events-none disabled:opacity-40", children: i18n.clearAll }) }), _jsxs(ScrollArea, { className: "min-h-0 flex-1", children: [_jsxs("div", { className: "sticky top-0 z-10 border-b border-border bg-popover px-3 py-2 text-xs text-muted-foreground", children: [_jsx("span", { className: "font-medium text-primary tabular-nums", children: count }), " ", i18n.columnsSelectedSuffix(count)] }), _jsx("div", { className: "flex flex-col gap-3", children: value.length > 0 ? (_jsxs("div", { className: "flex flex-col gap-2", children: [lockPinning && pinnedPrefixLen > 0 ? (_jsx("p", { className: "px-0.5 text-xs font-medium text-muted-foreground", children: i18n.pinnedHeading })) : null, _jsx(Sortable, { value: value, onValueChange: onChange, getItemValue: (item) => getSelectedColumnRowKey(item), onMove: handleColumnsMove, collisionDetection: lockedPartitionCollision, className: "flex flex-col divide-y divide-border", children: value.flatMap((row, index) => {
145
+ const def = columnMap[row.id];
146
+ if (!def)
147
+ return [];
148
+ const rowKey = getSelectedColumnRowKey(row);
149
+ const secondary = row.dateRange && def.supportsDateRange
150
+ ? formatColumnDateRangeSecondary(row.dateRange)
151
+ : null;
152
+ const showHighlight = activeRowKey === rowKey && Boolean(def.supportsDateRange);
153
+ const isPinned = row.pinned === true;
154
+ const showSectionDivider = lockPinning && pinnedPrefixLen > 0 && unpinnedCount > 0 && index === pinnedPrefixLen;
155
+ const iconBtnClass = cn('shrink-0 rounded-sm p-0.5 outline-none hover:bg-black/5 focus-visible:ring-2 focus-visible:ring-ring hover:[&_svg]:opacity-100', showHighlight && 'hover:bg-white/15');
156
+ const nodes = [];
157
+ if (showSectionDivider) {
158
+ nodes.push(_jsx("p", { className: "px-3 py-2 text-xs font-medium text-muted-foreground", children: i18n.unpinnedHeading }, `columns-unpinned-heading-${rowKey}`));
159
+ }
160
+ nodes.push(_jsxs(SortableItem, { value: rowKey, className: cn('flex min-w-0 items-center gap-1 px-2 py-2 text-sm transition-colors', showHighlight
161
+ ? 'border-primary bg-primary text-primary-foreground'
162
+ : 'border-border bg-card text-card-foreground', def.supportsDateRange && !showHighlight && 'hover:bg-accent'), children: [_jsx(SortableItemHandle, { className: cn('shrink-0 touch-none', showHighlight ? 'text-primary-foreground/80' : 'text-muted-foreground'), "aria-label": `Reorder ${def.name}`, children: _jsx(GripVertical, {}) }), _jsx(ColumnRowLabel, { name: def.name, secondary: secondary, showHighlight: showHighlight, activatable: Boolean(def.supportsDateRange), onActivate: def.supportsDateRange ? () => handleRowActivate(rowKey) : undefined }), !lockPinning && row.pinned !== undefined ? (_jsx("button", { type: "button", className: iconBtnClass, "aria-pressed": isPinned, "aria-label": isPinned ? i18n.unpinColumn : i18n.pinColumn, onClick: (e) => {
163
+ e.stopPropagation();
164
+ handleTogglePin(rowKey);
165
+ }, children: isPinned ? (_jsx(PinOff, { className: "size-4", "aria-hidden": true })) : (_jsx(Pin, { className: "size-4 opacity-30", "aria-hidden": true })) })) : null, _jsx("button", { type: "button", className: cn(iconBtnClass, '[&_svg]:opacity-30'), "aria-label": `Remove ${def.name}`, onClick: (e) => {
166
+ e.stopPropagation();
167
+ removeColumn(rowKey);
168
+ }, children: _jsx(X, {}) })] }, rowKey));
169
+ return nodes;
170
+ }) })] })) : (_jsx("p", { className: "text-xs text-muted-foreground", children: i18n.noColumnsSelected })) })] }), _jsx("div", { className: "shrink-0 border-t border-border bg-popover p-3", children: _jsx(Button, { type: "button", variant: "outline", size: "sm", className: "w-full", onClick: goToAddView, children: i18n.addColumnsAction }) })] })) : (_jsx(ColumnsAddView, { i18n: i18n, sections: sections, value: value, pendingAddIds: pendingAddIds, onTogglePendingAdd: togglePendingAdd, onCancel: cancelAddView, onConfirm: confirmAddColumns })) }), showRightPanel && activeDef && activeRowKey ? (_jsxs("div", { className: "flex min-h-0 w-[min(100%,380px)] min-w-[280px] flex-1 flex-col border-l border-border bg-popover", children: [_jsx(ColumnPopoverPanelHeader, { title: activeDef.name, explainer: activeDef.description ?? i18n.configureDateRangeHint, helpAriaLabel: i18n.headerSectionHelpAriaLabel, trailing: _jsx("button", { type: "button", className: "shrink-0 rounded-sm p-1 text-muted-foreground hover:bg-muted hover:text-foreground", "aria-label": "Close configuration", onClick: () => setActiveRowKey(null), children: _jsx(X, {}) }) }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden p-3", children: _jsx(DateSelector, { ...dateSelectorProps, value: activeDateRange, onChange: (next) => updateSelectedDateRange(activeRowKey, next), className: cn('min-h-0 max-w-none flex-1', dateMetricClassName) }) })] })) : null] }) }) })] }) }));
171
+ }
172
+ function ColumnRowLabel({ name, secondary, showHighlight, activatable, onActivate, }) {
173
+ const label = (_jsxs(_Fragment, { children: [_jsx("span", { className: cn('truncate font-medium', !secondary && 'flex-1'), children: name }), secondary ? (_jsxs("span", { className: cn('truncate text-xs', showHighlight ? 'text-primary-foreground/85' : 'text-muted-foreground'), children: ["\u00B7 ", secondary] })) : null] }));
174
+ if (activatable) {
175
+ return (_jsx("button", { type: "button", className: "flex min-w-0 flex-1 items-center gap-1.5 text-start", onClick: onActivate, children: label }));
176
+ }
177
+ return _jsx("div", { className: "flex min-w-0 flex-1 items-center gap-1.5", children: label });
140
178
  }
@@ -10,4 +10,5 @@ export default meta;
10
10
  type Story = StoryObj<typeof meta>;
11
11
  export declare const Default: Story;
12
12
  export declare const PinningLocked: Story;
13
+ export declare const Empty: Story;
13
14
  //# sourceMappingURL=columns.stories.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"columns.stories.d.ts","sourceRoot":"","sources":["../../../src/blocks/columns/columns.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AA6FvD,QAAA,MAAM,IAAI;;;;;;CAMM,CAAC;AAEjB,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAErB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,KAE3B,CAAC"}
1
+ {"version":3,"file":"columns.stories.d.ts","sourceRoot":"","sources":["../../../src/blocks/columns/columns.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAgHvD,QAAA,MAAM,IAAI;;;;;;CAMM,CAAC;AAEjB,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAErB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,KAE3B,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,KAEnB,CAAC"}
@@ -2,11 +2,17 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Activity, BarChart3, Home, Users } from 'lucide-react';
3
3
  import { useState } from 'react';
4
4
  import { Columns } from './columns';
5
- const rolling180 = {
5
+ const rolling30 = {
6
6
  period: 'day',
7
7
  operator: 'between',
8
8
  selectionMode: 'rolling-next',
9
- rollingDays: 180,
9
+ rollingDays: 30,
10
+ };
11
+ const rolling60 = {
12
+ period: 'day',
13
+ operator: 'between',
14
+ selectionMode: 'rolling-next',
15
+ rollingDays: 60,
10
16
  };
11
17
  const catalog = [
12
18
  {
@@ -52,7 +58,8 @@ const catalog = [
52
58
  const initialSelected = [
53
59
  { id: 'bedrooms', pinned: false },
54
60
  { id: 'bathrooms', pinned: false },
55
- { id: 'occupancy', dateRange: rolling180, pinned: false },
61
+ { id: 'occupancy', instanceId: 'occ-30', dateRange: rolling30, pinned: false },
62
+ { id: 'occupancy', instanceId: 'occ-60', dateRange: rolling60, pinned: false },
56
63
  { id: 'adr', pinned: false },
57
64
  ];
58
65
  /** Pinned-first order: pinned prefix, then unpinned. */
@@ -60,7 +67,8 @@ const initialPinningLocked = [
60
67
  { id: 'bedrooms', pinned: true },
61
68
  { id: 'address', pinned: true },
62
69
  { id: 'bathrooms' },
63
- { id: 'occupancy', dateRange: rolling180 },
70
+ { id: 'occupancy', instanceId: 'occ-30', dateRange: rolling30 },
71
+ { id: 'occupancy', instanceId: 'occ-60', dateRange: rolling60 },
64
72
  { id: 'adr' },
65
73
  ];
66
74
  function ColumnsPlayground() {
@@ -71,6 +79,10 @@ function ColumnsPinningLockedPlayground() {
71
79
  const [value, setValue] = useState(initialPinningLocked);
72
80
  return (_jsx("div", { className: "p-6", children: _jsx(Columns, { columns: catalog, value: value, onChange: setValue, lockPinning: true }) }));
73
81
  }
82
+ function ColumnsEmptyPlayground() {
83
+ const [value, setValue] = useState([]);
84
+ return (_jsx("div", { className: "p-6", children: _jsx(Columns, { columns: catalog, value: value, onChange: setValue }) }));
85
+ }
74
86
  const meta = {
75
87
  title: 'Blocks/Columns',
76
88
  tags: ['autodocs'],
@@ -85,3 +97,6 @@ export const Default = {
85
97
  export const PinningLocked = {
86
98
  render: () => _jsx(ColumnsPinningLockedPlayground, {}),
87
99
  };
100
+ export const Empty = {
101
+ render: () => _jsx(ColumnsEmptyPlayground, {}),
102
+ };