@styleflow.app/flowpress-astro-core 0.2.0-beta.9 → 1.0.0-beta.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/LICENSE +21 -0
  3. package/README.md +90 -3
  4. package/dist/astro/BaseLayout.astro +7 -2
  5. package/dist/astro/DefaultTemplate.astro +5 -7
  6. package/dist/astro/GutenbergRenderer.astro +132 -28
  7. package/dist/astro/blocks/GridBlock.astro +1 -1
  8. package/dist/astro/blocks/HeroBlock.astro +8 -2
  9. package/dist/astro/blocks/ImageBlock.astro +90 -0
  10. package/dist/astro/blocks/NumberSpinnerBlock.astro +130 -0
  11. package/dist/astro/blocks/TextBlock.astro +19 -6
  12. package/dist/astro/blocks/helpers.d.ts +10 -0
  13. package/dist/astro/blocks/helpers.d.ts.map +1 -1
  14. package/dist/astro/blocks/helpers.js +86 -5
  15. package/dist/astro/blocks/helpers.js.map +1 -1
  16. package/dist/astro/blocks/helpers.ts +111 -5
  17. package/dist/block-renderers.d.ts +22 -0
  18. package/dist/block-renderers.d.ts.map +1 -0
  19. package/dist/block-renderers.js +2 -0
  20. package/dist/block-renderers.js.map +1 -0
  21. package/dist/client.d.ts.map +1 -1
  22. package/dist/client.js +26 -1
  23. package/dist/client.js.map +1 -1
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/number-spinner.d.ts +6 -0
  29. package/dist/number-spinner.d.ts.map +1 -0
  30. package/dist/number-spinner.js +136 -0
  31. package/dist/number-spinner.js.map +1 -0
  32. package/dist/style-classes.d.ts +9 -0
  33. package/dist/style-classes.d.ts.map +1 -0
  34. package/dist/style-classes.js +49 -0
  35. package/dist/style-classes.js.map +1 -0
  36. package/dist/styleflow.d.ts +37 -8
  37. package/dist/styleflow.d.ts.map +1 -1
  38. package/dist/styleflow.js +85 -42
  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 -5
package/CHANGELOG.md CHANGED
@@ -2,6 +2,115 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.0.0-beta.1
6
+
7
+ - Updated the exact `@styleflow.app/astro` dependency to `1.0.0-beta.1`,
8
+ keeping consumer trees on one runtime while adopting deterministic CLI
9
+ reports. Runtime behavior is unchanged.
10
+
11
+ ## 1.0.0-beta.0
12
+
13
+ - Adopted the grouped Styleflow vNext project contract and the exact Astro v1
14
+ runtime dependency, removing the consumer dependency on Styleflow Core.
15
+ - Added project-aware validation for tone/intensity and type/variant pairs,
16
+ theme propagation and diagnostic-only explicit fallbacks.
17
+ - Made invalid CMS coordinates blocking in strict builds while preserving all
18
+ existing recursive renderers, forms, responsive media, trusted code, SEO,
19
+ RichText spacing and accessibility behavior.
20
+
21
+ ## 0.2.0-beta.23
22
+
23
+ - Added validated Gutenberg text alignment for FlowPress and core text blocks.
24
+ - Added a typed project icon renderer, recursive propagation, standalone icon
25
+ blocks and ordered button icons with the StyleFlow `control` layout role.
26
+ - Treated `foreground=inherit` as omission, matching inherited tone and
27
+ intensity behavior.
28
+ - Rendered trusted `sf/code` documents in the main DOM only when WordPress
29
+ serialization supplies `codeApproved=true`; unapproved documents fail closed.
30
+
31
+ ## 0.2.0-beta.22
32
+
33
+ - Added a dedicated `sf/image` renderer with WordPress responsive `srcset`,
34
+ authored or attachment-default `sizes`, intrinsic dimensions, per-image CSS
35
+ classes and validated loading, decoding and fetch-priority controls.
36
+ - Included per-image CSS classes in the recursive CMS class collector used by
37
+ consumer Tailwind safelists.
38
+
39
+ ## 0.2.0-beta.21
40
+
41
+ - Forced locale-aware thousands grouping for four-digit number spinner values,
42
+ matching the grouping already applied to larger values.
43
+
44
+ ## 0.2.0-beta.20
45
+
46
+ - Added typed installable feature capabilities and normalized public JSON
47
+ configuration returned by FlowPress WordPress Core.
48
+ - Added deterministic CSP, consent and tag-manager head slots before the
49
+ existing project-owned `head` slot.
50
+
51
+ ## 0.2.0-beta.19
52
+
53
+ - Let the number spinner's visual and accessible value spans inherit the
54
+ selected number typography instead of assigning the StyleFlow `body-md`
55
+ fallback to both technical descendants.
56
+
57
+ ## 0.2.0-beta.18
58
+
59
+ - Updated the exact StyleFlow Astro dependency to `0.3.0-beta.6`, matching
60
+ the final StyleFlow release that includes the corrected importer and the
61
+ branded Figma plugin bundle.
62
+
63
+ ## 0.2.0-beta.17
64
+
65
+ - Updated the exact StyleFlow Astro dependency to `0.3.0-beta.5`, keeping
66
+ consumers on one runtime aligned with the corrected Figma contract import
67
+ release.
68
+
69
+ ## 0.2.0-beta.16
70
+
71
+ - Updated the exact StyleFlow Astro dependency to `0.3.0-beta.4` so consumers
72
+ use the responsive compiler contract and a single matching runtime.
73
+
74
+ ## 0.2.0-beta.15
75
+
76
+ - Applied Hero image and video `object-position` from a sanitized pair of
77
+ percentage coordinates, with a safe centered fallback.
78
+
79
+ ## 0.2.0-beta.14
80
+
81
+ - Added a recursive project block-renderer registry to
82
+ `GutenbergRenderer.astro`, including imported `sf/template` and resolved
83
+ query-item descendants.
84
+
85
+ ## 0.2.0-beta.13
86
+
87
+ - Kept number spinners static on their final values when
88
+ `IntersectionObserver` or motion preference detection is unavailable,
89
+ rather than starting outside a verifiable viewport intersection.
90
+
91
+ ## 0.2.0-beta.12
92
+
93
+ - Added the reusable `sf/number-spinner` Astro renderer with independently
94
+ styled prefix, number and suffix text segments.
95
+ - Started counting only on first viewport intersection, retained the final
96
+ target in no-JavaScript markup and disabled animation for reduced-motion
97
+ preferences, including preference changes during an active count.
98
+ - Kept the final numeric value permanently available to assistive technology
99
+ while visual frame updates remain hidden from accessibility APIs.
100
+
101
+ ## 0.2.0-beta.11
102
+
103
+ - Rendered empty Gutenberg and FlowPress text blocks with an internal
104
+ ` `, preserving their typographic line height without visible
105
+ characters or synthetic break elements.
106
+
107
+ ## 0.2.0-beta.10
108
+
109
+ - Preserved intentionally empty core Gutenberg paragraphs as visible line
110
+ breaks, including paragraphs resolved through FlowPress templates.
111
+ - Exported safe recursive collection of CMS-authored block class names for
112
+ Tailwind `@source inline()` generation.
113
+
5
114
  ## 0.2.0-beta.9
6
115
 
7
116
  - Preserved repeated Gutenberg RichText spacing across `<br>` sequences,
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Styleflow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -9,11 +9,21 @@ The package must not import from an application or project plugin.
9
9
  ## Install
10
10
 
11
11
  ```bash
12
- pnpm add @styleflow.app/flowpress-astro-core@0.2.0-beta.9
12
+ pnpm add @styleflow.app/flowpress-astro-core@1.0.0-beta.1 \
13
+ @styleflow.app/astro@1.0.0-beta.1
13
14
  ```
14
15
 
15
16
  Prereleases are pinned exactly. The package expects Astro 7 or newer and the
16
- matching StyleFlow `0.3.0-beta.x` line.
17
+ exact Styleflow Astro v1 dependency. `styleflowAstro()` must be installed in
18
+ the project configuration and the bundle must first be compiled by the v1
19
+ CLI.
20
+
21
+ CMS coordinates are validated against the grouped project contract. Valid
22
+ tone/intensity and type/variant pairs are project-specific; numeric intensity,
23
+ v0.x density and browser states passed as component props are rejected.
24
+ `theme` is propagated as a semantic scope. A fallback is permitted only when
25
+ the catalog or `agentPolicy` declares an explicit default, always emits a
26
+ diagnostic, and becomes blocking in strict production builds.
17
27
 
18
28
  The client reports the GraphQL operation, sanitized endpoint, HTTP/content
19
29
  type failures and underlying network/TLS cause. Upstream GraphQL debug details
@@ -37,7 +47,80 @@ to the protected FlowPress REST endpoint, so the same component works in
37
47
  static and server output modes. Gutenberg `anchor` and `className` attributes
38
48
  are applied to the rendered block root as HTML `id` and `class`. Rich Text
39
49
  preserves intentional double, triple and longer line breaks, including content
40
- saved by older editor versions as empty paragraphs.
50
+ saved by older editor versions as empty paragraphs. Empty textual blocks render
51
+ an internal `&nbsp;`, preserving their typographic line height without adding
52
+ visible characters. The exported
53
+ `collectFlowpressClassNames()` helper recursively extracts safe class tokens
54
+ from rendered CMS blocks so consumers can generate Tailwind
55
+ `@source inline()` directives before compiling their frontend CSS.
56
+
57
+ Heading, rich-text, quote and matching core blocks accept Gutenberg alignment
58
+ from either `style.typography.textAlign` or the FlowPress `textAlign` field.
59
+ Only `left`, `center`, `right`, `justify`, `start` and `end` reach CSS.
60
+ `foreground=inherit` is omitted so icons and text inherit the surrounding
61
+ foreground instead of serializing an invalid StyleFlow role.
62
+
63
+ Hero image and video backgrounds apply the catalog `objectPosition` value as
64
+ two percentages (for example `77% 100%`). Invalid or missing CMS values safely
65
+ fall back to `50% 50%`.
66
+
67
+ `sf/image` renders the responsive attachment data serialized by WordPress as a
68
+ single `<img>` with `srcset`, `sizes` and positive intrinsic dimensions. The
69
+ block-root `className` remains on its StyleFlow wrapper while `imageClassName`
70
+ targets the image. Loading defaults to `lazy`, decoding to `async` and fetch
71
+ priority to `auto`; invalid stored values fail closed to those defaults.
72
+
73
+ Projects can add renderers for project-owned Gutenberg blocks without copying
74
+ or patching the core renderer. Pass a `FlowpressBlockRenderers` map keyed by the
75
+ full block name; the registry is propagated through every recursive rendering
76
+ path, including imported `sf/template` content and resolved query-item
77
+ templates.
78
+
79
+ ```astro
80
+ ---
81
+ import GutenbergRenderer from "@styleflow.app/flowpress-astro-core/GutenbergRenderer.astro";
82
+ import type {
83
+ FlowpressBlockRenderers
84
+ } from "@styleflow.app/flowpress-astro-core";
85
+ import ProjectTimelineBlock from "./ProjectTimelineBlock.astro";
86
+
87
+ const blockRenderers = {
88
+ "sf/timeline": ProjectTimelineBlock
89
+ } satisfies FlowpressBlockRenderers;
90
+ ---
91
+
92
+ <GutenbergRenderer blocks={blocks} blockRenderers={blockRenderers} />
93
+ ```
94
+
95
+ Each custom renderer receives the original `block` and optional StyleFlow
96
+ `contract`. Its default slot contains recursively rendered child blocks, so a
97
+ container override can place `<slot />` where its children belong.
98
+
99
+ Projects provide their SVG implementation through the typed `iconRenderer`
100
+ prop. It is used by `sf/icon`, propagated through all recursive paths and used
101
+ for optional button icons in `before` or `after` position.
102
+
103
+ ```astro
104
+ ---
105
+ import GutenbergRenderer from "@styleflow.app/flowpress-astro-core/GutenbergRenderer.astro";
106
+ import ProjectIcon from "@sf/ui/Icon.astro";
107
+ ---
108
+
109
+ <GutenbergRenderer blocks={blocks} iconRenderer={ProjectIcon} />
110
+ ```
111
+
112
+ `sf/code` is deliberately a trusted-code boundary, not a sandbox. The renderer
113
+ emits its complete HTML fragment (including style, script, iframe and embed
114
+ elements) in the main document only when the serialized block contains the
115
+ server-issued boolean `codeApproved=true`. The WordPress editor must never
116
+ execute the document, and consumers must not synthesize this flag.
117
+
118
+ `sf/number-spinner` renders prefix, target number and suffix with independent
119
+ semantic tags and StyleFlow text coordinates. Its numeric value counts from
120
+ zero only when it enters the viewport. The final target remains available to
121
+ assistive technology and without JavaScript; `prefers-reduced-motion: reduce`
122
+ skips the animation entirely. Browsers without viewport observation keep the
123
+ server-rendered final value instead of guessing when to start.
41
124
 
42
125
  ## Release
43
126
 
@@ -59,5 +142,9 @@ The TypeScript template-hierarchy suite is self-contained. Its additional PHP
59
142
  parity test runs when the sibling `flowpress-wordpress-core` checkout is
60
143
  available locally and is skipped in a standalone package checkout.
61
144
 
145
+ Publish v1 betas on `next` only after Components and WordPress Core are
146
+ available. Inspect `npm view`, `gitHead`, integrity and the downloaded tarball.
147
+ Promote to `latest` only after the DMO canary and rollback gate.
148
+
62
149
  Release activity reports are indexed in
63
150
  [`docs/activities/README.md`](docs/activities/README.md).
@@ -6,12 +6,14 @@ interface Props {
6
6
  lang?: string;
7
7
  seo?: SfSeo | null;
8
8
  siteName?: string;
9
+ theme?: string;
9
10
  }
10
11
 
11
12
  const {
12
13
  lang = "it",
13
14
  seo = null,
14
- siteName = import.meta.env.PROJECT_NAME ?? "FlowPress"
15
+ siteName = import.meta.env.PROJECT_NAME ?? "FlowPress",
16
+ theme
15
17
  } = Astro.props;
16
18
  const title = seo?.title || siteName;
17
19
  const description = seo?.description || "";
@@ -42,10 +44,13 @@ const jsonLd = seo?.jsonLd ? JSON.stringify(seo.jsonLd) : null;
42
44
  {seo?.twitter?.image?.url && <meta name="twitter:image" content={seo.twitter.image.url} />}
43
45
  {seo?.twitter?.image?.alt && <meta name="twitter:image:alt" content={seo.twitter.image.alt} />}
44
46
  {jsonLd && <script type="application/ld+json" set:html={jsonLd} />}
47
+ <slot name="head-csp" />
48
+ <slot name="head-consent" />
49
+ <slot name="head-tag-manager" />
45
50
  <slot name="head" />
46
51
  </head>
47
52
  <body>
48
- <S.main tone="neutral" intensity={1} surface="default" layoutRole="none" density="md">
53
+ <S.main theme={theme} surface="default" layoutRole="none" density="regular">
49
54
  <slot />
50
55
  </S.main>
51
56
  </body>
@@ -11,7 +11,7 @@ const hasGutembergBlocks = Array.isArray(content?.contentBlocks) && content.cont
11
11
  //TODO SET GLOBAL LOCAL ON ENV
12
12
  ---
13
13
 
14
- <BaseLayout lang={route.locale || "it"} seo={route.seo}>
14
+ <BaseLayout lang={route.locale || "it"} seo={route.seo} theme={styleflowContract?.defaultTheme}>
15
15
 
16
16
  {hasGutembergBlocks ? (
17
17
  <GutenbergRenderer
@@ -19,9 +19,9 @@ const hasGutembergBlocks = Array.isArray(content?.contentBlocks) && content.cont
19
19
  contract={styleflowContract}
20
20
  />
21
21
  ) : (
22
- <S.section tone="neutral" intensity={1} surface="default" layoutRole="section" density="md">
23
- <S.h1 ty="heading" v="1" w="strong">{isNotFound ? "Pagina non trovata" : title}</S.h1>
24
- {content?.excerpt && <S.p ty="body" v="md" w="default">{content.excerpt}</S.p>}
22
+ <S.section surface="default" layoutRole="section" density="regular">
23
+ <S.h1>{isNotFound ? "Pagina non trovata" : title}</S.h1>
24
+ {content?.excerpt && <S.p>{content.excerpt}</S.p>}
25
25
  {Array.isArray(content?.contentBlocks) && content.contentBlocks.length > 0 ? (
26
26
  <GutenbergRenderer
27
27
  blocks={content.contentBlocks as import("../types.js").FlowpressBlock[]}
@@ -29,11 +29,9 @@ const hasGutembergBlocks = Array.isArray(content?.contentBlocks) && content.cont
29
29
  />
30
30
  ) : content?.content && (
31
31
  <S.div
32
- tone="neutral"
33
- intensity={1}
34
32
  surface="default"
35
33
  layoutRole="stack"
36
- density="md"
34
+ density="regular"
37
35
  set:html={String(content.content)}
38
36
  />
39
37
  )}
@@ -1,12 +1,18 @@
1
1
  ---
2
2
  import S from "@styleflow.app/astro/S.astro";
3
+ import type {
4
+ FlowpressBlockRenderers,
5
+ FlowpressIconRenderer
6
+ } from "../block-renderers.js";
3
7
  import type { StyleflowContract } from "../styleflow.js";
4
8
  import type { FlowpressBlock } from "../types.js";
5
9
  import ColumnBlock from "./blocks/ColumnBlock.astro";
6
10
  import FormBlock from "./blocks/FormBlock.astro";
7
11
  import GridBlock from "./blocks/GridBlock.astro";
8
12
  import HeroBlock from "./blocks/HeroBlock.astro";
13
+ import ImageBlock from "./blocks/ImageBlock.astro";
9
14
  import ListItemFallback from "./blocks/ListItemFallback.astro";
15
+ import NumberSpinnerBlock from "./blocks/NumberSpinnerBlock.astro";
10
16
  import TextBlock from "./blocks/TextBlock.astro";
11
17
  import {
12
18
  blockHtmlAttributes,
@@ -19,9 +25,11 @@ import {
19
25
  interface Props {
20
26
  blocks: FlowpressBlock[];
21
27
  contract?: StyleflowContract;
28
+ blockRenderers?: FlowpressBlockRenderers;
29
+ iconRenderer?: FlowpressIconRenderer;
22
30
  }
23
31
 
24
- const { blocks, contract } = Astro.props;
32
+ const { blocks, contract, blockRenderers = {}, iconRenderer: IconRenderer } = Astro.props;
25
33
 
26
34
  function youtubeEmbedUrl(value: unknown): string | null {
27
35
  try {
@@ -46,11 +54,30 @@ function youtubeEmbedUrl(value: unknown): string | null {
46
54
  const children = block.innerBlocks ?? [];
47
55
  const semantic = semanticProps(block.attributes, contract);
48
56
  const htmlAttributes = blockHtmlAttributes(block.attributes);
57
+ const CustomBlockRenderer = blockRenderers[block.name];
58
+
59
+ if (CustomBlockRenderer) {
60
+ return (
61
+ <CustomBlockRenderer block={block} contract={contract}>
62
+ <Astro.self
63
+ blocks={children}
64
+ contract={contract}
65
+ blockRenderers={blockRenderers}
66
+ iconRenderer={IconRenderer}
67
+ />
68
+ </CustomBlockRenderer>
69
+ );
70
+ }
49
71
 
50
72
  if (name === "hero") {
51
73
  return (
52
74
  <HeroBlock block={block} contract={contract}>
53
- <Astro.self blocks={children} contract={contract} />
75
+ <Astro.self
76
+ blocks={children}
77
+ contract={contract}
78
+ blockRenderers={blockRenderers}
79
+ iconRenderer={IconRenderer}
80
+ />
54
81
  </HeroBlock>
55
82
  );
56
83
  }
@@ -58,7 +85,12 @@ function youtubeEmbedUrl(value: unknown): string | null {
58
85
  if (name === "grid") {
59
86
  return (
60
87
  <GridBlock block={block} contract={contract}>
61
- <Astro.self blocks={children} contract={contract} />
88
+ <Astro.self
89
+ blocks={children}
90
+ contract={contract}
91
+ blockRenderers={blockRenderers}
92
+ iconRenderer={IconRenderer}
93
+ />
62
94
  </GridBlock>
63
95
  );
64
96
  }
@@ -66,11 +98,20 @@ function youtubeEmbedUrl(value: unknown): string | null {
66
98
  if (name === "column") {
67
99
  return (
68
100
  <ColumnBlock block={block} contract={contract}>
69
- <Astro.self blocks={children} contract={contract} />
101
+ <Astro.self
102
+ blocks={children}
103
+ contract={contract}
104
+ blockRenderers={blockRenderers}
105
+ iconRenderer={IconRenderer}
106
+ />
70
107
  </ColumnBlock>
71
108
  );
72
109
  }
73
110
 
111
+ if (name === "number-spinner") {
112
+ return <NumberSpinnerBlock block={block} contract={contract} />;
113
+ }
114
+
74
115
  if ([
75
116
  "heading",
76
117
  "rich-text",
@@ -83,7 +124,12 @@ function youtubeEmbedUrl(value: unknown): string | null {
83
124
  ].includes(name)) {
84
125
  return (
85
126
  <TextBlock block={block} contract={contract} hasChildren={children.length > 0}>
86
- <Astro.self blocks={children} contract={contract} />
127
+ <Astro.self
128
+ blocks={children}
129
+ contract={contract}
130
+ blockRenderers={blockRenderers}
131
+ iconRenderer={IconRenderer}
132
+ />
87
133
  </TextBlock>
88
134
  );
89
135
  }
@@ -92,40 +138,71 @@ function youtubeEmbedUrl(value: unknown): string | null {
92
138
  const href = linkValue(block, "href") || "#";
93
139
  const link = objectValue(block, "href");
94
140
  const opensInNewTab = Boolean(link.opensInNewTab);
141
+ const icon = String(block.attributes.icon ?? "").trim();
142
+ const iconPosition = ["after", "right"].includes(
143
+ String(block.attributes.iconPosition ?? "before")
144
+ ) ? "after" : "before";
145
+ const buttonSemantic = semanticProps(block.attributes, contract, {
146
+ layoutRole: "control",
147
+ density: "regular"
148
+ });
149
+ const buttonIcon = IconRenderer && icon ? (
150
+ <IconRenderer name={icon} display="inline" size={4} />
151
+ ) : null;
95
152
  return (
96
153
  <S
97
154
  as="a"
98
155
  href={href}
99
156
  target={opensInNewTab ? "_blank" : undefined}
100
157
  rel={opensInNewTab ? "noopener noreferrer" : undefined}
101
- {...semantic}
158
+ {...buttonSemantic}
102
159
  {...htmlAttributes}
103
160
  >
161
+ {iconPosition === "before" ? buttonIcon : null}
104
162
  {textValue(block, "label")}
163
+ {iconPosition === "after" ? buttonIcon : null}
105
164
  </S>
106
165
  );
107
166
  }
108
167
 
109
- if (name === "image") {
110
- const image = block.attributes.image as {
111
- url?: string;
112
- sourceUrl?: string;
113
- alt?: string;
114
- altText?: string;
115
- } | undefined;
116
- const src = image?.url ?? image?.sourceUrl;
117
- return src ? (
118
- <S as="div" {...semantic} {...htmlAttributes}>
119
- <img
120
- src={src}
121
- alt={String(block.attributes.alt ?? image?.alt ?? image?.altText ?? "")}
122
- loading="lazy"
123
- decoding="async"
124
- />
125
- </S>
168
+ if (name === "icon") {
169
+ const icon = String(block.attributes.icon ?? "").trim();
170
+ const display = block.attributes.display === "block" ? "block" : "inline";
171
+ const numericSize = Number(block.attributes.size ?? 4);
172
+ const size = Number.isFinite(numericSize) && numericSize > 0
173
+ ? numericSize
174
+ : 4;
175
+ const iconSemantic = semanticProps({
176
+ foreground: block.attributes.foreground,
177
+ tone: block.attributes.tone,
178
+ intensity: block.attributes.intensity
179
+ }, contract, {});
180
+ return IconRenderer && icon ? (
181
+ <IconRenderer
182
+ name={icon}
183
+ display={display}
184
+ size={size}
185
+ label={String(block.attributes.label ?? "").trim() || undefined}
186
+ {...iconSemantic}
187
+ {...htmlAttributes}
188
+ />
189
+ ) : null;
190
+ }
191
+
192
+ if (name === "code") {
193
+ return block.attributes.codeApproved === true ? (
194
+ <div
195
+ data-flowpress-trusted-code
196
+ {...htmlAttributes}
197
+ set:html={String(block.attributes.document ?? "")}
198
+ />
126
199
  ) : null;
127
200
  }
128
201
 
202
+ if (name === "image") {
203
+ return <ImageBlock block={block} contract={contract} />;
204
+ }
205
+
129
206
  if (name === "gallery") {
130
207
  const images = Array.isArray(block.attributes.images)
131
208
  ? block.attributes.images as Array<Record<string, unknown>>
@@ -211,7 +288,12 @@ function youtubeEmbedUrl(value: unknown): string | null {
211
288
  {items.map((item, index) => (
212
289
  <S as="li">
213
290
  {resolvedBlocks[index]?.length ? (
214
- <Astro.self blocks={resolvedBlocks[index]} contract={contract} />
291
+ <Astro.self
292
+ blocks={resolvedBlocks[index]}
293
+ contract={contract}
294
+ blockRenderers={blockRenderers}
295
+ iconRenderer={IconRenderer}
296
+ />
215
297
  ) : (
216
298
  <ListItemFallback item={item} />
217
299
  )}
@@ -224,10 +306,20 @@ function youtubeEmbedUrl(value: unknown): string | null {
224
306
  if (name === "template") {
225
307
  return Object.keys(htmlAttributes).length > 0 ? (
226
308
  <div {...htmlAttributes}>
227
- <Astro.self blocks={children} contract={contract} />
309
+ <Astro.self
310
+ blocks={children}
311
+ contract={contract}
312
+ blockRenderers={blockRenderers}
313
+ iconRenderer={IconRenderer}
314
+ />
228
315
  </div>
229
316
  ) : (
230
- <Astro.self blocks={children} contract={contract} />
317
+ <Astro.self
318
+ blocks={children}
319
+ contract={contract}
320
+ blockRenderers={blockRenderers}
321
+ iconRenderer={IconRenderer}
322
+ />
231
323
  );
232
324
  }
233
325
 
@@ -240,7 +332,12 @@ function youtubeEmbedUrl(value: unknown): string | null {
240
332
  <S as="div" {...semantic} {...htmlAttributes}>
241
333
  <details>
242
334
  <summary>{String(block.attributes.title ?? "")}</summary>
243
- <Astro.self blocks={children} contract={contract} />
335
+ <Astro.self
336
+ blocks={children}
337
+ contract={contract}
338
+ blockRenderers={blockRenderers}
339
+ iconRenderer={IconRenderer}
340
+ />
244
341
  </details>
245
342
  </S>
246
343
  );
@@ -291,7 +388,14 @@ function youtubeEmbedUrl(value: unknown): string | null {
291
388
  <S as={layoutTag} {...semantic} {...htmlAttributes}>
292
389
  {block.innerHtml && children.length === 0
293
390
  ? <S as="div" set:html={block.innerHtml} />
294
- : <Astro.self blocks={children} contract={contract} />}
391
+ : (
392
+ <Astro.self
393
+ blocks={children}
394
+ contract={contract}
395
+ blockRenderers={blockRenderers}
396
+ iconRenderer={IconRenderer}
397
+ />
398
+ )}
295
399
  </S>
296
400
  );
297
401
  })}
@@ -25,7 +25,7 @@ const align = ["start", "center", "end", "stretch"].includes(
25
25
  const semantic = semanticProps(block.attributes, contract, {
26
26
  surface: "default",
27
27
  layoutRole: "container",
28
- density: "md"
28
+ density: "regular"
29
29
  });
30
30
  const htmlAttributes = blockHtmlAttributes(block.attributes, "flowpress-grid");
31
31
  const style = [
@@ -4,6 +4,7 @@ import type { FlowpressBlock } from "../../types.js";
4
4
  import type { StyleflowContract } from "../../styleflow.js";
5
5
  import {
6
6
  blockHtmlAttributes,
7
+ heroObjectPosition,
7
8
  objectValue,
8
9
  semanticProps
9
10
  } from "./helpers.js";
@@ -18,7 +19,7 @@ const attributes = block.attributes;
18
19
  const semantic = semanticProps(attributes, contract, {
19
20
  surface: "default",
20
21
  layoutRole: "region",
21
- density: "md"
22
+ density: "regular"
22
23
  });
23
24
  const htmlAttributes = blockHtmlAttributes(attributes, "flowpress-hero");
24
25
  const backgroundType = ["color", "image", "video"].includes(
@@ -28,6 +29,7 @@ const image = objectValue(block, "backgroundImage");
28
29
  const video = objectValue(block, "backgroundVideo");
29
30
  const imageSrc = String(image.url ?? image.sourceUrl ?? "");
30
31
  const videoSrc = String(video.url ?? video.sourceUrl ?? video.mediaItemUrl ?? "");
32
+ const objectPosition = heroObjectPosition(attributes.objectPosition);
31
33
  const scrimEnabled = Boolean(attributes.scrimEnabled) && backgroundType !== "color";
32
34
  const scrimIntensity = Math.min(10, Math.max(1, Number(attributes.scrimIntensity) || 6));
33
35
  ---
@@ -36,7 +38,10 @@ const scrimIntensity = Math.min(10, Math.max(1, Number(attributes.scrimIntensity
36
38
  as="section"
37
39
  {...semantic}
38
40
  {...htmlAttributes}
39
- style={`--flowpress-scrim-opacity:${scrimIntensity * 10}%`}
41
+ style={[
42
+ `--flowpress-hero-object-position:${objectPosition}`,
43
+ `--flowpress-scrim-opacity:${scrimIntensity * 10}%`
44
+ ].join(";")}
40
45
  >
41
46
  {backgroundType === "image" && imageSrc ? (
42
47
  <img
@@ -87,6 +92,7 @@ const scrimIntensity = Math.min(10, Math.max(1, Number(attributes.scrimIntensity
87
92
  .flowpress-hero__media {
88
93
  z-index: -2;
89
94
  object-fit: cover;
95
+ object-position: var(--flowpress-hero-object-position);
90
96
  }
91
97
 
92
98
  .flowpress-hero__scrim {