@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
@@ -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}
@@ -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>
@@ -4,8 +4,10 @@ import type { FlowpressBlock } from "../../types.js";
4
4
  import type { StyleflowContract } from "../../styleflow.js";
5
5
  import {
6
6
  blockHtmlAttributes,
7
+ preserveEmptyTextBlock,
7
8
  preserveRichTextSpacing,
8
9
  semanticProps,
10
+ textAlignmentStyle,
9
11
  textValue,
10
12
  unwrapCoreHtml
11
13
  } from "./helpers.js";
@@ -37,29 +39,39 @@ const defaults = heading
37
39
  };
38
40
  const semantic = semanticProps(attributes, contract, defaults);
39
41
  const htmlAttributes = blockHtmlAttributes(attributes);
40
- const html = name === "heading"
42
+ const alignmentStyle = textAlignmentStyle(attributes);
43
+ const coreHtml = String(
44
+ attributes.content
45
+ ?? attributes.value
46
+ ?? block.innerHtml
47
+ ?? ""
48
+ );
49
+ const normalizedHtml = name === "heading"
41
50
  ? textValue(block)
42
51
  : name === "rich-text"
43
52
  ? preserveRichTextSpacing(String(attributes.content ?? ""))
44
53
  : name === "quote"
45
54
  ? textValue(block, "quote")
46
- : unwrapCoreHtml(String(attributes.content ?? attributes.value ?? block.innerHtml ?? ""));
55
+ : unwrapCoreHtml(preserveRichTextSpacing(coreHtml));
56
+ const html = name === "core/list"
57
+ ? normalizedHtml
58
+ : preserveEmptyTextBlock(normalizedHtml);
47
59
  const listTag = Boolean(attributes.ordered) ? "ol" : "ul";
48
60
  ---
49
61
 
50
62
  {heading ? (
51
- <S as={headingTag} {...semantic} {...htmlAttributes} set:html={html} />
63
+ <S as={headingTag} {...semantic} {...htmlAttributes} style={alignmentStyle} set:html={html} />
52
64
  ) : name === "core/list" ? (
53
- <S as={listTag} {...semantic} {...htmlAttributes}>
65
+ <S as={listTag} {...semantic} {...htmlAttributes} style={alignmentStyle}>
54
66
  {hasChildren ? <slot /> : <Fragment set:html={html} />}
55
67
  </S>
56
68
  ) : name === "core/list-item" ? (
57
- <S as="li" {...semantic} {...htmlAttributes}>
69
+ <S as="li" {...semantic} {...htmlAttributes} style={alignmentStyle}>
58
70
  <span set:html={html}></span>
59
71
  {hasChildren ? <slot /> : null}
60
72
  </S>
61
73
  ) : name === "quote" || name === "core/quote" ? (
62
- <S as="blockquote" {...semantic} {...htmlAttributes}>
74
+ <S as="blockquote" {...semantic} {...htmlAttributes} style={alignmentStyle}>
63
75
  {hasChildren ? <slot /> : <div set:html={html}></div>}
64
76
  {attributes.citation ? <cite>{String(attributes.citation)}</cite> : null}
65
77
  </S>
@@ -68,6 +80,7 @@ const listTag = Boolean(attributes.ordered) ? "ol" : "ul";
68
80
  as={name === "core/paragraph" ? "p" : "div"}
69
81
  {...semantic}
70
82
  {...htmlAttributes}
83
+ style={alignmentStyle}
71
84
  set:html={html}
72
85
  />
73
86
  )}
@@ -5,11 +5,19 @@ export declare const DEFAULT_SEMANTIC_PROPS: {
5
5
  layoutRole: string;
6
6
  density: string;
7
7
  };
8
+ export declare const DEFAULT_HERO_OBJECT_POSITION = "50% 50%";
9
+ export declare function heroObjectPosition(value: unknown): string;
8
10
  export declare function blockHtmlAttributes(attributes: Record<string, unknown>, baseClass?: string): {
9
11
  id?: string;
10
12
  class?: string;
11
13
  };
14
+ export declare const TEXT_HTML_TAGS: readonly ["span", "p", "h1", "h2", "h3", "h4", "h5", "h6", "strong", "em", "small", "mark"];
15
+ export type TextHtmlTag = (typeof TEXT_HTML_TAGS)[number];
16
+ export declare function textHtmlTag(value: unknown): TextHtmlTag;
12
17
  export declare function semanticProps(attributes: Record<string, unknown>, contract?: StyleflowContract, defaults?: Record<string, unknown>): Record<string, unknown>;
18
+ /** Resolve only Gutenberg's supported text-alignment values to inline CSS. */
19
+ export declare function textAlignmentStyle(attributes: Record<string, unknown>): string | undefined;
20
+ export declare function prefixedTextSemanticProps(attributes: Record<string, unknown>, prefix: string, contract?: StyleflowContract, defaults?: Record<string, unknown>): Record<string, unknown>;
13
21
  export declare function objectValue(block: FlowpressBlock, key: string): Record<string, unknown>;
14
22
  export declare function textValue(block: FlowpressBlock, key?: string): string;
15
23
  /**
@@ -18,6 +26,8 @@ export declare function textValue(block: FlowpressBlock, key?: string): string;
18
26
  * empty paragraph elements, or literal newlines in an attribute value.
19
27
  */
20
28
  export declare function preserveRichTextSpacing(html: string): string;
29
+ /** Keep an empty text element at its typographic line height without visible text. */
30
+ export declare function preserveEmptyTextBlock(html: string): string;
21
31
  export declare function linkValue(block: FlowpressBlock, key?: string): string;
22
32
  export declare function responsiveValues(attributes: Record<string, unknown>, prefix: "columns" | "span", fallback: number): Record<"base" | "sm" | "md" | "lg" | "xl" | "2xl", number>;
23
33
  export declare function unwrapCoreHtml(html: string): string;
@@ -1 +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,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"}
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,eAAO,MAAM,4BAA4B,YAAY,CAAC;AAEtD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAmBzD;AAED,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;AAsBD,eAAO,MAAM,cAAc,6FAajB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1D,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAKvD;AAED,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,CAoBzB;AAWD,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,MAAM,GAAG,SAAS,CAYpB;AAED,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACrC,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"}
@@ -2,8 +2,23 @@ import { resolveDynamicStyleflowProps } from "../../styleflow.js";
2
2
  export const DEFAULT_SEMANTIC_PROPS = {
3
3
  surface: "default",
4
4
  layoutRole: "stack",
5
- density: "md"
5
+ density: "regular"
6
6
  };
7
+ export const DEFAULT_HERO_OBJECT_POSITION = "50% 50%";
8
+ export function heroObjectPosition(value) {
9
+ if (typeof value !== "string") {
10
+ return DEFAULT_HERO_OBJECT_POSITION;
11
+ }
12
+ const match = value.trim().match(/^(\d+(?:\.\d+)?|\.\d+)%\s+(\d+(?:\.\d+)?|\.\d+)%$/);
13
+ if (!match
14
+ || [match[1], match[2]].some((coordinate) => {
15
+ const numeric = Number(coordinate);
16
+ return !Number.isFinite(numeric) || numeric < 0 || numeric > 100;
17
+ })) {
18
+ return DEFAULT_HERO_OBJECT_POSITION;
19
+ }
20
+ return `${match[1]}% ${match[2]}%`;
21
+ }
7
22
  export function blockHtmlAttributes(attributes, baseClass = "") {
8
23
  const anchor = typeof attributes.anchor === "string"
9
24
  ? attributes.anchor.trim()
@@ -21,6 +36,7 @@ export function blockHtmlAttributes(attributes, baseClass = "") {
21
36
  };
22
37
  }
23
38
  const SEMANTIC_KEYS = [
39
+ "theme",
24
40
  "tone",
25
41
  "intensity",
26
42
  "surface",
@@ -38,13 +54,74 @@ const SEMANTIC_KEYS = [
38
54
  "noRadius",
39
55
  "noForeground"
40
56
  ];
57
+ export const TEXT_HTML_TAGS = [
58
+ "span",
59
+ "p",
60
+ "h1",
61
+ "h2",
62
+ "h3",
63
+ "h4",
64
+ "h5",
65
+ "h6",
66
+ "strong",
67
+ "em",
68
+ "small",
69
+ "mark"
70
+ ];
71
+ export function textHtmlTag(value) {
72
+ const candidate = String(value ?? "span");
73
+ return TEXT_HTML_TAGS.includes(candidate)
74
+ ? candidate
75
+ : "span";
76
+ }
41
77
  export function semanticProps(attributes, contract, defaults = DEFAULT_SEMANTIC_PROPS) {
42
78
  const input = Object.fromEntries(SEMANTIC_KEYS
43
79
  .filter((key) => attributes[key] !== undefined)
44
80
  .map((key) => [key, attributes[key]]));
45
- return contract
46
- ? resolveDynamicStyleflowProps(input, defaults, contract)
47
- : { ...defaults, ...input };
81
+ if (contract) {
82
+ return resolveDynamicStyleflowProps(input, defaults, contract, {
83
+ strict: process.env.NODE_ENV === "production" || process.env.STYLEFLOW_STRICT === "1",
84
+ onDiagnostic: (diagnostic) => console.warn(`[${diagnostic.code}] ${diagnostic.message}`)
85
+ });
86
+ }
87
+ const resolved = { ...defaults, ...input };
88
+ for (const key of ["tone", "intensity", "foreground"]) {
89
+ if (input[key] === "inherit") {
90
+ delete resolved[key];
91
+ }
92
+ }
93
+ return resolved;
94
+ }
95
+ const TEXT_ALIGNMENTS = new Set([
96
+ "left",
97
+ "center",
98
+ "right",
99
+ "justify",
100
+ "start",
101
+ "end"
102
+ ]);
103
+ /** Resolve only Gutenberg's supported text-alignment values to inline CSS. */
104
+ export function textAlignmentStyle(attributes) {
105
+ const style = attributes.style && typeof attributes.style === "object"
106
+ && !Array.isArray(attributes.style)
107
+ ? attributes.style
108
+ : {};
109
+ const typography = style.typography && typeof style.typography === "object"
110
+ && !Array.isArray(style.typography)
111
+ ? style.typography
112
+ : {};
113
+ const candidate = typography.textAlign ?? attributes.textAlign;
114
+ const alignment = String(candidate ?? "");
115
+ return TEXT_ALIGNMENTS.has(alignment) ? `text-align: ${alignment};` : undefined;
116
+ }
117
+ export function prefixedTextSemanticProps(attributes, prefix, contract, defaults = {}) {
118
+ const attributeName = (name) => `${prefix}${name.charAt(0).toUpperCase()}${name.slice(1)}`;
119
+ return semanticProps({
120
+ ty: attributes[attributeName("ty")],
121
+ v: attributes[attributeName("v")],
122
+ w: attributes[attributeName("w")],
123
+ foreground: attributes[attributeName("foreground")]
124
+ }, contract, defaults);
48
125
  }
49
126
  export function objectValue(block, key) {
50
127
  const value = block.attributes[key];
@@ -63,7 +140,7 @@ export function textValue(block, key = "text") {
63
140
  export function preserveRichTextSpacing(html) {
64
141
  const visibleEmptyBlocks = html
65
142
  .replace(/\r\n?/g, "\n")
66
- .replace(/<(p|div)(\s[^>]*)?>(?:\s|&nbsp;|&#160;)*<\/\1>/gi, (_match, tag, attributes = "") => `<${tag}${attributes}><br aria-hidden="true"></${tag}>`);
143
+ .replace(/<(p|div)(\s[^>]*)?>(?:\s|&nbsp;|&#160;)*<\/\1>/gi, (_match, tag, attributes = "") => `<${tag}${attributes}>&nbsp;</${tag}>`);
67
144
  return visibleEmptyBlocks
68
145
  .split(/(<[^>]+>)/g)
69
146
  .map((part) => {
@@ -74,6 +151,10 @@ export function preserveRichTextSpacing(html) {
74
151
  })
75
152
  .join("");
76
153
  }
154
+ /** Keep an empty text element at its typographic line height without visible text. */
155
+ export function preserveEmptyTextBlock(html) {
156
+ return html.trim() === "" ? "&nbsp;" : html;
157
+ }
77
158
  export function linkValue(block, key = "link") {
78
159
  const value = block.attributes[key];
79
160
  const href = typeof value === "string"
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../../src/astro/blocks/helpers.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n resolveDynamicStyleflowProps,\n type StyleflowContract\n} from \"../../styleflow.js\";\nimport type { FlowpressBlock } from \"../../types.js\";\n\nexport const DEFAULT_SEMANTIC_PROPS = {\n surface: \"default\",\n layoutRole: \"stack\",\n density: \"md\"\n};\n\nexport function blockHtmlAttributes(\n attributes: Record<string, unknown>,\n baseClass = \"\"\n): { id?: string; class?: string } {\n const anchor = typeof attributes.anchor === \"string\"\n ? attributes.anchor.trim()\n : \"\";\n const customClass = typeof attributes.className === \"string\"\n ? attributes.className.trim()\n : \"\";\n const className = [baseClass.trim(), customClass]\n .filter(Boolean)\n .join(\" \")\n .replace(/\\s+/g, \" \");\n\n return {\n ...(anchor ? { id: anchor } : {}),\n ...(className ? { class: className } : {})\n };\n}\n\nconst SEMANTIC_KEYS = [\n \"tone\",\n \"intensity\",\n \"surface\",\n \"layoutRole\",\n \"density\",\n \"ty\",\n \"v\",\n \"w\",\n \"foreground\",\n \"interactivePriority\",\n \"noBackground\",\n \"noBorder\",\n \"noPadding\",\n \"noGap\",\n \"noRadius\",\n \"noForeground\"\n] as const;\n\nexport function semanticProps(\n attributes: Record<string, unknown>,\n contract?: StyleflowContract,\n defaults: Record<string, unknown> = DEFAULT_SEMANTIC_PROPS\n): Record<string, unknown> {\n const input = Object.fromEntries(\n SEMANTIC_KEYS\n .filter((key) => attributes[key] !== undefined)\n .map((key) => [key, attributes[key]])\n );\n return contract\n ? resolveDynamicStyleflowProps(input, defaults, contract)\n : { ...defaults, ...input };\n}\n\nexport function objectValue(\n block: FlowpressBlock,\n key: string\n): Record<string, unknown> {\n const value = block.attributes[key];\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? value as Record<string, unknown>\n : {};\n}\n\nexport function textValue(block: FlowpressBlock, key = \"text\"): string {\n return String(block.attributes[key] ?? \"\");\n}\n\n/**\n * Keep intentional blank lines visible across current and legacy RichText\n * serializations. Gutenberg can represent them either as repeated `<br>` tags,\n * empty paragraph elements, or literal newlines in an attribute value.\n */\nexport function preserveRichTextSpacing(html: string): string {\n const visibleEmptyBlocks = html\n .replace(/\\r\\n?/g, \"\\n\")\n .replace(\n /<(p|div)(\\s[^>]*)?>(?:\\s|&nbsp;|&#160;)*<\\/\\1>/gi,\n (_match, tag: string, attributes = \"\") =>\n `<${tag}${attributes}><br aria-hidden=\"true\"></${tag}>`\n );\n\n return visibleEmptyBlocks\n .split(/(<[^>]+>)/g)\n .map((part) => {\n if (part.startsWith(\"<\") || part.trim() === \"\") {\n return part;\n }\n return part.replace(/\\n(?:[ \\t]*\\n)+/g, (newlines) =>\n \"<br>\".repeat((newlines.match(/\\n/g) || []).length)\n );\n })\n .join(\"\");\n}\n\nexport function linkValue(block: FlowpressBlock, key = \"link\"): string {\n const value = block.attributes[key];\n const href = typeof value === \"string\"\n ? value\n : String(objectValue(block, key).url ?? \"\");\n return /^(?:\\/(?!\\/)|#|https?:\\/\\/|mailto:|tel:)/i.test(href) ? href : \"\";\n}\n\nexport function responsiveValues(\n attributes: Record<string, unknown>,\n prefix: \"columns\" | \"span\",\n fallback: number\n): Record<\"base\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\", number> {\n const keys = [\"base\", \"sm\", \"md\", \"lg\", \"xl\", \"2xl\"] as const;\n const result = {} as Record<(typeof keys)[number], number>;\n let inherited = fallback;\n for (const key of keys) {\n const suffix = key === \"base\"\n ? \"Base\"\n : key === \"2xl\" ? \"2xl\" : key.charAt(0).toUpperCase() + key.slice(1);\n const candidate = Number(attributes[`${prefix}${suffix}`]);\n if (Number.isInteger(candidate) && candidate >= 1 && candidate <= 12) {\n inherited = candidate;\n }\n result[key] = inherited;\n }\n return result;\n}\n\nexport function unwrapCoreHtml(html: string): string {\n const match = html.trim().match(\n /^<(p|h[1-6]|ul|ol|li|blockquote)(?:\\s[^>]*)?>([\\s\\S]*)<\\/\\1>$/\n );\n return match?.[2] ?? html;\n}\n"]}
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../../src/astro/blocks/helpers.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n resolveDynamicStyleflowProps,\n type StyleflowContract\n} from \"../../styleflow.js\";\nimport type { FlowpressBlock } from \"../../types.js\";\n\nexport const DEFAULT_SEMANTIC_PROPS = {\n surface: \"default\",\n layoutRole: \"stack\",\n density: \"regular\"\n};\n\nexport const DEFAULT_HERO_OBJECT_POSITION = \"50% 50%\";\n\nexport function heroObjectPosition(value: unknown): string {\n if (typeof value !== \"string\") {\n return DEFAULT_HERO_OBJECT_POSITION;\n }\n\n const match = value.trim().match(\n /^(\\d+(?:\\.\\d+)?|\\.\\d+)%\\s+(\\d+(?:\\.\\d+)?|\\.\\d+)%$/\n );\n if (\n !match\n || [match[1], match[2]].some((coordinate) => {\n const numeric = Number(coordinate);\n return !Number.isFinite(numeric) || numeric < 0 || numeric > 100;\n })\n ) {\n return DEFAULT_HERO_OBJECT_POSITION;\n }\n\n return `${match[1]}% ${match[2]}%`;\n}\n\nexport function blockHtmlAttributes(\n attributes: Record<string, unknown>,\n baseClass = \"\"\n): { id?: string; class?: string } {\n const anchor = typeof attributes.anchor === \"string\"\n ? attributes.anchor.trim()\n : \"\";\n const customClass = typeof attributes.className === \"string\"\n ? attributes.className.trim()\n : \"\";\n const className = [baseClass.trim(), customClass]\n .filter(Boolean)\n .join(\" \")\n .replace(/\\s+/g, \" \");\n\n return {\n ...(anchor ? { id: anchor } : {}),\n ...(className ? { class: className } : {})\n };\n}\n\nconst SEMANTIC_KEYS = [\n \"theme\",\n \"tone\",\n \"intensity\",\n \"surface\",\n \"layoutRole\",\n \"density\",\n \"ty\",\n \"v\",\n \"w\",\n \"foreground\",\n \"interactivePriority\",\n \"noBackground\",\n \"noBorder\",\n \"noPadding\",\n \"noGap\",\n \"noRadius\",\n \"noForeground\"\n] as const;\n\nexport const TEXT_HTML_TAGS = [\n \"span\",\n \"p\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"strong\",\n \"em\",\n \"small\",\n \"mark\"\n] as const;\n\nexport type TextHtmlTag = (typeof TEXT_HTML_TAGS)[number];\n\nexport function textHtmlTag(value: unknown): TextHtmlTag {\n const candidate = String(value ?? \"span\");\n return TEXT_HTML_TAGS.includes(candidate as TextHtmlTag)\n ? candidate as TextHtmlTag\n : \"span\";\n}\n\nexport function semanticProps(\n attributes: Record<string, unknown>,\n contract?: StyleflowContract,\n defaults: Record<string, unknown> = DEFAULT_SEMANTIC_PROPS\n): Record<string, unknown> {\n const input = Object.fromEntries(\n SEMANTIC_KEYS\n .filter((key) => attributes[key] !== undefined)\n .map((key) => [key, attributes[key]])\n );\n if (contract) {\n return resolveDynamicStyleflowProps(input, defaults, contract, {\n strict: process.env.NODE_ENV === \"production\" || process.env.STYLEFLOW_STRICT === \"1\",\n onDiagnostic: (diagnostic) => console.warn(`[${diagnostic.code}] ${diagnostic.message}`)\n });\n }\n\n const resolved = { ...defaults, ...input };\n for (const key of [\"tone\", \"intensity\", \"foreground\"]) {\n if (input[key] === \"inherit\") {\n delete resolved[key];\n }\n }\n return resolved;\n}\n\nconst TEXT_ALIGNMENTS = new Set([\n \"left\",\n \"center\",\n \"right\",\n \"justify\",\n \"start\",\n \"end\"\n]);\n\n/** Resolve only Gutenberg's supported text-alignment values to inline CSS. */\nexport function textAlignmentStyle(\n attributes: Record<string, unknown>\n): string | undefined {\n const style = attributes.style && typeof attributes.style === \"object\"\n && !Array.isArray(attributes.style)\n ? attributes.style as Record<string, unknown>\n : {};\n const typography = style.typography && typeof style.typography === \"object\"\n && !Array.isArray(style.typography)\n ? style.typography as Record<string, unknown>\n : {};\n const candidate = typography.textAlign ?? attributes.textAlign;\n const alignment = String(candidate ?? \"\");\n return TEXT_ALIGNMENTS.has(alignment) ? `text-align: ${alignment};` : undefined;\n}\n\nexport function prefixedTextSemanticProps(\n attributes: Record<string, unknown>,\n prefix: string,\n contract?: StyleflowContract,\n defaults: Record<string, unknown> = {}\n): Record<string, unknown> {\n const attributeName = (name: string) =>\n `${prefix}${name.charAt(0).toUpperCase()}${name.slice(1)}`;\n return semanticProps({\n ty: attributes[attributeName(\"ty\")],\n v: attributes[attributeName(\"v\")],\n w: attributes[attributeName(\"w\")],\n foreground: attributes[attributeName(\"foreground\")]\n }, contract, defaults);\n}\n\nexport function objectValue(\n block: FlowpressBlock,\n key: string\n): Record<string, unknown> {\n const value = block.attributes[key];\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? value as Record<string, unknown>\n : {};\n}\n\nexport function textValue(block: FlowpressBlock, key = \"text\"): string {\n return String(block.attributes[key] ?? \"\");\n}\n\n/**\n * Keep intentional blank lines visible across current and legacy RichText\n * serializations. Gutenberg can represent them either as repeated `<br>` tags,\n * empty paragraph elements, or literal newlines in an attribute value.\n */\nexport function preserveRichTextSpacing(html: string): string {\n const visibleEmptyBlocks = html\n .replace(/\\r\\n?/g, \"\\n\")\n .replace(\n /<(p|div)(\\s[^>]*)?>(?:\\s|&nbsp;|&#160;)*<\\/\\1>/gi,\n (_match, tag: string, attributes = \"\") =>\n `<${tag}${attributes}>&nbsp;</${tag}>`\n );\n\n return visibleEmptyBlocks\n .split(/(<[^>]+>)/g)\n .map((part) => {\n if (part.startsWith(\"<\") || part.trim() === \"\") {\n return part;\n }\n return part.replace(/\\n(?:[ \\t]*\\n)+/g, (newlines) =>\n \"<br>\".repeat((newlines.match(/\\n/g) || []).length)\n );\n })\n .join(\"\");\n}\n\n/** Keep an empty text element at its typographic line height without visible text. */\nexport function preserveEmptyTextBlock(html: string): string {\n return html.trim() === \"\" ? \"&nbsp;\" : html;\n}\n\nexport function linkValue(block: FlowpressBlock, key = \"link\"): string {\n const value = block.attributes[key];\n const href = typeof value === \"string\"\n ? value\n : String(objectValue(block, key).url ?? \"\");\n return /^(?:\\/(?!\\/)|#|https?:\\/\\/|mailto:|tel:)/i.test(href) ? href : \"\";\n}\n\nexport function responsiveValues(\n attributes: Record<string, unknown>,\n prefix: \"columns\" | \"span\",\n fallback: number\n): Record<\"base\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\", number> {\n const keys = [\"base\", \"sm\", \"md\", \"lg\", \"xl\", \"2xl\"] as const;\n const result = {} as Record<(typeof keys)[number], number>;\n let inherited = fallback;\n for (const key of keys) {\n const suffix = key === \"base\"\n ? \"Base\"\n : key === \"2xl\" ? \"2xl\" : key.charAt(0).toUpperCase() + key.slice(1);\n const candidate = Number(attributes[`${prefix}${suffix}`]);\n if (Number.isInteger(candidate) && candidate >= 1 && candidate <= 12) {\n inherited = candidate;\n }\n result[key] = inherited;\n }\n return result;\n}\n\nexport function unwrapCoreHtml(html: string): string {\n const match = html.trim().match(\n /^<(p|h[1-6]|ul|ol|li|blockquote)(?:\\s[^>]*)?>([\\s\\S]*)<\\/\\1>$/\n );\n return match?.[2] ?? html;\n}\n"]}
@@ -7,9 +7,32 @@ import type { FlowpressBlock } from "../../types.js";
7
7
  export const DEFAULT_SEMANTIC_PROPS = {
8
8
  surface: "default",
9
9
  layoutRole: "stack",
10
- density: "md"
10
+ density: "regular"
11
11
  };
12
12
 
13
+ export const DEFAULT_HERO_OBJECT_POSITION = "50% 50%";
14
+
15
+ export function heroObjectPosition(value: unknown): string {
16
+ if (typeof value !== "string") {
17
+ return DEFAULT_HERO_OBJECT_POSITION;
18
+ }
19
+
20
+ const match = value.trim().match(
21
+ /^(\d+(?:\.\d+)?|\.\d+)%\s+(\d+(?:\.\d+)?|\.\d+)%$/
22
+ );
23
+ if (
24
+ !match
25
+ || [match[1], match[2]].some((coordinate) => {
26
+ const numeric = Number(coordinate);
27
+ return !Number.isFinite(numeric) || numeric < 0 || numeric > 100;
28
+ })
29
+ ) {
30
+ return DEFAULT_HERO_OBJECT_POSITION;
31
+ }
32
+
33
+ return `${match[1]}% ${match[2]}%`;
34
+ }
35
+
13
36
  export function blockHtmlAttributes(
14
37
  attributes: Record<string, unknown>,
15
38
  baseClass = ""
@@ -32,6 +55,7 @@ export function blockHtmlAttributes(
32
55
  }
33
56
 
34
57
  const SEMANTIC_KEYS = [
58
+ "theme",
35
59
  "tone",
36
60
  "intensity",
37
61
  "surface",
@@ -50,6 +74,30 @@ const SEMANTIC_KEYS = [
50
74
  "noForeground"
51
75
  ] as const;
52
76
 
77
+ export const TEXT_HTML_TAGS = [
78
+ "span",
79
+ "p",
80
+ "h1",
81
+ "h2",
82
+ "h3",
83
+ "h4",
84
+ "h5",
85
+ "h6",
86
+ "strong",
87
+ "em",
88
+ "small",
89
+ "mark"
90
+ ] as const;
91
+
92
+ export type TextHtmlTag = (typeof TEXT_HTML_TAGS)[number];
93
+
94
+ export function textHtmlTag(value: unknown): TextHtmlTag {
95
+ const candidate = String(value ?? "span");
96
+ return TEXT_HTML_TAGS.includes(candidate as TextHtmlTag)
97
+ ? candidate as TextHtmlTag
98
+ : "span";
99
+ }
100
+
53
101
  export function semanticProps(
54
102
  attributes: Record<string, unknown>,
55
103
  contract?: StyleflowContract,
@@ -60,9 +108,62 @@ export function semanticProps(
60
108
  .filter((key) => attributes[key] !== undefined)
61
109
  .map((key) => [key, attributes[key]])
62
110
  );
63
- return contract
64
- ? resolveDynamicStyleflowProps(input, defaults, contract)
65
- : { ...defaults, ...input };
111
+ if (contract) {
112
+ return resolveDynamicStyleflowProps(input, defaults, contract, {
113
+ strict: process.env.NODE_ENV === "production" || process.env.STYLEFLOW_STRICT === "1",
114
+ onDiagnostic: (diagnostic) => console.warn(`[${diagnostic.code}] ${diagnostic.message}`)
115
+ });
116
+ }
117
+
118
+ const resolved = { ...defaults, ...input };
119
+ for (const key of ["tone", "intensity", "foreground"]) {
120
+ if (input[key] === "inherit") {
121
+ delete resolved[key];
122
+ }
123
+ }
124
+ return resolved;
125
+ }
126
+
127
+ const TEXT_ALIGNMENTS = new Set([
128
+ "left",
129
+ "center",
130
+ "right",
131
+ "justify",
132
+ "start",
133
+ "end"
134
+ ]);
135
+
136
+ /** Resolve only Gutenberg's supported text-alignment values to inline CSS. */
137
+ export function textAlignmentStyle(
138
+ attributes: Record<string, unknown>
139
+ ): string | undefined {
140
+ const style = attributes.style && typeof attributes.style === "object"
141
+ && !Array.isArray(attributes.style)
142
+ ? attributes.style as Record<string, unknown>
143
+ : {};
144
+ const typography = style.typography && typeof style.typography === "object"
145
+ && !Array.isArray(style.typography)
146
+ ? style.typography as Record<string, unknown>
147
+ : {};
148
+ const candidate = typography.textAlign ?? attributes.textAlign;
149
+ const alignment = String(candidate ?? "");
150
+ return TEXT_ALIGNMENTS.has(alignment) ? `text-align: ${alignment};` : undefined;
151
+ }
152
+
153
+ export function prefixedTextSemanticProps(
154
+ attributes: Record<string, unknown>,
155
+ prefix: string,
156
+ contract?: StyleflowContract,
157
+ defaults: Record<string, unknown> = {}
158
+ ): Record<string, unknown> {
159
+ const attributeName = (name: string) =>
160
+ `${prefix}${name.charAt(0).toUpperCase()}${name.slice(1)}`;
161
+ return semanticProps({
162
+ ty: attributes[attributeName("ty")],
163
+ v: attributes[attributeName("v")],
164
+ w: attributes[attributeName("w")],
165
+ foreground: attributes[attributeName("foreground")]
166
+ }, contract, defaults);
66
167
  }
67
168
 
68
169
  export function objectValue(
@@ -90,7 +191,7 @@ export function preserveRichTextSpacing(html: string): string {
90
191
  .replace(
91
192
  /<(p|div)(\s[^>]*)?>(?:\s|&nbsp;|&#160;)*<\/\1>/gi,
92
193
  (_match, tag: string, attributes = "") =>
93
- `<${tag}${attributes}><br aria-hidden="true"></${tag}>`
194
+ `<${tag}${attributes}>&nbsp;</${tag}>`
94
195
  );
95
196
 
96
197
  return visibleEmptyBlocks
@@ -106,6 +207,11 @@ export function preserveRichTextSpacing(html: string): string {
106
207
  .join("");
107
208
  }
108
209
 
210
+ /** Keep an empty text element at its typographic line height without visible text. */
211
+ export function preserveEmptyTextBlock(html: string): string {
212
+ return html.trim() === "" ? "&nbsp;" : html;
213
+ }
214
+
109
215
  export function linkValue(block: FlowpressBlock, key = "link"): string {
110
216
  const value = block.attributes[key];
111
217
  const href = typeof value === "string"