pixelon 0.3.28 → 0.3.30

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.
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+ import type { UIComponentData } from './types';
3
+ export declare const AnsweredSummary: React.FC<{
4
+ component: UIComponentData;
5
+ }>;
@@ -1,7 +1,24 @@
1
1
  import type { ChatEventBus } from '../../../../../core/eventBus';
2
+ export interface ComponentAction {
3
+ id: string;
4
+ label: string;
5
+ kind?: string;
6
+ emits_value?: boolean;
7
+ value?: unknown;
8
+ intent?: 'primary' | 'secondary' | 'danger';
9
+ }
2
10
  export interface UIComponentData {
3
11
  type: string;
4
12
  props: Record<string, unknown>;
13
+ status?: string;
14
+ visibility_policy?: 'hide_on_terminal' | 'keep_visible' | 'collapse';
15
+ actions?: ComponentAction[];
16
+ /** SPEC-023: respuesta persistida por el backend al cerrar el componente */
17
+ selected_values?: Array<{
18
+ value: unknown;
19
+ label: string;
20
+ }>;
21
+ /** @deprecated contrato legacy (pre SPEC-023); usar status */
5
22
  answered?: boolean;
6
23
  }
7
24
  export interface ChatComponentProps {
@@ -10,4 +27,6 @@ export interface ChatComponentProps {
10
27
  assistantMessageId?: string;
11
28
  }
12
29
  export type OnConfirm = (value: unknown, label: string) => void;
30
+ export declare function getSubmitActionId(component: UIComponentData): string | undefined;
31
+ export declare const TERMINAL_STATUS: Set<string>;
13
32
  export declare const DISPLAY_COMPONENTS: Set<string>;
@@ -6,7 +6,7 @@ interface UseComponentInteractionParams {
6
6
  assistantMessageId: string;
7
7
  }
8
8
  export declare function useComponentInteraction({ eventBus, fieldName, componentType, assistantMessageId, }: UseComponentInteractionParams): {
9
- emitInteraction: (value: unknown, label: string) => void;
9
+ emitInteraction: (value: unknown, label: string, actionId?: string) => void;
10
10
  isConfirmed: boolean;
11
11
  };
12
12
  export {};
@@ -26,6 +26,8 @@ export type ChatEvents = {
26
26
  label: string;
27
27
  componentType: string;
28
28
  assistantMessageId: string;
29
+ /** SPEC-023 T8: id de la Action disparada (opcional) */
30
+ actionId?: string;
29
31
  };
30
32
  /** Consumer -> Chat: update the displayed messages */
31
33
  messagesUpdated: {
package/dist/index.esm.js CHANGED
@@ -1618,8 +1618,52 @@ function parseContent(content) {
1618
1618
  return null;
1619
1619
  }
1620
1620
 
1621
+ function getSubmitActionId(component) {
1622
+ const actions = component.actions;
1623
+ if (!actions || actions.length === 0) return void 0;
1624
+ return (actions.find((a) => a.kind === "submit") ?? actions[0]).id;
1625
+ }
1626
+ const TERMINAL_STATUS = /* @__PURE__ */ new Set(["answered", "cancelled", "expired"]);
1621
1627
  const DISPLAY_COMPONENTS = /* @__PURE__ */ new Set(["card", "badge", "reactive_form", "carousel"]);
1622
1628
 
1629
+ const AnsweredSummary = ({ component }) => {
1630
+ const status = component.status;
1631
+ const values = component.selected_values ?? [];
1632
+ const labels = values.map((v) => v.label).filter(Boolean).join(", ");
1633
+ let icon = "\u2713";
1634
+ let text = labels || "Respondido";
1635
+ let color = "#3a8a3a";
1636
+ if (status === "cancelled") {
1637
+ icon = "\u2715";
1638
+ text = "Cancelado";
1639
+ color = "#999";
1640
+ } else if (status === "expired") {
1641
+ icon = "\u231B";
1642
+ text = "Expir\xF3";
1643
+ color = "#b58a00";
1644
+ }
1645
+ return /* @__PURE__ */ jsxs(
1646
+ "div",
1647
+ {
1648
+ style: {
1649
+ display: "inline-flex",
1650
+ alignItems: "center",
1651
+ gap: "6px",
1652
+ padding: "6px 12px",
1653
+ borderRadius: "999px",
1654
+ background: "rgba(0,0,0,0.04)",
1655
+ color,
1656
+ fontSize: "0.875rem",
1657
+ fontWeight: 500
1658
+ },
1659
+ children: [
1660
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: icon }),
1661
+ /* @__PURE__ */ jsx("span", { children: text })
1662
+ ]
1663
+ }
1664
+ );
1665
+ };
1666
+
1623
1667
  const useAppHeight$5 = () => {
1624
1668
  useEffect(() => {
1625
1669
  const setDrawerHeight = () => {
@@ -1796,13 +1840,14 @@ function useComponentInteraction({
1796
1840
  }) {
1797
1841
  const [isConfirmed, setIsConfirmed] = useState(false);
1798
1842
  const emitInteraction = useCallback(
1799
- (value, label) => {
1843
+ (value, label, actionId) => {
1800
1844
  eventBus?.emit(CHAT_EVENT.COMPONENT_INTERACTION, {
1801
1845
  fieldName,
1802
1846
  value,
1803
1847
  label,
1804
1848
  componentType,
1805
- assistantMessageId
1849
+ assistantMessageId,
1850
+ actionId
1806
1851
  });
1807
1852
  setIsConfirmed(true);
1808
1853
  },
@@ -1959,7 +2004,7 @@ const SelectWithConfirm = ({
1959
2004
  });
1960
2005
  const handleConfirm = () => {
1961
2006
  if (selected) {
1962
- emitInteraction(selected.value, selected.label);
2007
+ emitInteraction(selected.value, selected.label, getSubmitActionId(component));
1963
2008
  setIsDrawerOpen(false);
1964
2009
  }
1965
2010
  };
@@ -2162,7 +2207,8 @@ const SelectMultipleWithConfirm = ({
2162
2207
  if (selectedOptions.length > 0) {
2163
2208
  emitInteraction(
2164
2209
  selectedOptions.map((o) => o.value),
2165
- selectedOptions.map((o) => o.label).join(", ")
2210
+ selectedOptions.map((o) => o.label).join(", "),
2211
+ getSubmitActionId(component)
2166
2212
  );
2167
2213
  setIsDrawerOpen(false);
2168
2214
  }
@@ -2365,7 +2411,7 @@ const InputWithConfirm = ({
2365
2411
  variant: "primary",
2366
2412
  size: "small",
2367
2413
  disabled: !inputValue,
2368
- onClick: () => emitInteraction(inputValue, inputValue),
2414
+ onClick: () => emitInteraction(inputValue, inputValue, getSubmitActionId(component)),
2369
2415
  children: "Confirmar"
2370
2416
  }
2371
2417
  )
@@ -2508,9 +2554,9 @@ const DateInputWithConfirm = ({
2508
2554
  if (!selectedDate) return;
2509
2555
  const isoValue = formatDateToISO$1(selectedDate);
2510
2556
  const displayValue2 = formatDateDisplay$1(selectedDate, locale, timezone);
2511
- emitInteraction(isoValue, displayValue2);
2557
+ emitInteraction(isoValue, displayValue2, getSubmitActionId(component));
2512
2558
  setIsOpen(false);
2513
- }, [selectedDate, emitInteraction, locale, timezone]);
2559
+ }, [selectedDate, emitInteraction, locale, timezone, component]);
2514
2560
  const displayValue = formatDateDisplay$1(selectedDate, locale, timezone);
2515
2561
  const placeholder = component.props.placeholder || "Selecciona una fecha";
2516
2562
  const label = component.props.label || "Selecciona una fecha";
@@ -2639,6 +2685,22 @@ const ConfirmButton = ({
2639
2685
  assistantMessageId
2640
2686
  });
2641
2687
  if (isConfirmed) return null;
2688
+ const actions = component.actions;
2689
+ if (actions && actions.length > 0) {
2690
+ return /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap" }, children: actions.map((a) => /* @__PURE__ */ jsx(
2691
+ Button$1,
2692
+ {
2693
+ variant: a.intent === "primary" ? "primary" : "secondary",
2694
+ onClick: () => emitInteraction(
2695
+ a.emits_value ? component.props.value : a.value,
2696
+ a.label,
2697
+ a.id
2698
+ ),
2699
+ children: a.label
2700
+ },
2701
+ a.id
2702
+ )) });
2703
+ }
2642
2704
  return /* @__PURE__ */ jsx(
2643
2705
  Button$1,
2644
2706
  {
@@ -2920,16 +2982,16 @@ const TimeInputWithConfirm = ({
2920
2982
  const value24h = componentsTo24h(nowComp);
2921
2983
  const display = formatTimeDisplay(value24h, locale, timezone);
2922
2984
  setConfirmedTime(value24h);
2923
- emitInteraction(value24h, display);
2985
+ emitInteraction(value24h, display, getSubmitActionId(component));
2924
2986
  setIsOpen(false);
2925
- }, [locale, timezone, emitInteraction]);
2987
+ }, [locale, timezone, emitInteraction, component]);
2926
2988
  const handleConfirm = useCallback(() => {
2927
2989
  const value24h = componentsTo24h(tempComponents);
2928
2990
  const display = formatTimeDisplay(value24h, locale, timezone);
2929
2991
  setConfirmedTime(value24h);
2930
- emitInteraction(value24h, display);
2992
+ emitInteraction(value24h, display, getSubmitActionId(component));
2931
2993
  setIsOpen(false);
2932
- }, [tempComponents, locale, timezone, emitInteraction]);
2994
+ }, [tempComponents, locale, timezone, emitInteraction, component]);
2933
2995
  const displayValue = confirmedTime ? formatTimeDisplay(confirmedTime, locale, timezone) : "";
2934
2996
  const placeholder = component.props.placeholder || "Selecciona una hora";
2935
2997
  const label = component.props.label || "Selecciona una hora";
@@ -4998,7 +5060,8 @@ const CarouselDisplay = ({
4998
5060
  actions.selection.toggle(item.id);
4999
5061
  emitInteraction(
5000
5062
  [{ value: item.id, label: item.title || item.id }],
5001
- item.title || item.id
5063
+ item.title || item.id,
5064
+ getSubmitActionId(component)
5002
5065
  );
5003
5066
  return;
5004
5067
  }
@@ -5006,7 +5069,7 @@ const CarouselDisplay = ({
5006
5069
  actions.selection.toggle(item.id);
5007
5070
  }
5008
5071
  },
5009
- [selectionMode, disabled, isConfirmed, actions, emitInteraction]
5072
+ [selectionMode, disabled, isConfirmed, actions, emitInteraction, component]
5010
5073
  );
5011
5074
  const handleRemove = useCallback(
5012
5075
  (item) => {
@@ -5021,14 +5084,14 @@ const CarouselDisplay = ({
5021
5084
  const selected = items.filter((i) => selectedIds.includes(i.id));
5022
5085
  const value = selected.map((i) => ({ value: i.id, label: i.title || i.id }));
5023
5086
  const label = selected.map((i) => i.title || i.id).join(", ");
5024
- emitInteraction(value, label);
5087
+ emitInteraction(value, label, getSubmitActionId(component));
5025
5088
  }
5026
5089
  if (selectionMode === "removable") {
5027
5090
  const value = items.map((i) => ({ value: i.id, label: i.title || i.id }));
5028
5091
  const label = items.map((i) => i.title || i.id).join(", ");
5029
- emitInteraction(value, label);
5092
+ emitInteraction(value, label, getSubmitActionId(component));
5030
5093
  }
5031
- }, [selectionMode, disabled, isConfirmed, items, selectedIds, emitInteraction]);
5094
+ }, [selectionMode, disabled, isConfirmed, items, selectedIds, emitInteraction, component]);
5032
5095
  const confirmDisabled = useMemo(() => {
5033
5096
  if (selectionMode === "multiple") return selectedIds.length === 0;
5034
5097
  if (selectionMode === "removable") {
@@ -5098,7 +5161,15 @@ const COMPONENT_MAP = {
5098
5161
  };
5099
5162
  function renderComponent(props) {
5100
5163
  const { component } = props;
5101
- if (component.answered && !DISPLAY_COMPONENTS.has(component.type)) {
5164
+ if (component.status) {
5165
+ if (TERMINAL_STATUS.has(component.status)) {
5166
+ const policy = component.visibility_policy ?? "hide_on_terminal";
5167
+ if (policy === "hide_on_terminal") return null;
5168
+ if (component.selected_values && component.selected_values.length > 0) {
5169
+ return /* @__PURE__ */ jsx(AnsweredSummary, { component });
5170
+ }
5171
+ }
5172
+ } else if (component.answered && !DISPLAY_COMPONENTS.has(component.type)) {
5102
5173
  return null;
5103
5174
  }
5104
5175
  const Component = COMPONENT_MAP[component.type];
package/dist/index.js CHANGED
@@ -1639,8 +1639,52 @@ function parseContent(content) {
1639
1639
  return null;
1640
1640
  }
1641
1641
 
1642
+ function getSubmitActionId(component) {
1643
+ const actions = component.actions;
1644
+ if (!actions || actions.length === 0) return void 0;
1645
+ return (actions.find((a) => a.kind === "submit") ?? actions[0]).id;
1646
+ }
1647
+ const TERMINAL_STATUS = /* @__PURE__ */ new Set(["answered", "cancelled", "expired"]);
1642
1648
  const DISPLAY_COMPONENTS = /* @__PURE__ */ new Set(["card", "badge", "reactive_form", "carousel"]);
1643
1649
 
1650
+ const AnsweredSummary = ({ component }) => {
1651
+ const status = component.status;
1652
+ const values = component.selected_values ?? [];
1653
+ const labels = values.map((v) => v.label).filter(Boolean).join(", ");
1654
+ let icon = "\u2713";
1655
+ let text = labels || "Respondido";
1656
+ let color = "#3a8a3a";
1657
+ if (status === "cancelled") {
1658
+ icon = "\u2715";
1659
+ text = "Cancelado";
1660
+ color = "#999";
1661
+ } else if (status === "expired") {
1662
+ icon = "\u231B";
1663
+ text = "Expir\xF3";
1664
+ color = "#b58a00";
1665
+ }
1666
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1667
+ "div",
1668
+ {
1669
+ style: {
1670
+ display: "inline-flex",
1671
+ alignItems: "center",
1672
+ gap: "6px",
1673
+ padding: "6px 12px",
1674
+ borderRadius: "999px",
1675
+ background: "rgba(0,0,0,0.04)",
1676
+ color,
1677
+ fontSize: "0.875rem",
1678
+ fontWeight: 500
1679
+ },
1680
+ children: [
1681
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, children: icon }),
1682
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: text })
1683
+ ]
1684
+ }
1685
+ );
1686
+ };
1687
+
1644
1688
  const useAppHeight$5 = () => {
1645
1689
  React.useEffect(() => {
1646
1690
  const setDrawerHeight = () => {
@@ -1817,13 +1861,14 @@ function useComponentInteraction({
1817
1861
  }) {
1818
1862
  const [isConfirmed, setIsConfirmed] = React.useState(false);
1819
1863
  const emitInteraction = React.useCallback(
1820
- (value, label) => {
1864
+ (value, label, actionId) => {
1821
1865
  eventBus?.emit(CHAT_EVENT.COMPONENT_INTERACTION, {
1822
1866
  fieldName,
1823
1867
  value,
1824
1868
  label,
1825
1869
  componentType,
1826
- assistantMessageId
1870
+ assistantMessageId,
1871
+ actionId
1827
1872
  });
1828
1873
  setIsConfirmed(true);
1829
1874
  },
@@ -1980,7 +2025,7 @@ const SelectWithConfirm = ({
1980
2025
  });
1981
2026
  const handleConfirm = () => {
1982
2027
  if (selected) {
1983
- emitInteraction(selected.value, selected.label);
2028
+ emitInteraction(selected.value, selected.label, getSubmitActionId(component));
1984
2029
  setIsDrawerOpen(false);
1985
2030
  }
1986
2031
  };
@@ -2183,7 +2228,8 @@ const SelectMultipleWithConfirm = ({
2183
2228
  if (selectedOptions.length > 0) {
2184
2229
  emitInteraction(
2185
2230
  selectedOptions.map((o) => o.value),
2186
- selectedOptions.map((o) => o.label).join(", ")
2231
+ selectedOptions.map((o) => o.label).join(", "),
2232
+ getSubmitActionId(component)
2187
2233
  );
2188
2234
  setIsDrawerOpen(false);
2189
2235
  }
@@ -2386,7 +2432,7 @@ const InputWithConfirm = ({
2386
2432
  variant: "primary",
2387
2433
  size: "small",
2388
2434
  disabled: !inputValue,
2389
- onClick: () => emitInteraction(inputValue, inputValue),
2435
+ onClick: () => emitInteraction(inputValue, inputValue, getSubmitActionId(component)),
2390
2436
  children: "Confirmar"
2391
2437
  }
2392
2438
  )
@@ -2529,9 +2575,9 @@ const DateInputWithConfirm = ({
2529
2575
  if (!selectedDate) return;
2530
2576
  const isoValue = formatDateToISO$1(selectedDate);
2531
2577
  const displayValue2 = formatDateDisplay$1(selectedDate, locale, timezone);
2532
- emitInteraction(isoValue, displayValue2);
2578
+ emitInteraction(isoValue, displayValue2, getSubmitActionId(component));
2533
2579
  setIsOpen(false);
2534
- }, [selectedDate, emitInteraction, locale, timezone]);
2580
+ }, [selectedDate, emitInteraction, locale, timezone, component]);
2535
2581
  const displayValue = formatDateDisplay$1(selectedDate, locale, timezone);
2536
2582
  const placeholder = component.props.placeholder || "Selecciona una fecha";
2537
2583
  const label = component.props.label || "Selecciona una fecha";
@@ -2660,6 +2706,22 @@ const ConfirmButton = ({
2660
2706
  assistantMessageId
2661
2707
  });
2662
2708
  if (isConfirmed) return null;
2709
+ const actions = component.actions;
2710
+ if (actions && actions.length > 0) {
2711
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap" }, children: actions.map((a) => /* @__PURE__ */ jsxRuntime.jsx(
2712
+ Button$1,
2713
+ {
2714
+ variant: a.intent === "primary" ? "primary" : "secondary",
2715
+ onClick: () => emitInteraction(
2716
+ a.emits_value ? component.props.value : a.value,
2717
+ a.label,
2718
+ a.id
2719
+ ),
2720
+ children: a.label
2721
+ },
2722
+ a.id
2723
+ )) });
2724
+ }
2663
2725
  return /* @__PURE__ */ jsxRuntime.jsx(
2664
2726
  Button$1,
2665
2727
  {
@@ -2941,16 +3003,16 @@ const TimeInputWithConfirm = ({
2941
3003
  const value24h = componentsTo24h(nowComp);
2942
3004
  const display = formatTimeDisplay(value24h, locale, timezone);
2943
3005
  setConfirmedTime(value24h);
2944
- emitInteraction(value24h, display);
3006
+ emitInteraction(value24h, display, getSubmitActionId(component));
2945
3007
  setIsOpen(false);
2946
- }, [locale, timezone, emitInteraction]);
3008
+ }, [locale, timezone, emitInteraction, component]);
2947
3009
  const handleConfirm = React.useCallback(() => {
2948
3010
  const value24h = componentsTo24h(tempComponents);
2949
3011
  const display = formatTimeDisplay(value24h, locale, timezone);
2950
3012
  setConfirmedTime(value24h);
2951
- emitInteraction(value24h, display);
3013
+ emitInteraction(value24h, display, getSubmitActionId(component));
2952
3014
  setIsOpen(false);
2953
- }, [tempComponents, locale, timezone, emitInteraction]);
3015
+ }, [tempComponents, locale, timezone, emitInteraction, component]);
2954
3016
  const displayValue = confirmedTime ? formatTimeDisplay(confirmedTime, locale, timezone) : "";
2955
3017
  const placeholder = component.props.placeholder || "Selecciona una hora";
2956
3018
  const label = component.props.label || "Selecciona una hora";
@@ -5019,7 +5081,8 @@ const CarouselDisplay = ({
5019
5081
  actions.selection.toggle(item.id);
5020
5082
  emitInteraction(
5021
5083
  [{ value: item.id, label: item.title || item.id }],
5022
- item.title || item.id
5084
+ item.title || item.id,
5085
+ getSubmitActionId(component)
5023
5086
  );
5024
5087
  return;
5025
5088
  }
@@ -5027,7 +5090,7 @@ const CarouselDisplay = ({
5027
5090
  actions.selection.toggle(item.id);
5028
5091
  }
5029
5092
  },
5030
- [selectionMode, disabled, isConfirmed, actions, emitInteraction]
5093
+ [selectionMode, disabled, isConfirmed, actions, emitInteraction, component]
5031
5094
  );
5032
5095
  const handleRemove = React.useCallback(
5033
5096
  (item) => {
@@ -5042,14 +5105,14 @@ const CarouselDisplay = ({
5042
5105
  const selected = items.filter((i) => selectedIds.includes(i.id));
5043
5106
  const value = selected.map((i) => ({ value: i.id, label: i.title || i.id }));
5044
5107
  const label = selected.map((i) => i.title || i.id).join(", ");
5045
- emitInteraction(value, label);
5108
+ emitInteraction(value, label, getSubmitActionId(component));
5046
5109
  }
5047
5110
  if (selectionMode === "removable") {
5048
5111
  const value = items.map((i) => ({ value: i.id, label: i.title || i.id }));
5049
5112
  const label = items.map((i) => i.title || i.id).join(", ");
5050
- emitInteraction(value, label);
5113
+ emitInteraction(value, label, getSubmitActionId(component));
5051
5114
  }
5052
- }, [selectionMode, disabled, isConfirmed, items, selectedIds, emitInteraction]);
5115
+ }, [selectionMode, disabled, isConfirmed, items, selectedIds, emitInteraction, component]);
5053
5116
  const confirmDisabled = React.useMemo(() => {
5054
5117
  if (selectionMode === "multiple") return selectedIds.length === 0;
5055
5118
  if (selectionMode === "removable") {
@@ -5119,7 +5182,15 @@ const COMPONENT_MAP = {
5119
5182
  };
5120
5183
  function renderComponent(props) {
5121
5184
  const { component } = props;
5122
- if (component.answered && !DISPLAY_COMPONENTS.has(component.type)) {
5185
+ if (component.status) {
5186
+ if (TERMINAL_STATUS.has(component.status)) {
5187
+ const policy = component.visibility_policy ?? "hide_on_terminal";
5188
+ if (policy === "hide_on_terminal") return null;
5189
+ if (component.selected_values && component.selected_values.length > 0) {
5190
+ return /* @__PURE__ */ jsxRuntime.jsx(AnsweredSummary, { component });
5191
+ }
5192
+ }
5193
+ } else if (component.answered && !DISPLAY_COMPONENTS.has(component.type)) {
5123
5194
  return null;
5124
5195
  }
5125
5196
  const Component = COMPONENT_MAP[component.type];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixelon",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
4
4
  "description": "Design system de la marca goluti",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,6 +24,8 @@
24
24
  "test:run": "vitest run",
25
25
  "mobile": "npx storybook dev -p 6006",
26
26
  "get:ip": "ipconfig getifaddr en0",
27
+ "gen:ux-types": "node scripts/gen-ux-types.mjs",
28
+ "gen:ux-types:check": "node scripts/gen-ux-types.mjs --check",
27
29
  "prepublishOnly": "npm run build:lib"
28
30
  },
29
31
  "keywords": [