@bettercms-ai/ui 0.6.1 → 0.7.1

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/README.md CHANGED
File without changes
@@ -2,10 +2,25 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { HeadingProps, TextProps, PortableTextDocument, ImageProps, ButtonProps, SpacerProps, ColumnsProps, ContentBlock, VideoProps } from '@bettercms-ai/types';
3
3
  import React from 'react';
4
4
 
5
+ /**
6
+ * The kinds an ELEMENT may declare, pinned to the canonical union rather than spelled again.
7
+ *
8
+ * `BindingKind` covers both lanes; `url`, `number` and `array` belong to the ATTRIBUTE lane
9
+ * (`data-bcms-props`), where a kind travels beside the DOM attribute it writes. An element's own
10
+ * `data-bcms-kind` names how its CONTENT is edited, and there are three answers.
11
+ *
12
+ * Declared as a VALUE with `satisfies`, not as `Extract<BindingKind, …>`: `Extract` of a member
13
+ * that no longer exists is silently the empty set, so a rename over there would narrow this type
14
+ * instead of failing. `satisfies readonly BindingKind[]` fails on this line, which is the point —
15
+ * hand-listing is how this file came to disagree with `block-bindings.ts` about how many kinds
16
+ * exist in the first place.
17
+ */
18
+ declare const ELEMENT_BINDING_KINDS: readonly ["text", "richtext", "image"];
19
+ type ElementBindingKind = (typeof ELEMENT_BINDING_KINDS)[number];
5
20
  /** Preview-only attributes understood by the BetterCMS visual editor. */
6
21
  type BcmsBindingProps = {
7
22
  "data-bcms-field"?: string;
8
- "data-bcms-kind"?: "text" | "richtext" | "image";
23
+ "data-bcms-kind"?: ElementBindingKind;
9
24
  };
10
25
 
11
26
  interface HeadingBlockProps extends HeadingProps, BcmsBindingProps {
package/dist/blocks.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { a as Button, C as Columns, H as Heading, I as Image, S as Spacer, T as Text, V as Video } from './Video-CZkWQ3Hm.js';
1
+ export { a as Button, C as Columns, H as Heading, I as Image, S as Spacer, T as Text, V as Video } from './Video-Bl_XrTrM.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { SectionProps, ContentBlock, NavbarProps, FooterProps, SliderProps, TabsProps, ComponentProps, FormProps } from '@bettercms-ai/types';
4
4
  import React from 'react';
package/dist/blocks.js CHANGED
@@ -47,7 +47,13 @@ var PT_TYPES = {
47
47
  */
48
48
  table: "table",
49
49
  /** The annotation carrying a hyperlink's `href`. Spelled `link` because that is the spec's convention. */
50
- link: "link"
50
+ link: "link",
51
+ /**
52
+ * The annotation carrying a site's OWN inline markup — `<span class>`, `<small>`, `<time
53
+ * datetime>`. Unprefixed, like `table`, because it describes HTML rather than a BetterCMS
54
+ * concept. See `styled.ts` for what it may hold and why the list is that short.
55
+ */
56
+ styled: "styled"
51
57
  };
52
58
  var BUILTIN_TABLE_KEY = "builtin:table";
53
59
  var STORABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
@@ -59,10 +65,71 @@ var STORABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
59
65
  PT_TYPES.mention
60
66
  ]);
61
67
 
68
+ // ../richtext/src/styled.ts
69
+ var STYLED_TAGS = ["span", "small", "sup", "sub", "kbd", "abbr", "time"];
70
+ var TAG_SET = new Set(STYLED_TAGS);
71
+ var isStyledTag = (tag) => typeof tag === "string" && TAG_SET.has(tag);
72
+ var ATTR_NAMES = /* @__PURE__ */ new Set(["title", "datetime", "style", "aria-label"]);
73
+ var DATA_ATTR = /^data-[a-z][a-z0-9-]*$/;
74
+ var isStyledAttr = (name) => ATTR_NAMES.has(name) || DATA_ATTR.test(name) && !name.startsWith("data-bcms-");
75
+ var STYLE_PROPS = /* @__PURE__ */ new Set([
76
+ "color",
77
+ "background-color",
78
+ "opacity",
79
+ "font",
80
+ "font-family",
81
+ "font-size",
82
+ "font-style",
83
+ "font-weight",
84
+ "font-variant",
85
+ "font-feature-settings",
86
+ "font-variation-settings",
87
+ "letter-spacing",
88
+ "line-height",
89
+ "word-spacing",
90
+ "white-space",
91
+ "word-break",
92
+ "text-align",
93
+ "text-decoration",
94
+ "text-decoration-color",
95
+ "text-decoration-line",
96
+ "text-decoration-style",
97
+ "text-transform",
98
+ "text-underline-offset",
99
+ "vertical-align"
100
+ ]);
101
+ var STYLE_VALUE_BAN = /url\(|expression\(|@import|\\|\/\*|[<>]/i;
102
+ function safeStyle(css) {
103
+ const kept = [];
104
+ for (const declaration of css.split(";")) {
105
+ const at = declaration.indexOf(":");
106
+ if (at < 0) continue;
107
+ const name = declaration.slice(0, at).trim().toLowerCase();
108
+ const value = declaration.slice(at + 1).trim();
109
+ if (!name || !value) continue;
110
+ if (!name.startsWith("--") && !STYLE_PROPS.has(name)) continue;
111
+ if (STYLE_VALUE_BAN.test(value)) continue;
112
+ kept.push(`${name}:${value}`);
113
+ }
114
+ return kept.length ? kept.join(";") : void 0;
115
+ }
116
+ function styledAttrEntries(attrs) {
117
+ if (!attrs || typeof attrs !== "object" || Array.isArray(attrs)) return [];
118
+ const out = [];
119
+ for (const [name, raw] of Object.entries(attrs)) {
120
+ if (!isStyledAttr(name) || typeof raw !== "string") continue;
121
+ const value = name === "style" ? safeStyle(raw) : raw;
122
+ if (value === void 0) continue;
123
+ out.push([name, value]);
124
+ }
125
+ return out;
126
+ }
127
+
62
128
  // ../richtext/src/href.ts
63
129
  var SAFE_HREF_SCHEMES = ["http:", "https:", "mailto:", "tel:"];
64
130
  function safeHref(raw) {
65
131
  const trimmed = raw.trim();
132
+ if (trimmed.startsWith("#")) return /^#[^\s<>"'`]*$/.test(trimmed) ? trimmed : null;
66
133
  if (trimmed.startsWith("/") && !trimmed.startsWith("//")) return trimmed;
67
134
  try {
68
135
  return SAFE_HREF_SCHEMES.includes(new URL(trimmed).protocol) ? trimmed : null;
@@ -233,6 +300,18 @@ function readStructuredContent(props) {
233
300
  // src/blocks/portableText.tsx
234
301
  import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
235
302
  var TOKEN = /^[a-z0-9-]+$/i;
303
+ function styleObject(css) {
304
+ const out = {};
305
+ for (const declaration of css.split(";")) {
306
+ const at = declaration.indexOf(":");
307
+ if (at < 0) continue;
308
+ const name = declaration.slice(0, at).trim();
309
+ const value = declaration.slice(at + 1).trim();
310
+ if (!name || !value) continue;
311
+ out[name.startsWith("--") ? name : name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = value;
312
+ }
313
+ return out;
314
+ }
236
315
  function Bound({
237
316
  base,
238
317
  nodeKey,
@@ -303,6 +382,36 @@ function buildComponents() {
303
382
  const blank = v.target === "_blank";
304
383
  return /* @__PURE__ */ jsx2("a", { href, ...blank ? { target: "_blank", rel: "noopener noreferrer" } : {}, children });
305
384
  },
385
+ /**
386
+ * The site's own inline markup, rendered as itself — the React half of
387
+ * `portableTextToHtml`'s `styled` mark, and it has to stay the same shape: Astro and the
388
+ * editor canvas render through that serialiser while a Next site renders through this
389
+ * one, and two renderers of one value that disagree is the whole bug class.
390
+ *
391
+ * The tag is re-checked against `STYLED_TAGS` and the attribute bag re-filtered, because
392
+ * a markDef is DATA and arrives from imports, the Management API and AI writes. React
393
+ * escapes the values it writes; the allowlist is what keeps a `href` or an `onClick` out
394
+ * of the props entirely. An unrecognised tag renders its CHILDREN — the words are the
395
+ * author's, the wrapper is only the design.
396
+ */
397
+ [PT_TYPES.styled]: ({ value, children }) => {
398
+ const v = value ?? {};
399
+ const tag = typeof v.tag === "string" ? v.tag.toLowerCase() : "";
400
+ if (!isStyledTag(tag)) return /* @__PURE__ */ jsx2(Fragment, { children });
401
+ const Tag = tag;
402
+ const className = typeof v.className === "string" && v.className ? v.className : void 0;
403
+ const attrs = styledAttrEntries(v.attrs);
404
+ const style = attrs.find(([name]) => name === "style")?.[1];
405
+ return /* @__PURE__ */ jsx2(
406
+ Tag,
407
+ {
408
+ className,
409
+ ...Object.fromEntries(attrs.filter(([name]) => name !== "style")),
410
+ ...style ? { style: styleObject(style) } : {},
411
+ children
412
+ }
413
+ );
414
+ },
306
415
  highlight: ({ children }) => /* @__PURE__ */ jsx2("mark", { children }),
307
416
  "strike-through": ({ children }) => /* @__PURE__ */ jsx2("s", { children }),
308
417
  /**