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

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/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.0-beta.22
6
+
7
+ - Added a dedicated `sf/image` renderer with WordPress responsive `srcset`,
8
+ authored or attachment-default `sizes`, intrinsic dimensions, per-image CSS
9
+ classes and validated loading, decoding and fetch-priority controls.
10
+ - Included per-image CSS classes in the recursive CMS class collector used by
11
+ consumer Tailwind safelists.
12
+
5
13
  ## 0.2.0-beta.21
6
14
 
7
15
  - Forced locale-aware thousands grouping for four-digit number spinner values,
package/README.md CHANGED
@@ -48,6 +48,12 @@ Hero image and video backgrounds apply the catalog `objectPosition` value as
48
48
  two percentages (for example `77% 100%`). Invalid or missing CMS values safely
49
49
  fall back to `50% 50%`.
50
50
 
51
+ `sf/image` renders the responsive attachment data serialized by WordPress as a
52
+ single `<img>` with `srcset`, `sizes` and positive intrinsic dimensions. The
53
+ block-root `className` remains on its StyleFlow wrapper while `imageClassName`
54
+ targets the image. Loading defaults to `lazy`, decoding to `async` and fetch
55
+ priority to `auto`; invalid stored values fail closed to those defaults.
56
+
51
57
  Projects can add renderers for project-owned Gutenberg blocks without copying
52
58
  or patching the core renderer. Pass a `FlowpressBlockRenderers` map keyed by the
53
59
  full block name; the registry is propagated through every recursive rendering
@@ -7,6 +7,7 @@ import ColumnBlock from "./blocks/ColumnBlock.astro";
7
7
  import FormBlock from "./blocks/FormBlock.astro";
8
8
  import GridBlock from "./blocks/GridBlock.astro";
9
9
  import HeroBlock from "./blocks/HeroBlock.astro";
10
+ import ImageBlock from "./blocks/ImageBlock.astro";
10
11
  import ListItemFallback from "./blocks/ListItemFallback.astro";
11
12
  import NumberSpinnerBlock from "./blocks/NumberSpinnerBlock.astro";
12
13
  import TextBlock from "./blocks/TextBlock.astro";
@@ -143,23 +144,7 @@ function youtubeEmbedUrl(value: unknown): string | null {
143
144
  }
144
145
 
145
146
  if (name === "image") {
146
- const image = block.attributes.image as {
147
- url?: string;
148
- sourceUrl?: string;
149
- alt?: string;
150
- altText?: string;
151
- } | undefined;
152
- const src = image?.url ?? image?.sourceUrl;
153
- return src ? (
154
- <S as="div" {...semantic} {...htmlAttributes}>
155
- <img
156
- src={src}
157
- alt={String(block.attributes.alt ?? image?.alt ?? image?.altText ?? "")}
158
- loading="lazy"
159
- decoding="async"
160
- />
161
- </S>
162
- ) : null;
147
+ return <ImageBlock block={block} contract={contract} />;
163
148
  }
164
149
 
165
150
  if (name === "gallery") {
@@ -0,0 +1,90 @@
1
+ ---
2
+ import S from "@styleflow.app/astro/S.astro";
3
+ import type { StyleflowContract } from "../../styleflow.js";
4
+ import type { FlowpressBlock } from "../../types.js";
5
+ import {
6
+ blockHtmlAttributes,
7
+ semanticProps
8
+ } from "./helpers.js";
9
+
10
+ interface Props {
11
+ block: FlowpressBlock;
12
+ contract?: StyleflowContract;
13
+ }
14
+
15
+ type ImageLoading = "eager" | "lazy";
16
+ type ImageDecoding = "async" | "auto" | "sync";
17
+ type ImageFetchPriority = "auto" | "high" | "low";
18
+
19
+ const { block, contract } = Astro.props;
20
+ const attributes = block.attributes;
21
+ const media = attributes.image && typeof attributes.image === "object"
22
+ ? attributes.image as Record<string, unknown>
23
+ : {};
24
+
25
+ function text(value: unknown): string {
26
+ return typeof value === "string" ? value.trim() : "";
27
+ }
28
+
29
+ function positiveInteger(value: unknown): number | undefined {
30
+ const number = typeof value === "number" ? value : Number(value);
31
+ return Number.isInteger(number) && number > 0 ? number : undefined;
32
+ }
33
+
34
+ function loadingValue(value: unknown): ImageLoading {
35
+ return value === "eager" ? "eager" : "lazy";
36
+ }
37
+
38
+ function decodingValue(value: unknown): ImageDecoding {
39
+ return ["async", "auto", "sync"].includes(String(value))
40
+ ? String(value) as ImageDecoding
41
+ : "async";
42
+ }
43
+
44
+ function fetchPriorityValue(value: unknown): ImageFetchPriority {
45
+ return ["auto", "high", "low"].includes(String(value))
46
+ ? String(value) as ImageFetchPriority
47
+ : "auto";
48
+ }
49
+
50
+ const src = text(media.url) || text(media.sourceUrl);
51
+ const srcSet = text(media.srcSet) || text(media.srcset);
52
+ const sizes = srcSet
53
+ ? text(attributes.sizes) || text(media.sizes) || undefined
54
+ : undefined;
55
+ const alt = typeof attributes.alt === "string"
56
+ ? attributes.alt
57
+ : typeof media.alt === "string"
58
+ ? media.alt
59
+ : typeof media.altText === "string"
60
+ ? media.altText
61
+ : "";
62
+ const imageClassName = text(attributes.imageClassName)
63
+ .replace(/\s+/g, " ") || undefined;
64
+ const width = positiveInteger(media.width);
65
+ const height = positiveInteger(media.height);
66
+ const loading = loadingValue(attributes.loading);
67
+ const decoding = decodingValue(attributes.decoding);
68
+ const fetchPriority = fetchPriorityValue(attributes.fetchPriority);
69
+ ---
70
+
71
+ {src ? (
72
+ <S
73
+ as="div"
74
+ {...semanticProps(attributes, contract)}
75
+ {...blockHtmlAttributes(attributes)}
76
+ >
77
+ <img
78
+ src={src}
79
+ srcset={srcSet || undefined}
80
+ sizes={sizes}
81
+ width={width}
82
+ height={height}
83
+ alt={alt}
84
+ class={imageClassName}
85
+ loading={loading}
86
+ decoding={decoding}
87
+ fetchpriority={fetchPriority}
88
+ />
89
+ </S>
90
+ ) : null}
@@ -1 +1 @@
1
- {"version":3,"file":"style-classes.d.ts","sourceRoot":"","sources":["../src/style-classes.ts"],"names":[],"mappings":"AAoBA;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,CA8BpE"}
1
+ {"version":3,"file":"style-classes.d.ts","sourceRoot":"","sources":["../src/style-classes.ts"],"names":[],"mappings":"AAoBA;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,CAgCpE"}
@@ -35,8 +35,10 @@ export function collectFlowpressClassNames(blocks) {
35
35
  }
36
36
  const block = value;
37
37
  if (block.attributes && typeof block.attributes === "object") {
38
- for (const token of classTokens(block.attributes.className)) {
39
- classes.add(token);
38
+ for (const attribute of ["className", "imageClassName"]) {
39
+ for (const token of classTokens(block.attributes[attribute])) {
40
+ classes.add(token);
41
+ }
40
42
  }
41
43
  }
42
44
  Object.values(value).forEach(visit);
@@ -1 +1 @@
1
- {"version":3,"file":"style-classes.js","sourceRoot":"","sources":["../src/style-classes.ts"],"names":[],"mappings":"AAEA,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,uBAAuB,GAAG,8BAA8B,CAAC;AAE/D,SAAS,WAAW,CAAC,KAAc;IACjC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,KAAK;SACT,KAAK,CAAC,KAAK,CAAC;SACZ,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACjB,KAAK,CAAC,MAAM,GAAG,CAAC;WACb,KAAK,CAAC,MAAM,IAAI,gBAAgB;WAChC,CAAC,uBAAuB,CAAC,IAAI,CAAC,KAAK,CAAC,CACxC,CAAC,CAAC;AACP,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B,CAAC,MAAe;IACxD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,OAAO,GAAG,IAAI,OAAO,EAAU,CAAC;IAEtC,MAAM,KAAK,GAAG,CAAC,KAAc,EAAQ,EAAE;QACrC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACxC,OAAO;QACT,CAAC;QACD,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO;QACT,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAEnB,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,KAAgC,CAAC;QAC/C,IAAI,KAAK,CAAC,UAAU,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;YAC7D,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC5D,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC;QACH,CAAC;QAED,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC;IAEF,KAAK,CAAC,MAAM,CAAC,CAAC;IACd,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;AACvE,CAAC","sourcesContent":["import type { FlowpressBlock } from \"./types.js\";\n\nconst MAX_CLASS_LENGTH = 256;\nconst UNSAFE_CLASS_CHARACTERS = /[\\s\\\\{};\\u0000-\\u001f\\u007f]/;\n\nfunction classTokens(value: unknown): string[] {\n if (typeof value !== \"string\") {\n return [];\n }\n\n return value\n .split(/\\s+/)\n .map((token) => token.trim())\n .filter((token) => (\n token.length > 0\n && token.length <= MAX_CLASS_LENGTH\n && !UNSAFE_CLASS_CHARACTERS.test(token)\n ));\n}\n\n/**\n * Collect class names authored in Gutenberg from serialized FlowPress blocks.\n *\n * The traversal includes resolved template/list blocks stored inside\n * attributes as well as ordinary inner blocks. Tokens that could terminate a\n * generated Tailwind `@source inline()` directive are discarded.\n */\nexport function collectFlowpressClassNames(blocks: unknown): string[] {\n const classes = new Set<string>();\n const visited = new WeakSet<object>();\n\n const visit = (value: unknown): void => {\n if (!value || typeof value !== \"object\") {\n return;\n }\n if (visited.has(value)) {\n return;\n }\n visited.add(value);\n\n if (Array.isArray(value)) {\n value.forEach(visit);\n return;\n }\n\n const block = value as Partial<FlowpressBlock>;\n if (block.attributes && typeof block.attributes === \"object\") {\n for (const token of classTokens(block.attributes.className)) {\n classes.add(token);\n }\n }\n\n Object.values(value).forEach(visit);\n };\n\n visit(blocks);\n return [...classes].sort((left, right) => left.localeCompare(right));\n}\n"]}
1
+ {"version":3,"file":"style-classes.js","sourceRoot":"","sources":["../src/style-classes.ts"],"names":[],"mappings":"AAEA,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,uBAAuB,GAAG,8BAA8B,CAAC;AAE/D,SAAS,WAAW,CAAC,KAAc;IACjC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,KAAK;SACT,KAAK,CAAC,KAAK,CAAC;SACZ,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACjB,KAAK,CAAC,MAAM,GAAG,CAAC;WACb,KAAK,CAAC,MAAM,IAAI,gBAAgB;WAChC,CAAC,uBAAuB,CAAC,IAAI,CAAC,KAAK,CAAC,CACxC,CAAC,CAAC;AACP,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B,CAAC,MAAe;IACxD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,MAAM,OAAO,GAAG,IAAI,OAAO,EAAU,CAAC;IAEtC,MAAM,KAAK,GAAG,CAAC,KAAc,EAAQ,EAAE;QACrC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACxC,OAAO;QACT,CAAC;QACD,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO;QACT,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAEnB,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACrB,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,KAAgC,CAAC;QAC/C,IAAI,KAAK,CAAC,UAAU,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;YAC7D,KAAK,MAAM,SAAS,IAAI,CAAC,WAAW,EAAE,gBAAgB,CAAU,EAAE,CAAC;gBACjE,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;oBAC7D,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;gBACrB,CAAC;YACH,CAAC;QACH,CAAC;QAED,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC;IAEF,KAAK,CAAC,MAAM,CAAC,CAAC;IACd,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;AACvE,CAAC","sourcesContent":["import type { FlowpressBlock } from \"./types.js\";\n\nconst MAX_CLASS_LENGTH = 256;\nconst UNSAFE_CLASS_CHARACTERS = /[\\s\\\\{};\\u0000-\\u001f\\u007f]/;\n\nfunction classTokens(value: unknown): string[] {\n if (typeof value !== \"string\") {\n return [];\n }\n\n return value\n .split(/\\s+/)\n .map((token) => token.trim())\n .filter((token) => (\n token.length > 0\n && token.length <= MAX_CLASS_LENGTH\n && !UNSAFE_CLASS_CHARACTERS.test(token)\n ));\n}\n\n/**\n * Collect class names authored in Gutenberg from serialized FlowPress blocks.\n *\n * The traversal includes resolved template/list blocks stored inside\n * attributes as well as ordinary inner blocks. Tokens that could terminate a\n * generated Tailwind `@source inline()` directive are discarded.\n */\nexport function collectFlowpressClassNames(blocks: unknown): string[] {\n const classes = new Set<string>();\n const visited = new WeakSet<object>();\n\n const visit = (value: unknown): void => {\n if (!value || typeof value !== \"object\") {\n return;\n }\n if (visited.has(value)) {\n return;\n }\n visited.add(value);\n\n if (Array.isArray(value)) {\n value.forEach(visit);\n return;\n }\n\n const block = value as Partial<FlowpressBlock>;\n if (block.attributes && typeof block.attributes === \"object\") {\n for (const attribute of [\"className\", \"imageClassName\"] as const) {\n for (const token of classTokens(block.attributes[attribute])) {\n classes.add(token);\n }\n }\n }\n\n Object.values(value).forEach(visit);\n };\n\n visit(blocks);\n return [...classes].sort((left, right) => left.localeCompare(right));\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@styleflow.app/flowpress-astro-core",
3
- "version": "0.2.0-beta.21",
3
+ "version": "0.2.0-beta.22",
4
4
  "description": "Project-agnostic WordPress routing, templates, SEO and GraphQL runtime for FlowPress Astro apps.",
5
5
  "type": "module",
6
6
  "private": false,