@styleflow.app/flowpress-astro-core 0.2.0-beta.2 → 0.2.0-beta.21

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 (44) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +59 -1
  3. package/dist/astro/BaseLayout.astro +7 -1
  4. package/dist/astro/DefaultTemplate.astro +31 -21
  5. package/dist/astro/GutenbergRenderer.astro +151 -120
  6. package/dist/astro/blocks/ColumnBlock.astro +83 -0
  7. package/dist/astro/blocks/FormBlock.astro +233 -0
  8. package/dist/astro/blocks/GridBlock.astro +70 -0
  9. package/dist/astro/blocks/HeroBlock.astro +119 -0
  10. package/dist/astro/blocks/ListItemFallback.astro +28 -0
  11. package/dist/astro/blocks/NumberSpinnerBlock.astro +130 -0
  12. package/dist/astro/blocks/TextBlock.astro +83 -0
  13. package/dist/astro/blocks/helpers.d.ts +32 -0
  14. package/dist/astro/blocks/helpers.d.ts.map +1 -0
  15. package/dist/astro/blocks/helpers.js +152 -0
  16. package/dist/astro/blocks/helpers.js.map +1 -0
  17. package/dist/astro/blocks/helpers.ts +211 -0
  18. package/dist/block-renderers.d.ts +10 -0
  19. package/dist/block-renderers.d.ts.map +1 -0
  20. package/dist/block-renderers.js +2 -0
  21. package/dist/block-renderers.js.map +1 -0
  22. package/dist/client.d.ts.map +1 -1
  23. package/dist/client.js +26 -1
  24. package/dist/client.js.map +1 -1
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +3 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/number-spinner.d.ts +6 -0
  30. package/dist/number-spinner.d.ts.map +1 -0
  31. package/dist/number-spinner.js +136 -0
  32. package/dist/number-spinner.js.map +1 -0
  33. package/dist/style-classes.d.ts +9 -0
  34. package/dist/style-classes.d.ts.map +1 -0
  35. package/dist/style-classes.js +47 -0
  36. package/dist/style-classes.js.map +1 -0
  37. package/dist/styleflow.d.ts.map +1 -1
  38. package/dist/styleflow.js +7 -0
  39. package/dist/styleflow.js.map +1 -1
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/dist/types.d.ts +8 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/types.js.map +1 -1
  44. package/package.json +6 -4
@@ -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
+ 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(block.attributes, "span", 1);
18
+ const partial = ["start", "end"].includes(String(block.attributes.partialContainer))
19
+ ? String(block.attributes.partialContainer)
20
+ : "none";
21
+ const partialBreakpoint = ["base", "sm", "md", "lg", "xl", "2xl"].includes(
22
+ String(block.attributes.partialContainerBreakpoint)
23
+ ) ? String(block.attributes.partialContainerBreakpoint) : "lg";
24
+ const semantic = semanticProps(block.attributes, contract);
25
+ const htmlAttributes = blockHtmlAttributes(block.attributes, "flowpress-column");
26
+ const style = [
27
+ `--flowpress-span-base:${values.base}`,
28
+ `--flowpress-span-sm:${values.sm}`,
29
+ `--flowpress-span-md:${values.md}`,
30
+ `--flowpress-span-lg:${values.lg}`,
31
+ `--flowpress-span-xl:${values.xl}`,
32
+ `--flowpress-span-2xl:${values["2xl"]}`
33
+ ].join(";");
34
+ ---
35
+
36
+ <S
37
+ as="div"
38
+ {...semantic}
39
+ {...htmlAttributes}
40
+ data-partial={partial}
41
+ data-partial-breakpoint={partialBreakpoint}
42
+ style={style}
43
+ >
44
+ <slot />
45
+ </S>
46
+
47
+ <style>
48
+ .flowpress-column {
49
+ grid-column: span var(--flowpress-span-base) / span var(--flowpress-span-base);
50
+ min-width: 0;
51
+ }
52
+ .flowpress-column[data-partial="start"][data-partial-breakpoint="base"] {
53
+ padding-inline-start: max(var(--sf-padding-inline, 0px), calc((100vw - var(--sf-container-max-width, 100vw)) / 2));
54
+ }
55
+ .flowpress-column[data-partial="end"][data-partial-breakpoint="base"] {
56
+ padding-inline-end: max(var(--sf-padding-inline, 0px), calc((100vw - var(--sf-container-max-width, 100vw)) / 2));
57
+ }
58
+ @media (min-width: 40rem) {
59
+ .flowpress-column { grid-column: span var(--flowpress-span-sm) / span var(--flowpress-span-sm); }
60
+ .flowpress-column[data-partial="start"][data-partial-breakpoint="sm"] { padding-inline-start: max(var(--sf-padding-inline, 0px), calc((100vw - var(--sf-container-max-width, 100vw)) / 2)); }
61
+ .flowpress-column[data-partial="end"][data-partial-breakpoint="sm"] { padding-inline-end: max(var(--sf-padding-inline, 0px), calc((100vw - var(--sf-container-max-width, 100vw)) / 2)); }
62
+ }
63
+ @media (min-width: 48rem) {
64
+ .flowpress-column { grid-column: span var(--flowpress-span-md) / span var(--flowpress-span-md); }
65
+ .flowpress-column[data-partial="start"][data-partial-breakpoint="md"] { padding-inline-start: max(var(--sf-padding-inline, 0px), calc((100vw - var(--sf-container-max-width, 100vw)) / 2)); }
66
+ .flowpress-column[data-partial="end"][data-partial-breakpoint="md"] { padding-inline-end: max(var(--sf-padding-inline, 0px), calc((100vw - var(--sf-container-max-width, 100vw)) / 2)); }
67
+ }
68
+ @media (min-width: 64rem) {
69
+ .flowpress-column { grid-column: span var(--flowpress-span-lg) / span var(--flowpress-span-lg); }
70
+ .flowpress-column[data-partial="start"][data-partial-breakpoint="lg"] { padding-inline-start: max(var(--sf-padding-inline, 0px), calc((100vw - var(--sf-container-max-width, 100vw)) / 2)); }
71
+ .flowpress-column[data-partial="end"][data-partial-breakpoint="lg"] { padding-inline-end: max(var(--sf-padding-inline, 0px), calc((100vw - var(--sf-container-max-width, 100vw)) / 2)); }
72
+ }
73
+ @media (min-width: 80rem) {
74
+ .flowpress-column { grid-column: span var(--flowpress-span-xl) / span var(--flowpress-span-xl); }
75
+ .flowpress-column[data-partial="start"][data-partial-breakpoint="xl"] { padding-inline-start: max(var(--sf-padding-inline, 0px), calc((100vw - var(--sf-container-max-width, 100vw)) / 2)); }
76
+ .flowpress-column[data-partial="end"][data-partial-breakpoint="xl"] { padding-inline-end: max(var(--sf-padding-inline, 0px), calc((100vw - var(--sf-container-max-width, 100vw)) / 2)); }
77
+ }
78
+ @media (min-width: 96rem) {
79
+ .flowpress-column { grid-column: span var(--flowpress-span-2xl) / span var(--flowpress-span-2xl); }
80
+ .flowpress-column[data-partial="start"][data-partial-breakpoint="2xl"] { padding-inline-start: max(var(--sf-padding-inline, 0px), calc((100vw - var(--sf-container-max-width, 100vw)) / 2)); }
81
+ .flowpress-column[data-partial="end"][data-partial-breakpoint="2xl"] { padding-inline-end: max(var(--sf-padding-inline, 0px), calc((100vw - var(--sf-container-max-width, 100vw)) / 2)); }
82
+ }
83
+ </style>
@@ -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,119 @@
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
+ heroObjectPosition,
8
+ objectValue,
9
+ semanticProps
10
+ } from "./helpers.js";
11
+
12
+ interface Props {
13
+ block: FlowpressBlock;
14
+ contract?: StyleflowContract;
15
+ }
16
+
17
+ const { block, contract } = Astro.props;
18
+ const attributes = block.attributes;
19
+ const semantic = semanticProps(attributes, contract, {
20
+ surface: "default",
21
+ layoutRole: "region",
22
+ density: "md"
23
+ });
24
+ const htmlAttributes = blockHtmlAttributes(attributes, "flowpress-hero");
25
+ const backgroundType = ["color", "image", "video"].includes(
26
+ String(attributes.backgroundType)
27
+ ) ? String(attributes.backgroundType) : "color";
28
+ const image = objectValue(block, "backgroundImage");
29
+ const video = objectValue(block, "backgroundVideo");
30
+ const imageSrc = String(image.url ?? image.sourceUrl ?? "");
31
+ const videoSrc = String(video.url ?? video.sourceUrl ?? video.mediaItemUrl ?? "");
32
+ const objectPosition = heroObjectPosition(attributes.objectPosition);
33
+ const scrimEnabled = Boolean(attributes.scrimEnabled) && backgroundType !== "color";
34
+ const scrimIntensity = Math.min(10, Math.max(1, Number(attributes.scrimIntensity) || 6));
35
+ ---
36
+
37
+ <S
38
+ as="section"
39
+ {...semantic}
40
+ {...htmlAttributes}
41
+ style={[
42
+ `--flowpress-hero-object-position:${objectPosition}`,
43
+ `--flowpress-scrim-opacity:${scrimIntensity * 10}%`
44
+ ].join(";")}
45
+ >
46
+ {backgroundType === "image" && imageSrc ? (
47
+ <img
48
+ class="flowpress-hero__media"
49
+ src={imageSrc}
50
+ alt=""
51
+ aria-hidden="true"
52
+ loading="eager"
53
+ decoding="async"
54
+ />
55
+ ) : null}
56
+ {backgroundType === "video" && videoSrc ? (
57
+ <video
58
+ class="flowpress-hero__media"
59
+ src={videoSrc}
60
+ poster={imageSrc || undefined}
61
+ autoplay
62
+ muted
63
+ loop
64
+ playsinline
65
+ preload="metadata"
66
+ aria-hidden="true"
67
+ tabindex="-1"
68
+ />
69
+ ) : null}
70
+ {scrimEnabled ? <span class="flowpress-hero__scrim" aria-hidden="true"></span> : null}
71
+ <div class="flowpress-hero__content">
72
+ <slot />
73
+ </div>
74
+ </S>
75
+
76
+ <style>
77
+ .flowpress-hero {
78
+ position: relative;
79
+ display: grid;
80
+ overflow: hidden;
81
+ isolation: isolate;
82
+ }
83
+
84
+ .flowpress-hero__media,
85
+ .flowpress-hero__scrim {
86
+ position: absolute;
87
+ inset: 0;
88
+ width: 100%;
89
+ height: 100%;
90
+ }
91
+
92
+ .flowpress-hero__media {
93
+ z-index: -2;
94
+ object-fit: cover;
95
+ object-position: var(--flowpress-hero-object-position);
96
+ }
97
+
98
+ .flowpress-hero__scrim {
99
+ z-index: -1;
100
+ background: color-mix(
101
+ in srgb,
102
+ var(--sf-background) var(--flowpress-scrim-opacity),
103
+ transparent
104
+ );
105
+ pointer-events: none;
106
+ }
107
+
108
+ .flowpress-hero__content {
109
+ position: relative;
110
+ display: grid;
111
+ gap: inherit;
112
+ }
113
+
114
+ @media (prefers-reduced-motion: reduce) {
115
+ .flowpress-hero__media:is(video) {
116
+ display: none;
117
+ }
118
+ }
119
+ </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,130 @@
1
+ ---
2
+ import S from "@styleflow.app/astro/S.astro";
3
+ import {
4
+ clampSpinnerDuration,
5
+ spinnerFractionDigits
6
+ } from "../../number-spinner.js";
7
+ import type { StyleflowContract } from "../../styleflow.js";
8
+ import type { FlowpressBlock } from "../../types.js";
9
+ import {
10
+ blockHtmlAttributes,
11
+ prefixedTextSemanticProps,
12
+ semanticProps,
13
+ textHtmlTag
14
+ } from "./helpers.js";
15
+
16
+ interface Props {
17
+ block: FlowpressBlock;
18
+ contract?: StyleflowContract;
19
+ }
20
+
21
+ const { block, contract } = Astro.props;
22
+ const attributes = block.attributes;
23
+ const prefix = String(attributes.prefix ?? "");
24
+ const suffix = String(attributes.suffix ?? "");
25
+ const candidateNumber = Number(attributes.number);
26
+ const number = Number.isFinite(candidateNumber) ? candidateNumber : 0;
27
+ const duration = clampSpinnerDuration(attributes.duration);
28
+ const fractionDigits = spinnerFractionDigits(number);
29
+ const locale = Astro.currentLocale ?? "";
30
+ const numberText = String(number);
31
+ const semantic = semanticProps(attributes, contract);
32
+ const htmlAttributes = blockHtmlAttributes(
33
+ attributes,
34
+ "flowpress-number-spinner"
35
+ );
36
+ const prefixSemantic = prefixedTextSemanticProps(
37
+ attributes,
38
+ "prefix",
39
+ contract,
40
+ { ty: "body", v: "md", w: "default", foreground: "primary" }
41
+ );
42
+ const numberSemantic = prefixedTextSemanticProps(
43
+ attributes,
44
+ "number",
45
+ contract,
46
+ { ty: "heading", v: "2", w: "strong", foreground: "primary" }
47
+ );
48
+ const suffixSemantic = prefixedTextSemanticProps(
49
+ attributes,
50
+ "suffix",
51
+ contract,
52
+ { ty: "body", v: "md", w: "default", foreground: "primary" }
53
+ );
54
+ const prefixTag = textHtmlTag(attributes.prefixTag);
55
+ const numberTag = textHtmlTag(attributes.numberTag);
56
+ const suffixTag = textHtmlTag(attributes.suffixTag);
57
+ ---
58
+
59
+ <S
60
+ as="div"
61
+ {...semantic}
62
+ {...htmlAttributes}
63
+ data-flowpress-number-spinner
64
+ data-spinner-target={number}
65
+ data-spinner-duration={duration}
66
+ data-spinner-fraction-digits={fractionDigits}
67
+ data-spinner-locale={locale || undefined}
68
+ >
69
+ {prefix ? (
70
+ <S
71
+ as={prefixTag}
72
+ class="flowpress-number-spinner__segment"
73
+ {...prefixSemantic}
74
+ >
75
+ {prefix}
76
+ </S>
77
+ ) : null}
78
+ <S
79
+ as={numberTag}
80
+ class="flowpress-number-spinner__segment flowpress-number-spinner__number"
81
+ {...numberSemantic}
82
+ >
83
+ <span class="flowpress-number-spinner__accessible-value">
84
+ {numberText}
85
+ </span>
86
+ <span
87
+ aria-hidden="true"
88
+ data-flowpress-number-spinner-value
89
+ >
90
+ {numberText}
91
+ </span>
92
+ </S>
93
+ {suffix ? (
94
+ <S
95
+ as={suffixTag}
96
+ class="flowpress-number-spinner__segment"
97
+ {...suffixSemantic}
98
+ >
99
+ {suffix}
100
+ </S>
101
+ ) : null}
102
+ </S>
103
+
104
+ <script>
105
+ import { initializeNumberSpinners } from "../../number-spinner.js";
106
+
107
+ initializeNumberSpinners();
108
+ </script>
109
+
110
+ <style>
111
+ .flowpress-number-spinner {
112
+ display: inline-flex;
113
+ align-items: baseline;
114
+ }
115
+
116
+ .flowpress-number-spinner__segment {
117
+ display: inline;
118
+ margin: 0;
119
+ }
120
+
121
+ .flowpress-number-spinner__accessible-value {
122
+ position: absolute;
123
+ width: 1px;
124
+ height: 1px;
125
+ overflow: hidden;
126
+ clip: rect(0 0 0 0);
127
+ clip-path: inset(50%);
128
+ white-space: nowrap;
129
+ }
130
+ </style>