@particle-academy/react-fancy 5.66.0 → 5.67.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/button.cjs +3 -4
  3. package/dist/button.js +1 -2
  4. package/dist/{chunk-J44EV6SP.js → chunk-23AVEYUP.js} +3 -3
  5. package/dist/{chunk-J44EV6SP.js.map → chunk-23AVEYUP.js.map} +1 -1
  6. package/dist/{chunk-C5YX5XVL.cjs → chunk-3U7PUZLI.cjs} +214 -4
  7. package/dist/chunk-3U7PUZLI.cjs.map +1 -0
  8. package/dist/{chunk-ATIDUWY6.cjs → chunk-4S5DFN5N.cjs} +12 -12
  9. package/dist/{chunk-ATIDUWY6.cjs.map → chunk-4S5DFN5N.cjs.map} +1 -1
  10. package/dist/{chunk-QP35Z2WB.js → chunk-BL5IKSB3.js} +3 -3
  11. package/dist/{chunk-QP35Z2WB.js.map → chunk-BL5IKSB3.js.map} +1 -1
  12. package/dist/{chunk-4EUDW2WU.js → chunk-DCAD2JPP.js} +3 -3
  13. package/dist/{chunk-4EUDW2WU.js.map → chunk-DCAD2JPP.js.map} +1 -1
  14. package/dist/{chunk-MCQJFMDP.cjs → chunk-E5UU666U.cjs} +478 -36
  15. package/dist/chunk-E5UU666U.cjs.map +1 -0
  16. package/dist/{chunk-L24LUSEE.js → chunk-FXOT4P4P.js} +478 -36
  17. package/dist/chunk-FXOT4P4P.js.map +1 -0
  18. package/dist/{chunk-CAUEKGMR.js → chunk-HEY4SMS2.js} +4 -4
  19. package/dist/{chunk-CAUEKGMR.js.map → chunk-HEY4SMS2.js.map} +1 -1
  20. package/dist/{chunk-TLRRMYNV.js → chunk-OXERDFFF.js} +212 -4
  21. package/dist/chunk-OXERDFFF.js.map +1 -0
  22. package/dist/{chunk-SYEHWUM5.cjs → chunk-RALJMI7C.cjs} +4 -4
  23. package/dist/{chunk-SYEHWUM5.cjs.map → chunk-RALJMI7C.cjs.map} +1 -1
  24. package/dist/{chunk-UM4JXBRZ.cjs → chunk-S3NB6LDV.cjs} +4 -4
  25. package/dist/{chunk-UM4JXBRZ.cjs.map → chunk-S3NB6LDV.cjs.map} +1 -1
  26. package/dist/{chunk-MCQWTOTV.cjs → chunk-SAG6DHQK.cjs} +6 -6
  27. package/dist/{chunk-MCQWTOTV.cjs.map → chunk-SAG6DHQK.cjs.map} +1 -1
  28. package/dist/{chunk-7HXFEYOV.cjs → chunk-U5LKNGHU.cjs} +15 -15
  29. package/dist/{chunk-7HXFEYOV.cjs.map → chunk-U5LKNGHU.cjs.map} +1 -1
  30. package/dist/{chunk-M52S3TFI.js → chunk-WFR7HKJL.js} +3 -3
  31. package/dist/{chunk-M52S3TFI.js.map → chunk-WFR7HKJL.js.map} +1 -1
  32. package/dist/content-renderer.cjs +7 -5
  33. package/dist/content-renderer.d.cts +56 -4
  34. package/dist/content-renderer.d.ts +56 -4
  35. package/dist/content-renderer.js +4 -2
  36. package/dist/editor.cjs +6 -5
  37. package/dist/editor.d.cts +1 -1
  38. package/dist/editor.d.ts +1 -1
  39. package/dist/editor.js +4 -3
  40. package/dist/{extensions-6PVd3-vB.d.cts → extensions-DiByZIpD.d.cts} +26 -3
  41. package/dist/{extensions-6PVd3-vB.d.ts → extensions-DiByZIpD.d.ts} +26 -3
  42. package/dist/index.cjs +37 -38
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +2 -2
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.js +7 -8
  47. package/dist/index.js.map +1 -1
  48. package/dist/json-editor.cjs +13 -14
  49. package/dist/json-editor.js +2 -3
  50. package/dist/magic-wand.cjs +3 -4
  51. package/dist/magic-wand.js +2 -3
  52. package/dist/prompt-input.cjs +5 -6
  53. package/dist/prompt-input.js +2 -3
  54. package/dist/reason-tag.cjs +3 -4
  55. package/dist/reason-tag.js +2 -3
  56. package/docs/ContentRenderer.md +138 -3
  57. package/package.json +1 -1
  58. package/dist/chunk-6TBRBN7H.js +0 -212
  59. package/dist/chunk-6TBRBN7H.js.map +0 -1
  60. package/dist/chunk-C5YX5XVL.cjs.map +0 -1
  61. package/dist/chunk-L24LUSEE.js.map +0 -1
  62. package/dist/chunk-MCQJFMDP.cjs.map +0 -1
  63. package/dist/chunk-TLRRMYNV.js.map +0 -1
  64. package/dist/chunk-X2FC7ODX.cjs +0 -215
  65. package/dist/chunk-X2FC7ODX.cjs.map +0 -1
@@ -1,4 +1,4 @@
1
- export { ReasonTag } from './chunk-QP35Z2WB.js';
1
+ export { ReasonTag } from './chunk-BL5IKSB3.js';
2
2
  import './chunk-QTS4Q2NK.js';
3
3
  import './chunk-MQ6VGG62.js';
4
4
  import './chunk-5VMILVWD.js';
@@ -6,8 +6,7 @@ import './chunk-RBCMKPBX.js';
6
6
  import './chunk-RGANGU7X.js';
7
7
  import './chunk-YX6FTJ6N.js';
8
8
  import './chunk-LQ4ICCN2.js';
9
- import './chunk-TLRRMYNV.js';
10
- import './chunk-6TBRBN7H.js';
9
+ import './chunk-OXERDFFF.js';
11
10
  import './chunk-45BBNJQD.js';
12
11
  import './chunk-J3A4Z2P7.js';
13
12
  import './chunk-MMW2JGKZ.js';
@@ -24,6 +24,98 @@ import { ContentRenderer } from "@particle-academy/react-fancy";
24
24
  | extensions | `RenderExtension[]` | - | Per-instance render extensions (merged with global) |
25
25
  | className | `string` | - | Additional CSS classes |
26
26
  | unsafe | `boolean` | `false` | Skip HTML sanitization. See **Sanitization** below. |
27
+ | copyButtons | `boolean \| { code?: boolean; table?: boolean }` | `true` | Copy buttons on code blocks and tables. See **Copy buttons**. |
28
+ | onCopy | `(event: ContentCopyEvent) => void` | - | Called after every copy attempt, successful or not |
29
+ | clipboardWriter | `ContentClipboardWriter` | - | Host-supplied clipboard writer. See **Clipboard writer**. |
30
+ | streaming | `boolean` | `false` | The value is still arriving: hold back an unfinished trailing tag or fence. See **Streaming**. |
31
+
32
+ ## Copy buttons
33
+
34
+ Every code block and table gets a react-fancy `Button` (icon `copy`, then
35
+ `check` for two seconds after a copy, `x` after a failure). What is copied:
36
+
37
+ - **Code**: exactly the code between the fences. No language label, no
38
+ trailing newline added. (Marked itself drops trailing blank lines inside a
39
+ fence, so they are not in the copy either; indentation, inner blank lines and
40
+ characters like `<` and `&` are.)
41
+ - **Table**: TSV. A tab between cells, a newline between rows, no trailing
42
+ newline. Tabs and line breaks inside a cell become spaces, so the shape
43
+ survives a paste into a spreadsheet.
44
+
45
+ Each copy is announced as `Copied` or `Copy failed` through a polite
46
+ `aria-live` region (`[data-react-fancy-content-renderer-live]`).
47
+
48
+ ```tsx
49
+ <ContentRenderer value={reply} copyButtons={false} /> // none
50
+ <ContentRenderer value={reply} copyButtons={{ table: false }} /> // code only
51
+ ```
52
+
53
+ The button sits beside the block, never inside it, so its own text never ends
54
+ up in the copy. Buttons are added after mount, so server-rendered HTML has none
55
+ until hydration.
56
+
57
+ ### Clipboard writer
58
+
59
+ The default writer puts **text/html and text/plain** on the clipboard with a
60
+ `ClipboardItem`, so a table pastes as a table into a rich editor and as cells
61
+ into a spreadsheet. If `ClipboardItem` is missing or that write is refused, it
62
+ falls back to `navigator.clipboard.writeText`.
63
+
64
+ `navigator.clipboard` can fail silently in some hosts (Genie found it does in a
65
+ sandboxed Electron window). Pass your own writer there:
66
+
67
+ ```tsx
68
+ import { clipboard } from "electron";
69
+
70
+ <ContentRenderer
71
+ value={reply}
72
+ clipboardWriter={({ text, html }) => clipboard.write({ text, html })}
73
+ />
74
+ ```
75
+
76
+ A writer receives `{ kind, text, html }`. **Throwing, rejecting, or returning
77
+ `false` means the copy failed**, and so does a write that has not settled after
78
+ 5 seconds. A failure is never silent: the button shows `x`, the live region says
79
+ `Copy failed`, and `onCopy` reports `ok: false`.
80
+
81
+ ### Observing copies (Human+)
82
+
83
+ ContentRenderer is a read-only surface: it exposes stable DOM handles rather
84
+ than state. Copy follows that:
85
+
86
+ | Handle | On |
87
+ |--------|----|
88
+ | `data-react-fancy-content-copy="code" \| "table"` | the copy button |
89
+ | `data-copy-state="idle" \| "copied" \| "failed"` | the copy button |
90
+ | `data-react-fancy-content-copy-block="code" \| "table"` | the wrapper around the block |
91
+ | `data-react-fancy-content-renderer-live` | the live region |
92
+ | `data-streaming="true"` | the root, while `streaming` |
93
+
94
+ `onCopy` receives `{ kind, index, text, html, ok }`, where `index` is the
95
+ button's position among this renderer's copy buttons in document order. Wire it
96
+ to your activity feed if agents should see copies.
97
+
98
+ ## Streaming
99
+
100
+ While a reply is still arriving, pass `streaming`:
101
+
102
+ ```tsx
103
+ <ContentRenderer value={partial} streaming={!done} extensions={[resultTag]} />
104
+ ```
105
+
106
+ Held back until it is complete:
107
+
108
+ - an opened tag of a **registered extension** with no closing tag yet, from its
109
+ opening tag onwards (`<result>` + half a body, or `<result>body</res`);
110
+ - a partial tag at the very end that could still become one: `<`, `</`,
111
+ `<res`, `<result title="Do`;
112
+ - an open code fence, from its fence line, and a fence marker still arriving
113
+ (` `` `).
114
+
115
+ A `<` that cannot become a registered tag is prose and always renders
116
+ (`a < b`, `a<b`, `3 <= 4`), as do unknown tags and anything in inline code.
117
+ A lone `<` at the very end is held for one chunk, because it may be the start of
118
+ a tag. When `streaming` goes back to `false`, everything renders.
27
119
 
28
120
  ## Sanitization
29
121
 
@@ -45,7 +137,7 @@ The helpers are also exported for use elsewhere:
45
137
  import { sanitizeHtml, sanitizeHref } from "@particle-academy/react-fancy";
46
138
  ```
47
139
 
48
- > **Render extensions still need to sanitize their own input.** The `content` prop passed to your extension component is the raw text inside the matched tag — treat it as untrusted and call `sanitizeHtml(content)` if you render it as HTML.
140
+ > **Render extensions still need to sanitize their own input.** The `content` prop passed to your extension component is the raw text inside the matched tag — treat it as untrusted and call `sanitizeHtml(content)` if you render it as HTML. A `markdown` extension gets an already-sanitised `children` instead; use that.
49
141
 
50
142
  ## Render Extensions
51
143
 
@@ -78,13 +170,56 @@ registerExtension({
78
170
  | tag | `string` | - | Tag name to match (case-insensitive) |
79
171
  | component | `ComponentType<RenderExtensionProps>` | - | React component to render the tag |
80
172
  | block | `boolean` | `true` | Block-level (`<div>`) vs inline (`<span>`) wrapping |
173
+ | markdown | `boolean` | `false` | The body is markdown. See **Tags with a markdown body**. |
81
174
 
82
175
  ### RenderExtensionProps
83
176
 
84
177
  | Prop | Type | Description |
85
178
  |------|------|-------------|
86
- | content | `string` | Inner content of the custom tag |
87
- | attributes | `Record<string, string>` | Parsed HTML attributes from the opening tag |
179
+ | content | `string` | Inner content of the custom tag. Raw markdown for a `markdown` extension in markdown content. Untrusted. |
180
+ | attributes | `Record<string, string>` | Parsed attributes from the opening tag, with `on*`, `srcdoc` and unsafe URLs removed |
181
+ | children | `ReactNode` | `markdown` extensions only: the body, rendered and sanitised |
182
+
183
+ ### Tags with a markdown body
184
+
185
+ Custom tags are normally found after the markdown has become HTML, and a
186
+ CommonMark HTML block keeps the markdown inside it as literal text, so
187
+ `<result>**done**</result>` would show its asterisks. Mark the extension
188
+ `markdown: true` and the tag is lifted out of the markdown **source** first:
189
+
190
+ ```tsx
191
+ const resultTag: RenderExtension = {
192
+ tag: "result",
193
+ markdown: true,
194
+ component: ({ attributes, children }) => (
195
+ <Card>
196
+ <Card.Header>{attributes.title}</Card.Header>
197
+ <Card.Body>{children}</Card.Body>
198
+ </Card>
199
+ ),
200
+ };
201
+
202
+ <ContentRenderer
203
+ value={'Here it is:\n\n<result title="Build">\n**Passed**, `12/12`\n\n- lint\n- test\n</result>'}
204
+ extensions={[resultTag]}
205
+ />
206
+ ```
207
+
208
+ - `content` is the raw markdown body; `children` is that body rendered through
209
+ the same pipeline as everything else: sanitised, with nested extensions and
210
+ copy buttons. **Render `children`, not `content`.**
211
+ - A tag is lifted when its opening tag starts a line and it has a matching
212
+ closing tag. Same-name nesting is balanced, other markdown tags nest inside
213
+ it, and a tag inside a code fence or inline code is left alone.
214
+ - Anywhere else (mid-line, unclosed, or in HTML content) it is matched in the
215
+ HTML as before, and `children` renders that HTML body.
216
+ - Nesting stops at depth 8: deeper bodies render as plain sanitised content.
217
+
218
+ Sanitisation is unchanged by lifting. Every piece of markdown, including each
219
+ body, is converted and then run through `sanitizeHtml` before it is injected,
220
+ and the tag's attributes lose `on*`, `srcdoc` and unsafe URLs before the
221
+ component sees them. Unknown tags stay in the markdown and are sanitised like
222
+ any HTML. `unsafe` turns all of this off, as it always has.
88
223
 
89
224
  ## Markdown Rendering
90
225
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/react-fancy",
3
- "version": "5.66.0",
3
+ "version": "5.67.0",
4
4
  "description": "React UI component library for Human+ UX — controlled, agent-bridgeable primitives",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,212 +0,0 @@
1
- // src/utils/sanitize.ts
2
- var DANGEROUS_TAGS = /* @__PURE__ */ new Set([
3
- "script",
4
- "style",
5
- "iframe",
6
- "object",
7
- "embed",
8
- "link",
9
- "meta",
10
- "base",
11
- "form",
12
- // A <title> anywhere in the document retitles the host's tab.
13
- "title"
14
- ]);
15
- var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href"]);
16
- var SAFE_PROTOCOL = /^(?:https?:|mailto:|tel:|sms:|ftp:|#|\/|\.\/|\.\.\/|[^:]*$)/i;
17
- function sanitizeHref(href) {
18
- if (href == null) return void 0;
19
- const trimmed = href.trim();
20
- if (!trimmed) return void 0;
21
- return SAFE_PROTOCOL.test(trimmed) ? trimmed : void 0;
22
- }
23
- function stripDangerousAttrs(el) {
24
- const names = [];
25
- for (let i = 0; i < el.attributes.length; i++) {
26
- names.push(el.attributes[i].name);
27
- }
28
- for (const name of names) {
29
- const lower = name.toLowerCase();
30
- if (lower.startsWith("on")) {
31
- el.removeAttribute(name);
32
- continue;
33
- }
34
- if (URL_ATTRS.has(lower)) {
35
- const sanitized = sanitizeHref(el.getAttribute(name));
36
- if (sanitized === void 0) {
37
- el.removeAttribute(name);
38
- } else {
39
- el.setAttribute(name, sanitized);
40
- }
41
- continue;
42
- }
43
- if (lower === "srcdoc") {
44
- el.removeAttribute(name);
45
- }
46
- }
47
- }
48
- function walk(el, removeQueue) {
49
- const tag = el.tagName.toLowerCase();
50
- if (DANGEROUS_TAGS.has(tag)) {
51
- removeQueue.push(el);
52
- return;
53
- }
54
- stripDangerousAttrs(el);
55
- const children = Array.from(el.children);
56
- for (const child of children) {
57
- walk(child, removeQueue);
58
- }
59
- }
60
- var ALWAYS_REMOVED_TAGS = /* @__PURE__ */ new Set([
61
- ...DANGEROUS_TAGS,
62
- "frame",
63
- "frameset",
64
- "applet",
65
- "template",
66
- "noscript",
67
- "noembed",
68
- "noframes",
69
- "xmp",
70
- "plaintext",
71
- // Foreign content: namespace confusion is how mutation XSS gets past
72
- // sanitisers that re-serialise, as this one does.
73
- "svg",
74
- "math"
75
- ]);
76
- var ALLOWLIST_URL_ATTRS = /* @__PURE__ */ new Set([
77
- "href",
78
- "src",
79
- "action",
80
- "formaction",
81
- "cite",
82
- "poster",
83
- "background",
84
- "ping",
85
- "xlink:href"
86
- ]);
87
- var NEVER_ALLOWED_ATTRS = /* @__PURE__ */ new Set(["srcdoc", "srcset"]);
88
- var NEVER_ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["javascript:", "vbscript:"]);
89
- var DEFAULT_SCHEMES = ["http:", "https:", "mailto:"];
90
- var SCHEME = /^([a-z][a-z0-9+.-]*:)/i;
91
- var SLASH = 47;
92
- var BACKSLASH = 92;
93
- function lowerAll(list) {
94
- return (list ?? []).map((s) => s.toLowerCase());
95
- }
96
- function toPolicy(options) {
97
- const tagAttrs = /* @__PURE__ */ new Map();
98
- let globalAttrs = [];
99
- const attrs = options.allowedAttributes;
100
- if (Array.isArray(attrs)) {
101
- globalAttrs = lowerAll(attrs);
102
- } else if (attrs) {
103
- for (const [tag, list] of Object.entries(attrs)) {
104
- if (tag === "*") globalAttrs = lowerAll(list);
105
- else tagAttrs.set(tag.toLowerCase(), lowerAll(list));
106
- }
107
- }
108
- return {
109
- tags: new Set(lowerAll(options.allowedTags)),
110
- globalAttrs,
111
- tagAttrs,
112
- schemes: new Set(lowerAll(options.allowedSchemes ?? DEFAULT_SCHEMES)),
113
- allowRelative: options.allowRelative === true
114
- };
115
- }
116
- function attrListed(name, list) {
117
- if (!list) return false;
118
- for (const entry of list) {
119
- if (entry === name) return true;
120
- if (entry.endsWith("*") && name.startsWith(entry.slice(0, -1))) return true;
121
- }
122
- return false;
123
- }
124
- function stripControlAndSpace(value) {
125
- let out = "";
126
- for (let i = 0; i < value.length; i++) {
127
- if (value.charCodeAt(i) > 32) out += value[i];
128
- }
129
- return out;
130
- }
131
- function urlAllowed(value, policy) {
132
- const url = stripControlAndSpace(value);
133
- const match = SCHEME.exec(url);
134
- if (match) {
135
- const scheme = match[1].toLowerCase();
136
- return !NEVER_ALLOWED_SCHEMES.has(scheme) && policy.schemes.has(scheme);
137
- }
138
- if (!policy.allowRelative) return false;
139
- const a = url.charCodeAt(0);
140
- const b = url.charCodeAt(1);
141
- const isSlash = (c) => c === SLASH || c === BACKSLASH;
142
- return !(isSlash(a) && isSlash(b));
143
- }
144
- function cleanAttributes(el, tag, policy) {
145
- const names = [];
146
- for (let i = 0; i < el.attributes.length; i++) {
147
- names.push(el.attributes[i].name);
148
- }
149
- for (const name of names) {
150
- const lower = name.toLowerCase();
151
- const keep = !lower.startsWith("on") && !NEVER_ALLOWED_ATTRS.has(lower) && (attrListed(lower, policy.globalAttrs) || attrListed(lower, policy.tagAttrs.get(tag))) && (!ALLOWLIST_URL_ATTRS.has(lower) || urlAllowed(el.getAttribute(name) ?? "", policy));
152
- if (!keep) el.removeAttribute(name);
153
- }
154
- }
155
- function cleanChildren(parent, policy) {
156
- for (const node of Array.from(parent.childNodes)) {
157
- if (node.nodeType === 3) continue;
158
- if (node.nodeType !== 1) {
159
- parent.removeChild(node);
160
- continue;
161
- }
162
- const el = node;
163
- const tag = el.tagName.toLowerCase();
164
- if (ALWAYS_REMOVED_TAGS.has(tag)) {
165
- parent.removeChild(el);
166
- continue;
167
- }
168
- cleanChildren(el, policy);
169
- if (!policy.tags.has(tag)) {
170
- while (el.firstChild) parent.insertBefore(el.firstChild, el);
171
- parent.removeChild(el);
172
- continue;
173
- }
174
- cleanAttributes(el, tag, policy);
175
- }
176
- }
177
- function escapeText(text) {
178
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
179
- }
180
- function sanitizeAllowlist(html, options) {
181
- if (typeof window === "undefined" || typeof DOMParser === "undefined") {
182
- return escapeText(html);
183
- }
184
- const doc = new DOMParser().parseFromString(`<body>${html}</body>`, "text/html");
185
- const body = doc.body;
186
- if (!body) return "";
187
- cleanChildren(body, toPolicy(options));
188
- return body.innerHTML;
189
- }
190
- function sanitizeHtml(html, options) {
191
- if (options !== void 0 && options !== null) {
192
- return sanitizeAllowlist(html, options);
193
- }
194
- if (typeof window === "undefined" || typeof DOMParser === "undefined") {
195
- return html;
196
- }
197
- const doc = new DOMParser().parseFromString(`<body>${html}</body>`, "text/html");
198
- const body = doc.body;
199
- if (!body) return html;
200
- const removeQueue = [];
201
- for (const child of Array.from(body.children)) {
202
- walk(child, removeQueue);
203
- }
204
- for (const el of removeQueue) {
205
- el.parentNode?.removeChild(el);
206
- }
207
- return body.innerHTML;
208
- }
209
-
210
- export { sanitizeHref, sanitizeHtml };
211
- //# sourceMappingURL=chunk-6TBRBN7H.js.map
212
- //# sourceMappingURL=chunk-6TBRBN7H.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/utils/sanitize.ts"],"names":[],"mappings":"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","file":"chunk-6TBRBN7H.js","sourcesContent":["/**\n * HTML and URL sanitization utilities.\n *\n * Hand-rolled (no third-party deps) using browser-native `DOMParser`. Designed\n * for the trust model \"consumer passes user-generated markdown/HTML to a\n * react-fancy component\" — strips script tags, event handlers, and dangerous\n * URI schemes from `href`/`src` attributes.\n *\n * For server-side rendering (no `window`), `sanitizeHtml` returns the input\n * unchanged. Consumers SSR-rendering untrusted content should sanitize on the\n * server with their own pipeline.\n */\n\nconst DANGEROUS_TAGS = new Set([\n \"script\",\n \"style\",\n \"iframe\",\n \"object\",\n \"embed\",\n \"link\",\n \"meta\",\n \"base\",\n \"form\",\n // A <title> anywhere in the document retitles the host's tab.\n \"title\",\n]);\n\nconst URL_ATTRS = new Set([\"href\", \"src\", \"action\", \"formaction\", \"xlink:href\"]);\n\nconst SAFE_PROTOCOL = /^(?:https?:|mailto:|tel:|sms:|ftp:|#|\\/|\\.\\/|\\.\\.\\/|[^:]*$)/i;\n\n/**\n * Validate a URL/href against an allow-list of safe protocols. Returns the\n * input if safe, or `undefined` if it begins with a dangerous scheme like\n * `javascript:`, `data:`, or `vbscript:`. Relative URLs and fragment links\n * are allowed.\n */\nexport function sanitizeHref(href: string | undefined | null): string | undefined {\n if (href == null) return undefined;\n const trimmed = href.trim();\n if (!trimmed) return undefined;\n return SAFE_PROTOCOL.test(trimmed) ? trimmed : undefined;\n}\n\nfunction stripDangerousAttrs(el: Element): void {\n // Collect names first; mutating during iteration is awkward with NamedNodeMap.\n const names: string[] = [];\n for (let i = 0; i < el.attributes.length; i++) {\n names.push(el.attributes[i].name);\n }\n\n for (const name of names) {\n const lower = name.toLowerCase();\n\n // All on* event handlers\n if (lower.startsWith(\"on\")) {\n el.removeAttribute(name);\n continue;\n }\n\n // URL attributes — protocol-filter\n if (URL_ATTRS.has(lower)) {\n const sanitized = sanitizeHref(el.getAttribute(name));\n if (sanitized === undefined) {\n el.removeAttribute(name);\n } else {\n el.setAttribute(name, sanitized);\n }\n continue;\n }\n\n // srcdoc executes JS in the iframe — drop wholesale\n if (lower === \"srcdoc\") {\n el.removeAttribute(name);\n }\n }\n}\n\nfunction walk(el: Element, removeQueue: Element[]): void {\n const tag = el.tagName.toLowerCase();\n\n if (DANGEROUS_TAGS.has(tag)) {\n removeQueue.push(el);\n return;\n }\n\n stripDangerousAttrs(el);\n\n const children = Array.from(el.children);\n for (const child of children) {\n walk(child, removeQueue);\n }\n}\n\n/**\n * Allowlist options for {@link sanitizeHtml}. Passing ANY options object\n * (even `{}`) switches from the default blocklist to allowlist mode, where\n * everything not listed is removed.\n */\nexport interface SanitizeHtmlOptions {\n /**\n * Tags to keep, e.g. `[\"p\", \"a\", \"strong\"]`. A tag that is not listed is\n * removed but its text and (cleaned) children are kept. Default: none, so\n * only text survives.\n *\n * Some tags are removed WITH their content even when listed, because no\n * attribute filtering makes them safe: `script`, `style`, `iframe`, `frame`,\n * `frameset`, `object`, `embed`, `applet`, `link`, `meta`, `base`, `form`,\n * `title`, `template`, `noscript`, `noembed`, `noframes`, `xmp`,\n * `plaintext`, `svg` and `math`.\n */\n allowedTags?: readonly string[];\n /**\n * Attributes to keep. Either one list for every allowed tag, or a map from\n * tag name to its list, where the key `\"*\"` applies to every tag. A trailing\n * `*` matches a prefix (`\"data-*\"`, `\"aria-*\"`). Default: none.\n *\n * Never kept, even when listed: event handlers (`on*`), `srcdoc` and\n * `srcset`.\n */\n allowedAttributes?: readonly string[] | Readonly<Record<string, readonly string[]>>;\n /**\n * URL schemes allowed in URL attributes (`href`, `src`, `action`,\n * `formaction`, `cite`, `poster`, `background`, `ping`, `xlink:href`), with\n * the colon: `[\"https:\", \"mailto:\"]`. A URL attribute whose scheme is not\n * listed is removed. Default: `[\"http:\", \"https:\", \"mailto:\"]`.\n *\n * `javascript:` and `vbscript:` are never kept, even when listed.\n */\n allowedSchemes?: readonly string[];\n /**\n * Keep URLs with no scheme that stay on this site: `/x`, `x/y`, `../x`,\n * `?q`, `#frag`. Protocol-relative URLs (`//host/x`, and the backslash\n * spellings a browser treats the same way) leave the site and are always\n * removed. Default: `false`.\n */\n allowRelative?: boolean;\n}\n\n/** Removed with their content in allowlist mode, whatever the options say. */\nconst ALWAYS_REMOVED_TAGS = new Set([\n ...DANGEROUS_TAGS,\n \"frame\",\n \"frameset\",\n \"applet\",\n \"template\",\n \"noscript\",\n \"noembed\",\n \"noframes\",\n \"xmp\",\n \"plaintext\",\n // Foreign content: namespace confusion is how mutation XSS gets past\n // sanitisers that re-serialise, as this one does.\n \"svg\",\n \"math\",\n]);\n\nconst ALLOWLIST_URL_ATTRS = new Set([\n \"href\",\n \"src\",\n \"action\",\n \"formaction\",\n \"cite\",\n \"poster\",\n \"background\",\n \"ping\",\n \"xlink:href\",\n]);\n\nconst NEVER_ALLOWED_ATTRS = new Set([\"srcdoc\", \"srcset\"]);\n\nconst NEVER_ALLOWED_SCHEMES = new Set([\"javascript:\", \"vbscript:\"]);\n\nconst DEFAULT_SCHEMES = [\"http:\", \"https:\", \"mailto:\"];\n\nconst SCHEME = /^([a-z][a-z0-9+.-]*:)/i;\n\nconst SLASH = 47;\nconst BACKSLASH = 92;\n\ninterface Policy {\n tags: Set<string>;\n globalAttrs: string[];\n tagAttrs: Map<string, string[]>;\n schemes: Set<string>;\n allowRelative: boolean;\n}\n\nfunction lowerAll(list: readonly string[] | undefined): string[] {\n return (list ?? []).map((s) => s.toLowerCase());\n}\n\nfunction toPolicy(options: SanitizeHtmlOptions): Policy {\n const tagAttrs = new Map<string, string[]>();\n let globalAttrs: string[] = [];\n const attrs = options.allowedAttributes;\n if (Array.isArray(attrs)) {\n globalAttrs = lowerAll(attrs);\n } else if (attrs) {\n for (const [tag, list] of Object.entries(attrs as Record<string, readonly string[]>)) {\n if (tag === \"*\") globalAttrs = lowerAll(list);\n else tagAttrs.set(tag.toLowerCase(), lowerAll(list));\n }\n }\n return {\n tags: new Set(lowerAll(options.allowedTags)),\n globalAttrs,\n tagAttrs,\n schemes: new Set(lowerAll(options.allowedSchemes ?? DEFAULT_SCHEMES)),\n allowRelative: options.allowRelative === true,\n };\n}\n\nfunction attrListed(name: string, list: string[] | undefined): boolean {\n if (!list) return false;\n for (const entry of list) {\n if (entry === name) return true;\n if (entry.endsWith(\"*\") && name.startsWith(entry.slice(0, -1))) return true;\n }\n return false;\n}\n\n/**\n * Browsers ignore ASCII whitespace and control characters inside a URL's\n * scheme (`java<TAB>script:` runs), so judge the URL with them removed.\n */\nfunction stripControlAndSpace(value: string): string {\n let out = \"\";\n for (let i = 0; i < value.length; i++) {\n if (value.charCodeAt(i) > 32) out += value[i];\n }\n return out;\n}\n\nfunction urlAllowed(value: string, policy: Policy): boolean {\n const url = stripControlAndSpace(value);\n const match = SCHEME.exec(url);\n if (match) {\n const scheme = match[1].toLowerCase();\n return !NEVER_ALLOWED_SCHEMES.has(scheme) && policy.schemes.has(scheme);\n }\n if (!policy.allowRelative) return false;\n const a = url.charCodeAt(0);\n const b = url.charCodeAt(1);\n const isSlash = (c: number) => c === SLASH || c === BACKSLASH;\n // `//host`, and its backslash spellings, is another site.\n return !(isSlash(a) && isSlash(b));\n}\n\nfunction cleanAttributes(el: Element, tag: string, policy: Policy): void {\n const names: string[] = [];\n for (let i = 0; i < el.attributes.length; i++) {\n names.push(el.attributes[i].name);\n }\n for (const name of names) {\n const lower = name.toLowerCase();\n const keep =\n !lower.startsWith(\"on\") &&\n !NEVER_ALLOWED_ATTRS.has(lower) &&\n (attrListed(lower, policy.globalAttrs) || attrListed(lower, policy.tagAttrs.get(tag))) &&\n (!ALLOWLIST_URL_ATTRS.has(lower) || urlAllowed(el.getAttribute(name) ?? \"\", policy));\n if (!keep) el.removeAttribute(name);\n }\n}\n\nfunction cleanChildren(parent: Node, policy: Policy): void {\n for (const node of Array.from(parent.childNodes)) {\n if (node.nodeType === 3) continue; // text\n if (node.nodeType !== 1) {\n // comments, processing instructions, CDATA\n parent.removeChild(node);\n continue;\n }\n const el = node as Element;\n const tag = el.tagName.toLowerCase();\n if (ALWAYS_REMOVED_TAGS.has(tag)) {\n parent.removeChild(el);\n continue;\n }\n cleanChildren(el, policy);\n if (!policy.tags.has(tag)) {\n while (el.firstChild) parent.insertBefore(el.firstChild, el);\n parent.removeChild(el);\n continue;\n }\n cleanAttributes(el, tag, policy);\n }\n}\n\nfunction escapeText(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n\nfunction sanitizeAllowlist(html: string, options: SanitizeHtmlOptions): string {\n if (typeof window === \"undefined\" || typeof DOMParser === \"undefined\") {\n // Fail closed: with no parser we cannot tell markup from text, so the\n // whole input is returned as escaped text rather than passed through.\n return escapeText(html);\n }\n const doc = new DOMParser().parseFromString(`<body>${html}</body>`, \"text/html\");\n const body = doc.body;\n if (!body) return \"\";\n cleanChildren(body, toPolicy(options));\n return body.innerHTML;\n}\n\n/**\n * Sanitize an HTML string.\n *\n * **With no options** (the default blocklist): removes dangerous tags (script,\n * iframe, etc.), event-handler attributes (`onclick`, `onerror`, ...), and\n * dangerous URI schemes in `href`/`src` attributes. Everything else is kept.\n * In non-browser environments returns the input unchanged — sanitize on the\n * server in those cases.\n *\n * **With options** (allowlist mode): keeps ONLY the listed tags, attributes and\n * URL schemes; see {@link SanitizeHtmlOptions}. The allowlist can only narrow:\n * event handlers, `javascript:` URLs and script-capable tags never survive,\n * even when listed. In non-browser environments the input is returned as\n * escaped text (fail closed).\n *\n * Uses the browser's `DOMParser`.\n */\nexport function sanitizeHtml(html: string, options?: SanitizeHtmlOptions): string {\n if (options !== undefined && options !== null) {\n return sanitizeAllowlist(html, options);\n }\n if (typeof window === \"undefined\" || typeof DOMParser === \"undefined\") {\n return html;\n }\n\n const doc = new DOMParser().parseFromString(`<body>${html}</body>`, \"text/html\");\n const body = doc.body;\n if (!body) return html;\n\n const removeQueue: Element[] = [];\n for (const child of Array.from(body.children)) {\n walk(child, removeQueue);\n }\n\n for (const el of removeQueue) {\n el.parentNode?.removeChild(el);\n }\n\n return body.innerHTML;\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/Button/Button.colors.ts","../src/components/Button/Button.tsx"],"names":["forwardRef","resolve","jsx","cn","Icon","jsxs","Fragment","sanitizeHref"],"mappings":"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","file":"chunk-C5YX5XVL.cjs","sourcesContent":["// AUTO-GENERATED by scripts/gen-button-colors — do not edit by hand.\n// Literal Tailwind class strings for every color in the v4 palette so the\n// scanner can detect them in the published dist (template-built names are\n// invisible to Tailwind's JIT). Regenerate after changing the palette.\nimport type { Color } from \"../../utils/types\";\n\nexport const buttonColorClasses: Record<Color, string> = {\n // The host's colour, from CSS variables -- no dark: variant, because a\n // host that themes per mode scopes the variables themselves.\n brand: \"bg-brand text-brand-contrast border border-brand hover:bg-brand-hover\",\n slate: \"bg-slate-500 text-white border border-slate-600 hover:bg-slate-600 dark:bg-slate-600 dark:border-slate-500 dark:hover:bg-slate-500\",\n gray: \"bg-gray-500 text-white border border-gray-600 hover:bg-gray-600 dark:bg-gray-600 dark:border-gray-500 dark:hover:bg-gray-500\",\n zinc: \"bg-zinc-500 text-white border border-zinc-600 hover:bg-zinc-600 dark:bg-zinc-600 dark:border-zinc-500 dark:hover:bg-zinc-500\",\n neutral: \"bg-neutral-500 text-white border border-neutral-600 hover:bg-neutral-600 dark:bg-neutral-600 dark:border-neutral-500 dark:hover:bg-neutral-500\",\n stone: \"bg-stone-500 text-white border border-stone-600 hover:bg-stone-600 dark:bg-stone-600 dark:border-stone-500 dark:hover:bg-stone-500\",\n red: \"bg-red-500 text-white border border-red-600 hover:bg-red-600 dark:bg-red-600 dark:border-red-500 dark:hover:bg-red-500\",\n orange: \"bg-orange-500 text-white border border-orange-600 hover:bg-orange-600 dark:bg-orange-600 dark:border-orange-500 dark:hover:bg-orange-500\",\n amber: \"bg-amber-400 text-amber-950 border border-amber-500 hover:bg-amber-500 dark:bg-amber-300 dark:text-amber-950 dark:border-amber-400 dark:hover:bg-amber-400\",\n yellow: \"bg-yellow-400 text-yellow-950 border border-yellow-500 hover:bg-yellow-500 dark:bg-yellow-300 dark:text-yellow-950 dark:border-yellow-400 dark:hover:bg-yellow-400\",\n lime: \"bg-lime-400 text-lime-950 border border-lime-500 hover:bg-lime-500 dark:bg-lime-300 dark:text-lime-950 dark:border-lime-400 dark:hover:bg-lime-400\",\n green: \"bg-green-500 text-white border border-green-600 hover:bg-green-600 dark:bg-green-600 dark:border-green-500 dark:hover:bg-green-500\",\n emerald: \"bg-emerald-500 text-white border border-emerald-600 hover:bg-emerald-600 dark:bg-emerald-600 dark:border-emerald-500 dark:hover:bg-emerald-500\",\n teal: \"bg-teal-500 text-white border border-teal-600 hover:bg-teal-600 dark:bg-teal-600 dark:border-teal-500 dark:hover:bg-teal-500\",\n cyan: \"bg-cyan-500 text-white border border-cyan-600 hover:bg-cyan-600 dark:bg-cyan-600 dark:border-cyan-500 dark:hover:bg-cyan-500\",\n sky: \"bg-sky-500 text-white border border-sky-600 hover:bg-sky-600 dark:bg-sky-600 dark:border-sky-500 dark:hover:bg-sky-500\",\n blue: \"bg-blue-500 text-white border border-blue-600 hover:bg-blue-600 dark:bg-blue-600 dark:border-blue-500 dark:hover:bg-blue-500\",\n indigo: \"bg-indigo-500 text-white border border-indigo-600 hover:bg-indigo-600 dark:bg-indigo-600 dark:border-indigo-500 dark:hover:bg-indigo-500\",\n violet: \"bg-violet-500 text-white border border-violet-600 hover:bg-violet-600 dark:bg-violet-600 dark:border-violet-500 dark:hover:bg-violet-500\",\n purple: \"bg-purple-500 text-white border border-purple-600 hover:bg-purple-600 dark:bg-purple-600 dark:border-purple-500 dark:hover:bg-purple-500\",\n fuchsia: \"bg-fuchsia-500 text-white border border-fuchsia-600 hover:bg-fuchsia-600 dark:bg-fuchsia-600 dark:border-fuchsia-500 dark:hover:bg-fuchsia-500\",\n pink: \"bg-pink-500 text-white border border-pink-600 hover:bg-pink-600 dark:bg-pink-600 dark:border-pink-500 dark:hover:bg-pink-500\",\n rose: \"bg-rose-500 text-white border border-rose-600 hover:bg-rose-600 dark:bg-rose-600 dark:border-rose-500 dark:hover:bg-rose-500\",\n};\n\nexport const buttonGhostClasses: Record<Color, string> = {\n // The host's colour, from CSS variables -- no dark: variant, because a\n // host that themes per mode scopes the variables themselves.\n brand: \"bg-transparent text-brand hover:bg-brand/10\",\n slate: \"bg-transparent text-slate-600 hover:bg-slate-50 dark:text-slate-400 dark:hover:bg-slate-950/50\",\n gray: \"bg-transparent text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-950/50\",\n zinc: \"bg-transparent text-zinc-600 hover:bg-zinc-50 dark:text-zinc-400 dark:hover:bg-zinc-950/50\",\n neutral: \"bg-transparent text-neutral-600 hover:bg-neutral-50 dark:text-neutral-400 dark:hover:bg-neutral-950/50\",\n stone: \"bg-transparent text-stone-600 hover:bg-stone-50 dark:text-stone-400 dark:hover:bg-stone-950/50\",\n red: \"bg-transparent text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950/50\",\n orange: \"bg-transparent text-orange-600 hover:bg-orange-50 dark:text-orange-400 dark:hover:bg-orange-950/50\",\n amber: \"bg-transparent text-amber-600 hover:bg-amber-50 dark:text-amber-400 dark:hover:bg-amber-950/50\",\n yellow: \"bg-transparent text-yellow-600 hover:bg-yellow-50 dark:text-yellow-400 dark:hover:bg-yellow-950/50\",\n lime: \"bg-transparent text-lime-600 hover:bg-lime-50 dark:text-lime-400 dark:hover:bg-lime-950/50\",\n green: \"bg-transparent text-green-600 hover:bg-green-50 dark:text-green-400 dark:hover:bg-green-950/50\",\n emerald: \"bg-transparent text-emerald-600 hover:bg-emerald-50 dark:text-emerald-400 dark:hover:bg-emerald-950/50\",\n teal: \"bg-transparent text-teal-600 hover:bg-teal-50 dark:text-teal-400 dark:hover:bg-teal-950/50\",\n cyan: \"bg-transparent text-cyan-600 hover:bg-cyan-50 dark:text-cyan-400 dark:hover:bg-cyan-950/50\",\n sky: \"bg-transparent text-sky-600 hover:bg-sky-50 dark:text-sky-400 dark:hover:bg-sky-950/50\",\n blue: \"bg-transparent text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-950/50\",\n indigo: \"bg-transparent text-indigo-600 hover:bg-indigo-50 dark:text-indigo-400 dark:hover:bg-indigo-950/50\",\n violet: \"bg-transparent text-violet-600 hover:bg-violet-50 dark:text-violet-400 dark:hover:bg-violet-950/50\",\n purple: \"bg-transparent text-purple-600 hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-purple-950/50\",\n fuchsia: \"bg-transparent text-fuchsia-600 hover:bg-fuchsia-50 dark:text-fuchsia-400 dark:hover:bg-fuchsia-950/50\",\n pink: \"bg-transparent text-pink-600 hover:bg-pink-50 dark:text-pink-400 dark:hover:bg-pink-950/50\",\n rose: \"bg-transparent text-rose-600 hover:bg-rose-50 dark:text-rose-400 dark:hover:bg-rose-950/50\",\n};\n","import { forwardRef, type AnchorHTMLAttributes, type ElementType, type ForwardedRef, type ReactNode } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { sanitizeHref } from \"../../utils/sanitize\";\nimport { resolve } from \"../../data/emoji-utils\";\nimport { Icon } from \"../Icon/Icon\";\nimport type { ButtonProps } from \"./Button.types\";\nimport type { ButtonColor, Size } from \"../../utils/types\";\nimport {\n buttonColorClasses as colorClasses,\n buttonGhostClasses as ghostColorClasses,\n} from \"./Button.colors\";\n\nconst iconSizeMap: Record<Size, \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"> = {\n xs: \"xs\",\n sm: \"xs\",\n md: \"sm\",\n lg: \"md\",\n xl: \"lg\",\n};\n\nconst alertIconSizeMap: Record<Size, \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"> = {\n xs: \"xs\",\n sm: \"xs\",\n md: \"xs\",\n lg: \"sm\",\n xl: \"sm\",\n};\n\n// ---------------------------------------------------------------------------\n// Color classes\n//\n// Solid + ghost color maps live in ./Button.colors (AUTO-GENERATED, all 22\n// Tailwind v4 hues as literal strings). The state-color classes below\n// (default/active/checked/warn) branch on flags, not specific colors.\n// ---------------------------------------------------------------------------\n\n// The default button's fill reads the theme tokens, like its text and border\n// already did, so a host palette reaches it. Each fallback is the colour it\n// used before: light is the surface (white), dark is the muted surface\n// (zinc-800), and the light hover is surface-hover / border-strong. The dark\n// hover stays zinc-700: no role defaults to it (surface-hover-dark is a\n// translucent zinc-800, which would all but erase the hover on a zinc-800\n// button), so tokenising it would change every host that sets nothing.\nconst defaultClasses =\n \"bg-[color:var(--fancy-surface,var(--color-white))] text-[color:var(--fancy-text-soft,var(--color-zinc-700))] border border-[color:var(--fancy-border,var(--color-zinc-200))] hover:bg-[color:var(--fancy-surface-hover,var(--color-zinc-50))] hover:border-[color:var(--fancy-border-strong,var(--color-zinc-300))] dark:bg-[color:var(--fancy-surface-muted-dark,var(--color-zinc-800))] dark:text-[color:var(--fancy-text-soft-dark,var(--color-zinc-300))] dark:border-[color:var(--fancy-border-dark,var(--color-zinc-700))] dark:hover:bg-zinc-700\";\n\nconst activeClasses =\n \"bg-blue-500 text-white border border-blue-600 hover:bg-blue-600 dark:bg-blue-600 dark:border-blue-500 dark:hover:bg-blue-500\";\n\nconst checkedClasses =\n \"bg-emerald-500 text-white border border-emerald-600 hover:bg-emerald-600 dark:bg-emerald-600 dark:border-emerald-500 dark:hover:bg-emerald-500\";\n\nconst warnClasses =\n \"bg-amber-50 text-amber-700 border border-amber-200 hover:bg-amber-100 dark:bg-amber-900/30 dark:text-amber-300 dark:border-amber-700 dark:hover:bg-amber-900/50\";\n\nconst ghostDefaultClasses =\n \"bg-transparent text-[color:var(--fancy-text-soft,var(--color-zinc-700))] hover:bg-[color:var(--fancy-surface-muted,var(--color-zinc-100))] dark:text-[color:var(--fancy-text-soft-dark,var(--color-zinc-300))] dark:hover:bg-[color:var(--fancy-surface-muted-dark,var(--color-zinc-800))]\";\n\nconst ghostActiveClasses =\n \"bg-blue-50 text-blue-700 hover:bg-blue-100 dark:bg-blue-950/50 dark:text-blue-300 dark:hover:bg-blue-950\";\n\nconst ghostCheckedClasses =\n \"bg-emerald-50 text-emerald-700 hover:bg-emerald-100 dark:bg-emerald-950/50 dark:text-emerald-300 dark:hover:bg-emerald-950\";\n\nconst ghostWarnClasses =\n \"bg-transparent text-amber-700 hover:bg-amber-50 dark:text-amber-300 dark:hover:bg-amber-950/50\";\n\n// ---------------------------------------------------------------------------\n// Size helpers\n// ---------------------------------------------------------------------------\n\nconst horizontalSize: Record<Size, string> = {\n xs: \"px-2 py-1 text-xs\",\n sm: \"px-2 py-1 text-xs\",\n md: \"px-3 py-1.5 text-sm\",\n lg: \"px-4 py-2.5 text-base\",\n xl: \"px-5 py-3 text-lg\",\n};\n\nconst circleSize: Record<Size, string> = {\n xs: \"w-7 h-7 text-xs\",\n sm: \"w-8 h-8 text-xs\",\n md: \"w-10 h-10 text-sm\",\n lg: \"w-12 h-12 text-base\",\n xl: \"w-14 h-14 text-lg\",\n};\n\nconst iconSize: Record<Size, string> = {\n xs: \"w-3 h-3\",\n sm: \"w-3 h-3\",\n md: \"w-4 h-4\",\n lg: \"w-5 h-5\",\n xl: \"w-6 h-6\",\n};\n\nconst avatarSize: Record<Size, string> = {\n xs: \"w-4 h-4\",\n sm: \"w-4 h-4\",\n md: \"w-5 h-5\",\n lg: \"w-6 h-6\",\n xl: \"w-7 h-7\",\n};\n\nconst alertIconSize: Record<Size, string> = {\n xs: \"w-2 h-2\",\n sm: \"w-2.5 h-2.5\",\n md: \"w-3 h-3\",\n lg: \"w-4 h-4\",\n xl: \"w-4 h-4\",\n};\n\nconst badgeSize: Record<Size, string> = {\n xs: \"text-[10px] px-1 min-w-[14px] h-3.5\",\n sm: \"text-[10px] px-1.5 min-w-[16px] h-4\",\n md: \"text-[11px] px-1.5 min-w-[18px] h-[18px]\",\n lg: \"text-xs px-2 min-w-[22px] h-5\",\n xl: \"text-xs px-2 min-w-[24px] h-5\",\n};\n\nconst externalIconOffset: Record<Size, string> = {\n xs: \"0.75rem\",\n sm: \"0.75rem\",\n md: \"1rem\",\n lg: \"1.25rem\",\n xl: \"1.5rem\",\n};\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction isColored(color?: ButtonColor, active?: boolean, checked?: boolean): boolean {\n return !!(color || active || checked);\n}\n\nfunction getColorClasses(\n color?: ButtonColor,\n active?: boolean,\n checked?: boolean,\n warn?: boolean,\n ghost?: boolean,\n): string {\n if (ghost) {\n if (color) return ghostColorClasses[color];\n if (checked) return ghostCheckedClasses;\n if (active) return ghostActiveClasses;\n if (warn) return ghostWarnClasses;\n return ghostDefaultClasses;\n }\n if (color) return colorClasses[color];\n if (checked) return checkedClasses;\n if (active) return activeClasses;\n if (warn) return warnClasses;\n return defaultClasses;\n}\n\nfunction getIconColorClasses(\n color?: ButtonColor,\n active?: boolean,\n checked?: boolean,\n warn?: boolean,\n ghost?: boolean,\n): string {\n if (ghost) {\n // Ghost variant: icon inherits currentColor from the button text\n return \"text-current\";\n }\n // A filled button's icon takes the label's colour — whatever that colour class\n // says, including a host's per-mode `text-brand-contrast` and the dark labels on\n // amber / yellow / lime. Hard-coded white was invisible on any light fill.\n if (color || active || checked) return \"text-current\";\n if (warn) return \"text-amber-600 dark:text-amber-400\";\n return \"text-[color:var(--fancy-text-muted,var(--color-zinc-500))] dark:text-[color:var(--fancy-text-muted-dark,var(--color-zinc-400))]\";\n}\n\nfunction getBadgeClasses(\n color?: ButtonColor,\n active?: boolean,\n checked?: boolean,\n warn?: boolean,\n ghost?: boolean,\n): string {\n if (ghost) {\n return \"bg-current/10 text-current\";\n }\n // Tinted with the label's own colour, for the same reason as the icon.\n if (isColored(color, active, checked)) return \"bg-current/20 text-current\";\n if (warn) return \"bg-amber-200 text-amber-800 dark:bg-amber-800 dark:text-amber-200\";\n return \"bg-zinc-200 text-zinc-700 dark:bg-zinc-600 dark:text-zinc-200\";\n}\n\n// ---------------------------------------------------------------------------\n// Placement parsing\n// ---------------------------------------------------------------------------\n\ntype PlacementAxis = \"left\" | \"right\" | \"top\" | \"bottom\";\n\nfunction parsePlacement(iconPlace: string): { primary: PlacementAxis; secondary?: PlacementAxis } {\n const parts = iconPlace.split(\" \") as PlacementAxis[];\n return { primary: parts[0], secondary: parts[1] };\n}\n\nfunction isVerticalPrimary(primary: PlacementAxis): boolean {\n return primary === \"top\" || primary === \"bottom\";\n}\n\n// ---------------------------------------------------------------------------\n// Sort parsing\n// ---------------------------------------------------------------------------\n\ntype ElementKey = \"e\" | \"i\" | \"a\" | \"b\";\nconst DEFAULT_SORT = \"eiab\";\n\nfunction parseSortOrder(sort?: string): ElementKey[] {\n const chars = (sort ?? DEFAULT_SORT).split(\"\") as ElementKey[];\n const valid: ElementKey[] = [];\n const seen = new Set<string>();\n for (const c of chars) {\n if (\"eiab\".includes(c) && !seen.has(c)) {\n valid.push(c);\n seen.add(c);\n }\n }\n // Add missing keys in default order\n for (const c of DEFAULT_SORT.split(\"\") as ElementKey[]) {\n if (!seen.has(c)) valid.push(c);\n }\n return valid;\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n children,\n className,\n variant = \"default\",\n color,\n size = \"md\",\n active,\n checked,\n warn,\n alert: alertProp,\n icon,\n iconTrailing,\n iconPlace = \"left\",\n alertIcon,\n alertIconTrailing,\n emoji,\n emojiTrailing,\n avatar,\n avatarTrailing,\n badge,\n badgeTrailing,\n sort,\n loading = false,\n disabled,\n href,\n as: As,\n responsive,\n labelClassName,\n ...props\n },\n ref,\n ) => {\n const isCircle = variant === \"circle\";\n const isGhost = variant === \"ghost\";\n const { primary, secondary } = parsePlacement(iconPlace);\n const isVertical = isVerticalPrimary(primary);\n const isCompound = !!secondary;\n const needsWrapper = isVertical || isCompound;\n const isReversed = iconPlace === \"right\";\n\n // -----------------------------------------------------------------------\n // Sizing — always use horizontalSize for the button itself\n // -----------------------------------------------------------------------\n const sizeClass = isCircle ? circleSize[size] : horizontalSize[size];\n\n // -----------------------------------------------------------------------\n // Build decorative elements\n // -----------------------------------------------------------------------\n const iconColorCls = getIconColorClasses(color, active, checked, warn, isGhost);\n\n const buildElement = (key: ElementKey, trailing: boolean): ReactNode => {\n switch (key) {\n case \"e\": {\n const slug = trailing ? emojiTrailing : emoji;\n if (!slug) return null;\n const char = resolve(slug);\n if (!char) return null;\n return (\n <span key={`emoji-${trailing ? \"t\" : \"l\"}`} data-action-emoji>\n {char}\n </span>\n );\n }\n case \"i\": {\n if (loading && !trailing) {\n return (\n <span\n key=\"spinner\"\n className={cn(\"animate-spin border-2 border-current border-t-transparent rounded-full\", iconSize[size])}\n />\n );\n }\n const iconSlug = trailing ? iconTrailing : icon;\n if (!iconSlug) return null;\n // For vertical/compound placements, icons render outside the button — not inside\n if (needsWrapper) return null;\n return (\n <span\n key={`icon-${trailing ? \"t\" : \"l\"}`}\n data-action-icon={trailing ? \"trailing\" : \"leading\"}\n className={cn(\"inline-flex items-center flex-shrink-0\", iconColorCls)}\n >\n <Icon name={iconSlug} size={iconSizeMap[size]} />\n </span>\n );\n }\n case \"a\": {\n if (!avatar) return null;\n const isTrailing = avatarTrailing ?? false;\n if (isTrailing !== trailing) return null;\n return (\n <img\n key=\"avatar\"\n src={avatar}\n alt=\"\"\n className={cn(avatarSize[size], \"rounded-full object-cover flex-shrink-0\")}\n data-action-avatar\n />\n );\n }\n case \"b\": {\n if (!badge) return null;\n const isTrailing = badgeTrailing ?? false;\n if (isTrailing !== trailing) return null;\n return (\n <span\n key=\"badge\"\n className={cn(\n \"inline-flex items-center justify-center font-medium rounded-full\",\n badgeSize[size],\n getBadgeClasses(color, active, checked, warn, isGhost),\n )}\n data-action-badge\n >\n {badge}\n </span>\n );\n }\n default:\n return null;\n }\n };\n\n // Alert icon element\n const renderAlertIcon = (trailing: boolean): ReactNode => {\n if (!alertIcon) return null;\n const isTrailing = alertIconTrailing ?? false;\n if (isTrailing !== trailing) return null;\n const alertIconEl = <Icon name={alertIcon} size={alertIconSizeMap[size]} />;\n return (\n <span\n key=\"alert-icon\"\n className=\"relative inline-flex flex-shrink-0\"\n data-action-alert\n >\n <span className=\"text-red-500 dark:text-red-400 animate-pulse\">\n {alertIconEl}\n </span>\n <span className=\"absolute inset-0 flex items-center justify-center text-red-400 dark:text-red-300 animate-ping opacity-75\">\n {alertIconEl}\n </span>\n </span>\n );\n };\n\n const sortOrder = parseSortOrder(sort);\n\n const leadingElements: ReactNode[] = [];\n const trailingElements: ReactNode[] = [];\n\n for (const key of sortOrder) {\n const leading = buildElement(key, false);\n if (leading) leadingElements.push(leading);\n const trailing = buildElement(key, true);\n if (trailing) trailingElements.push(trailing);\n }\n\n // Insert alert icon\n const leadingAlert = renderAlertIcon(false);\n if (leadingAlert) leadingElements.push(leadingAlert);\n const trailingAlert = renderAlertIcon(true);\n if (trailingAlert) trailingElements.push(trailingAlert);\n\n // -----------------------------------------------------------------------\n // Classes\n // -----------------------------------------------------------------------\n const classes = cn(\n // `text-left` overrides the UA <button> center default so a wrapped,\n // width-constrained multi-word label stacks left-aligned. Overridable —\n // twMerge lets a `text-center` / `text-right` in `className` win.\n \"inline-flex items-center justify-center text-left font-medium transition-all duration-200\",\n \"focus:outline-none focus:ring-2 focus:ring-offset-1\",\n \"disabled:opacity-50 disabled:cursor-not-allowed\",\n isCircle ? \"rounded-full\" : \"rounded-lg\",\n sizeClass,\n getColorClasses(color, active, checked, warn, isGhost),\n !isCircle && !needsWrapper && \"gap-2\",\n !isCircle && needsWrapper && \"gap-2\",\n isReversed && !needsWrapper && \"flex-row-reverse\",\n alertProp && \"animate-pulse\",\n className,\n );\n\n // -----------------------------------------------------------------------\n // Content\n // -----------------------------------------------------------------------\n const content = isCircle ? (\n <>\n {loading ? (\n <span\n className={cn(\n \"animate-spin border-2 border-current border-t-transparent rounded-full\",\n iconSize[size],\n )}\n />\n ) : (\n icon ? <Icon name={icon} size={iconSizeMap[size]} /> : children\n )}\n </>\n ) : (\n <>\n {leadingElements}\n {children != null && (\n <span\n data-react-fancy-button-label=\"\"\n className={cn(\n // A button narrower than its label WRAPS the label inside itself\n // (left-aligned, via the button's `text-left`); the icon is its own\n // shrink-0 flex item, so it stays put beside the text. `min-w-0`\n // lets the span shrink below its text. Words are never split\n // (measured: in the showcase's 104px column `break-words` turned\n // \"customer\" into \"custome/r\"). Unconstrained, nothing wraps and\n // the button looks as it always did.\n //\n // No white-space class here, on purpose: `labelClassName=\"truncate\"`\n // must win uncontested to give one line with an ellipsis, and a\n // `whitespace-normal` would fight its nowrap in the cascade.\n // (5.54.0 and earlier forced `whitespace-nowrap`, so a constrained\n // label spilled out of the button and over its neighbours.)\n \"min-w-0\",\n // `responsive` collapses to icon-only when squeezed, while keeping\n // the label readable to screen readers.\n responsive && \"sr-only sm:not-sr-only\",\n labelClassName,\n )}\n >\n {children}\n </span>\n )}\n {trailingElements}\n </>\n );\n\n // -----------------------------------------------------------------------\n // Build the button element\n // -----------------------------------------------------------------------\n const safeHref = sanitizeHref(href);\n // Anchor mode renders `as` when provided (e.g. a router's <Link> for\n // client-side navigation — Inertia, React Router, Next). Previously `as`\n // fell through ...props onto the <a> where React silently DROPS a\n // function-valued unknown attribute — so `<Button as={Link}>` looked fine\n // but did a full page load. `as` only applies in anchor mode; without an\n // href (or when disabled) the plain <button> renders as always.\n const Anchor = (As ?? \"a\") as ElementType;\n const buttonEl = safeHref && !disabled ? (\n // Anchor mode forwards the same rest props as the <button> branch so\n // onClick / target / rel / ref / ARIA / data-* reach the <a> (issue #7).\n <Anchor\n ref={ref as ForwardedRef<HTMLAnchorElement>}\n href={safeHref}\n className={classes}\n data-react-fancy-button=\"\"\n data-react-fancy-action=\"\"\n {...(props as AnchorHTMLAttributes<HTMLAnchorElement>)}\n >\n {content}\n </Anchor>\n ) : (\n <button\n ref={ref}\n className={classes}\n disabled={disabled || loading}\n data-react-fancy-button=\"\"\n data-react-fancy-action=\"\"\n {...props}\n >\n {content}\n </button>\n );\n\n // -----------------------------------------------------------------------\n // Wrap for vertical/compound icon placement\n // The icon is absolutely positioned so it never shifts the button.\n // -----------------------------------------------------------------------\n if (needsWrapper && (icon || iconTrailing)) {\n const iconSlug = icon ?? iconTrailing;\n const offset = externalIconOffset[size];\n\n // Determine position styles for the icon\n const posStyle: React.CSSProperties = { position: \"absolute\" };\n\n // Primary axis: which side of the button\n if (primary === \"top\") {\n posStyle.bottom = \"100%\";\n posStyle.marginBottom = \"0.125rem\";\n } else if (primary === \"bottom\") {\n posStyle.top = \"100%\";\n posStyle.marginTop = \"0.125rem\";\n } else if (primary === \"left\") {\n posStyle.right = \"100%\";\n posStyle.marginRight = \"0.125rem\";\n } else if (primary === \"right\") {\n posStyle.left = \"100%\";\n posStyle.marginLeft = \"0.125rem\";\n }\n\n // Secondary axis: cross-axis alignment\n if (isVerticalPrimary(primary)) {\n // Vertical primary → secondary controls horizontal position\n if (secondary === \"left\") {\n posStyle.left = \"0\";\n } else if (secondary === \"right\") {\n posStyle.right = \"0\";\n } else {\n // center (default)\n posStyle.left = \"50%\";\n posStyle.transform = \"translateX(-50%)\";\n }\n } else {\n // Horizontal primary → secondary controls vertical position\n if (secondary === \"top\") {\n posStyle.top = \"0\";\n } else if (secondary === \"bottom\") {\n posStyle.bottom = \"0\";\n } else {\n // center (default)\n posStyle.top = \"50%\";\n posStyle.transform = \"translateY(-50%)\";\n }\n }\n\n const externalIcon = (\n <span\n className={cn(\"absolute flex-shrink-0\", iconColorCls)}\n style={posStyle}\n >\n <Icon name={iconSlug!} size={iconSizeMap[size]} />\n </span>\n );\n\n return (\n <span\n data-react-fancy-action-group=\"\"\n className=\"relative inline-flex\"\n >\n {buttonEl}\n {externalIcon}\n </span>\n );\n }\n\n return buttonEl;\n },\n);\n\nButton.displayName = \"Button\";\n\n/**\n * @deprecated Renamed to {@link Button}. `Action` remains as an alias for\n * backward compatibility and will be removed in a future major version.\n * Prefer importing `Button` from \"@particle-academy/react-fancy\".\n */\nexport const Action = Button;\n"]}