@styleflow.app/flowpress-astro-core 0.2.0-beta.0 → 0.2.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,233 @@
1
+ ---
2
+ import S from "@styleflow.app/astro/S.astro";
3
+ import type { FlowpressBlock } from "../../types.js";
4
+ import type { StyleflowContract } from "../../styleflow.js";
5
+ import { blockHtmlAttributes, semanticProps } from "./helpers.js";
6
+
7
+ interface FormField {
8
+ name: string;
9
+ type: string;
10
+ required?: boolean;
11
+ values?: string[];
12
+ labels?: string[];
13
+ options?: string[];
14
+ }
15
+
16
+ interface FormSchema {
17
+ id: number;
18
+ title: string;
19
+ fields: FormField[];
20
+ submitUrl: string;
21
+ turnstileSiteKey: string;
22
+ available: boolean;
23
+ }
24
+
25
+ interface Props {
26
+ block: FlowpressBlock;
27
+ contract?: StyleflowContract;
28
+ }
29
+
30
+ const { block, contract } = Astro.props;
31
+ const schema = block.attributes.formSchema as FormSchema | undefined;
32
+ const semantic = semanticProps(block.attributes, contract);
33
+ const htmlAttributes = blockHtmlAttributes(block.attributes);
34
+ const labelFor = (field: FormField): string => field.name
35
+ .replace(/[-_]+/g, " ")
36
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
37
+ const optionLabel = (field: FormField, index: number): string =>
38
+ field.labels?.[index] || field.values?.[index] || "";
39
+ ---
40
+
41
+ {schema?.available ? (
42
+ <S as="div" {...semantic} {...htmlAttributes}>
43
+ <flowpress-cf7-form>
44
+ <form action={schema.submitUrl} method="post" enctype="multipart/form-data" novalidate>
45
+ {block.attributes.title || schema.title ? (
46
+ <S as="h2">{String(block.attributes.title || schema.title)}</S>
47
+ ) : null}
48
+ {schema.fields.map((field) => {
49
+ if (field.type === "submit") {
50
+ return (
51
+ <button type="submit">
52
+ {field.values?.[0] || "Invia"}
53
+ </button>
54
+ );
55
+ }
56
+ if (field.type === "hidden") {
57
+ return <input type="hidden" name={field.name} value={field.values?.[0] || ""} />;
58
+ }
59
+ if (["checkbox", "radio"].includes(field.type)) {
60
+ return (
61
+ <fieldset>
62
+ <legend>{labelFor(field)}</legend>
63
+ {(field.values || []).map((value, index) => (
64
+ <label>
65
+ <input
66
+ type={field.type}
67
+ name={field.type === "checkbox" ? `${field.name}[]` : field.name}
68
+ value={value}
69
+ required={field.required}
70
+ />
71
+ {optionLabel(field, index)}
72
+ </label>
73
+ ))}
74
+ <span data-field-error={field.name}></span>
75
+ </fieldset>
76
+ );
77
+ }
78
+ if (field.type === "acceptance") {
79
+ return (
80
+ <label>
81
+ <input type="checkbox" name={field.name} value="1" required={field.required} />
82
+ {field.values?.[0] || labelFor(field)}
83
+ <span data-field-error={field.name}></span>
84
+ </label>
85
+ );
86
+ }
87
+ if (field.type === "select") {
88
+ return (
89
+ <label>
90
+ {labelFor(field)}
91
+ <select
92
+ name={field.options?.includes("multiple") ? `${field.name}[]` : field.name}
93
+ required={field.required}
94
+ multiple={field.options?.includes("multiple")}
95
+ >
96
+ {(field.values || []).map((value, index) => (
97
+ <option value={value}>{optionLabel(field, index)}</option>
98
+ ))}
99
+ </select>
100
+ <span data-field-error={field.name}></span>
101
+ </label>
102
+ );
103
+ }
104
+ if (field.type === "textarea") {
105
+ return (
106
+ <label>
107
+ {labelFor(field)}
108
+ <textarea name={field.name} required={field.required}></textarea>
109
+ <span data-field-error={field.name}></span>
110
+ </label>
111
+ );
112
+ }
113
+ return (
114
+ <label>
115
+ {labelFor(field)}
116
+ <input
117
+ type={field.type === "file" ? "file" : field.type}
118
+ name={field.name}
119
+ required={field.required}
120
+ />
121
+ <span data-field-error={field.name}></span>
122
+ </label>
123
+ );
124
+ })}
125
+ <div
126
+ data-turnstile
127
+ data-sitekey={schema.turnstileSiteKey}
128
+ aria-label="Verifica anti-spam"
129
+ ></div>
130
+ <p data-form-status role="status" aria-live="polite"></p>
131
+ </form>
132
+ </flowpress-cf7-form>
133
+ </S>
134
+ ) : (
135
+ <S as="p" {...semantic} {...htmlAttributes} role="status">
136
+ Il modulo non è temporaneamente disponibile.
137
+ </S>
138
+ )}
139
+
140
+ <script>
141
+ declare global {
142
+ interface Window {
143
+ turnstile?: {
144
+ render(target: Element, options: Record<string, unknown>): string;
145
+ reset(widgetId?: string): void;
146
+ };
147
+ }
148
+ }
149
+
150
+ let turnstilePromise: Promise<void> | undefined;
151
+ const loadTurnstile = (): Promise<void> => {
152
+ if (window.turnstile) return Promise.resolve();
153
+ if (turnstilePromise) return turnstilePromise;
154
+ turnstilePromise = new Promise((resolve, reject) => {
155
+ const existing = document.querySelector<HTMLScriptElement>(
156
+ 'script[data-flowpress-turnstile]'
157
+ );
158
+ if (existing) {
159
+ existing.addEventListener("load", () => resolve(), { once: true });
160
+ existing.addEventListener("error", () => reject(new Error("Turnstile unavailable")), { once: true });
161
+ return;
162
+ }
163
+ const script = document.createElement("script");
164
+ script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
165
+ script.async = true;
166
+ script.defer = true;
167
+ script.dataset.flowpressTurnstile = "true";
168
+ script.addEventListener("load", () => resolve(), { once: true });
169
+ script.addEventListener("error", () => reject(new Error("Turnstile unavailable")), { once: true });
170
+ document.head.append(script);
171
+ });
172
+ return turnstilePromise;
173
+ };
174
+
175
+ if (!customElements.get("flowpress-cf7-form")) {
176
+ customElements.define("flowpress-cf7-form", class extends HTMLElement {
177
+ private widgetId?: string;
178
+
179
+ connectedCallback() {
180
+ const form = this.querySelector<HTMLFormElement>("form");
181
+ const target = this.querySelector<HTMLElement>("[data-turnstile]");
182
+ const status = this.querySelector<HTMLElement>("[data-form-status]");
183
+ if (!form || !target || !status) return;
184
+
185
+ loadTurnstile()
186
+ .then(() => {
187
+ if (!window.turnstile || this.widgetId) return;
188
+ this.widgetId = window.turnstile.render(target, {
189
+ sitekey: target.dataset.sitekey
190
+ });
191
+ })
192
+ .catch(() => {
193
+ status.textContent = "La verifica anti-spam non è disponibile.";
194
+ });
195
+
196
+ form.addEventListener("submit", async (event) => {
197
+ event.preventDefault();
198
+ status.textContent = "Invio in corso…";
199
+ form.querySelectorAll<HTMLElement>("[data-field-error]")
200
+ .forEach((node) => { node.textContent = ""; });
201
+ const submit = form.querySelector<HTMLButtonElement>('[type="submit"]');
202
+ if (submit) submit.disabled = true;
203
+ try {
204
+ const response = await fetch(form.action, {
205
+ method: "POST",
206
+ body: new FormData(form),
207
+ headers: { Accept: "application/json" }
208
+ });
209
+ const result = await response.json() as {
210
+ message?: string;
211
+ invalidFields?: Array<{ field?: string; into?: string; message?: string }>;
212
+ };
213
+ status.textContent = result.message || (response.ok ? "Messaggio inviato." : "Invio non riuscito.");
214
+ for (const invalid of result.invalidFields || []) {
215
+ const name = invalid.field || invalid.into?.replace(/^span\./, "") || "";
216
+ const node = name
217
+ ? form.querySelector<HTMLElement>(`[data-field-error="${CSS.escape(name)}"]`)
218
+ : null;
219
+ if (node) node.textContent = invalid.message || "";
220
+ }
221
+ if (response.ok) form.reset();
222
+ if (window.turnstile) window.turnstile.reset(this.widgetId);
223
+ } catch {
224
+ status.textContent = "Impossibile inviare il modulo. Riprova.";
225
+ if (window.turnstile) window.turnstile.reset(this.widgetId);
226
+ } finally {
227
+ if (submit) submit.disabled = false;
228
+ }
229
+ });
230
+ }
231
+ });
232
+ }
233
+ </script>
@@ -0,0 +1,70 @@
1
+ ---
2
+ import S from "@styleflow.app/astro/S.astro";
3
+ import type { FlowpressBlock } from "../../types.js";
4
+ import type { StyleflowContract } from "../../styleflow.js";
5
+ import {
6
+ blockHtmlAttributes,
7
+ responsiveValues,
8
+ semanticProps
9
+ } from "./helpers.js";
10
+
11
+ interface Props {
12
+ block: FlowpressBlock;
13
+ contract?: StyleflowContract;
14
+ }
15
+
16
+ const { block, contract } = Astro.props;
17
+ const values = responsiveValues(
18
+ block.attributes,
19
+ "columns",
20
+ Number(block.attributes.columns) || 12
21
+ );
22
+ const align = ["start", "center", "end", "stretch"].includes(
23
+ String(block.attributes.align)
24
+ ) ? String(block.attributes.align) : "stretch";
25
+ const semantic = semanticProps(block.attributes, contract, {
26
+ surface: "default",
27
+ layoutRole: "container",
28
+ density: "md"
29
+ });
30
+ const htmlAttributes = blockHtmlAttributes(block.attributes, "flowpress-grid");
31
+ const style = [
32
+ `--flowpress-grid-base:${values.base}`,
33
+ `--flowpress-grid-sm:${values.sm}`,
34
+ `--flowpress-grid-md:${values.md}`,
35
+ `--flowpress-grid-lg:${values.lg}`,
36
+ `--flowpress-grid-xl:${values.xl}`,
37
+ `--flowpress-grid-2xl:${values["2xl"]}`
38
+ ].join(";");
39
+ ---
40
+
41
+ <S as="div" {...semantic} {...htmlAttributes} data-align={align} style={style}>
42
+ <slot />
43
+ </S>
44
+
45
+ <style>
46
+ .flowpress-grid {
47
+ display: grid;
48
+ grid-template-columns: repeat(var(--flowpress-grid-base), minmax(0, 1fr));
49
+ align-items: var(--flowpress-grid-align, stretch);
50
+ }
51
+ .flowpress-grid[data-align="start"] { --flowpress-grid-align: start; }
52
+ .flowpress-grid[data-align="center"] { --flowpress-grid-align: center; }
53
+ .flowpress-grid[data-align="end"] { --flowpress-grid-align: end; }
54
+ .flowpress-grid[data-align="stretch"] { --flowpress-grid-align: stretch; }
55
+ @media (min-width: 40rem) {
56
+ .flowpress-grid { grid-template-columns: repeat(var(--flowpress-grid-sm), minmax(0, 1fr)); }
57
+ }
58
+ @media (min-width: 48rem) {
59
+ .flowpress-grid { grid-template-columns: repeat(var(--flowpress-grid-md), minmax(0, 1fr)); }
60
+ }
61
+ @media (min-width: 64rem) {
62
+ .flowpress-grid { grid-template-columns: repeat(var(--flowpress-grid-lg), minmax(0, 1fr)); }
63
+ }
64
+ @media (min-width: 80rem) {
65
+ .flowpress-grid { grid-template-columns: repeat(var(--flowpress-grid-xl), minmax(0, 1fr)); }
66
+ }
67
+ @media (min-width: 96rem) {
68
+ .flowpress-grid { grid-template-columns: repeat(var(--flowpress-grid-2xl), minmax(0, 1fr)); }
69
+ }
70
+ </style>
@@ -0,0 +1,113 @@
1
+ ---
2
+ import S from "@styleflow.app/astro/S.astro";
3
+ import type { FlowpressBlock } from "../../types.js";
4
+ import type { StyleflowContract } from "../../styleflow.js";
5
+ import {
6
+ blockHtmlAttributes,
7
+ objectValue,
8
+ semanticProps
9
+ } from "./helpers.js";
10
+
11
+ interface Props {
12
+ block: FlowpressBlock;
13
+ contract?: StyleflowContract;
14
+ }
15
+
16
+ const { block, contract } = Astro.props;
17
+ const attributes = block.attributes;
18
+ const semantic = semanticProps(attributes, contract, {
19
+ surface: "default",
20
+ layoutRole: "region",
21
+ density: "md"
22
+ });
23
+ const htmlAttributes = blockHtmlAttributes(attributes, "flowpress-hero");
24
+ const backgroundType = ["color", "image", "video"].includes(
25
+ String(attributes.backgroundType)
26
+ ) ? String(attributes.backgroundType) : "color";
27
+ const image = objectValue(block, "backgroundImage");
28
+ const video = objectValue(block, "backgroundVideo");
29
+ const imageSrc = String(image.url ?? image.sourceUrl ?? "");
30
+ const videoSrc = String(video.url ?? video.sourceUrl ?? video.mediaItemUrl ?? "");
31
+ const scrimEnabled = Boolean(attributes.scrimEnabled) && backgroundType !== "color";
32
+ const scrimIntensity = Math.min(10, Math.max(1, Number(attributes.scrimIntensity) || 6));
33
+ ---
34
+
35
+ <S
36
+ as="section"
37
+ {...semantic}
38
+ {...htmlAttributes}
39
+ style={`--flowpress-scrim-opacity:${scrimIntensity * 10}%`}
40
+ >
41
+ {backgroundType === "image" && imageSrc ? (
42
+ <img
43
+ class="flowpress-hero__media"
44
+ src={imageSrc}
45
+ alt=""
46
+ aria-hidden="true"
47
+ loading="eager"
48
+ decoding="async"
49
+ />
50
+ ) : null}
51
+ {backgroundType === "video" && videoSrc ? (
52
+ <video
53
+ class="flowpress-hero__media"
54
+ src={videoSrc}
55
+ poster={imageSrc || undefined}
56
+ autoplay
57
+ muted
58
+ loop
59
+ playsinline
60
+ preload="metadata"
61
+ aria-hidden="true"
62
+ tabindex="-1"
63
+ />
64
+ ) : null}
65
+ {scrimEnabled ? <span class="flowpress-hero__scrim" aria-hidden="true"></span> : null}
66
+ <div class="flowpress-hero__content">
67
+ <slot />
68
+ </div>
69
+ </S>
70
+
71
+ <style>
72
+ .flowpress-hero {
73
+ position: relative;
74
+ display: grid;
75
+ overflow: hidden;
76
+ isolation: isolate;
77
+ }
78
+
79
+ .flowpress-hero__media,
80
+ .flowpress-hero__scrim {
81
+ position: absolute;
82
+ inset: 0;
83
+ width: 100%;
84
+ height: 100%;
85
+ }
86
+
87
+ .flowpress-hero__media {
88
+ z-index: -2;
89
+ object-fit: cover;
90
+ }
91
+
92
+ .flowpress-hero__scrim {
93
+ z-index: -1;
94
+ background: color-mix(
95
+ in srgb,
96
+ var(--sf-background) var(--flowpress-scrim-opacity),
97
+ transparent
98
+ );
99
+ pointer-events: none;
100
+ }
101
+
102
+ .flowpress-hero__content {
103
+ position: relative;
104
+ display: grid;
105
+ gap: inherit;
106
+ }
107
+
108
+ @media (prefers-reduced-motion: reduce) {
109
+ .flowpress-hero__media:is(video) {
110
+ display: none;
111
+ }
112
+ }
113
+ </style>
@@ -0,0 +1,28 @@
1
+ ---
2
+ import S from "@styleflow.app/astro/S.astro";
3
+
4
+ interface Props {
5
+ item: Record<string, unknown>;
6
+ }
7
+
8
+ const { item } = Astro.props;
9
+ const image = item.image && typeof item.image === "object" && !Array.isArray(item.image)
10
+ ? item.image as Record<string, unknown>
11
+ : {};
12
+ ---
13
+
14
+ {image.url ? (
15
+ <img
16
+ src={String(image.url)}
17
+ alt={String(image.alt ?? "")}
18
+ loading="lazy"
19
+ decoding="async"
20
+ />
21
+ ) : null}
22
+ {item.uri ? (
23
+ <S as="a" href={String(item.uri)}>{String(item.title ?? "")}</S>
24
+ ) : (
25
+ <S as="span">{String(item.title ?? "")}</S>
26
+ )}
27
+ {item.excerpt ? <S as="p">{String(item.excerpt)}</S> : null}
28
+ {item.priceHtml ? <S as="div" set:html={String(item.priceHtml)} /> : null}
@@ -0,0 +1,83 @@
1
+ ---
2
+ import S from "@styleflow.app/astro/S.astro";
3
+ import type { FlowpressBlock } from "../../types.js";
4
+ import type { StyleflowContract } from "../../styleflow.js";
5
+ import {
6
+ blockHtmlAttributes,
7
+ preserveEmptyTextBlock,
8
+ preserveRichTextSpacing,
9
+ semanticProps,
10
+ textValue,
11
+ unwrapCoreHtml
12
+ } from "./helpers.js";
13
+
14
+ interface Props {
15
+ block: FlowpressBlock;
16
+ contract?: StyleflowContract;
17
+ hasChildren?: boolean;
18
+ }
19
+
20
+ const { block, contract, hasChildren = false } = Astro.props;
21
+ const name = block.name.replace(/^sf\//, "");
22
+ const attributes = block.attributes;
23
+ const heading = name === "heading" || name === "core/heading";
24
+ const level = Math.min(6, Math.max(1, Number(attributes.level ?? 2)));
25
+ const headingTag = `h${level}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
26
+ const defaults = heading
27
+ ? {
28
+ ty: "heading",
29
+ v: String(attributes.v ?? attributes.variant ?? level),
30
+ w: String(attributes.w ?? attributes.weight ?? "strong"),
31
+ foreground: "primary"
32
+ }
33
+ : {
34
+ ty: "body",
35
+ v: String(attributes.v ?? (name.includes("quote") ? "lg" : "md")),
36
+ w: String(attributes.w ?? "default"),
37
+ foreground: "primary"
38
+ };
39
+ const semantic = semanticProps(attributes, contract, defaults);
40
+ const htmlAttributes = blockHtmlAttributes(attributes);
41
+ const coreHtml = String(
42
+ attributes.content
43
+ ?? attributes.value
44
+ ?? block.innerHtml
45
+ ?? ""
46
+ );
47
+ const normalizedHtml = name === "heading"
48
+ ? textValue(block)
49
+ : name === "rich-text"
50
+ ? preserveRichTextSpacing(String(attributes.content ?? ""))
51
+ : name === "quote"
52
+ ? textValue(block, "quote")
53
+ : unwrapCoreHtml(preserveRichTextSpacing(coreHtml));
54
+ const html = name === "core/list"
55
+ ? normalizedHtml
56
+ : preserveEmptyTextBlock(normalizedHtml);
57
+ const listTag = Boolean(attributes.ordered) ? "ol" : "ul";
58
+ ---
59
+
60
+ {heading ? (
61
+ <S as={headingTag} {...semantic} {...htmlAttributes} set:html={html} />
62
+ ) : name === "core/list" ? (
63
+ <S as={listTag} {...semantic} {...htmlAttributes}>
64
+ {hasChildren ? <slot /> : <Fragment set:html={html} />}
65
+ </S>
66
+ ) : name === "core/list-item" ? (
67
+ <S as="li" {...semantic} {...htmlAttributes}>
68
+ <span set:html={html}></span>
69
+ {hasChildren ? <slot /> : null}
70
+ </S>
71
+ ) : name === "quote" || name === "core/quote" ? (
72
+ <S as="blockquote" {...semantic} {...htmlAttributes}>
73
+ {hasChildren ? <slot /> : <div set:html={html}></div>}
74
+ {attributes.citation ? <cite>{String(attributes.citation)}</cite> : null}
75
+ </S>
76
+ ) : (
77
+ <S
78
+ as={name === "core/paragraph" ? "p" : "div"}
79
+ {...semantic}
80
+ {...htmlAttributes}
81
+ set:html={html}
82
+ />
83
+ )}
@@ -0,0 +1,26 @@
1
+ import { type StyleflowContract } from "../../styleflow.js";
2
+ import type { FlowpressBlock } from "../../types.js";
3
+ export declare const DEFAULT_SEMANTIC_PROPS: {
4
+ surface: string;
5
+ layoutRole: string;
6
+ density: string;
7
+ };
8
+ export declare function blockHtmlAttributes(attributes: Record<string, unknown>, baseClass?: string): {
9
+ id?: string;
10
+ class?: string;
11
+ };
12
+ export declare function semanticProps(attributes: Record<string, unknown>, contract?: StyleflowContract, defaults?: Record<string, unknown>): Record<string, unknown>;
13
+ export declare function objectValue(block: FlowpressBlock, key: string): Record<string, unknown>;
14
+ export declare function textValue(block: FlowpressBlock, key?: string): string;
15
+ /**
16
+ * Keep intentional blank lines visible across current and legacy RichText
17
+ * serializations. Gutenberg can represent them either as repeated `<br>` tags,
18
+ * empty paragraph elements, or literal newlines in an attribute value.
19
+ */
20
+ export declare function preserveRichTextSpacing(html: string): string;
21
+ /** Keep an empty text element at its typographic line height without visible text. */
22
+ export declare function preserveEmptyTextBlock(html: string): string;
23
+ export declare function linkValue(block: FlowpressBlock, key?: string): string;
24
+ export declare function responsiveValues(attributes: Record<string, unknown>, prefix: "columns" | "span", fallback: number): Record<"base" | "sm" | "md" | "lg" | "xl" | "2xl", number>;
25
+ export declare function unwrapCoreHtml(html: string): string;
26
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../src/astro/blocks/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,eAAO,MAAM,sBAAsB;;;;CAIlC,CAAC;AAEF,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,SAAS,SAAK,GACb;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAgBjC;AAqBD,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAA0B,GACzD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED,wBAAgB,WAAW,CACzB,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,MAAM,GACV,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAKzB;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,SAAS,GAAG,MAAM,CAErE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoB5D;AAED,sFAAsF;AACtF,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,SAAS,GAAG,MAAM,CAMrE;AAED,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,MAAM,EAAE,SAAS,GAAG,MAAM,EAC1B,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,EAAE,MAAM,CAAC,CAe5D;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKnD"}
@@ -0,0 +1,108 @@
1
+ import { resolveDynamicStyleflowProps } from "../../styleflow.js";
2
+ export const DEFAULT_SEMANTIC_PROPS = {
3
+ surface: "default",
4
+ layoutRole: "stack",
5
+ density: "md"
6
+ };
7
+ export function blockHtmlAttributes(attributes, baseClass = "") {
8
+ const anchor = typeof attributes.anchor === "string"
9
+ ? attributes.anchor.trim()
10
+ : "";
11
+ const customClass = typeof attributes.className === "string"
12
+ ? attributes.className.trim()
13
+ : "";
14
+ const className = [baseClass.trim(), customClass]
15
+ .filter(Boolean)
16
+ .join(" ")
17
+ .replace(/\s+/g, " ");
18
+ return {
19
+ ...(anchor ? { id: anchor } : {}),
20
+ ...(className ? { class: className } : {})
21
+ };
22
+ }
23
+ const SEMANTIC_KEYS = [
24
+ "tone",
25
+ "intensity",
26
+ "surface",
27
+ "layoutRole",
28
+ "density",
29
+ "ty",
30
+ "v",
31
+ "w",
32
+ "foreground",
33
+ "interactivePriority",
34
+ "noBackground",
35
+ "noBorder",
36
+ "noPadding",
37
+ "noGap",
38
+ "noRadius",
39
+ "noForeground"
40
+ ];
41
+ export function semanticProps(attributes, contract, defaults = DEFAULT_SEMANTIC_PROPS) {
42
+ const input = Object.fromEntries(SEMANTIC_KEYS
43
+ .filter((key) => attributes[key] !== undefined)
44
+ .map((key) => [key, attributes[key]]));
45
+ return contract
46
+ ? resolveDynamicStyleflowProps(input, defaults, contract)
47
+ : { ...defaults, ...input };
48
+ }
49
+ export function objectValue(block, key) {
50
+ const value = block.attributes[key];
51
+ return value && typeof value === "object" && !Array.isArray(value)
52
+ ? value
53
+ : {};
54
+ }
55
+ export function textValue(block, key = "text") {
56
+ return String(block.attributes[key] ?? "");
57
+ }
58
+ /**
59
+ * Keep intentional blank lines visible across current and legacy RichText
60
+ * serializations. Gutenberg can represent them either as repeated `<br>` tags,
61
+ * empty paragraph elements, or literal newlines in an attribute value.
62
+ */
63
+ export function preserveRichTextSpacing(html) {
64
+ const visibleEmptyBlocks = html
65
+ .replace(/\r\n?/g, "\n")
66
+ .replace(/<(p|div)(\s[^>]*)?>(?:\s|&nbsp;|&#160;)*<\/\1>/gi, (_match, tag, attributes = "") => `<${tag}${attributes}>&nbsp;</${tag}>`);
67
+ return visibleEmptyBlocks
68
+ .split(/(<[^>]+>)/g)
69
+ .map((part) => {
70
+ if (part.startsWith("<") || part.trim() === "") {
71
+ return part;
72
+ }
73
+ return part.replace(/\n(?:[ \t]*\n)+/g, (newlines) => "<br>".repeat((newlines.match(/\n/g) || []).length));
74
+ })
75
+ .join("");
76
+ }
77
+ /** Keep an empty text element at its typographic line height without visible text. */
78
+ export function preserveEmptyTextBlock(html) {
79
+ return html.trim() === "" ? "&nbsp;" : html;
80
+ }
81
+ export function linkValue(block, key = "link") {
82
+ const value = block.attributes[key];
83
+ const href = typeof value === "string"
84
+ ? value
85
+ : String(objectValue(block, key).url ?? "");
86
+ return /^(?:\/(?!\/)|#|https?:\/\/|mailto:|tel:)/i.test(href) ? href : "";
87
+ }
88
+ export function responsiveValues(attributes, prefix, fallback) {
89
+ const keys = ["base", "sm", "md", "lg", "xl", "2xl"];
90
+ const result = {};
91
+ let inherited = fallback;
92
+ for (const key of keys) {
93
+ const suffix = key === "base"
94
+ ? "Base"
95
+ : key === "2xl" ? "2xl" : key.charAt(0).toUpperCase() + key.slice(1);
96
+ const candidate = Number(attributes[`${prefix}${suffix}`]);
97
+ if (Number.isInteger(candidate) && candidate >= 1 && candidate <= 12) {
98
+ inherited = candidate;
99
+ }
100
+ result[key] = inherited;
101
+ }
102
+ return result;
103
+ }
104
+ export function unwrapCoreHtml(html) {
105
+ const match = html.trim().match(/^<(p|h[1-6]|ul|ol|li|blockquote)(?:\s[^>]*)?>([\s\S]*)<\/\1>$/);
106
+ return match?.[2] ?? html;
107
+ }
108
+ //# sourceMappingURL=helpers.js.map