@hexclave/shared 1.0.14 → 1.0.15

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 (116) hide show
  1. package/dist/apps/apps-config.d.ts +6 -0
  2. package/dist/apps/apps-config.js +6 -0
  3. package/dist/apps/apps-config.js.map +1 -1
  4. package/dist/config/schema.d.ts +68 -68
  5. package/dist/config-authoring.d.ts.map +1 -1
  6. package/dist/config-authoring.js.map +1 -1
  7. package/dist/config-authoring.test.js +1 -1
  8. package/dist/config-authoring.test.js.map +1 -1
  9. package/dist/config-rendering.d.ts +2 -2
  10. package/dist/config-rendering.js +16 -0
  11. package/dist/config-rendering.js.map +1 -1
  12. package/dist/esm/apps/apps-config.d.ts +6 -0
  13. package/dist/esm/apps/apps-config.js +6 -0
  14. package/dist/esm/apps/apps-config.js.map +1 -1
  15. package/dist/esm/config/schema.d.ts +68 -68
  16. package/dist/esm/config-authoring.d.ts.map +1 -1
  17. package/dist/esm/config-authoring.js.map +1 -1
  18. package/dist/esm/config-authoring.test.js +1 -1
  19. package/dist/esm/config-authoring.test.js.map +1 -1
  20. package/dist/esm/config-rendering.d.ts +2 -2
  21. package/dist/esm/config-rendering.js +16 -2
  22. package/dist/esm/config-rendering.js.map +1 -1
  23. package/dist/esm/hexclave-config-file.d.ts +24 -1
  24. package/dist/esm/hexclave-config-file.d.ts.map +1 -1
  25. package/dist/esm/hexclave-config-file.js +69 -2
  26. package/dist/esm/hexclave-config-file.js.map +1 -1
  27. package/dist/esm/interface/admin-interface.d.ts +19 -1
  28. package/dist/esm/interface/admin-interface.d.ts.map +1 -1
  29. package/dist/esm/interface/admin-interface.js +14 -0
  30. package/dist/esm/interface/admin-interface.js.map +1 -1
  31. package/dist/esm/interface/admin-metrics.d.ts +101 -7
  32. package/dist/esm/interface/admin-metrics.d.ts.map +1 -1
  33. package/dist/esm/interface/admin-metrics.js +61 -1
  34. package/dist/esm/interface/admin-metrics.js.map +1 -1
  35. package/dist/esm/interface/conversations.d.ts +38 -38
  36. package/dist/esm/interface/crud/current-user.d.ts +2 -2
  37. package/dist/esm/interface/crud/email-outbox.d.ts +150 -150
  38. package/dist/esm/interface/crud/invoices.d.ts +2 -2
  39. package/dist/esm/interface/crud/products.d.ts +4 -4
  40. package/dist/esm/interface/crud/project-api-keys.d.ts +2 -2
  41. package/dist/esm/interface/crud/projects.d.ts +51 -51
  42. package/dist/esm/interface/crud/team-member-profiles.d.ts +10 -10
  43. package/dist/esm/interface/crud/transactions.d.ts +2 -2
  44. package/dist/esm/interface/crud/users.d.ts +10 -10
  45. package/dist/esm/interface/webhooks.d.ts +2 -2
  46. package/dist/esm/schema-fields.d.ts +3 -3
  47. package/dist/esm/sessions.d.ts +2 -2
  48. package/dist/esm/utils/analytics-clickmap-overlay.d.ts +18 -0
  49. package/dist/esm/utils/analytics-clickmap-overlay.d.ts.map +1 -0
  50. package/dist/esm/utils/analytics-clickmap-overlay.js +19 -0
  51. package/dist/esm/utils/analytics-clickmap-overlay.js.map +1 -0
  52. package/dist/esm/utils/dev-tool.d.ts +31 -0
  53. package/dist/esm/utils/dev-tool.d.ts.map +1 -0
  54. package/dist/esm/utils/dev-tool.js +32 -0
  55. package/dist/esm/utils/dev-tool.js.map +1 -0
  56. package/dist/esm/utils/dom.d.ts +7 -1
  57. package/dist/esm/utils/dom.d.ts.map +1 -1
  58. package/dist/esm/utils/dom.js +20 -1
  59. package/dist/esm/utils/dom.js.map +1 -1
  60. package/dist/esm/utils/elements-chain.d.ts +38 -0
  61. package/dist/esm/utils/elements-chain.d.ts.map +1 -0
  62. package/dist/esm/utils/elements-chain.js +297 -0
  63. package/dist/esm/utils/elements-chain.js.map +1 -0
  64. package/dist/hexclave-config-file.d.ts +24 -1
  65. package/dist/hexclave-config-file.d.ts.map +1 -1
  66. package/dist/hexclave-config-file.js +71 -1
  67. package/dist/hexclave-config-file.js.map +1 -1
  68. package/dist/interface/admin-interface.d.ts +19 -1
  69. package/dist/interface/admin-interface.d.ts.map +1 -1
  70. package/dist/interface/admin-interface.js +14 -0
  71. package/dist/interface/admin-interface.js.map +1 -1
  72. package/dist/interface/admin-metrics.d.ts +101 -7
  73. package/dist/interface/admin-metrics.d.ts.map +1 -1
  74. package/dist/interface/admin-metrics.js +65 -0
  75. package/dist/interface/admin-metrics.js.map +1 -1
  76. package/dist/interface/conversations.d.ts +38 -38
  77. package/dist/interface/crud/current-user.d.ts +2 -2
  78. package/dist/interface/crud/email-outbox.d.ts +150 -150
  79. package/dist/interface/crud/invoices.d.ts +2 -2
  80. package/dist/interface/crud/products.d.ts +4 -4
  81. package/dist/interface/crud/project-api-keys.d.ts +2 -2
  82. package/dist/interface/crud/projects.d.ts +51 -51
  83. package/dist/interface/crud/team-member-profiles.d.ts +10 -10
  84. package/dist/interface/crud/transactions.d.ts +2 -2
  85. package/dist/interface/crud/users.d.ts +10 -10
  86. package/dist/interface/webhooks.d.ts +2 -2
  87. package/dist/schema-fields.d.ts +3 -3
  88. package/dist/sessions.d.ts +2 -2
  89. package/dist/utils/analytics-clickmap-overlay.d.ts +18 -0
  90. package/dist/utils/analytics-clickmap-overlay.d.ts.map +1 -0
  91. package/dist/utils/analytics-clickmap-overlay.js +23 -0
  92. package/dist/utils/analytics-clickmap-overlay.js.map +1 -0
  93. package/dist/utils/dev-tool.d.ts +31 -0
  94. package/dist/utils/dev-tool.d.ts.map +1 -0
  95. package/dist/utils/dev-tool.js +37 -0
  96. package/dist/utils/dev-tool.js.map +1 -0
  97. package/dist/utils/dom.d.ts +7 -1
  98. package/dist/utils/dom.d.ts.map +1 -1
  99. package/dist/utils/dom.js +20 -0
  100. package/dist/utils/dom.js.map +1 -1
  101. package/dist/utils/elements-chain.d.ts +38 -0
  102. package/dist/utils/elements-chain.d.ts.map +1 -0
  103. package/dist/utils/elements-chain.js +304 -0
  104. package/dist/utils/elements-chain.js.map +1 -0
  105. package/package.json +1 -1
  106. package/src/apps/apps-config.ts +6 -0
  107. package/src/config-authoring.test.ts +1 -1
  108. package/src/config-authoring.ts +6 -6
  109. package/src/config-rendering.ts +24 -2
  110. package/src/hexclave-config-file.ts +90 -1
  111. package/src/interface/admin-interface.ts +43 -1
  112. package/src/interface/admin-metrics.ts +92 -4
  113. package/src/utils/analytics-clickmap-overlay.tsx +15 -0
  114. package/src/utils/dev-tool.tsx +30 -0
  115. package/src/utils/dom.tsx +42 -0
  116. package/src/utils/elements-chain.tsx +349 -0
@@ -0,0 +1,23 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/utils/analytics-clickmap-overlay.tsx
4
+ /**
5
+ * Wire protocol for handing a clickmap overlay token from the dashboard to the
6
+ * in-page dev tool via `sessionStorage` + a window event.
7
+ *
8
+ * The token is a self-describing JWT: its payload already carries the
9
+ * `project_id` and `origin` it was minted for, so the reader derives both from
10
+ * the token itself and the writer only has to hand over a single value. The
11
+ * dashboard (writer) and the dev tool (reader) live in different packages but
12
+ * must agree on these exact names — this module is the single source of truth so
13
+ * they can never silently desync.
14
+ */
15
+ const CLICKMAP_OVERLAY_TOKEN_STORAGE_KEY = "hexclave-clickmap-token";
16
+ const CLICKMAP_OVERLAY_RESUME_STORAGE_KEY = "hexclave-clickmap-resume";
17
+ const CLICKMAP_OVERLAY_TOKEN_UPDATED_EVENT = "hexclave:clickmap-token-updated";
18
+
19
+ //#endregion
20
+ exports.CLICKMAP_OVERLAY_RESUME_STORAGE_KEY = CLICKMAP_OVERLAY_RESUME_STORAGE_KEY;
21
+ exports.CLICKMAP_OVERLAY_TOKEN_STORAGE_KEY = CLICKMAP_OVERLAY_TOKEN_STORAGE_KEY;
22
+ exports.CLICKMAP_OVERLAY_TOKEN_UPDATED_EVENT = CLICKMAP_OVERLAY_TOKEN_UPDATED_EVENT;
23
+ //# sourceMappingURL=analytics-clickmap-overlay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics-clickmap-overlay.js","names":[],"sources":["../../src/utils/analytics-clickmap-overlay.tsx"],"sourcesContent":["/**\n * Wire protocol for handing a clickmap overlay token from the dashboard to the\n * in-page dev tool via `sessionStorage` + a window event.\n *\n * The token is a self-describing JWT: its payload already carries the\n * `project_id` and `origin` it was minted for, so the reader derives both from\n * the token itself and the writer only has to hand over a single value. The\n * dashboard (writer) and the dev tool (reader) live in different packages but\n * must agree on these exact names — this module is the single source of truth so\n * they can never silently desync.\n */\n\nexport const CLICKMAP_OVERLAY_TOKEN_STORAGE_KEY = \"hexclave-clickmap-token\";\nexport const CLICKMAP_OVERLAY_RESUME_STORAGE_KEY = \"hexclave-clickmap-resume\";\nexport const CLICKMAP_OVERLAY_TOKEN_UPDATED_EVENT = \"hexclave:clickmap-token-updated\";\n"],"mappings":";;;;;;;;;;;;;;AAYA,MAAa,qCAAqC;AAClD,MAAa,sCAAsC;AACnD,MAAa,uCAAuC"}
@@ -0,0 +1,31 @@
1
+ //#region src/utils/dev-tool.d.ts
2
+ /**
3
+ * Shared identity of the Hexclave in-page dev tool / clickmap overlay.
4
+ *
5
+ * These constants are the single source of truth for "is this DOM / event /
6
+ * stored click part of the dev tool itself?". They are consumed across package
7
+ * boundaries:
8
+ * - the dev tool mounts its root element with {@link DEV_TOOL_ROOT_ID} and
9
+ * prefixes every generated class with {@link DEV_TOOL_CLASS_PREFIX};
10
+ * - the event tracker uses them to skip self-clicks at ingest;
11
+ * - the backend clickmap query uses them to filter dev-tool clicks out of
12
+ * aggregate clickmaps server-side.
13
+ *
14
+ * Keep them here so a rename can never silently desync the SQL filter from the
15
+ * actual DOM identity.
16
+ */
17
+ declare const DEV_TOOL_ROOT_ID = "__hexclave-dev-tool-root";
18
+ /**
19
+ * Root element id of the standalone clickmap overlay. The clickmap is an
20
+ * independent feature with its own mount (it must survive the dev tool being
21
+ * removed), so it gets its own root — but its self-clicks need the exact same
22
+ * ingest/query exclusions as the dev tool's.
23
+ */
24
+ declare const CLICKMAP_ROOT_ID = "__hexclave-clickmap-root";
25
+ /** Prefix applied to every class/generated id the dev tool renders. */
26
+ declare const DEV_TOOL_CLASS_PREFIX = "sdt-";
27
+ /** Legacy class marker still present on older dev-tool builds. */
28
+ declare const DEV_TOOL_LEGACY_CLASS = "stack-devtool";
29
+ //#endregion
30
+ export { CLICKMAP_ROOT_ID, DEV_TOOL_CLASS_PREFIX, DEV_TOOL_LEGACY_CLASS, DEV_TOOL_ROOT_ID };
31
+ //# sourceMappingURL=dev-tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-tool.d.ts","names":[],"sources":["../../src/utils/dev-tool.tsx"],"mappings":";;AAeA;;;;;AAQA;;;;;AAGA;;;;cAXa,gBAAA;AAcb;;;;;;AAAA,cANa,gBAAA;;cAGA,qBAAA;;cAGA,qBAAA"}
@@ -0,0 +1,37 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/utils/dev-tool.tsx
4
+ /**
5
+ * Shared identity of the Hexclave in-page dev tool / clickmap overlay.
6
+ *
7
+ * These constants are the single source of truth for "is this DOM / event /
8
+ * stored click part of the dev tool itself?". They are consumed across package
9
+ * boundaries:
10
+ * - the dev tool mounts its root element with {@link DEV_TOOL_ROOT_ID} and
11
+ * prefixes every generated class with {@link DEV_TOOL_CLASS_PREFIX};
12
+ * - the event tracker uses them to skip self-clicks at ingest;
13
+ * - the backend clickmap query uses them to filter dev-tool clicks out of
14
+ * aggregate clickmaps server-side.
15
+ *
16
+ * Keep them here so a rename can never silently desync the SQL filter from the
17
+ * actual DOM identity.
18
+ */
19
+ const DEV_TOOL_ROOT_ID = "__hexclave-dev-tool-root";
20
+ /**
21
+ * Root element id of the standalone clickmap overlay. The clickmap is an
22
+ * independent feature with its own mount (it must survive the dev tool being
23
+ * removed), so it gets its own root — but its self-clicks need the exact same
24
+ * ingest/query exclusions as the dev tool's.
25
+ */
26
+ const CLICKMAP_ROOT_ID = "__hexclave-clickmap-root";
27
+ /** Prefix applied to every class/generated id the dev tool renders. */
28
+ const DEV_TOOL_CLASS_PREFIX = "sdt-";
29
+ /** Legacy class marker still present on older dev-tool builds. */
30
+ const DEV_TOOL_LEGACY_CLASS = "stack-devtool";
31
+
32
+ //#endregion
33
+ exports.CLICKMAP_ROOT_ID = CLICKMAP_ROOT_ID;
34
+ exports.DEV_TOOL_CLASS_PREFIX = DEV_TOOL_CLASS_PREFIX;
35
+ exports.DEV_TOOL_LEGACY_CLASS = DEV_TOOL_LEGACY_CLASS;
36
+ exports.DEV_TOOL_ROOT_ID = DEV_TOOL_ROOT_ID;
37
+ //# sourceMappingURL=dev-tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-tool.js","names":[],"sources":["../../src/utils/dev-tool.tsx"],"sourcesContent":["/**\n * Shared identity of the Hexclave in-page dev tool / clickmap overlay.\n *\n * These constants are the single source of truth for \"is this DOM / event /\n * stored click part of the dev tool itself?\". They are consumed across package\n * boundaries:\n * - the dev tool mounts its root element with {@link DEV_TOOL_ROOT_ID} and\n * prefixes every generated class with {@link DEV_TOOL_CLASS_PREFIX};\n * - the event tracker uses them to skip self-clicks at ingest;\n * - the backend clickmap query uses them to filter dev-tool clicks out of\n * aggregate clickmaps server-side.\n *\n * Keep them here so a rename can never silently desync the SQL filter from the\n * actual DOM identity.\n */\nexport const DEV_TOOL_ROOT_ID = \"__hexclave-dev-tool-root\";\n\n/**\n * Root element id of the standalone clickmap overlay. The clickmap is an\n * independent feature with its own mount (it must survive the dev tool being\n * removed), so it gets its own root — but its self-clicks need the exact same\n * ingest/query exclusions as the dev tool's.\n */\nexport const CLICKMAP_ROOT_ID = \"__hexclave-clickmap-root\";\n\n/** Prefix applied to every class/generated id the dev tool renders. */\nexport const DEV_TOOL_CLASS_PREFIX = \"sdt-\";\n\n/** Legacy class marker still present on older dev-tool builds. */\nexport const DEV_TOOL_LEGACY_CLASS = \"stack-devtool\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AAeA,MAAa,mBAAmB;;;;;;;AAQhC,MAAa,mBAAmB;;AAGhC,MAAa,wBAAwB;;AAGrC,MAAa,wBAAwB"}
@@ -1,5 +1,11 @@
1
1
  //#region src/utils/dom.d.ts
2
2
  declare function hasClickableParent(element: HTMLElement): boolean;
3
+ /**
4
+ * Escape a string so it is safe to use as a CSS identifier (id/class) inside a selector.
5
+ * Prefers the native `CSS.escape` when available, falling back to a conservative
6
+ * backslash-escape for non-DOM environments (SSR, tests, older runtimes).
7
+ */
8
+ declare function cssEscapeIdent(value: string): string;
3
9
  //#endregion
4
- export { hasClickableParent };
10
+ export { cssEscapeIdent, hasClickableParent };
5
11
  //# sourceMappingURL=dom.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dom.d.ts","names":[],"sources":["../../src/utils/dom.tsx"],"mappings":";iBAAgB,kBAAA,CAAmB,OAAA,EAAS,WAAA"}
1
+ {"version":3,"file":"dom.d.ts","names":[],"sources":["../../src/utils/dom.tsx"],"mappings":";iBAAgB,kBAAA,CAAmB,OAAA,EAAS,WAAA;AAA5C;;;;;AAAA,iBAagB,cAAA,CAAe,KAAA"}
package/dist/utils/dom.js CHANGED
@@ -7,7 +7,27 @@ function hasClickableParent(element) {
7
7
  if (parent.dataset.n2Clickable) return true;
8
8
  return hasClickableParent(element.parentElement);
9
9
  }
10
+ /**
11
+ * Escape a string so it is safe to use as a CSS identifier (id/class) inside a selector.
12
+ * Prefers the native `CSS.escape` when available, falling back to a conservative
13
+ * backslash-escape for non-DOM environments (SSR, tests, older runtimes).
14
+ */
15
+ function cssEscapeIdent(value) {
16
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(value);
17
+ let escaped = "";
18
+ for (let i = 0; i < value.length; i += 1) {
19
+ const char = value.charAt(i);
20
+ const codeUnit = value.charCodeAt(i);
21
+ if (codeUnit === 0) escaped += "�";
22
+ else if (codeUnit >= 1 && codeUnit <= 31 || codeUnit === 127 || i === 0 && codeUnit >= 48 && codeUnit <= 57 || i === 1 && codeUnit >= 48 && codeUnit <= 57 && value.charCodeAt(0) === 45) escaped += `\\${codeUnit.toString(16)} `;
23
+ else if (i === 0 && codeUnit === 45 && value.length === 1) escaped += "\\-";
24
+ else if (codeUnit >= 128 || codeUnit === 45 || codeUnit === 95 || codeUnit >= 48 && codeUnit <= 57 || codeUnit >= 65 && codeUnit <= 90 || codeUnit >= 97 && codeUnit <= 122) escaped += char;
25
+ else escaped += `\\${char}`;
26
+ }
27
+ return escaped;
28
+ }
10
29
 
11
30
  //#endregion
31
+ exports.cssEscapeIdent = cssEscapeIdent;
12
32
  exports.hasClickableParent = hasClickableParent;
13
33
  //# sourceMappingURL=dom.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dom.js","names":[],"sources":["../../src/utils/dom.tsx"],"sourcesContent":["export function hasClickableParent(element: HTMLElement): boolean {\n const parent = element.parentElement;\n if (!parent) return false;\n if (parent.dataset.n2Clickable) return true;\n\n return hasClickableParent(element.parentElement);\n}\n"],"mappings":";;;AAAA,SAAgB,mBAAmB,SAA+B;CAChE,MAAM,SAAS,QAAQ;AACvB,KAAI,CAAC,OAAQ,QAAO;AACpB,KAAI,OAAO,QAAQ,YAAa,QAAO;AAEvC,QAAO,mBAAmB,QAAQ,cAAc"}
1
+ {"version":3,"file":"dom.js","names":[],"sources":["../../src/utils/dom.tsx"],"sourcesContent":["export function hasClickableParent(element: HTMLElement): boolean {\n const parent = element.parentElement;\n if (!parent) return false;\n if (parent.dataset.n2Clickable) return true;\n\n return hasClickableParent(element.parentElement);\n}\n\n/**\n * Escape a string so it is safe to use as a CSS identifier (id/class) inside a selector.\n * Prefers the native `CSS.escape` when available, falling back to a conservative\n * backslash-escape for non-DOM environments (SSR, tests, older runtimes).\n */\nexport function cssEscapeIdent(value: string): string {\n if (typeof CSS !== \"undefined\" && typeof CSS.escape === \"function\") {\n return CSS.escape(value);\n }\n\n let escaped = \"\";\n for (let i = 0; i < value.length; i += 1) {\n const char = value.charAt(i);\n const codeUnit = value.charCodeAt(i);\n\n if (codeUnit === 0x0000) {\n escaped += \"\\uFFFD\";\n } else if (\n (codeUnit >= 0x0001 && codeUnit <= 0x001f) ||\n codeUnit === 0x007f ||\n (i === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||\n (i === 1 && codeUnit >= 0x0030 && codeUnit <= 0x0039 && value.charCodeAt(0) === 0x002d)\n ) {\n escaped += `\\\\${codeUnit.toString(16)} `;\n } else if (i === 0 && codeUnit === 0x002d && value.length === 1) {\n escaped += \"\\\\-\";\n } else if (\n codeUnit >= 0x0080 ||\n codeUnit === 0x002d ||\n codeUnit === 0x005f ||\n (codeUnit >= 0x0030 && codeUnit <= 0x0039) ||\n (codeUnit >= 0x0041 && codeUnit <= 0x005a) ||\n (codeUnit >= 0x0061 && codeUnit <= 0x007a)\n ) {\n escaped += char;\n } else {\n escaped += `\\\\${char}`;\n }\n }\n return escaped;\n}\n"],"mappings":";;;AAAA,SAAgB,mBAAmB,SAA+B;CAChE,MAAM,SAAS,QAAQ;AACvB,KAAI,CAAC,OAAQ,QAAO;AACpB,KAAI,OAAO,QAAQ,YAAa,QAAO;AAEvC,QAAO,mBAAmB,QAAQ,cAAc;;;;;;;AAQlD,SAAgB,eAAe,OAAuB;AACpD,KAAI,OAAO,QAAQ,eAAe,OAAO,IAAI,WAAW,WACtD,QAAO,IAAI,OAAO,MAAM;CAG1B,IAAI,UAAU;AACd,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxC,MAAM,OAAO,MAAM,OAAO,EAAE;EAC5B,MAAM,WAAW,MAAM,WAAW,EAAE;AAEpC,MAAI,aAAa,EACf,YAAW;WAEV,YAAY,KAAU,YAAY,MACnC,aAAa,OACZ,MAAM,KAAK,YAAY,MAAU,YAAY,MAC7C,MAAM,KAAK,YAAY,MAAU,YAAY,MAAU,MAAM,WAAW,EAAE,KAAK,GAEhF,YAAW,KAAK,SAAS,SAAS,GAAG,CAAC;WAC7B,MAAM,KAAK,aAAa,MAAU,MAAM,WAAW,EAC5D,YAAW;WAEX,YAAY,OACZ,aAAa,MACb,aAAa,MACZ,YAAY,MAAU,YAAY,MAClC,YAAY,MAAU,YAAY,MAClC,YAAY,MAAU,YAAY,IAEnC,YAAW;MAEX,YAAW,KAAK;;AAGpB,QAAO"}
@@ -0,0 +1,38 @@
1
+ //#region src/utils/elements-chain.d.ts
2
+ /**
3
+ * PostHog-style `elements_chain` format — the single owner of both halves of the
4
+ * contract. The event tracker {@link buildElementsChain serializes} a clicked
5
+ * element (and its ancestors) into a string; the clickmap overlay
6
+ * {@link parseElementsChain parses} that string back into structured segments so
7
+ * it can re-locate the element in a live DOM.
8
+ *
9
+ * Encode and decode MUST round-trip exactly, which is why they live together:
10
+ * the escaping applied here on the write side is reversed by the parser below,
11
+ * and a single round-trip test in `elements-chain.test.tsx` guards the pair.
12
+ *
13
+ * Segment shape (leaf-first, joined by `;`):
14
+ * tag.class1.class2:nth-child="2":nth-of-type="1":text="Save":attr__id="x":href="..."
15
+ */
16
+ type ElementsChainSegment = {
17
+ tag: string;
18
+ classes: string[];
19
+ attrs: Record<string, string>;
20
+ text: string | null;
21
+ nthChild: number | null;
22
+ nthOfType: number | null;
23
+ href: string | null;
24
+ };
25
+ declare const ELEMENTS_CHAIN_MAX_DEPTH = 8;
26
+ declare const ELEMENTS_CHAIN_TEXT_MAX = 80;
27
+ declare const ELEMENTS_CHAIN_ATTR_MAX = 200;
28
+ declare const ELEMENTS_CHAIN_ATTRS: readonly ["id", "data-testid", "data-test-id", "data-hexclave-id", "name", "type", "role", "aria-label", "placeholder", "title"];
29
+ /**
30
+ * Serialise a clicked element and up to {@link ELEMENTS_CHAIN_MAX_DEPTH}
31
+ * ancestors (leaf-first) into an `elements_chain` string.
32
+ */
33
+ declare function buildElementsChain(element: Element): string;
34
+ /** Parse an `elements_chain` string into structured, leaf-first segments. */
35
+ declare function parseElementsChain(chain: string): ElementsChainSegment[];
36
+ //#endregion
37
+ export { ELEMENTS_CHAIN_ATTRS, ELEMENTS_CHAIN_ATTR_MAX, ELEMENTS_CHAIN_MAX_DEPTH, ELEMENTS_CHAIN_TEXT_MAX, ElementsChainSegment, buildElementsChain, parseElementsChain };
38
+ //# sourceMappingURL=elements-chain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements-chain.d.ts","names":[],"sources":["../../src/utils/elements-chain.tsx"],"mappings":";;AAeA;;;;;;;;;;;;;KAAY,oBAAA;EACV,GAAA;EACA,OAAA;EACA,KAAA,EAAO,MAAA;EACP,IAAA;EACA,QAAA;EACA,SAAA;EACA,IAAA;AAAA;AAAA,cAGW,wBAAA;AAAA,cACA,uBAAA;AAAA,cACA,uBAAA;AAAA,cAKA,oBAAA;;;;AAAb;iBAyFgB,kBAAA,CAAmB,OAAA,EAAS,OAAA;;iBA0K5B,kBAAA,CAAmB,KAAA,WAAgB,oBAAA"}
@@ -0,0 +1,304 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/utils/elements-chain.tsx
4
+ const ELEMENTS_CHAIN_MAX_DEPTH = 8;
5
+ const ELEMENTS_CHAIN_TEXT_MAX = 80;
6
+ const ELEMENTS_CHAIN_ATTR_MAX = 200;
7
+ const ELEMENTS_CHAIN_ATTRS = [
8
+ "id",
9
+ "data-testid",
10
+ "data-test-id",
11
+ "data-hexclave-id",
12
+ "name",
13
+ "type",
14
+ "role",
15
+ "aria-label",
16
+ "placeholder",
17
+ "title"
18
+ ];
19
+ function escapeElementsChainValue(value) {
20
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
21
+ }
22
+ function escapeElementsChainClass(value) {
23
+ return value.replace(/\\/g, "\\\\").replace(/\./g, "\\.").replace(/:/g, "\\:");
24
+ }
25
+ function getElementClasses(element) {
26
+ const className = element.className;
27
+ if (typeof className !== "string" || className.trim() === "") return [];
28
+ return className.trim().split(/\s+/).filter(Boolean).slice(0, 4);
29
+ }
30
+ function getNthChildIndex(element) {
31
+ const parent = element.parentElement;
32
+ if (parent == null) return null;
33
+ const index = Array.prototype.indexOf.call(parent.children, element);
34
+ return index >= 0 ? index + 1 : null;
35
+ }
36
+ function getNthOfTypeIndex(element) {
37
+ const parent = element.parentElement;
38
+ if (parent == null) return null;
39
+ const tagName = element.tagName;
40
+ const siblings = Array.from(parent.children).filter((child) => child.tagName === tagName);
41
+ if (siblings.length <= 1) return null;
42
+ const index = siblings.indexOf(element);
43
+ return index >= 0 ? index + 1 : null;
44
+ }
45
+ function serializeElementsChainSegment(element) {
46
+ const parts = [];
47
+ parts.push(element.tagName.toLowerCase());
48
+ const classes = getElementClasses(element);
49
+ if (classes.length > 0) parts.push(`.${classes.map(escapeElementsChainClass).join(".")}`);
50
+ const text = element.textContent.trim().replace(/\s+/g, " ").slice(0, ELEMENTS_CHAIN_TEXT_MAX);
51
+ const nthChild = getNthChildIndex(element);
52
+ const nthOfType = getNthOfTypeIndex(element);
53
+ const attrPairs = [];
54
+ if (nthChild != null) attrPairs.push(`nth-child="${nthChild}"`);
55
+ if (nthOfType != null) attrPairs.push(`nth-of-type="${nthOfType}"`);
56
+ if (text !== "") attrPairs.push(`text="${escapeElementsChainValue(text)}"`);
57
+ for (const attrName of ELEMENTS_CHAIN_ATTRS) {
58
+ const value = element.getAttribute(attrName);
59
+ if (value == null || value === "") continue;
60
+ attrPairs.push(`attr__${attrName}="${escapeElementsChainValue(value.slice(0, ELEMENTS_CHAIN_ATTR_MAX))}"`);
61
+ }
62
+ if (element.tagName === "A") {
63
+ const href = element.getAttribute("href");
64
+ if (href != null && href !== "") attrPairs.push(`href="${escapeElementsChainValue(href.slice(0, ELEMENTS_CHAIN_ATTR_MAX))}"`);
65
+ }
66
+ if (attrPairs.length > 0) parts.push(`:${attrPairs.join(":")}`);
67
+ return parts.join("");
68
+ }
69
+ /**
70
+ * Serialise a clicked element and up to {@link ELEMENTS_CHAIN_MAX_DEPTH}
71
+ * ancestors (leaf-first) into an `elements_chain` string.
72
+ */
73
+ function buildElementsChain(element) {
74
+ const segments = [];
75
+ let current = element;
76
+ let depth = 0;
77
+ while (current != null && depth < ELEMENTS_CHAIN_MAX_DEPTH && current !== document.documentElement) {
78
+ segments.push(serializeElementsChainSegment(current));
79
+ current = current.parentElement;
80
+ depth += 1;
81
+ }
82
+ return segments.join(";");
83
+ }
84
+ function splitEscapedDots(input) {
85
+ const out = [];
86
+ let cur = "";
87
+ for (let i = 0; i < input.length; i++) {
88
+ const ch = input[i];
89
+ if (ch === "\\" && i + 1 < input.length) {
90
+ cur += input[i + 1];
91
+ i += 1;
92
+ continue;
93
+ }
94
+ if (ch === ".") {
95
+ out.push(cur);
96
+ cur = "";
97
+ continue;
98
+ }
99
+ cur += ch;
100
+ }
101
+ out.push(cur);
102
+ return out;
103
+ }
104
+ function applyElementsChainAttr(key, value) {
105
+ if (key === "nth-child") {
106
+ const n = Number.parseInt(value, 10);
107
+ return Number.isFinite(n) ? { nthChild: n } : {};
108
+ }
109
+ if (key === "nth-of-type") {
110
+ const n = Number.parseInt(value, 10);
111
+ return Number.isFinite(n) ? { nthOfType: n } : {};
112
+ }
113
+ if (key === "text") return { text: value };
114
+ if (key === "href") return {
115
+ href: value,
116
+ attrKey: key,
117
+ attrValue: value
118
+ };
119
+ if (key.startsWith("attr__")) return {
120
+ attrKey: key.slice(6),
121
+ attrValue: value
122
+ };
123
+ return {
124
+ attrKey: key,
125
+ attrValue: value
126
+ };
127
+ }
128
+ function parseElementsChainSegment(segment) {
129
+ const trimmed = segment.trim();
130
+ if (trimmed === "") return null;
131
+ let prefixEnd = trimmed.length;
132
+ let inQuotes = false;
133
+ for (let i = 0; i < trimmed.length; i++) {
134
+ const ch = trimmed[i];
135
+ if (ch === "\\" && i + 1 < trimmed.length) {
136
+ i += 1;
137
+ continue;
138
+ }
139
+ if (ch === "\"") {
140
+ inQuotes = !inQuotes;
141
+ continue;
142
+ }
143
+ if (ch === ":" && !inQuotes) {
144
+ prefixEnd = i;
145
+ break;
146
+ }
147
+ }
148
+ const prefix = trimmed.slice(0, prefixEnd);
149
+ const rest = trimmed.slice(prefixEnd);
150
+ const prefixParts = splitEscapedDots(prefix);
151
+ const tag = prefixParts[0].trim().toLowerCase();
152
+ if (tag === "") return null;
153
+ const classes = prefixParts.slice(1).map((c) => c.trim()).filter((c) => c !== "");
154
+ const attrs = {};
155
+ let nthChild = null;
156
+ let nthOfType = null;
157
+ let text = null;
158
+ let href = null;
159
+ let i = 0;
160
+ while (i < rest.length) {
161
+ if (rest[i] !== ":") {
162
+ i += 1;
163
+ continue;
164
+ }
165
+ i += 1;
166
+ let keyEnd = i;
167
+ while (keyEnd < rest.length && rest[keyEnd] !== "=" && rest[keyEnd] !== ":") keyEnd += 1;
168
+ const key = rest.slice(i, keyEnd).trim();
169
+ if (keyEnd >= rest.length || rest[keyEnd] !== "=") {
170
+ i = keyEnd;
171
+ continue;
172
+ }
173
+ let valStart = keyEnd + 1;
174
+ if (rest[valStart] !== "\"") {
175
+ let end = valStart;
176
+ while (end < rest.length && rest[end] !== ":") end += 1;
177
+ const result = applyElementsChainAttr(key, rest.slice(valStart, end));
178
+ if (result.nthChild != null) nthChild = result.nthChild;
179
+ if (result.nthOfType != null) nthOfType = result.nthOfType;
180
+ if (result.text != null) text = result.text;
181
+ if (result.href != null) href = result.href;
182
+ if (result.attrKey != null) attrs[result.attrKey] = result.attrValue ?? "";
183
+ i = end;
184
+ continue;
185
+ }
186
+ valStart += 1;
187
+ let end = valStart;
188
+ let value = "";
189
+ while (end < rest.length) {
190
+ const ch = rest[end];
191
+ if (ch === "\\" && end + 1 < rest.length) {
192
+ const next = rest[end + 1];
193
+ if (next === "\"" || next === "\\") {
194
+ value += next;
195
+ end += 2;
196
+ continue;
197
+ }
198
+ value += ch;
199
+ end += 1;
200
+ continue;
201
+ }
202
+ if (ch === "\"") break;
203
+ value += ch;
204
+ end += 1;
205
+ }
206
+ const result = applyElementsChainAttr(key, value);
207
+ if (result.nthChild != null) nthChild = result.nthChild;
208
+ if (result.nthOfType != null) nthOfType = result.nthOfType;
209
+ if (result.text != null) text = result.text;
210
+ if (result.href != null) href = result.href;
211
+ if (result.attrKey != null) attrs[result.attrKey] = result.attrValue ?? "";
212
+ i = end + 1;
213
+ }
214
+ return {
215
+ tag,
216
+ classes,
217
+ attrs,
218
+ text,
219
+ nthChild,
220
+ nthOfType,
221
+ href
222
+ };
223
+ }
224
+ /** Parse an `elements_chain` string into structured, leaf-first segments. */
225
+ function parseElementsChain(chain) {
226
+ const segments = [];
227
+ let current = "";
228
+ let inQuotes = false;
229
+ for (let i = 0; i < chain.length; i++) {
230
+ const ch = chain[i];
231
+ if (ch === "\\" && i + 1 < chain.length) {
232
+ current += ch + chain[i + 1];
233
+ i += 1;
234
+ continue;
235
+ }
236
+ if (ch === "\"") {
237
+ inQuotes = !inQuotes;
238
+ current += ch;
239
+ continue;
240
+ }
241
+ if (ch === ";" && !inQuotes) {
242
+ segments.push(current);
243
+ current = "";
244
+ continue;
245
+ }
246
+ current += ch;
247
+ }
248
+ if (current.length > 0) segments.push(current);
249
+ return segments.map(parseElementsChainSegment).filter((segment) => segment != null);
250
+ }
251
+ (void 0)?.test("parseElementsChain parses a simple leaf+ancestor chain", ({ expect }) => {
252
+ expect(parseElementsChain("button.btn.btn-primary:nth-of-type=\"1\":text=\"Save\":attr__id=\"save-btn\";div.container")).toEqual([{
253
+ tag: "button",
254
+ classes: ["btn", "btn-primary"],
255
+ attrs: { id: "save-btn" },
256
+ text: "Save",
257
+ nthChild: null,
258
+ nthOfType: 1,
259
+ href: null
260
+ }, {
261
+ tag: "div",
262
+ classes: ["container"],
263
+ attrs: {},
264
+ text: null,
265
+ nthChild: null,
266
+ nthOfType: null,
267
+ href: null
268
+ }]);
269
+ });
270
+ (void 0)?.test("parseElementsChain reverses class escaping for Tailwind-style tokens", ({ expect }) => {
271
+ expect(parseElementsChain("a.md\\:hover\\:bg-blue-500.w-\\[1\\.5rem\\]:href=\"/p?a=1\"")).toEqual([{
272
+ tag: "a",
273
+ classes: ["md:hover:bg-blue-500", "w-[1.5rem]"],
274
+ attrs: { href: "/p?a=1" },
275
+ text: null,
276
+ nthChild: null,
277
+ nthOfType: null,
278
+ href: "/p?a=1"
279
+ }]);
280
+ });
281
+ (void 0)?.test("parseElementsChain unescapes quotes/backslashes and ignores ';' inside quoted values", ({ expect }) => {
282
+ expect(parseElementsChain("span:text=\"a \\\"b\\\"; c \\\\ d\"")).toEqual([{
283
+ tag: "span",
284
+ classes: [],
285
+ attrs: {},
286
+ text: "a \"b\"; c \\ d",
287
+ nthChild: null,
288
+ nthOfType: null,
289
+ href: null
290
+ }]);
291
+ });
292
+ (void 0)?.test("parseElementsChain drops empty/tagless segments", ({ expect }) => {
293
+ expect(parseElementsChain("")).toEqual([]);
294
+ expect(parseElementsChain(";;")).toEqual([]);
295
+ });
296
+
297
+ //#endregion
298
+ exports.ELEMENTS_CHAIN_ATTRS = ELEMENTS_CHAIN_ATTRS;
299
+ exports.ELEMENTS_CHAIN_ATTR_MAX = ELEMENTS_CHAIN_ATTR_MAX;
300
+ exports.ELEMENTS_CHAIN_MAX_DEPTH = ELEMENTS_CHAIN_MAX_DEPTH;
301
+ exports.ELEMENTS_CHAIN_TEXT_MAX = ELEMENTS_CHAIN_TEXT_MAX;
302
+ exports.buildElementsChain = buildElementsChain;
303
+ exports.parseElementsChain = parseElementsChain;
304
+ //# sourceMappingURL=elements-chain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elements-chain.js","names":[],"sources":["../../src/utils/elements-chain.tsx"],"sourcesContent":["/**\n * PostHog-style `elements_chain` format — the single owner of both halves of the\n * contract. The event tracker {@link buildElementsChain serializes} a clicked\n * element (and its ancestors) into a string; the clickmap overlay\n * {@link parseElementsChain parses} that string back into structured segments so\n * it can re-locate the element in a live DOM.\n *\n * Encode and decode MUST round-trip exactly, which is why they live together:\n * the escaping applied here on the write side is reversed by the parser below,\n * and a single round-trip test in `elements-chain.test.tsx` guards the pair.\n *\n * Segment shape (leaf-first, joined by `;`):\n * tag.class1.class2:nth-child=\"2\":nth-of-type=\"1\":text=\"Save\":attr__id=\"x\":href=\"...\"\n */\n\nexport type ElementsChainSegment = {\n tag: string,\n classes: string[],\n attrs: Record<string, string>,\n text: string | null,\n nthChild: number | null,\n nthOfType: number | null,\n href: string | null,\n};\n\nexport const ELEMENTS_CHAIN_MAX_DEPTH = 8;\nexport const ELEMENTS_CHAIN_TEXT_MAX = 80;\nexport const ELEMENTS_CHAIN_ATTR_MAX = 200;\n\n// Attributes we serialise into elements_chain. Mirrors the set PostHog persists:\n// stable identifiers (id, data-testid), semantics (role, type, name, aria-label),\n// and a few we expect downstream tooling to want to match against.\nexport const ELEMENTS_CHAIN_ATTRS = [\n \"id\",\n \"data-testid\",\n \"data-test-id\",\n \"data-hexclave-id\",\n \"name\",\n \"type\",\n \"role\",\n \"aria-label\",\n \"placeholder\",\n \"title\",\n] as const;\n\n// ---------------------------------------------------------------------------\n// Serialization (DOM element -> elements_chain string)\n// ---------------------------------------------------------------------------\n\nfunction escapeElementsChainValue(value: string): string {\n return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\n// Class tokens are written into the unquoted, dot-joined prefix of a segment, so\n// any \".\" or \":\" inside a class (e.g. Tailwind variants like `md:hover:bg-blue-500`\n// or arbitrary values like `w-[1.5rem]`) must be escaped to round-trip through the\n// parser, which splits the prefix on unescaped \".\" and the segment on unescaped \":\".\nfunction escapeElementsChainClass(value: string): string {\n return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\\./g, \"\\\\.\").replace(/:/g, \"\\\\:\");\n}\n\nfunction getElementClasses(element: Element): string[] {\n const className = (element as HTMLElement).className;\n if (typeof className !== \"string\" || className.trim() === \"\") {\n return [];\n }\n return className.trim().split(/\\s+/).filter(Boolean).slice(0, 4);\n}\n\nfunction getNthChildIndex(element: Element): number | null {\n const parent = element.parentElement;\n if (parent == null) return null;\n const index = Array.prototype.indexOf.call(parent.children, element);\n return index >= 0 ? index + 1 : null;\n}\n\nfunction getNthOfTypeIndex(element: Element): number | null {\n const parent = element.parentElement;\n if (parent == null) return null;\n const tagName = element.tagName;\n const siblings = Array.from(parent.children).filter((child) => child.tagName === tagName);\n if (siblings.length <= 1) return null;\n const index = siblings.indexOf(element);\n return index >= 0 ? index + 1 : null;\n}\n\nfunction serializeElementsChainSegment(element: Element): string {\n const parts: string[] = [];\n parts.push(element.tagName.toLowerCase());\n const classes = getElementClasses(element);\n if (classes.length > 0) {\n parts.push(`.${classes.map(escapeElementsChainClass).join(\".\")}`);\n }\n const text = element.textContent.trim().replace(/\\s+/g, \" \").slice(0, ELEMENTS_CHAIN_TEXT_MAX);\n const nthChild = getNthChildIndex(element);\n const nthOfType = getNthOfTypeIndex(element);\n const attrPairs: string[] = [];\n if (nthChild != null) attrPairs.push(`nth-child=\"${nthChild}\"`);\n if (nthOfType != null) attrPairs.push(`nth-of-type=\"${nthOfType}\"`);\n if (text !== \"\") attrPairs.push(`text=\"${escapeElementsChainValue(text)}\"`);\n for (const attrName of ELEMENTS_CHAIN_ATTRS) {\n const value = element.getAttribute(attrName);\n if (value == null || value === \"\") continue;\n attrPairs.push(`attr__${attrName}=\"${escapeElementsChainValue(value.slice(0, ELEMENTS_CHAIN_ATTR_MAX))}\"`);\n }\n if (element.tagName === \"A\") {\n const href = element.getAttribute(\"href\");\n if (href != null && href !== \"\") {\n attrPairs.push(`href=\"${escapeElementsChainValue(href.slice(0, ELEMENTS_CHAIN_ATTR_MAX))}\"`);\n }\n }\n if (attrPairs.length > 0) {\n parts.push(`:${attrPairs.join(\":\")}`);\n }\n return parts.join(\"\");\n}\n\n/**\n * Serialise a clicked element and up to {@link ELEMENTS_CHAIN_MAX_DEPTH}\n * ancestors (leaf-first) into an `elements_chain` string.\n */\nexport function buildElementsChain(element: Element): string {\n const segments: string[] = [];\n let current: Element | null = element;\n let depth = 0;\n while (current != null && depth < ELEMENTS_CHAIN_MAX_DEPTH && current !== document.documentElement) {\n segments.push(serializeElementsChainSegment(current));\n current = current.parentElement;\n depth += 1;\n }\n return segments.join(\";\");\n}\n\n// ---------------------------------------------------------------------------\n// Parsing (elements_chain string -> structured segments)\n// ---------------------------------------------------------------------------\n\n// Split a string on unescaped occurrences of `.`, unescaping `\\.`, `\\:` and `\\\\`\n// back to their literal characters. Reverses `escapeElementsChainClass`.\nfunction splitEscapedDots(input: string): string[] {\n const out: string[] = [];\n let cur = '';\n for (let i = 0; i < input.length; i++) {\n const ch = input[i];\n if (ch === '\\\\' && i + 1 < input.length) {\n cur += input[i + 1];\n i += 1;\n continue;\n }\n if (ch === '.') {\n out.push(cur);\n cur = '';\n continue;\n }\n cur += ch;\n }\n out.push(cur);\n return out;\n}\n\ntype ElementsChainAttrResult = {\n nthChild?: number,\n nthOfType?: number,\n text?: string,\n href?: string,\n attrKey?: string,\n attrValue?: string,\n};\n\nfunction applyElementsChainAttr(key: string, value: string): ElementsChainAttrResult {\n if (key === 'nth-child') {\n const n = Number.parseInt(value, 10);\n return Number.isFinite(n) ? { nthChild: n } : {};\n }\n if (key === 'nth-of-type') {\n const n = Number.parseInt(value, 10);\n return Number.isFinite(n) ? { nthOfType: n } : {};\n }\n if (key === 'text') {\n return { text: value };\n }\n if (key === 'href') {\n return { href: value, attrKey: key, attrValue: value };\n }\n if (key.startsWith('attr__')) {\n return { attrKey: key.slice('attr__'.length), attrValue: value };\n }\n return { attrKey: key, attrValue: value };\n}\n\nfunction parseElementsChainSegment(segment: string): ElementsChainSegment | null {\n const trimmed = segment.trim();\n if (trimmed === '') return null;\n\n // Find first ':' at top level — separates tag/classes prefix from attribute pairs.\n let prefixEnd = trimmed.length;\n let inQuotes = false;\n for (let i = 0; i < trimmed.length; i++) {\n const ch = trimmed[i];\n if (ch === '\\\\' && i + 1 < trimmed.length) {\n i += 1;\n continue;\n }\n if (ch === '\"') {\n inQuotes = !inQuotes;\n continue;\n }\n if (ch === ':' && !inQuotes) {\n prefixEnd = i;\n break;\n }\n }\n\n const prefix = trimmed.slice(0, prefixEnd);\n const rest = trimmed.slice(prefixEnd);\n const prefixParts = splitEscapedDots(prefix);\n const tag = prefixParts[0].trim().toLowerCase();\n if (tag === '') return null;\n const classes = prefixParts.slice(1).map((c) => c.trim()).filter((c) => c !== '');\n\n const attrs: Record<string, string> = {};\n let nthChild: number | null = null;\n let nthOfType: number | null = null;\n let text: string | null = null;\n let href: string | null = null;\n\n // Parse :key=\"value\" pairs from rest.\n let i = 0;\n while (i < rest.length) {\n if (rest[i] !== ':') {\n i += 1;\n continue;\n }\n i += 1; // skip ':'\n // read key up to '='\n let keyEnd = i;\n while (keyEnd < rest.length && rest[keyEnd] !== '=' && rest[keyEnd] !== ':') keyEnd += 1;\n const key = rest.slice(i, keyEnd).trim();\n if (keyEnd >= rest.length || rest[keyEnd] !== '=') {\n i = keyEnd;\n continue;\n }\n let valStart = keyEnd + 1;\n if (rest[valStart] !== '\"') {\n // unquoted — read until next ':' at top level\n let end = valStart;\n while (end < rest.length && rest[end] !== ':') end += 1;\n const value = rest.slice(valStart, end);\n const result = applyElementsChainAttr(key, value);\n if (result.nthChild != null) nthChild = result.nthChild;\n if (result.nthOfType != null) nthOfType = result.nthOfType;\n if (result.text != null) text = result.text;\n if (result.href != null) href = result.href;\n if (result.attrKey != null) attrs[result.attrKey] = result.attrValue ?? '';\n i = end;\n continue;\n }\n // quoted value — find unescaped closing quote\n valStart += 1;\n let end = valStart;\n let value = '';\n while (end < rest.length) {\n const ch = rest[end];\n if (ch === '\\\\' && end + 1 < rest.length) {\n const next = rest[end + 1];\n if (next === '\"' || next === '\\\\') {\n value += next;\n end += 2;\n continue;\n }\n value += ch;\n end += 1;\n continue;\n }\n if (ch === '\"') break;\n value += ch;\n end += 1;\n }\n const result = applyElementsChainAttr(key, value);\n if (result.nthChild != null) nthChild = result.nthChild;\n if (result.nthOfType != null) nthOfType = result.nthOfType;\n if (result.text != null) text = result.text;\n if (result.href != null) href = result.href;\n if (result.attrKey != null) attrs[result.attrKey] = result.attrValue ?? '';\n i = end + 1; // skip closing quote\n }\n\n return { tag, classes, attrs, text, nthChild, nthOfType, href };\n}\n\n/** Parse an `elements_chain` string into structured, leaf-first segments. */\nexport function parseElementsChain(chain: string): ElementsChainSegment[] {\n // Split top-level by ';' respecting quoted strings.\n const segments: string[] = [];\n let current = '';\n let inQuotes = false;\n for (let i = 0; i < chain.length; i++) {\n const ch = chain[i];\n if (ch === '\\\\' && i + 1 < chain.length) {\n current += ch + chain[i + 1];\n i += 1;\n continue;\n }\n if (ch === '\"') {\n inQuotes = !inQuotes;\n current += ch;\n continue;\n }\n if (ch === ';' && !inQuotes) {\n segments.push(current);\n current = '';\n continue;\n }\n current += ch;\n }\n if (current.length > 0) {\n segments.push(current);\n }\n return segments.map(parseElementsChainSegment).filter((segment): segment is ElementsChainSegment => segment != null);\n}\n\nimport.meta.vitest?.test(\"parseElementsChain parses a simple leaf+ancestor chain\", ({ expect }) => {\n const parsed = parseElementsChain('button.btn.btn-primary:nth-of-type=\"1\":text=\"Save\":attr__id=\"save-btn\";div.container');\n expect(parsed).toEqual([\n { tag: \"button\", classes: [\"btn\", \"btn-primary\"], attrs: { id: \"save-btn\" }, text: \"Save\", nthChild: null, nthOfType: 1, href: null },\n { tag: \"div\", classes: [\"container\"], attrs: {}, text: null, nthChild: null, nthOfType: null, href: null },\n ]);\n});\n\nimport.meta.vitest?.test(\"parseElementsChain reverses class escaping for Tailwind-style tokens\", ({ expect }) => {\n // `md:hover:bg-blue-500` and `w-[1.5rem]` contain the prefix delimiters `:`/`.`,\n // so the serializer escapes them; the parser must recover the literal classes.\n const parsed = parseElementsChain('a.md\\\\:hover\\\\:bg-blue-500.w-\\\\[1\\\\.5rem\\\\]:href=\"/p?a=1\"');\n expect(parsed).toEqual([\n { tag: \"a\", classes: [\"md:hover:bg-blue-500\", \"w-[1.5rem]\"], attrs: { href: \"/p?a=1\" }, text: null, nthChild: null, nthOfType: null, href: \"/p?a=1\" },\n ]);\n});\n\nimport.meta.vitest?.test(\"parseElementsChain unescapes quotes/backslashes and ignores ';' inside quoted values\", ({ expect }) => {\n const parsed = parseElementsChain('span:text=\"a \\\\\"b\\\\\"; c \\\\\\\\ d\"');\n expect(parsed).toEqual([\n { tag: \"span\", classes: [], attrs: {}, text: 'a \"b\"; c \\\\ d', nthChild: null, nthOfType: null, href: null },\n ]);\n});\n\nimport.meta.vitest?.test(\"parseElementsChain drops empty/tagless segments\", ({ expect }) => {\n expect(parseElementsChain(\"\")).toEqual([]);\n expect(parseElementsChain(\";;\")).toEqual([]);\n});\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexclave/shared",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "repository": "https://github.com/hexclave/hexclave",
5
5
  "files": [
6
6
  "README.md",
@@ -168,6 +168,12 @@ export const ALL_APPS = {
168
168
  tags: ["developers", "operations"],
169
169
  stage: "stable",
170
170
  },
171
+ "clickmaps": {
172
+ displayName: "Clickmaps",
173
+ subtitle: "Visualize where users click across your app",
174
+ tags: ["developers", "operations"],
175
+ stage: "alpha",
176
+ },
171
177
  "session-replays": {
172
178
  displayName: "Session Replays",
173
179
  subtitle: "Watch real user sessions to understand how people use your app",
@@ -1,6 +1,6 @@
1
1
  import { expect, it } from "vitest";
2
- import { typeAssertExtends } from "./utils/types";
3
2
  import { defineStackConfig, type StackConfig } from "./config-authoring";
3
+ import { typeAssertExtends } from "./utils/types";
4
4
 
5
5
  const validConfig = defineStackConfig({
6
6
  payments: {
@@ -10,16 +10,16 @@ export type HexclaveConfig = StackConfigObject | typeof showOnboardingHexclaveCo
10
10
 
11
11
  type StrictConfigShape<Actual, Expected> =
12
12
  Expected extends readonly unknown[]
13
- ? Actual extends readonly unknown[]
13
+ ? Actual extends readonly unknown[]
14
14
  ? { [K in keyof Actual]: K extends keyof Expected ? StrictConfigShape<Actual[K], Expected[K]> : never }
15
- : Actual
15
+ : Actual
16
16
  : Expected extends object
17
- ? Actual extends object
17
+ ? Actual extends object
18
18
  ? Exclude<keyof Actual, keyof Expected> extends never
19
19
  ? { [K in keyof Actual]: K extends keyof Expected ? StrictConfigShape<Actual[K], Expected[K]> : never }
20
- : never
21
- : Actual
22
- : Actual;
20
+ : never
21
+ : Actual
22
+ : Actual;
23
23
 
24
24
  type StrictStackConfig<T extends StackConfig> =
25
25
  T extends StackConfigObject
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from "fs";
2
2
  import path from "path";
3
- import { parseHexclaveConfigFileContent, renderConfigFileContent } from "./hexclave-config-file";
4
- export { parseHexclaveConfigFileContent, renderConfigFileContent };
3
+ import { hexclaveConfigFileExportsConfig, parseHexclaveConfigFileContent, renderConfigFileContent, tryParseHexclaveConfigFileContent } from "./hexclave-config-file";
4
+ export { hexclaveConfigFileExportsConfig, parseHexclaveConfigFileContent, renderConfigFileContent, tryParseHexclaveConfigFileContent };
5
5
 
6
6
  /**
7
7
  * Packages that export the `HexclaveConfig` type, in priority order.
@@ -106,6 +106,28 @@ import.meta.vitest?.test("parseHexclaveConfigFileContent rejects dynamic config
106
106
  expect(() => parseHexclaveConfigFileContent("export const config = makeConfig();", "stack.config.ts")).toThrow(/Unsupported config expression/);
107
107
  });
108
108
 
109
+ import.meta.vitest?.test("tryParseHexclaveConfigFileContent returns the config for static exports", ({ expect }) => {
110
+ expect(tryParseHexclaveConfigFileContent("export const config = { auth: { allowSignUp: true } };", "stack.config.ts")).toEqual({
111
+ auth: { allowSignUp: true },
112
+ });
113
+ });
114
+
115
+ import.meta.vitest?.test("tryParseHexclaveConfigFileContent returns null for non-static exports", ({ expect }) => {
116
+ // Wrapped in a helper call (e.g. defineStackConfig) -> not a plain literal.
117
+ expect(tryParseHexclaveConfigFileContent("export const config = makeConfig();", "stack.config.ts")).toBeNull();
118
+ // References an imported value -> has structure to preserve.
119
+ expect(tryParseHexclaveConfigFileContent('import x from "./x.txt" with { type: "text" };\nexport const config = { a: x };', "stack.config.ts")).toBeNull();
120
+ // Syntax error.
121
+ expect(tryParseHexclaveConfigFileContent("export const config = {", "stack.config.ts")).toBeNull();
122
+ });
123
+
124
+ import.meta.vitest?.test("hexclaveConfigFileExportsConfig detects a config export", ({ expect }) => {
125
+ expect(hexclaveConfigFileExportsConfig("export const config = { a: 1 };", "stack.config.ts")).toBe(true);
126
+ expect(hexclaveConfigFileExportsConfig('import x from "./x.txt" with { type: "text" };\nexport const config = { a: x };', "stack.config.ts")).toBe(true);
127
+ expect(hexclaveConfigFileExportsConfig("export const notConfig = { a: 1 };", "stack.config.ts")).toBe(false);
128
+ expect(hexclaveConfigFileExportsConfig("export const config = {", "stack.config.ts")).toBe(false);
129
+ });
130
+
109
131
  import.meta.vitest?.test("renderConfigFileContent rejects conflicting dotted keys", ({ expect }) => {
110
132
  expect(() => renderConfigFileContent({
111
133
  "a.b": 1,