@ai-matrx/kit 0.5.1 → 0.6.0
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.
- package/CHANGELOG.md +15 -0
- package/dist/confirm.cjs +2 -1
- package/dist/confirm.cjs.map +1 -1
- package/dist/confirm.js +2 -1
- package/dist/confirm.js.map +1 -1
- package/dist/index.cjs +2 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/react-tree.cjs +2 -1
- package/dist/react-tree.cjs.map +1 -1
- package/dist/react-tree.js +2 -1
- package/dist/react-tree.js.map +1 -1
- package/dist/short-link-react.cjs +159 -0
- package/dist/short-link-react.cjs.map +1 -0
- package/dist/short-link-react.d.cts +59 -0
- package/dist/short-link-react.d.ts +59 -0
- package/dist/short-link-react.js +139 -0
- package/dist/short-link-react.js.map +1 -0
- package/dist/short-link.cjs +97 -0
- package/dist/short-link.cjs.map +1 -0
- package/dist/short-link.d.cts +94 -0
- package/dist/short-link.d.ts +94 -0
- package/dist/short-link.js +76 -0
- package/dist/short-link.js.map +1 -0
- package/package.json +23 -2
package/dist/react-tree.cjs
CHANGED
|
@@ -57,7 +57,8 @@ function treeContainsComponent(node, Component) {
|
|
|
57
57
|
(child) => treeContainsComponent(child, Component)
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
const runtimeProcess = globalThis.process;
|
|
61
|
+
if (runtimeProcess?.env?.NODE_ENV !== "production") {
|
|
61
62
|
const keys = typeof node === "object" ? ` with keys {${Object.keys(node).join(", ")}}` : "";
|
|
62
63
|
console.error(
|
|
63
64
|
`[treeContainsComponent] A non-renderable value${keys} is being passed as a React child. React will throw 'Objects are not valid as a React child' at the real render site. Stringify it (e.g. JSON.stringify) before rendering.`,
|
package/dist/react-tree.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react-tree.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/react-tree — safe React children-tree scanning.\n *\n * Ported from matrx-frontend `lib/react/treeContainsComponent.ts`; this\n * subpath is the ONE home of the scanner inside the kit (the `/confirm`\n * subpath's alert-dialog imports it from here — no duplicate bodies).\n *\n * Two deliberate divergences from the frontend original, both documented:\n * - the dev-mode scream checks `typeof process` first, since this package may\n * load in an unbundled browser context where `process` is undefined;\n * - a React PORTAL child (`createPortal(...)` passed as a child) is a valid\n * React child, but it is not an element, an iterable, or a primitive — the\n * original fell through to the non-renderable branch, screamed a false\n * positive in dev, and skipped the portal's content. Portals are now\n * recognized and their children traversed.\n */\n\nimport * as React from \"react\";\n\nconst REACT_PORTAL_TYPE = Symbol.for(\"react.portal\");\n\n/**\n * Returns true if `Component` appears anywhere in the React element tree under\n * `node`. Used to detect optional a11y children (e.g. DialogDescription)\n * without rendering duplicates.\n *\n * DEFENSIVE + LOUD. A non-renderable node — a plain object or function passed\n * as a React child — is a real bug: React throws \"Objects are not valid as a\n * React child\" the instant it renders one. This a11y probe must NOT be the\n * crash site. `React.Children.toArray` would throw HERE, producing a trace that\n * points at the dialog primitive instead of the component that leaked the\n * object (this misdirection has burned real debugging hours). So we walk the\n * tree by hand, SKIP any non-renderable node, and scream in dev with its keys —\n * then let React report the defect at the true render site with the offending\n * component in the stack. For every VALID tree the result is identical to the\n * old `React.Children.toArray(node).some(...)`.\n */\nexport function treeContainsComponent(\n node: React.ReactNode,\n Component: React.ElementType,\n): boolean {\n if (node == null || typeof node === \"boolean\") return false;\n\n if (Array.isArray(node)) {\n return node.some((child) => treeContainsComponent(child, Component));\n }\n\n if (React.isValidElement(node)) {\n if (node.type === Component) return true;\n const props = node.props as { children?: React.ReactNode };\n return props.children != null\n ? treeContainsComponent(props.children, Component)\n : false;\n }\n\n // Strings / numbers are valid leaf children but never the Component.\n if (typeof node === \"string\" || typeof node === \"number\") return false;\n\n // A portal is a valid child that is NOT an element: traverse its content.\n if (\n typeof node === \"object\" &&\n (node as { $$typeof?: unknown }).$$typeof === REACT_PORTAL_TYPE\n ) {\n return treeContainsComponent(\n (node as { children?: React.ReactNode }).children,\n Component,\n );\n }\n\n // Non-array iterables (Set, Map, generator) are valid React children — React\n // supports them — so traverse rather than reject.\n if (typeof node === \"object\" && Symbol.iterator in node) {\n return Array.from(node as Iterable<React.ReactNode>).some((child) =>\n treeContainsComponent(child, Component),\n );\n }\n\n // Anything else (a raw object, a function) is NOT a valid React child. React\n // will throw when it renders this; we must not throw first and hide the cause.\n
|
|
1
|
+
{"version":3,"sources":["../src/react-tree.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/react-tree — safe React children-tree scanning.\n *\n * Ported from matrx-frontend `lib/react/treeContainsComponent.ts`; this\n * subpath is the ONE home of the scanner inside the kit (the `/confirm`\n * subpath's alert-dialog imports it from here — no duplicate bodies).\n *\n * Two deliberate divergences from the frontend original, both documented:\n * - the dev-mode scream checks `typeof process` first, since this package may\n * load in an unbundled browser context where `process` is undefined;\n * - a React PORTAL child (`createPortal(...)` passed as a child) is a valid\n * React child, but it is not an element, an iterable, or a primitive — the\n * original fell through to the non-renderable branch, screamed a false\n * positive in dev, and skipped the portal's content. Portals are now\n * recognized and their children traversed.\n */\n\nimport * as React from \"react\";\n\nconst REACT_PORTAL_TYPE = Symbol.for(\"react.portal\");\n\n/**\n * Returns true if `Component` appears anywhere in the React element tree under\n * `node`. Used to detect optional a11y children (e.g. DialogDescription)\n * without rendering duplicates.\n *\n * DEFENSIVE + LOUD. A non-renderable node — a plain object or function passed\n * as a React child — is a real bug: React throws \"Objects are not valid as a\n * React child\" the instant it renders one. This a11y probe must NOT be the\n * crash site. `React.Children.toArray` would throw HERE, producing a trace that\n * points at the dialog primitive instead of the component that leaked the\n * object (this misdirection has burned real debugging hours). So we walk the\n * tree by hand, SKIP any non-renderable node, and scream in dev with its keys —\n * then let React report the defect at the true render site with the offending\n * component in the stack. For every VALID tree the result is identical to the\n * old `React.Children.toArray(node).some(...)`.\n */\nexport function treeContainsComponent(\n node: React.ReactNode,\n Component: React.ElementType,\n): boolean {\n if (node == null || typeof node === \"boolean\") return false;\n\n if (Array.isArray(node)) {\n return node.some((child) => treeContainsComponent(child, Component));\n }\n\n if (React.isValidElement(node)) {\n if (node.type === Component) return true;\n const props = node.props as { children?: React.ReactNode };\n return props.children != null\n ? treeContainsComponent(props.children, Component)\n : false;\n }\n\n // Strings / numbers are valid leaf children but never the Component.\n if (typeof node === \"string\" || typeof node === \"number\") return false;\n\n // A portal is a valid child that is NOT an element: traverse its content.\n if (\n typeof node === \"object\" &&\n (node as { $$typeof?: unknown }).$$typeof === REACT_PORTAL_TYPE\n ) {\n return treeContainsComponent(\n (node as { children?: React.ReactNode }).children,\n Component,\n );\n }\n\n // Non-array iterables (Set, Map, generator) are valid React children — React\n // supports them — so traverse rather than reject.\n if (typeof node === \"object\" && Symbol.iterator in node) {\n return Array.from(node as Iterable<React.ReactNode>).some((child) =>\n treeContainsComponent(child, Component),\n );\n }\n\n // Anything else (a raw object, a function) is NOT a valid React child. React\n // will throw when it renders this; we must not throw first and hide the cause.\n const runtimeProcess = (\n globalThis as { process?: { env?: { NODE_ENV?: string } } }\n ).process;\n if (runtimeProcess?.env?.NODE_ENV !== \"production\") {\n const keys =\n typeof node === \"object\"\n ? ` with keys {${Object.keys(node).join(\", \")}}`\n : \"\";\n console.error(\n `[treeContainsComponent] A non-renderable value${keys} is being passed as a React child. ` +\n \"React will throw 'Objects are not valid as a React child' at the real render site. \" +\n \"Stringify it (e.g. JSON.stringify) before rendering.\",\n node,\n );\n }\n return false;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,YAAuB;AAEvB,IAAM,oBAAoB,uBAAO,IAAI,cAAc;AAkB5C,SAAS,sBACd,MACA,WACS;AACT,MAAI,QAAQ,QAAQ,OAAO,SAAS,UAAW,QAAO;AAEtD,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,WAAO,KAAK,KAAK,CAAC,UAAU,sBAAsB,OAAO,SAAS,CAAC;AAAA,EACrE;AAEA,MAAU,qBAAe,IAAI,GAAG;AAC9B,QAAI,KAAK,SAAS,UAAW,QAAO;AACpC,UAAM,QAAQ,KAAK;AACnB,WAAO,MAAM,YAAY,OACrB,sBAAsB,MAAM,UAAU,SAAS,IAC/C;AAAA,EACN;AAGA,MAAI,OAAO,SAAS,YAAY,OAAO,SAAS,SAAU,QAAO;AAGjE,MACE,OAAO,SAAS,YACf,KAAgC,aAAa,mBAC9C;AACA,WAAO;AAAA,MACJ,KAAwC;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AAIA,MAAI,OAAO,SAAS,YAAY,OAAO,YAAY,MAAM;AACvD,WAAO,MAAM,KAAK,IAAiC,EAAE;AAAA,MAAK,CAAC,UACzD,sBAAsB,OAAO,SAAS;AAAA,IACxC;AAAA,EACF;AAIA,QAAM,iBACJ,WACA;AACF,MAAI,gBAAgB,KAAK,aAAa,cAAc;AAClD,UAAM,OACJ,OAAO,SAAS,WACZ,eAAe,OAAO,KAAK,IAAI,EAAE,KAAK,IAAI,CAAC,MAC3C;AACN,YAAQ;AAAA,MACN,iDAAiD,IAAI;AAAA,MAGrD;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
package/dist/react-tree.js
CHANGED
|
@@ -23,7 +23,8 @@ function treeContainsComponent(node, Component) {
|
|
|
23
23
|
(child) => treeContainsComponent(child, Component)
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
const runtimeProcess = globalThis.process;
|
|
27
|
+
if (runtimeProcess?.env?.NODE_ENV !== "production") {
|
|
27
28
|
const keys = typeof node === "object" ? ` with keys {${Object.keys(node).join(", ")}}` : "";
|
|
28
29
|
console.error(
|
|
29
30
|
`[treeContainsComponent] A non-renderable value${keys} is being passed as a React child. React will throw 'Objects are not valid as a React child' at the real render site. Stringify it (e.g. JSON.stringify) before rendering.`,
|
package/dist/react-tree.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/react-tree.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/react-tree — safe React children-tree scanning.\n *\n * Ported from matrx-frontend `lib/react/treeContainsComponent.ts`; this\n * subpath is the ONE home of the scanner inside the kit (the `/confirm`\n * subpath's alert-dialog imports it from here — no duplicate bodies).\n *\n * Two deliberate divergences from the frontend original, both documented:\n * - the dev-mode scream checks `typeof process` first, since this package may\n * load in an unbundled browser context where `process` is undefined;\n * - a React PORTAL child (`createPortal(...)` passed as a child) is a valid\n * React child, but it is not an element, an iterable, or a primitive — the\n * original fell through to the non-renderable branch, screamed a false\n * positive in dev, and skipped the portal's content. Portals are now\n * recognized and their children traversed.\n */\n\nimport * as React from \"react\";\n\nconst REACT_PORTAL_TYPE = Symbol.for(\"react.portal\");\n\n/**\n * Returns true if `Component` appears anywhere in the React element tree under\n * `node`. Used to detect optional a11y children (e.g. DialogDescription)\n * without rendering duplicates.\n *\n * DEFENSIVE + LOUD. A non-renderable node — a plain object or function passed\n * as a React child — is a real bug: React throws \"Objects are not valid as a\n * React child\" the instant it renders one. This a11y probe must NOT be the\n * crash site. `React.Children.toArray` would throw HERE, producing a trace that\n * points at the dialog primitive instead of the component that leaked the\n * object (this misdirection has burned real debugging hours). So we walk the\n * tree by hand, SKIP any non-renderable node, and scream in dev with its keys —\n * then let React report the defect at the true render site with the offending\n * component in the stack. For every VALID tree the result is identical to the\n * old `React.Children.toArray(node).some(...)`.\n */\nexport function treeContainsComponent(\n node: React.ReactNode,\n Component: React.ElementType,\n): boolean {\n if (node == null || typeof node === \"boolean\") return false;\n\n if (Array.isArray(node)) {\n return node.some((child) => treeContainsComponent(child, Component));\n }\n\n if (React.isValidElement(node)) {\n if (node.type === Component) return true;\n const props = node.props as { children?: React.ReactNode };\n return props.children != null\n ? treeContainsComponent(props.children, Component)\n : false;\n }\n\n // Strings / numbers are valid leaf children but never the Component.\n if (typeof node === \"string\" || typeof node === \"number\") return false;\n\n // A portal is a valid child that is NOT an element: traverse its content.\n if (\n typeof node === \"object\" &&\n (node as { $$typeof?: unknown }).$$typeof === REACT_PORTAL_TYPE\n ) {\n return treeContainsComponent(\n (node as { children?: React.ReactNode }).children,\n Component,\n );\n }\n\n // Non-array iterables (Set, Map, generator) are valid React children — React\n // supports them — so traverse rather than reject.\n if (typeof node === \"object\" && Symbol.iterator in node) {\n return Array.from(node as Iterable<React.ReactNode>).some((child) =>\n treeContainsComponent(child, Component),\n );\n }\n\n // Anything else (a raw object, a function) is NOT a valid React child. React\n // will throw when it renders this; we must not throw first and hide the cause.\n
|
|
1
|
+
{"version":3,"sources":["../src/react-tree.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/react-tree — safe React children-tree scanning.\n *\n * Ported from matrx-frontend `lib/react/treeContainsComponent.ts`; this\n * subpath is the ONE home of the scanner inside the kit (the `/confirm`\n * subpath's alert-dialog imports it from here — no duplicate bodies).\n *\n * Two deliberate divergences from the frontend original, both documented:\n * - the dev-mode scream checks `typeof process` first, since this package may\n * load in an unbundled browser context where `process` is undefined;\n * - a React PORTAL child (`createPortal(...)` passed as a child) is a valid\n * React child, but it is not an element, an iterable, or a primitive — the\n * original fell through to the non-renderable branch, screamed a false\n * positive in dev, and skipped the portal's content. Portals are now\n * recognized and their children traversed.\n */\n\nimport * as React from \"react\";\n\nconst REACT_PORTAL_TYPE = Symbol.for(\"react.portal\");\n\n/**\n * Returns true if `Component` appears anywhere in the React element tree under\n * `node`. Used to detect optional a11y children (e.g. DialogDescription)\n * without rendering duplicates.\n *\n * DEFENSIVE + LOUD. A non-renderable node — a plain object or function passed\n * as a React child — is a real bug: React throws \"Objects are not valid as a\n * React child\" the instant it renders one. This a11y probe must NOT be the\n * crash site. `React.Children.toArray` would throw HERE, producing a trace that\n * points at the dialog primitive instead of the component that leaked the\n * object (this misdirection has burned real debugging hours). So we walk the\n * tree by hand, SKIP any non-renderable node, and scream in dev with its keys —\n * then let React report the defect at the true render site with the offending\n * component in the stack. For every VALID tree the result is identical to the\n * old `React.Children.toArray(node).some(...)`.\n */\nexport function treeContainsComponent(\n node: React.ReactNode,\n Component: React.ElementType,\n): boolean {\n if (node == null || typeof node === \"boolean\") return false;\n\n if (Array.isArray(node)) {\n return node.some((child) => treeContainsComponent(child, Component));\n }\n\n if (React.isValidElement(node)) {\n if (node.type === Component) return true;\n const props = node.props as { children?: React.ReactNode };\n return props.children != null\n ? treeContainsComponent(props.children, Component)\n : false;\n }\n\n // Strings / numbers are valid leaf children but never the Component.\n if (typeof node === \"string\" || typeof node === \"number\") return false;\n\n // A portal is a valid child that is NOT an element: traverse its content.\n if (\n typeof node === \"object\" &&\n (node as { $$typeof?: unknown }).$$typeof === REACT_PORTAL_TYPE\n ) {\n return treeContainsComponent(\n (node as { children?: React.ReactNode }).children,\n Component,\n );\n }\n\n // Non-array iterables (Set, Map, generator) are valid React children — React\n // supports them — so traverse rather than reject.\n if (typeof node === \"object\" && Symbol.iterator in node) {\n return Array.from(node as Iterable<React.ReactNode>).some((child) =>\n treeContainsComponent(child, Component),\n );\n }\n\n // Anything else (a raw object, a function) is NOT a valid React child. React\n // will throw when it renders this; we must not throw first and hide the cause.\n const runtimeProcess = (\n globalThis as { process?: { env?: { NODE_ENV?: string } } }\n ).process;\n if (runtimeProcess?.env?.NODE_ENV !== \"production\") {\n const keys =\n typeof node === \"object\"\n ? ` with keys {${Object.keys(node).join(\", \")}}`\n : \"\";\n console.error(\n `[treeContainsComponent] A non-renderable value${keys} is being passed as a React child. ` +\n \"React will throw 'Objects are not valid as a React child' at the real render site. \" +\n \"Stringify it (e.g. JSON.stringify) before rendering.\",\n node,\n );\n }\n return false;\n}\n"],"mappings":";AAiBA,YAAY,WAAW;AAEvB,IAAM,oBAAoB,uBAAO,IAAI,cAAc;AAkB5C,SAAS,sBACd,MACA,WACS;AACT,MAAI,QAAQ,QAAQ,OAAO,SAAS,UAAW,QAAO;AAEtD,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,WAAO,KAAK,KAAK,CAAC,UAAU,sBAAsB,OAAO,SAAS,CAAC;AAAA,EACrE;AAEA,MAAU,qBAAe,IAAI,GAAG;AAC9B,QAAI,KAAK,SAAS,UAAW,QAAO;AACpC,UAAM,QAAQ,KAAK;AACnB,WAAO,MAAM,YAAY,OACrB,sBAAsB,MAAM,UAAU,SAAS,IAC/C;AAAA,EACN;AAGA,MAAI,OAAO,SAAS,YAAY,OAAO,SAAS,SAAU,QAAO;AAGjE,MACE,OAAO,SAAS,YACf,KAAgC,aAAa,mBAC9C;AACA,WAAO;AAAA,MACJ,KAAwC;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AAIA,MAAI,OAAO,SAAS,YAAY,OAAO,YAAY,MAAM;AACvD,WAAO,MAAM,KAAK,IAAiC,EAAE;AAAA,MAAK,CAAC,UACzD,sBAAsB,OAAO,SAAS;AAAA,IACxC;AAAA,EACF;AAIA,QAAM,iBACJ,WACA;AACF,MAAI,gBAAgB,KAAK,aAAa,cAAc;AAClD,UAAM,OACJ,OAAO,SAAS,WACZ,eAAe,OAAO,KAAK,IAAI,EAAE,KAAK,IAAI,CAAC,MAC3C;AACN,YAAQ;AAAA,MACN,iDAAiD,IAAI;AAAA,MAGrD;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/short-link-react.tsx
|
|
22
|
+
var short_link_react_exports = {};
|
|
23
|
+
__export(short_link_react_exports, {
|
|
24
|
+
CopyShortLinkButton: () => CopyShortLinkButton,
|
|
25
|
+
useShortLink: () => useShortLink
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(short_link_react_exports);
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
|
|
30
|
+
// src/short-link.ts
|
|
31
|
+
var SHORT_LINK_TOKEN_ALPHABET = "23456789abcdefghijkmnpqrstuvwxyz";
|
|
32
|
+
var SHORT_LINK_TOKEN_LENGTH = 10;
|
|
33
|
+
var SHORT_LINK_PATH_PREFIX = "/r/";
|
|
34
|
+
var TOKEN_RE = new RegExp(`^[${SHORT_LINK_TOKEN_ALPHABET}]{${SHORT_LINK_TOKEN_LENGTH}}$`);
|
|
35
|
+
function normalizeShortLinkToken(candidate) {
|
|
36
|
+
const token = (candidate ?? "").trim().toLowerCase();
|
|
37
|
+
return TOKEN_RE.test(token) ? token : null;
|
|
38
|
+
}
|
|
39
|
+
function shortLinkPath(token) {
|
|
40
|
+
const normalized = normalizeShortLinkToken(token);
|
|
41
|
+
if (!normalized) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`shortLinkPath: ${JSON.stringify(token)} is not a short-link token (${SHORT_LINK_TOKEN_LENGTH} chars of "${SHORT_LINK_TOKEN_ALPHABET}")`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return `${SHORT_LINK_PATH_PREFIX}${normalized}`;
|
|
47
|
+
}
|
|
48
|
+
function shortLinkUrl(origin, token) {
|
|
49
|
+
return `${origin.replace(/\/+$/, "")}${shortLinkPath(token)}`;
|
|
50
|
+
}
|
|
51
|
+
function resolveOrigin(origin) {
|
|
52
|
+
if (origin) return origin;
|
|
53
|
+
if (typeof window !== "undefined" && window.location?.origin) {
|
|
54
|
+
return window.location.origin;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
async function mintShortLink(client, options) {
|
|
59
|
+
if (!options.path.startsWith("/") || options.path.startsWith("//")) {
|
|
60
|
+
return { ok: false, error: "Only a same-app path (starting with a single '/') can be shortened" };
|
|
61
|
+
}
|
|
62
|
+
const origin = resolveOrigin(options.origin);
|
|
63
|
+
if (!origin) {
|
|
64
|
+
return { ok: false, error: "No origin: pass options.origin outside a browser" };
|
|
65
|
+
}
|
|
66
|
+
const { data, error } = await client.rpc("shorten_app_url", {
|
|
67
|
+
p_path: options.path,
|
|
68
|
+
p_organization_id: options.organizationId,
|
|
69
|
+
...options.expiresAt ? { p_expires_at: options.expiresAt } : {}
|
|
70
|
+
});
|
|
71
|
+
if (error) return { ok: false, error: error.message };
|
|
72
|
+
const result = data;
|
|
73
|
+
const token = result?.ok ? normalizeShortLinkToken(result.token) : null;
|
|
74
|
+
if (!token) {
|
|
75
|
+
return { ok: false, error: result?.error ?? "The mint door returned no token" };
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, token, path: shortLinkPath(token), url: shortLinkUrl(origin, token) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/short-link-react.tsx
|
|
81
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
82
|
+
async function writeClipboard(text) {
|
|
83
|
+
try {
|
|
84
|
+
await navigator.clipboard.writeText(text);
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function useShortLink(client, options) {
|
|
91
|
+
const [url, setUrl] = (0, import_react.useState)(null);
|
|
92
|
+
const [minting, setMinting] = (0, import_react.useState)(false);
|
|
93
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
94
|
+
const cached = (0, import_react.useRef)(null);
|
|
95
|
+
const mint = (0, import_react.useCallback)(async () => {
|
|
96
|
+
if (cached.current?.ok) return cached.current;
|
|
97
|
+
setMinting(true);
|
|
98
|
+
setError(null);
|
|
99
|
+
try {
|
|
100
|
+
const result = await mintShortLink(client, options);
|
|
101
|
+
cached.current = result;
|
|
102
|
+
if (result.ok) {
|
|
103
|
+
setUrl(result.url);
|
|
104
|
+
} else {
|
|
105
|
+
setError(result.error);
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
} finally {
|
|
109
|
+
setMinting(false);
|
|
110
|
+
}
|
|
111
|
+
}, [client, options.path, options.organizationId, options.expiresAt, options.origin]);
|
|
112
|
+
return { mint, url, minting, error };
|
|
113
|
+
}
|
|
114
|
+
function CopyShortLinkButton({
|
|
115
|
+
client,
|
|
116
|
+
label = "Copy short link",
|
|
117
|
+
className,
|
|
118
|
+
onCopied,
|
|
119
|
+
onError,
|
|
120
|
+
...options
|
|
121
|
+
}) {
|
|
122
|
+
const { mint, minting } = useShortLink(client, options);
|
|
123
|
+
const [phase, setPhase] = (0, import_react.useState)("idle");
|
|
124
|
+
const resetTimer = (0, import_react.useRef)(null);
|
|
125
|
+
const flash = (0, import_react.useCallback)((next) => {
|
|
126
|
+
setPhase(next);
|
|
127
|
+
if (resetTimer.current) clearTimeout(resetTimer.current);
|
|
128
|
+
resetTimer.current = setTimeout(() => setPhase("idle"), 2e3);
|
|
129
|
+
}, []);
|
|
130
|
+
const onClick = (0, import_react.useCallback)(async () => {
|
|
131
|
+
const result = await mint();
|
|
132
|
+
if (!result.ok) {
|
|
133
|
+
flash("error");
|
|
134
|
+
onError?.(result.error);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const copied = await writeClipboard(result.url);
|
|
138
|
+
if (copied) {
|
|
139
|
+
flash("copied");
|
|
140
|
+
onCopied?.(result.url);
|
|
141
|
+
} else {
|
|
142
|
+
flash("error");
|
|
143
|
+
onError?.("Could not write to the clipboard");
|
|
144
|
+
}
|
|
145
|
+
}, [mint, flash, onCopied, onError]);
|
|
146
|
+
const text = phase === "copied" ? "Copied" : phase === "error" ? "Copy failed" : minting ? "Creating\u2026" : label;
|
|
147
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
148
|
+
"button",
|
|
149
|
+
{
|
|
150
|
+
type: "button",
|
|
151
|
+
onClick,
|
|
152
|
+
disabled: minting,
|
|
153
|
+
"aria-live": "polite",
|
|
154
|
+
className: "inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-2.5 py-1.5 text-sm font-medium text-foreground hover:bg-muted disabled:opacity-60 " + (phase === "error" ? "text-destructive " : "") + (className ?? ""),
|
|
155
|
+
children: text
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=short-link-react.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/short-link-react.tsx","../src/short-link.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/short-link-react — the drop-in short-link UI.\n *\n * The five-minute story: import the button, hand it your Supabase client and a\n * path, done. The mint call (the org-gated `shorten_app_url` door), the URL\n * shape, the clipboard write, and the state feedback all live HERE — a\n * consuming app writes zero shortener logic.\n *\n * <CopyShortLinkButton client={supabase} path={`/notes/${id}`} organizationId={orgId} />\n *\n * Runtime deps of this subpath: none beyond React.\n * Styling is the Matrx Tailwind semantic-token vocabulary;\n * override via `className` (last-wins merge is the host's concern — the class\n * string is appended).\n */\n\nimport { useCallback, useRef, useState } from \"react\";\n\nimport {\n mintShortLink,\n type MintShortLinkOptions,\n type MintShortLinkResult,\n type ShortLinkClient,\n} from \"./short-link\";\n\nasync function writeClipboard(text: string): Promise<boolean> {\n try {\n await navigator.clipboard.writeText(text);\n return true;\n } catch {\n return false;\n }\n}\n\nexport interface UseShortLinkResult {\n /** Mint (once) and return the short URL; re-calls return the cached mint. */\n mint: () => Promise<MintShortLinkResult>;\n /** The minted URL, once `mint` has succeeded. */\n url: string | null;\n minting: boolean;\n error: string | null;\n}\n\n/** Mint-on-demand with caching — the logic under `CopyShortLinkButton`. */\nexport function useShortLink(\n client: ShortLinkClient,\n options: MintShortLinkOptions,\n): UseShortLinkResult {\n const [url, setUrl] = useState<string | null>(null);\n const [minting, setMinting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const cached = useRef<MintShortLinkResult | null>(null);\n\n const mint = useCallback(async (): Promise<MintShortLinkResult> => {\n if (cached.current?.ok) return cached.current;\n setMinting(true);\n setError(null);\n try {\n const result = await mintShortLink(client, options);\n cached.current = result;\n if (result.ok) {\n setUrl(result.url);\n } else {\n setError(result.error);\n }\n return result;\n } finally {\n setMinting(false);\n }\n }, [client, options.path, options.organizationId, options.expiresAt, options.origin]);\n\n return { mint, url, minting, error };\n}\n\nexport interface CopyShortLinkButtonProps extends MintShortLinkOptions {\n /** Your Supabase client (anything with a compatible `.rpc`). */\n client: ShortLinkClient;\n /** Button label; default \"Copy short link\". */\n label?: string;\n className?: string;\n /** Called with the short URL after it lands on the clipboard. */\n onCopied?: (url: string) => void;\n onError?: (error: string) => void;\n}\n\n/**\n * One click: mint (first time only), copy the short URL, confirm inline.\n * The mint is lazy — no short-link row exists until someone actually asks\n * for the link.\n */\nexport function CopyShortLinkButton({\n client,\n label = \"Copy short link\",\n className,\n onCopied,\n onError,\n ...options\n}: CopyShortLinkButtonProps) {\n const { mint, minting } = useShortLink(client, options);\n const [phase, setPhase] = useState<\"idle\" | \"copied\" | \"error\">(\"idle\");\n const resetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const flash = useCallback((next: \"copied\" | \"error\") => {\n setPhase(next);\n if (resetTimer.current) clearTimeout(resetTimer.current);\n resetTimer.current = setTimeout(() => setPhase(\"idle\"), 2000);\n }, []);\n\n const onClick = useCallback(async () => {\n const result = await mint();\n if (!result.ok) {\n flash(\"error\");\n onError?.(result.error);\n return;\n }\n const copied = await writeClipboard(result.url);\n if (copied) {\n flash(\"copied\");\n onCopied?.(result.url);\n } else {\n flash(\"error\");\n onError?.(\"Could not write to the clipboard\");\n }\n }, [mint, flash, onCopied, onError]);\n\n const text =\n phase === \"copied\" ? \"Copied\" : phase === \"error\" ? \"Copy failed\" : minting ? \"Creating…\" : label;\n\n return (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={minting}\n aria-live=\"polite\"\n className={\n \"inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-2.5 py-1.5 \" +\n \"text-sm font-medium text-foreground hover:bg-muted disabled:opacity-60 \" +\n (phase === \"error\" ? \"text-destructive \" : \"\") +\n (className ?? \"\")\n }\n >\n {text}\n </button>\n );\n}\n","/**\n * @ai-matrx/kit/short-link — the platform short-link CONTRACT, in one place.\n *\n * The primitive itself lives in the database (migration `0557_platform_short_links`):\n * `platform.short_links` holds `token → same-app path`, org-scoped and expiring;\n * `platform.create_short_link` mints (server-side only); `public.resolve_short_link`\n * resolves (anon-callable; answers with a PATH, never content — the target route's\n * own auth gates everything). The frontend redirect route is `/r/[token]`\n * (matrx-frontend `app/(public)/r/[token]`), and the notification spine mints per\n * SMS leg (aidream `services/notifications/short_links.py`).\n *\n * This module is the shared vocabulary every client needs to speak about those\n * tokens — alphabet, length, validation, and the URL shape — so no consumer ever\n * re-declares it. 🚨 `SHORT_LINK_TOKEN_ALPHABET` and `SHORT_LINK_TOKEN_LENGTH`\n * mirror `platform.create_short_link` EXACTLY; change one and you change both.\n *\n * 🚨 A short link is a URL, not a permission. The primitive that grants\n * anonymous access to content is `platform.share_links` (`/s/[token]`, 64-hex\n * tokens) — a different system, deliberately. Never shorten by minting a share\n * link, and never share by minting a short link.\n *\n * Pure logic — no DOM, no React, no network; safe in Server Components, route\n * handlers, workers, and Node scripts.\n */\n\n/**\n * 32 characters: digits 2–9 plus a–z minus `l` and `o`. 256 % 32 = 0, so the\n * mint maps random bytes without modulo bias; lowercase-only survives channels\n * that case-mangle (the resolver lowercases before lookup); the ambiguous\n * glyphs (`0/o`, `1/l`) never appear.\n */\nexport const SHORT_LINK_TOKEN_ALPHABET = \"23456789abcdefghijkmnpqrstuvwxyz\";\n\n/** 10 characters × 5 bits = 50 bits — unguessable at any realistic probe rate. */\nexport const SHORT_LINK_TOKEN_LENGTH = 10;\n\n/** The resolve route on the app origin (matrx-frontend `app/(public)/r/[token]`). */\nexport const SHORT_LINK_PATH_PREFIX = \"/r/\";\n\nconst TOKEN_RE = new RegExp(`^[${SHORT_LINK_TOKEN_ALPHABET}]{${SHORT_LINK_TOKEN_LENGTH}}$`);\n\n/**\n * Trim, lowercase, and shape-check a candidate token. Returns the normalized\n * token, or `null` for anything outside the contract — the resolver would\n * refuse it anyway, so callers can skip the round trip.\n */\nexport function normalizeShortLinkToken(candidate: string | null | undefined): string | null {\n const token = (candidate ?? \"\").trim().toLowerCase();\n return TOKEN_RE.test(token) ? token : null;\n}\n\n/** Whether a string is a well-formed short-link token (after normalization). */\nexport function isShortLinkToken(candidate: string | null | undefined): boolean {\n return normalizeShortLinkToken(candidate) !== null;\n}\n\n/** The app-relative path a token resolves at: `/r/<token>`. */\nexport function shortLinkPath(token: string): string {\n const normalized = normalizeShortLinkToken(token);\n if (!normalized) {\n throw new Error(\n `shortLinkPath: ${JSON.stringify(token)} is not a short-link token ` +\n `(${SHORT_LINK_TOKEN_LENGTH} chars of \"${SHORT_LINK_TOKEN_ALPHABET}\")`,\n );\n }\n return `${SHORT_LINK_PATH_PREFIX}${normalized}`;\n}\n\n/**\n * The absolute short URL for a token on a given origin, e.g.\n * `shortLinkUrl(\"https://app.aimatrx.com\", token)` → `https://app.aimatrx.com/r/<token>`.\n */\nexport function shortLinkUrl(origin: string, token: string): string {\n return `${origin.replace(/\\/+$/, \"\")}${shortLinkPath(token)}`;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The client half — the WHOLE mint/resolve logic, so a consumer adds nothing.\n//\n// Pass your Supabase client (or anything with a compatible `.rpc`) and you are\n// done: `mintShortLink(supabase, { path, organizationId })` calls the platform's\n// authenticated mint door (`public.shorten_app_url`, org-membership-gated) and\n// hands back the finished short URL; `resolveShortLinkPath(supabase, token)`\n// calls the anon resolver (`public.resolve_short_link`). No app writes its own\n// shortener logic, ever — the only thing outside this module is the shared\n// database that stores the tokens, which is exactly where shared state lives.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Anything with a Supabase-shaped `.rpc` — pass the Supabase client itself. */\nexport interface ShortLinkClient {\n rpc(\n fn: string,\n args?: Record<string, unknown>,\n ): PromiseLike<{ data: unknown; error: { message: string } | null }>;\n}\n\nexport interface MintShortLinkOptions {\n /** Same-app path to shorten (must start with a single `/`). */\n path: string;\n /** The organization the link belongs to — the caller must be a member. */\n organizationId: string;\n /** ISO timestamp; the platform default (365 days) applies when omitted. */\n expiresAt?: string;\n /** Origin for the returned URL; defaults to `window.location.origin`. */\n origin?: string;\n}\n\nexport type MintShortLinkResult =\n | { ok: true; token: string; path: string; url: string }\n | { ok: false; error: string };\n\nfunction resolveOrigin(origin?: string): string | null {\n if (origin) return origin;\n if (typeof window !== \"undefined\" && window.location?.origin) {\n return window.location.origin;\n }\n return null;\n}\n\n/** Mint a short link through the platform's authenticated mint door. */\nexport async function mintShortLink(\n client: ShortLinkClient,\n options: MintShortLinkOptions,\n): Promise<MintShortLinkResult> {\n if (!options.path.startsWith(\"/\") || options.path.startsWith(\"//\")) {\n return { ok: false, error: \"Only a same-app path (starting with a single '/') can be shortened\" };\n }\n const origin = resolveOrigin(options.origin);\n if (!origin) {\n return { ok: false, error: \"No origin: pass options.origin outside a browser\" };\n }\n const { data, error } = await client.rpc(\"shorten_app_url\", {\n p_path: options.path,\n p_organization_id: options.organizationId,\n ...(options.expiresAt ? { p_expires_at: options.expiresAt } : {}),\n });\n if (error) return { ok: false, error: error.message };\n const result = data as { ok?: boolean; token?: string; error?: string } | null;\n const token = result?.ok ? normalizeShortLinkToken(result.token) : null;\n if (!token) {\n return { ok: false, error: result?.error ?? \"The mint door returned no token\" };\n }\n return { ok: true, token, path: shortLinkPath(token), url: shortLinkUrl(origin, token) };\n}\n\nexport type ResolveShortLinkResult =\n | { ok: true; targetPath: string }\n | { ok: false };\n\n/**\n * Resolve a short token to its target path through the anon resolver.\n * `{ok:false}` covers invalid, unknown, and expired identically — the platform\n * deliberately does not distinguish them.\n */\nexport async function resolveShortLinkPath(\n client: ShortLinkClient,\n token: string,\n): Promise<ResolveShortLinkResult> {\n const normalized = normalizeShortLinkToken(token);\n if (!normalized) return { ok: false };\n const { data, error } = await client.rpc(\"resolve_short_link\", { p_token: normalized });\n if (error) return { ok: false };\n const result = data as { ok?: boolean; target_path?: string } | null;\n const targetPath = result?.ok ? result.target_path : undefined;\n if (!targetPath || !targetPath.startsWith(\"/\") || targetPath.startsWith(\"//\")) {\n return { ok: false };\n }\n return { ok: true, targetPath };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/** Anything with a Supabase-shaped `.rpc` — pass the Supabase client itself. */
|
|
4
|
+
interface ShortLinkClient {
|
|
5
|
+
rpc(fn: string, args?: Record<string, unknown>): PromiseLike<{
|
|
6
|
+
data: unknown;
|
|
7
|
+
error: {
|
|
8
|
+
message: string;
|
|
9
|
+
} | null;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
interface MintShortLinkOptions {
|
|
13
|
+
/** Same-app path to shorten (must start with a single `/`). */
|
|
14
|
+
path: string;
|
|
15
|
+
/** The organization the link belongs to — the caller must be a member. */
|
|
16
|
+
organizationId: string;
|
|
17
|
+
/** ISO timestamp; the platform default (365 days) applies when omitted. */
|
|
18
|
+
expiresAt?: string;
|
|
19
|
+
/** Origin for the returned URL; defaults to `window.location.origin`. */
|
|
20
|
+
origin?: string;
|
|
21
|
+
}
|
|
22
|
+
type MintShortLinkResult = {
|
|
23
|
+
ok: true;
|
|
24
|
+
token: string;
|
|
25
|
+
path: string;
|
|
26
|
+
url: string;
|
|
27
|
+
} | {
|
|
28
|
+
ok: false;
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
interface UseShortLinkResult {
|
|
33
|
+
/** Mint (once) and return the short URL; re-calls return the cached mint. */
|
|
34
|
+
mint: () => Promise<MintShortLinkResult>;
|
|
35
|
+
/** The minted URL, once `mint` has succeeded. */
|
|
36
|
+
url: string | null;
|
|
37
|
+
minting: boolean;
|
|
38
|
+
error: string | null;
|
|
39
|
+
}
|
|
40
|
+
/** Mint-on-demand with caching — the logic under `CopyShortLinkButton`. */
|
|
41
|
+
declare function useShortLink(client: ShortLinkClient, options: MintShortLinkOptions): UseShortLinkResult;
|
|
42
|
+
interface CopyShortLinkButtonProps extends MintShortLinkOptions {
|
|
43
|
+
/** Your Supabase client (anything with a compatible `.rpc`). */
|
|
44
|
+
client: ShortLinkClient;
|
|
45
|
+
/** Button label; default "Copy short link". */
|
|
46
|
+
label?: string;
|
|
47
|
+
className?: string;
|
|
48
|
+
/** Called with the short URL after it lands on the clipboard. */
|
|
49
|
+
onCopied?: (url: string) => void;
|
|
50
|
+
onError?: (error: string) => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* One click: mint (first time only), copy the short URL, confirm inline.
|
|
54
|
+
* The mint is lazy — no short-link row exists until someone actually asks
|
|
55
|
+
* for the link.
|
|
56
|
+
*/
|
|
57
|
+
declare function CopyShortLinkButton({ client, label, className, onCopied, onError, ...options }: CopyShortLinkButtonProps): React.JSX.Element;
|
|
58
|
+
|
|
59
|
+
export { CopyShortLinkButton, type CopyShortLinkButtonProps, type UseShortLinkResult, useShortLink };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/** Anything with a Supabase-shaped `.rpc` — pass the Supabase client itself. */
|
|
4
|
+
interface ShortLinkClient {
|
|
5
|
+
rpc(fn: string, args?: Record<string, unknown>): PromiseLike<{
|
|
6
|
+
data: unknown;
|
|
7
|
+
error: {
|
|
8
|
+
message: string;
|
|
9
|
+
} | null;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
interface MintShortLinkOptions {
|
|
13
|
+
/** Same-app path to shorten (must start with a single `/`). */
|
|
14
|
+
path: string;
|
|
15
|
+
/** The organization the link belongs to — the caller must be a member. */
|
|
16
|
+
organizationId: string;
|
|
17
|
+
/** ISO timestamp; the platform default (365 days) applies when omitted. */
|
|
18
|
+
expiresAt?: string;
|
|
19
|
+
/** Origin for the returned URL; defaults to `window.location.origin`. */
|
|
20
|
+
origin?: string;
|
|
21
|
+
}
|
|
22
|
+
type MintShortLinkResult = {
|
|
23
|
+
ok: true;
|
|
24
|
+
token: string;
|
|
25
|
+
path: string;
|
|
26
|
+
url: string;
|
|
27
|
+
} | {
|
|
28
|
+
ok: false;
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
interface UseShortLinkResult {
|
|
33
|
+
/** Mint (once) and return the short URL; re-calls return the cached mint. */
|
|
34
|
+
mint: () => Promise<MintShortLinkResult>;
|
|
35
|
+
/** The minted URL, once `mint` has succeeded. */
|
|
36
|
+
url: string | null;
|
|
37
|
+
minting: boolean;
|
|
38
|
+
error: string | null;
|
|
39
|
+
}
|
|
40
|
+
/** Mint-on-demand with caching — the logic under `CopyShortLinkButton`. */
|
|
41
|
+
declare function useShortLink(client: ShortLinkClient, options: MintShortLinkOptions): UseShortLinkResult;
|
|
42
|
+
interface CopyShortLinkButtonProps extends MintShortLinkOptions {
|
|
43
|
+
/** Your Supabase client (anything with a compatible `.rpc`). */
|
|
44
|
+
client: ShortLinkClient;
|
|
45
|
+
/** Button label; default "Copy short link". */
|
|
46
|
+
label?: string;
|
|
47
|
+
className?: string;
|
|
48
|
+
/** Called with the short URL after it lands on the clipboard. */
|
|
49
|
+
onCopied?: (url: string) => void;
|
|
50
|
+
onError?: (error: string) => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* One click: mint (first time only), copy the short URL, confirm inline.
|
|
54
|
+
* The mint is lazy — no short-link row exists until someone actually asks
|
|
55
|
+
* for the link.
|
|
56
|
+
*/
|
|
57
|
+
declare function CopyShortLinkButton({ client, label, className, onCopied, onError, ...options }: CopyShortLinkButtonProps): React.JSX.Element;
|
|
58
|
+
|
|
59
|
+
export { CopyShortLinkButton, type CopyShortLinkButtonProps, type UseShortLinkResult, useShortLink };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/short-link-react.tsx
|
|
4
|
+
import { useCallback, useRef, useState } from "react";
|
|
5
|
+
|
|
6
|
+
// src/short-link.ts
|
|
7
|
+
var SHORT_LINK_TOKEN_ALPHABET = "23456789abcdefghijkmnpqrstuvwxyz";
|
|
8
|
+
var SHORT_LINK_TOKEN_LENGTH = 10;
|
|
9
|
+
var SHORT_LINK_PATH_PREFIX = "/r/";
|
|
10
|
+
var TOKEN_RE = new RegExp(`^[${SHORT_LINK_TOKEN_ALPHABET}]{${SHORT_LINK_TOKEN_LENGTH}}$`);
|
|
11
|
+
function normalizeShortLinkToken(candidate) {
|
|
12
|
+
const token = (candidate ?? "").trim().toLowerCase();
|
|
13
|
+
return TOKEN_RE.test(token) ? token : null;
|
|
14
|
+
}
|
|
15
|
+
function shortLinkPath(token) {
|
|
16
|
+
const normalized = normalizeShortLinkToken(token);
|
|
17
|
+
if (!normalized) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`shortLinkPath: ${JSON.stringify(token)} is not a short-link token (${SHORT_LINK_TOKEN_LENGTH} chars of "${SHORT_LINK_TOKEN_ALPHABET}")`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return `${SHORT_LINK_PATH_PREFIX}${normalized}`;
|
|
23
|
+
}
|
|
24
|
+
function shortLinkUrl(origin, token) {
|
|
25
|
+
return `${origin.replace(/\/+$/, "")}${shortLinkPath(token)}`;
|
|
26
|
+
}
|
|
27
|
+
function resolveOrigin(origin) {
|
|
28
|
+
if (origin) return origin;
|
|
29
|
+
if (typeof window !== "undefined" && window.location?.origin) {
|
|
30
|
+
return window.location.origin;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
async function mintShortLink(client, options) {
|
|
35
|
+
if (!options.path.startsWith("/") || options.path.startsWith("//")) {
|
|
36
|
+
return { ok: false, error: "Only a same-app path (starting with a single '/') can be shortened" };
|
|
37
|
+
}
|
|
38
|
+
const origin = resolveOrigin(options.origin);
|
|
39
|
+
if (!origin) {
|
|
40
|
+
return { ok: false, error: "No origin: pass options.origin outside a browser" };
|
|
41
|
+
}
|
|
42
|
+
const { data, error } = await client.rpc("shorten_app_url", {
|
|
43
|
+
p_path: options.path,
|
|
44
|
+
p_organization_id: options.organizationId,
|
|
45
|
+
...options.expiresAt ? { p_expires_at: options.expiresAt } : {}
|
|
46
|
+
});
|
|
47
|
+
if (error) return { ok: false, error: error.message };
|
|
48
|
+
const result = data;
|
|
49
|
+
const token = result?.ok ? normalizeShortLinkToken(result.token) : null;
|
|
50
|
+
if (!token) {
|
|
51
|
+
return { ok: false, error: result?.error ?? "The mint door returned no token" };
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, token, path: shortLinkPath(token), url: shortLinkUrl(origin, token) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/short-link-react.tsx
|
|
57
|
+
import { jsx } from "react/jsx-runtime";
|
|
58
|
+
async function writeClipboard(text) {
|
|
59
|
+
try {
|
|
60
|
+
await navigator.clipboard.writeText(text);
|
|
61
|
+
return true;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function useShortLink(client, options) {
|
|
67
|
+
const [url, setUrl] = useState(null);
|
|
68
|
+
const [minting, setMinting] = useState(false);
|
|
69
|
+
const [error, setError] = useState(null);
|
|
70
|
+
const cached = useRef(null);
|
|
71
|
+
const mint = useCallback(async () => {
|
|
72
|
+
if (cached.current?.ok) return cached.current;
|
|
73
|
+
setMinting(true);
|
|
74
|
+
setError(null);
|
|
75
|
+
try {
|
|
76
|
+
const result = await mintShortLink(client, options);
|
|
77
|
+
cached.current = result;
|
|
78
|
+
if (result.ok) {
|
|
79
|
+
setUrl(result.url);
|
|
80
|
+
} else {
|
|
81
|
+
setError(result.error);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
} finally {
|
|
85
|
+
setMinting(false);
|
|
86
|
+
}
|
|
87
|
+
}, [client, options.path, options.organizationId, options.expiresAt, options.origin]);
|
|
88
|
+
return { mint, url, minting, error };
|
|
89
|
+
}
|
|
90
|
+
function CopyShortLinkButton({
|
|
91
|
+
client,
|
|
92
|
+
label = "Copy short link",
|
|
93
|
+
className,
|
|
94
|
+
onCopied,
|
|
95
|
+
onError,
|
|
96
|
+
...options
|
|
97
|
+
}) {
|
|
98
|
+
const { mint, minting } = useShortLink(client, options);
|
|
99
|
+
const [phase, setPhase] = useState("idle");
|
|
100
|
+
const resetTimer = useRef(null);
|
|
101
|
+
const flash = useCallback((next) => {
|
|
102
|
+
setPhase(next);
|
|
103
|
+
if (resetTimer.current) clearTimeout(resetTimer.current);
|
|
104
|
+
resetTimer.current = setTimeout(() => setPhase("idle"), 2e3);
|
|
105
|
+
}, []);
|
|
106
|
+
const onClick = useCallback(async () => {
|
|
107
|
+
const result = await mint();
|
|
108
|
+
if (!result.ok) {
|
|
109
|
+
flash("error");
|
|
110
|
+
onError?.(result.error);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const copied = await writeClipboard(result.url);
|
|
114
|
+
if (copied) {
|
|
115
|
+
flash("copied");
|
|
116
|
+
onCopied?.(result.url);
|
|
117
|
+
} else {
|
|
118
|
+
flash("error");
|
|
119
|
+
onError?.("Could not write to the clipboard");
|
|
120
|
+
}
|
|
121
|
+
}, [mint, flash, onCopied, onError]);
|
|
122
|
+
const text = phase === "copied" ? "Copied" : phase === "error" ? "Copy failed" : minting ? "Creating\u2026" : label;
|
|
123
|
+
return /* @__PURE__ */ jsx(
|
|
124
|
+
"button",
|
|
125
|
+
{
|
|
126
|
+
type: "button",
|
|
127
|
+
onClick,
|
|
128
|
+
disabled: minting,
|
|
129
|
+
"aria-live": "polite",
|
|
130
|
+
className: "inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-2.5 py-1.5 text-sm font-medium text-foreground hover:bg-muted disabled:opacity-60 " + (phase === "error" ? "text-destructive " : "") + (className ?? ""),
|
|
131
|
+
children: text
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
export {
|
|
136
|
+
CopyShortLinkButton,
|
|
137
|
+
useShortLink
|
|
138
|
+
};
|
|
139
|
+
//# sourceMappingURL=short-link-react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/short-link-react.tsx","../src/short-link.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/short-link-react — the drop-in short-link UI.\n *\n * The five-minute story: import the button, hand it your Supabase client and a\n * path, done. The mint call (the org-gated `shorten_app_url` door), the URL\n * shape, the clipboard write, and the state feedback all live HERE — a\n * consuming app writes zero shortener logic.\n *\n * <CopyShortLinkButton client={supabase} path={`/notes/${id}`} organizationId={orgId} />\n *\n * Runtime deps of this subpath: none beyond React.\n * Styling is the Matrx Tailwind semantic-token vocabulary;\n * override via `className` (last-wins merge is the host's concern — the class\n * string is appended).\n */\n\nimport { useCallback, useRef, useState } from \"react\";\n\nimport {\n mintShortLink,\n type MintShortLinkOptions,\n type MintShortLinkResult,\n type ShortLinkClient,\n} from \"./short-link\";\n\nasync function writeClipboard(text: string): Promise<boolean> {\n try {\n await navigator.clipboard.writeText(text);\n return true;\n } catch {\n return false;\n }\n}\n\nexport interface UseShortLinkResult {\n /** Mint (once) and return the short URL; re-calls return the cached mint. */\n mint: () => Promise<MintShortLinkResult>;\n /** The minted URL, once `mint` has succeeded. */\n url: string | null;\n minting: boolean;\n error: string | null;\n}\n\n/** Mint-on-demand with caching — the logic under `CopyShortLinkButton`. */\nexport function useShortLink(\n client: ShortLinkClient,\n options: MintShortLinkOptions,\n): UseShortLinkResult {\n const [url, setUrl] = useState<string | null>(null);\n const [minting, setMinting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const cached = useRef<MintShortLinkResult | null>(null);\n\n const mint = useCallback(async (): Promise<MintShortLinkResult> => {\n if (cached.current?.ok) return cached.current;\n setMinting(true);\n setError(null);\n try {\n const result = await mintShortLink(client, options);\n cached.current = result;\n if (result.ok) {\n setUrl(result.url);\n } else {\n setError(result.error);\n }\n return result;\n } finally {\n setMinting(false);\n }\n }, [client, options.path, options.organizationId, options.expiresAt, options.origin]);\n\n return { mint, url, minting, error };\n}\n\nexport interface CopyShortLinkButtonProps extends MintShortLinkOptions {\n /** Your Supabase client (anything with a compatible `.rpc`). */\n client: ShortLinkClient;\n /** Button label; default \"Copy short link\". */\n label?: string;\n className?: string;\n /** Called with the short URL after it lands on the clipboard. */\n onCopied?: (url: string) => void;\n onError?: (error: string) => void;\n}\n\n/**\n * One click: mint (first time only), copy the short URL, confirm inline.\n * The mint is lazy — no short-link row exists until someone actually asks\n * for the link.\n */\nexport function CopyShortLinkButton({\n client,\n label = \"Copy short link\",\n className,\n onCopied,\n onError,\n ...options\n}: CopyShortLinkButtonProps) {\n const { mint, minting } = useShortLink(client, options);\n const [phase, setPhase] = useState<\"idle\" | \"copied\" | \"error\">(\"idle\");\n const resetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const flash = useCallback((next: \"copied\" | \"error\") => {\n setPhase(next);\n if (resetTimer.current) clearTimeout(resetTimer.current);\n resetTimer.current = setTimeout(() => setPhase(\"idle\"), 2000);\n }, []);\n\n const onClick = useCallback(async () => {\n const result = await mint();\n if (!result.ok) {\n flash(\"error\");\n onError?.(result.error);\n return;\n }\n const copied = await writeClipboard(result.url);\n if (copied) {\n flash(\"copied\");\n onCopied?.(result.url);\n } else {\n flash(\"error\");\n onError?.(\"Could not write to the clipboard\");\n }\n }, [mint, flash, onCopied, onError]);\n\n const text =\n phase === \"copied\" ? \"Copied\" : phase === \"error\" ? \"Copy failed\" : minting ? \"Creating…\" : label;\n\n return (\n <button\n type=\"button\"\n onClick={onClick}\n disabled={minting}\n aria-live=\"polite\"\n className={\n \"inline-flex items-center gap-1.5 rounded-md border border-border bg-card px-2.5 py-1.5 \" +\n \"text-sm font-medium text-foreground hover:bg-muted disabled:opacity-60 \" +\n (phase === \"error\" ? \"text-destructive \" : \"\") +\n (className ?? \"\")\n }\n >\n {text}\n </button>\n );\n}\n","/**\n * @ai-matrx/kit/short-link — the platform short-link CONTRACT, in one place.\n *\n * The primitive itself lives in the database (migration `0557_platform_short_links`):\n * `platform.short_links` holds `token → same-app path`, org-scoped and expiring;\n * `platform.create_short_link` mints (server-side only); `public.resolve_short_link`\n * resolves (anon-callable; answers with a PATH, never content — the target route's\n * own auth gates everything). The frontend redirect route is `/r/[token]`\n * (matrx-frontend `app/(public)/r/[token]`), and the notification spine mints per\n * SMS leg (aidream `services/notifications/short_links.py`).\n *\n * This module is the shared vocabulary every client needs to speak about those\n * tokens — alphabet, length, validation, and the URL shape — so no consumer ever\n * re-declares it. 🚨 `SHORT_LINK_TOKEN_ALPHABET` and `SHORT_LINK_TOKEN_LENGTH`\n * mirror `platform.create_short_link` EXACTLY; change one and you change both.\n *\n * 🚨 A short link is a URL, not a permission. The primitive that grants\n * anonymous access to content is `platform.share_links` (`/s/[token]`, 64-hex\n * tokens) — a different system, deliberately. Never shorten by minting a share\n * link, and never share by minting a short link.\n *\n * Pure logic — no DOM, no React, no network; safe in Server Components, route\n * handlers, workers, and Node scripts.\n */\n\n/**\n * 32 characters: digits 2–9 plus a–z minus `l` and `o`. 256 % 32 = 0, so the\n * mint maps random bytes without modulo bias; lowercase-only survives channels\n * that case-mangle (the resolver lowercases before lookup); the ambiguous\n * glyphs (`0/o`, `1/l`) never appear.\n */\nexport const SHORT_LINK_TOKEN_ALPHABET = \"23456789abcdefghijkmnpqrstuvwxyz\";\n\n/** 10 characters × 5 bits = 50 bits — unguessable at any realistic probe rate. */\nexport const SHORT_LINK_TOKEN_LENGTH = 10;\n\n/** The resolve route on the app origin (matrx-frontend `app/(public)/r/[token]`). */\nexport const SHORT_LINK_PATH_PREFIX = \"/r/\";\n\nconst TOKEN_RE = new RegExp(`^[${SHORT_LINK_TOKEN_ALPHABET}]{${SHORT_LINK_TOKEN_LENGTH}}$`);\n\n/**\n * Trim, lowercase, and shape-check a candidate token. Returns the normalized\n * token, or `null` for anything outside the contract — the resolver would\n * refuse it anyway, so callers can skip the round trip.\n */\nexport function normalizeShortLinkToken(candidate: string | null | undefined): string | null {\n const token = (candidate ?? \"\").trim().toLowerCase();\n return TOKEN_RE.test(token) ? token : null;\n}\n\n/** Whether a string is a well-formed short-link token (after normalization). */\nexport function isShortLinkToken(candidate: string | null | undefined): boolean {\n return normalizeShortLinkToken(candidate) !== null;\n}\n\n/** The app-relative path a token resolves at: `/r/<token>`. */\nexport function shortLinkPath(token: string): string {\n const normalized = normalizeShortLinkToken(token);\n if (!normalized) {\n throw new Error(\n `shortLinkPath: ${JSON.stringify(token)} is not a short-link token ` +\n `(${SHORT_LINK_TOKEN_LENGTH} chars of \"${SHORT_LINK_TOKEN_ALPHABET}\")`,\n );\n }\n return `${SHORT_LINK_PATH_PREFIX}${normalized}`;\n}\n\n/**\n * The absolute short URL for a token on a given origin, e.g.\n * `shortLinkUrl(\"https://app.aimatrx.com\", token)` → `https://app.aimatrx.com/r/<token>`.\n */\nexport function shortLinkUrl(origin: string, token: string): string {\n return `${origin.replace(/\\/+$/, \"\")}${shortLinkPath(token)}`;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// The client half — the WHOLE mint/resolve logic, so a consumer adds nothing.\n//\n// Pass your Supabase client (or anything with a compatible `.rpc`) and you are\n// done: `mintShortLink(supabase, { path, organizationId })` calls the platform's\n// authenticated mint door (`public.shorten_app_url`, org-membership-gated) and\n// hands back the finished short URL; `resolveShortLinkPath(supabase, token)`\n// calls the anon resolver (`public.resolve_short_link`). No app writes its own\n// shortener logic, ever — the only thing outside this module is the shared\n// database that stores the tokens, which is exactly where shared state lives.\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Anything with a Supabase-shaped `.rpc` — pass the Supabase client itself. */\nexport interface ShortLinkClient {\n rpc(\n fn: string,\n args?: Record<string, unknown>,\n ): PromiseLike<{ data: unknown; error: { message: string } | null }>;\n}\n\nexport interface MintShortLinkOptions {\n /** Same-app path to shorten (must start with a single `/`). */\n path: string;\n /** The organization the link belongs to — the caller must be a member. */\n organizationId: string;\n /** ISO timestamp; the platform default (365 days) applies when omitted. */\n expiresAt?: string;\n /** Origin for the returned URL; defaults to `window.location.origin`. */\n origin?: string;\n}\n\nexport type MintShortLinkResult =\n | { ok: true; token: string; path: string; url: string }\n | { ok: false; error: string };\n\nfunction resolveOrigin(origin?: string): string | null {\n if (origin) return origin;\n if (typeof window !== \"undefined\" && window.location?.origin) {\n return window.location.origin;\n }\n return null;\n}\n\n/** Mint a short link through the platform's authenticated mint door. */\nexport async function mintShortLink(\n client: ShortLinkClient,\n options: MintShortLinkOptions,\n): Promise<MintShortLinkResult> {\n if (!options.path.startsWith(\"/\") || options.path.startsWith(\"//\")) {\n return { ok: false, error: \"Only a same-app path (starting with a single '/') can be shortened\" };\n }\n const origin = resolveOrigin(options.origin);\n if (!origin) {\n return { ok: false, error: \"No origin: pass options.origin outside a browser\" };\n }\n const { data, error } = await client.rpc(\"shorten_app_url\", {\n p_path: options.path,\n p_organization_id: options.organizationId,\n ...(options.expiresAt ? { p_expires_at: options.expiresAt } : {}),\n });\n if (error) return { ok: false, error: error.message };\n const result = data as { ok?: boolean; token?: string; error?: string } | null;\n const token = result?.ok ? normalizeShortLinkToken(result.token) : null;\n if (!token) {\n return { ok: false, error: result?.error ?? \"The mint door returned no token\" };\n }\n return { ok: true, token, path: shortLinkPath(token), url: shortLinkUrl(origin, token) };\n}\n\nexport type ResolveShortLinkResult =\n | { ok: true; targetPath: string }\n | { ok: false };\n\n/**\n * Resolve a short token to its target path through the anon resolver.\n * `{ok:false}` covers invalid, unknown, and expired identically — the platform\n * deliberately does not distinguish them.\n */\nexport async function resolveShortLinkPath(\n client: ShortLinkClient,\n token: string,\n): Promise<ResolveShortLinkResult> {\n const normalized = normalizeShortLinkToken(token);\n if (!normalized) return { ok: false };\n const { data, error } = await client.rpc(\"resolve_short_link\", { p_token: normalized });\n if (error) return { ok: false };\n const result = data as { ok?: boolean; target_path?: string } | null;\n const targetPath = result?.ok ? result.target_path : undefined;\n if (!targetPath || !targetPath.startsWith(\"/\") || targetPath.startsWith(\"//\")) {\n return { ok: false };\n }\n return { ok: true, targetPath };\n}\n"],"mappings":";;;AAgBA,SAAS,aAAa,QAAQ,gBAAgB;;;ACevC,IAAM,4BAA4B;AAGlC,IAAM,0BAA0B;AAGhC,IAAM,yBAAyB;AAEtC,IAAM,WAAW,IAAI,OAAO,KAAK,yBAAyB,KAAK,uBAAuB,IAAI;AAOnF,SAAS,wBAAwB,WAAqD;AAC3F,QAAM,SAAS,aAAa,IAAI,KAAK,EAAE,YAAY;AACnD,SAAO,SAAS,KAAK,KAAK,IAAI,QAAQ;AACxC;AAQO,SAAS,cAAc,OAAuB;AACnD,QAAM,aAAa,wBAAwB,KAAK;AAChD,MAAI,CAAC,YAAY;AACf,UAAM,IAAI;AAAA,MACR,kBAAkB,KAAK,UAAU,KAAK,CAAC,+BACjC,uBAAuB,cAAc,yBAAyB;AAAA,IACtE;AAAA,EACF;AACA,SAAO,GAAG,sBAAsB,GAAG,UAAU;AAC/C;AAMO,SAAS,aAAa,QAAgB,OAAuB;AAClE,SAAO,GAAG,OAAO,QAAQ,QAAQ,EAAE,CAAC,GAAG,cAAc,KAAK,CAAC;AAC7D;AAqCA,SAAS,cAAc,QAAgC;AACrD,MAAI,OAAQ,QAAO;AACnB,MAAI,OAAO,WAAW,eAAe,OAAO,UAAU,QAAQ;AAC5D,WAAO,OAAO,SAAS;AAAA,EACzB;AACA,SAAO;AACT;AAGA,eAAsB,cACpB,QACA,SAC8B;AAC9B,MAAI,CAAC,QAAQ,KAAK,WAAW,GAAG,KAAK,QAAQ,KAAK,WAAW,IAAI,GAAG;AAClE,WAAO,EAAE,IAAI,OAAO,OAAO,qEAAqE;AAAA,EAClG;AACA,QAAM,SAAS,cAAc,QAAQ,MAAM;AAC3C,MAAI,CAAC,QAAQ;AACX,WAAO,EAAE,IAAI,OAAO,OAAO,mDAAmD;AAAA,EAChF;AACA,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,IAAI,mBAAmB;AAAA,IAC1D,QAAQ,QAAQ;AAAA,IAChB,mBAAmB,QAAQ;AAAA,IAC3B,GAAI,QAAQ,YAAY,EAAE,cAAc,QAAQ,UAAU,IAAI,CAAC;AAAA,EACjE,CAAC;AACD,MAAI,MAAO,QAAO,EAAE,IAAI,OAAO,OAAO,MAAM,QAAQ;AACpD,QAAM,SAAS;AACf,QAAM,QAAQ,QAAQ,KAAK,wBAAwB,OAAO,KAAK,IAAI;AACnE,MAAI,CAAC,OAAO;AACV,WAAO,EAAE,IAAI,OAAO,OAAO,QAAQ,SAAS,kCAAkC;AAAA,EAChF;AACA,SAAO,EAAE,IAAI,MAAM,OAAO,MAAM,cAAc,KAAK,GAAG,KAAK,aAAa,QAAQ,KAAK,EAAE;AACzF;;;ADdI;AAxGJ,eAAe,eAAe,MAAgC;AAC5D,MAAI;AACF,UAAM,UAAU,UAAU,UAAU,IAAI;AACxC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAYO,SAAS,aACd,QACA,SACoB;AACpB,QAAM,CAAC,KAAK,MAAM,IAAI,SAAwB,IAAI;AAClD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AACtD,QAAM,SAAS,OAAmC,IAAI;AAEtD,QAAM,OAAO,YAAY,YAA0C;AACjE,QAAI,OAAO,SAAS,GAAI,QAAO,OAAO;AACtC,eAAW,IAAI;AACf,aAAS,IAAI;AACb,QAAI;AACF,YAAM,SAAS,MAAM,cAAc,QAAQ,OAAO;AAClD,aAAO,UAAU;AACjB,UAAI,OAAO,IAAI;AACb,eAAO,OAAO,GAAG;AAAA,MACnB,OAAO;AACL,iBAAS,OAAO,KAAK;AAAA,MACvB;AACA,aAAO;AAAA,IACT,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,QAAQ,QAAQ,MAAM,QAAQ,gBAAgB,QAAQ,WAAW,QAAQ,MAAM,CAAC;AAEpF,SAAO,EAAE,MAAM,KAAK,SAAS,MAAM;AACrC;AAkBO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAA6B;AAC3B,QAAM,EAAE,MAAM,QAAQ,IAAI,aAAa,QAAQ,OAAO;AACtD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAsC,MAAM;AACtE,QAAM,aAAa,OAA6C,IAAI;AAEpE,QAAM,QAAQ,YAAY,CAAC,SAA6B;AACtD,aAAS,IAAI;AACb,QAAI,WAAW,QAAS,cAAa,WAAW,OAAO;AACvD,eAAW,UAAU,WAAW,MAAM,SAAS,MAAM,GAAG,GAAI;AAAA,EAC9D,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,YAAY,YAAY;AACtC,UAAM,SAAS,MAAM,KAAK;AAC1B,QAAI,CAAC,OAAO,IAAI;AACd,YAAM,OAAO;AACb,gBAAU,OAAO,KAAK;AACtB;AAAA,IACF;AACA,UAAM,SAAS,MAAM,eAAe,OAAO,GAAG;AAC9C,QAAI,QAAQ;AACV,YAAM,QAAQ;AACd,iBAAW,OAAO,GAAG;AAAA,IACvB,OAAO;AACL,YAAM,OAAO;AACb,gBAAU,kCAAkC;AAAA,IAC9C;AAAA,EACF,GAAG,CAAC,MAAM,OAAO,UAAU,OAAO,CAAC;AAEnC,QAAM,OACJ,UAAU,WAAW,WAAW,UAAU,UAAU,gBAAgB,UAAU,mBAAc;AAE9F,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL;AAAA,MACA,UAAU;AAAA,MACV,aAAU;AAAA,MACV,WACE,oKAEC,UAAU,UAAU,sBAAsB,OAC1C,aAAa;AAAA,MAGf;AAAA;AAAA,EACH;AAEJ;","names":[]}
|