@styleflow.app/flowpress-astro-core 0.2.0-beta.10 → 0.2.0-beta.12

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,22 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.0-beta.12
6
+
7
+ - Added the reusable `sf/number-spinner` Astro renderer with independently
8
+ styled prefix, number and suffix text segments.
9
+ - Started counting only on first viewport intersection, retained the final
10
+ target in no-JavaScript markup and disabled animation for reduced-motion
11
+ preferences, including preference changes during an active count.
12
+ - Kept the final numeric value permanently available to assistive technology
13
+ while visual frame updates remain hidden from accessibility APIs.
14
+
15
+ ## 0.2.0-beta.11
16
+
17
+ - Rendered empty Gutenberg and FlowPress text blocks with an internal
18
+ ` `, preserving their typographic line height without visible
19
+ characters or synthetic break elements.
20
+
5
21
  ## 0.2.0-beta.10
6
22
 
7
23
  - Preserved intentionally empty core Gutenberg paragraphs as visible line
package/README.md CHANGED
@@ -9,7 +9,7 @@ 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.10
12
+ pnpm add @styleflow.app/flowpress-astro-core@0.2.0-beta.12
13
13
  ```
14
14
 
15
15
  Prereleases are pinned exactly. The package expects Astro 7 or newer and the
@@ -37,11 +37,19 @@ to the protected FlowPress REST endpoint, so the same component works in
37
37
  static and server output modes. Gutenberg `anchor` and `className` attributes
38
38
  are applied to the rendered block root as HTML `id` and `class`. Rich Text
39
39
  preserves intentional double, triple and longer line breaks, including content
40
- saved by older editor versions as empty paragraphs. The exported
40
+ saved by older editor versions as empty paragraphs. Empty textual blocks render
41
+ an internal ` `, preserving their typographic line height without adding
42
+ visible characters. The exported
41
43
  `collectFlowpressClassNames()` helper recursively extracts safe class tokens
42
44
  from rendered CMS blocks so consumers can generate Tailwind
43
45
  `@source inline()` directives before compiling their frontend CSS.
44
46
 
47
+ `sf/number-spinner` renders prefix, target number and suffix with independent
48
+ semantic tags and StyleFlow text coordinates. Its numeric value counts from
49
+ zero only when it enters the viewport. The final target remains available to
50
+ assistive technology and without JavaScript; `prefers-reduced-motion: reduce`
51
+ skips the animation entirely.
52
+
45
53
  ## Release
46
54
 
47
55
  ```bash
@@ -7,6 +7,7 @@ import FormBlock from "./blocks/FormBlock.astro";
7
7
  import GridBlock from "./blocks/GridBlock.astro";
8
8
  import HeroBlock from "./blocks/HeroBlock.astro";
9
9
  import ListItemFallback from "./blocks/ListItemFallback.astro";
10
+ import NumberSpinnerBlock from "./blocks/NumberSpinnerBlock.astro";
10
11
  import TextBlock from "./blocks/TextBlock.astro";
11
12
  import {
12
13
  blockHtmlAttributes,
@@ -71,6 +72,10 @@ function youtubeEmbedUrl(value: unknown): string | null {
71
72
  );
72
73
  }
73
74
 
75
+ if (name === "number-spinner") {
76
+ return <NumberSpinnerBlock block={block} contract={contract} />;
77
+ }
78
+
74
79
  if ([
75
80
  "heading",
76
81
  "rich-text",
@@ -0,0 +1,131 @@
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
+ <S as="span" class="flowpress-number-spinner__accessible-value">
84
+ {numberText}
85
+ </S>
86
+ <S
87
+ as="span"
88
+ aria-hidden="true"
89
+ data-flowpress-number-spinner-value
90
+ >
91
+ {numberText}
92
+ </S>
93
+ </S>
94
+ {suffix ? (
95
+ <S
96
+ as={suffixTag}
97
+ class="flowpress-number-spinner__segment"
98
+ {...suffixSemantic}
99
+ >
100
+ {suffix}
101
+ </S>
102
+ ) : null}
103
+ </S>
104
+
105
+ <script>
106
+ import { initializeNumberSpinners } from "../../number-spinner.js";
107
+
108
+ initializeNumberSpinners();
109
+ </script>
110
+
111
+ <style>
112
+ .flowpress-number-spinner {
113
+ display: inline-flex;
114
+ align-items: baseline;
115
+ }
116
+
117
+ .flowpress-number-spinner__segment {
118
+ display: inline;
119
+ margin: 0;
120
+ }
121
+
122
+ .flowpress-number-spinner__accessible-value {
123
+ position: absolute;
124
+ width: 1px;
125
+ height: 1px;
126
+ overflow: hidden;
127
+ clip: rect(0 0 0 0);
128
+ clip-path: inset(50%);
129
+ white-space: nowrap;
130
+ }
131
+ </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
+ preserveEmptyTextBlock,
7
8
  preserveRichTextSpacing,
8
9
  semanticProps,
9
10
  textValue,
@@ -43,13 +44,16 @@ const coreHtml = String(
43
44
  ?? block.innerHtml
44
45
  ?? ""
45
46
  );
46
- const html = name === "heading"
47
+ const normalizedHtml = name === "heading"
47
48
  ? textValue(block)
48
49
  : name === "rich-text"
49
50
  ? preserveRichTextSpacing(String(attributes.content ?? ""))
50
51
  : name === "quote"
51
52
  ? textValue(block, "quote")
52
53
  : unwrapCoreHtml(preserveRichTextSpacing(coreHtml));
54
+ const html = name === "core/list"
55
+ ? normalizedHtml
56
+ : preserveEmptyTextBlock(normalizedHtml);
53
57
  const listTag = Boolean(attributes.ordered) ? "ol" : "ul";
54
58
  ---
55
59
 
@@ -9,7 +9,11 @@ export declare function blockHtmlAttributes(attributes: Record<string, unknown>,
9
9
  id?: string;
10
10
  class?: string;
11
11
  };
12
+ export declare const TEXT_HTML_TAGS: readonly ["span", "p", "h1", "h2", "h3", "h4", "h5", "h6", "strong", "em", "small", "mark"];
13
+ export type TextHtmlTag = (typeof TEXT_HTML_TAGS)[number];
14
+ export declare function textHtmlTag(value: unknown): TextHtmlTag;
12
15
  export declare function semanticProps(attributes: Record<string, unknown>, contract?: StyleflowContract, defaults?: Record<string, unknown>): Record<string, unknown>;
16
+ export declare function prefixedTextSemanticProps(attributes: Record<string, unknown>, prefix: string, contract?: StyleflowContract, defaults?: Record<string, unknown>): Record<string, unknown>;
13
17
  export declare function objectValue(block: FlowpressBlock, key: string): Record<string, unknown>;
14
18
  export declare function textValue(block: FlowpressBlock, key?: string): string;
15
19
  /**
@@ -18,6 +22,8 @@ export declare function textValue(block: FlowpressBlock, key?: string): string;
18
22
  * empty paragraph elements, or literal newlines in an attribute value.
19
23
  */
20
24
  export declare function preserveRichTextSpacing(html: string): string;
25
+ /** Keep an empty text element at its typographic line height without visible text. */
26
+ export declare function preserveEmptyTextBlock(html: string): string;
21
27
  export declare function linkValue(block: FlowpressBlock, key?: string): string;
22
28
  export declare function responsiveValues(attributes: Record<string, unknown>, prefix: "columns" | "span", fallback: number): Record<"base" | "sm" | "md" | "lg" | "xl" | "2xl", number>;
23
29
  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,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,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,CASzB;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"}
@@ -38,6 +38,26 @@ const SEMANTIC_KEYS = [
38
38
  "noRadius",
39
39
  "noForeground"
40
40
  ];
41
+ export const TEXT_HTML_TAGS = [
42
+ "span",
43
+ "p",
44
+ "h1",
45
+ "h2",
46
+ "h3",
47
+ "h4",
48
+ "h5",
49
+ "h6",
50
+ "strong",
51
+ "em",
52
+ "small",
53
+ "mark"
54
+ ];
55
+ export function textHtmlTag(value) {
56
+ const candidate = String(value ?? "span");
57
+ return TEXT_HTML_TAGS.includes(candidate)
58
+ ? candidate
59
+ : "span";
60
+ }
41
61
  export function semanticProps(attributes, contract, defaults = DEFAULT_SEMANTIC_PROPS) {
42
62
  const input = Object.fromEntries(SEMANTIC_KEYS
43
63
  .filter((key) => attributes[key] !== undefined)
@@ -46,6 +66,15 @@ export function semanticProps(attributes, contract, defaults = DEFAULT_SEMANTIC_
46
66
  ? resolveDynamicStyleflowProps(input, defaults, contract)
47
67
  : { ...defaults, ...input };
48
68
  }
69
+ export function prefixedTextSemanticProps(attributes, prefix, contract, defaults = {}) {
70
+ const attributeName = (name) => `${prefix}${name.charAt(0).toUpperCase()}${name.slice(1)}`;
71
+ return semanticProps({
72
+ ty: attributes[attributeName("ty")],
73
+ v: attributes[attributeName("v")],
74
+ w: attributes[attributeName("w")],
75
+ foreground: attributes[attributeName("foreground")]
76
+ }, contract, defaults);
77
+ }
49
78
  export function objectValue(block, key) {
50
79
  const value = block.attributes[key];
51
80
  return value && typeof value === "object" && !Array.isArray(value)
@@ -63,7 +92,7 @@ export function textValue(block, key = "text") {
63
92
  export function preserveRichTextSpacing(html) {
64
93
  const visibleEmptyBlocks = html
65
94
  .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}>`);
95
+ .replace(/<(p|div)(\s[^>]*)?>(?:\s|&nbsp;|&#160;)*<\/\1>/gi, (_match, tag, attributes = "") => `<${tag}${attributes}>&nbsp;</${tag}>`);
67
96
  return visibleEmptyBlocks
68
97
  .split(/(<[^>]+>)/g)
69
98
  .map((part) => {
@@ -74,6 +103,10 @@ export function preserveRichTextSpacing(html) {
74
103
  })
75
104
  .join("");
76
105
  }
106
+ /** Keep an empty text element at its typographic line height without visible text. */
107
+ export function preserveEmptyTextBlock(html) {
108
+ return html.trim() === "" ? "&nbsp;" : html;
109
+ }
77
110
  export function linkValue(block, key = "link") {
78
111
  const value = block.attributes[key];
79
112
  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: \"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 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 return contract\n ? resolveDynamicStyleflowProps(input, defaults, contract)\n : { ...defaults, ...input };\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"]}
@@ -50,6 +50,30 @@ const SEMANTIC_KEYS = [
50
50
  "noForeground"
51
51
  ] as const;
52
52
 
53
+ export const TEXT_HTML_TAGS = [
54
+ "span",
55
+ "p",
56
+ "h1",
57
+ "h2",
58
+ "h3",
59
+ "h4",
60
+ "h5",
61
+ "h6",
62
+ "strong",
63
+ "em",
64
+ "small",
65
+ "mark"
66
+ ] as const;
67
+
68
+ export type TextHtmlTag = (typeof TEXT_HTML_TAGS)[number];
69
+
70
+ export function textHtmlTag(value: unknown): TextHtmlTag {
71
+ const candidate = String(value ?? "span");
72
+ return TEXT_HTML_TAGS.includes(candidate as TextHtmlTag)
73
+ ? candidate as TextHtmlTag
74
+ : "span";
75
+ }
76
+
53
77
  export function semanticProps(
54
78
  attributes: Record<string, unknown>,
55
79
  contract?: StyleflowContract,
@@ -65,6 +89,22 @@ export function semanticProps(
65
89
  : { ...defaults, ...input };
66
90
  }
67
91
 
92
+ export function prefixedTextSemanticProps(
93
+ attributes: Record<string, unknown>,
94
+ prefix: string,
95
+ contract?: StyleflowContract,
96
+ defaults: Record<string, unknown> = {}
97
+ ): Record<string, unknown> {
98
+ const attributeName = (name: string) =>
99
+ `${prefix}${name.charAt(0).toUpperCase()}${name.slice(1)}`;
100
+ return semanticProps({
101
+ ty: attributes[attributeName("ty")],
102
+ v: attributes[attributeName("v")],
103
+ w: attributes[attributeName("w")],
104
+ foreground: attributes[attributeName("foreground")]
105
+ }, contract, defaults);
106
+ }
107
+
68
108
  export function objectValue(
69
109
  block: FlowpressBlock,
70
110
  key: string
@@ -90,7 +130,7 @@ export function preserveRichTextSpacing(html: string): string {
90
130
  .replace(
91
131
  /<(p|div)(\s[^>]*)?>(?:\s|&nbsp;|&#160;)*<\/\1>/gi,
92
132
  (_match, tag: string, attributes = "") =>
93
- `<${tag}${attributes}><br aria-hidden="true"></${tag}>`
133
+ `<${tag}${attributes}>&nbsp;</${tag}>`
94
134
  );
95
135
 
96
136
  return visibleEmptyBlocks
@@ -106,6 +146,11 @@ export function preserveRichTextSpacing(html: string): string {
106
146
  .join("");
107
147
  }
108
148
 
149
+ /** Keep an empty text element at its typographic line height without visible text. */
150
+ export function preserveEmptyTextBlock(html: string): string {
151
+ return html.trim() === "" ? "&nbsp;" : html;
152
+ }
153
+
109
154
  export function linkValue(block: FlowpressBlock, key = "link"): string {
110
155
  const value = block.attributes[key];
111
156
  const href = typeof value === "string"
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./client.js";
2
2
  export * from "./env.js";
3
+ export * from "./number-spinner.js";
3
4
  export * from "./styleflow.js";
4
5
  export * from "./style-classes.js";
5
6
  export * from "./template-hierarchy.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AACxC,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AACxC,cAAc,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./client.js";
2
2
  export * from "./env.js";
3
+ export * from "./number-spinner.js";
3
4
  export * from "./styleflow.js";
4
5
  export * from "./style-classes.js";
5
6
  export * from "./template-hierarchy.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AACxC,cAAc,YAAY,CAAC","sourcesContent":["export * from \"./client.js\";\nexport * from \"./env.js\";\nexport * from \"./styleflow.js\";\nexport * from \"./style-classes.js\";\nexport * from \"./template-hierarchy.js\";\nexport * from \"./types.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AACxC,cAAc,YAAY,CAAC","sourcesContent":["export * from \"./client.js\";\nexport * from \"./env.js\";\nexport * from \"./number-spinner.js\";\nexport * from \"./styleflow.js\";\nexport * from \"./style-classes.js\";\nexport * from \"./template-hierarchy.js\";\nexport * from \"./types.js\";\n"]}
@@ -0,0 +1,6 @@
1
+ export declare function spinnerFractionDigits(value: number): number;
2
+ export declare function clampSpinnerDuration(value: unknown): number;
3
+ export declare function spinnerFrameValue(target: number, progress: number): number;
4
+ export declare function formatSpinnerValue(value: number, locale: string, fractionDigits: number): string;
5
+ export declare function initializeNumberSpinners(root?: ParentNode): void;
6
+ //# sourceMappingURL=number-spinner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-spinner.d.ts","sourceRoot":"","sources":["../src/number-spinner.ts"],"names":[],"mappings":"AAIA,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAS3D;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAK3D;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI1E;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GACrB,MAAM,CAMR;AA2ED,wBAAgB,wBAAwB,CAAC,IAAI,GAAE,UAAqB,GAAG,IAAI,CAqD1E"}
@@ -0,0 +1,131 @@
1
+ const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
2
+ const initializedSpinners = new WeakSet();
3
+ const runningFrames = new Map();
4
+ export function spinnerFractionDigits(value) {
5
+ if (!Number.isFinite(value)) {
6
+ return 0;
7
+ }
8
+ const [coefficient = "", exponentText] = String(value).toLowerCase().split("e");
9
+ const coefficientDecimals = coefficient.split(".")[1]?.length ?? 0;
10
+ const exponent = Number(exponentText ?? 0);
11
+ return Math.min(20, Math.max(0, coefficientDecimals - exponent));
12
+ }
13
+ export function clampSpinnerDuration(value) {
14
+ const duration = Number(value);
15
+ return Number.isFinite(duration)
16
+ ? Math.min(10_000, Math.max(0, duration))
17
+ : 1200;
18
+ }
19
+ export function spinnerFrameValue(target, progress) {
20
+ const normalized = Math.min(1, Math.max(0, progress));
21
+ const eased = 1 - Math.pow(1 - normalized, 3);
22
+ return target * eased;
23
+ }
24
+ export function formatSpinnerValue(value, locale, fractionDigits) {
25
+ const digits = Math.min(20, Math.max(0, Math.trunc(fractionDigits)));
26
+ return new Intl.NumberFormat(locale || undefined, {
27
+ minimumFractionDigits: digits,
28
+ maximumFractionDigits: digits
29
+ }).format(value);
30
+ }
31
+ function spinnerDisplay(element) {
32
+ return element.querySelector("[data-flowpress-number-spinner-value]");
33
+ }
34
+ function spinnerTarget(element) {
35
+ const target = Number(element.dataset.spinnerTarget);
36
+ return Number.isFinite(target) ? target : 0;
37
+ }
38
+ function spinnerLocale(element) {
39
+ return element.dataset.spinnerLocale
40
+ || document.documentElement.lang
41
+ || navigator.language
42
+ || "en";
43
+ }
44
+ function renderSpinnerValue(element, value, final = false) {
45
+ const display = spinnerDisplay(element);
46
+ if (!display) {
47
+ return;
48
+ }
49
+ const target = spinnerTarget(element);
50
+ const digits = Number(element.dataset.spinnerFractionDigits) || 0;
51
+ display.textContent = formatSpinnerValue(final ? target : value, spinnerLocale(element), digits);
52
+ if (final) {
53
+ element.dataset.spinnerState = "complete";
54
+ }
55
+ }
56
+ function finishSpinner(element) {
57
+ const frame = runningFrames.get(element);
58
+ if (frame !== undefined) {
59
+ cancelAnimationFrame(frame);
60
+ runningFrames.delete(element);
61
+ }
62
+ renderSpinnerValue(element, spinnerTarget(element), true);
63
+ }
64
+ function animateSpinner(element) {
65
+ const duration = clampSpinnerDuration(element.dataset.spinnerDuration);
66
+ const target = spinnerTarget(element);
67
+ if (duration === 0 || target === 0) {
68
+ finishSpinner(element);
69
+ return;
70
+ }
71
+ element.dataset.spinnerState = "running";
72
+ renderSpinnerValue(element, 0);
73
+ const startedAt = performance.now();
74
+ const step = (now) => {
75
+ const progress = Math.min(1, (now - startedAt) / duration);
76
+ renderSpinnerValue(element, spinnerFrameValue(target, progress));
77
+ if (progress >= 1) {
78
+ finishSpinner(element);
79
+ return;
80
+ }
81
+ runningFrames.set(element, requestAnimationFrame(step));
82
+ };
83
+ runningFrames.set(element, requestAnimationFrame(step));
84
+ }
85
+ export function initializeNumberSpinners(root = document) {
86
+ if (typeof window === "undefined") {
87
+ return;
88
+ }
89
+ const elements = Array.from(root.querySelectorAll("[data-flowpress-number-spinner]")).filter((element) => {
90
+ if (initializedSpinners.has(element)) {
91
+ return false;
92
+ }
93
+ initializedSpinners.add(element);
94
+ return true;
95
+ });
96
+ if (!elements.length) {
97
+ return;
98
+ }
99
+ const reducedMotion = window.matchMedia(REDUCED_MOTION_QUERY);
100
+ if (reducedMotion.matches) {
101
+ elements.forEach(finishSpinner);
102
+ return;
103
+ }
104
+ if (!("IntersectionObserver" in window)) {
105
+ elements.forEach(animateSpinner);
106
+ return;
107
+ }
108
+ const observer = new IntersectionObserver((entries) => {
109
+ for (const entry of entries) {
110
+ if (!entry.isIntersecting) {
111
+ continue;
112
+ }
113
+ const element = entry.target;
114
+ observer.unobserve(element);
115
+ animateSpinner(element);
116
+ }
117
+ }, {
118
+ threshold: 0.15
119
+ });
120
+ elements.forEach((element) => observer.observe(element));
121
+ const stopForReducedMotion = (event) => {
122
+ if (!event.matches) {
123
+ return;
124
+ }
125
+ observer.disconnect();
126
+ elements.forEach(finishSpinner);
127
+ reducedMotion.removeEventListener("change", stopForReducedMotion);
128
+ };
129
+ reducedMotion.addEventListener("change", stopForReducedMotion);
130
+ }
131
+ //# sourceMappingURL=number-spinner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-spinner.js","sourceRoot":"","sources":["../src/number-spinner.ts"],"names":[],"mappings":"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","sourcesContent":["const REDUCED_MOTION_QUERY = \"(prefers-reduced-motion: reduce)\";\nconst initializedSpinners = new WeakSet<HTMLElement>();\nconst runningFrames = new Map<HTMLElement, number>();\n\nexport function spinnerFractionDigits(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n\n const [coefficient = \"\", exponentText] = String(value).toLowerCase().split(\"e\");\n const coefficientDecimals = coefficient.split(\".\")[1]?.length ?? 0;\n const exponent = Number(exponentText ?? 0);\n return Math.min(20, Math.max(0, coefficientDecimals - exponent));\n}\n\nexport function clampSpinnerDuration(value: unknown): number {\n const duration = Number(value);\n return Number.isFinite(duration)\n ? Math.min(10_000, Math.max(0, duration))\n : 1200;\n}\n\nexport function spinnerFrameValue(target: number, progress: number): number {\n const normalized = Math.min(1, Math.max(0, progress));\n const eased = 1 - Math.pow(1 - normalized, 3);\n return target * eased;\n}\n\nexport function formatSpinnerValue(\n value: number,\n locale: string,\n fractionDigits: number\n): string {\n const digits = Math.min(20, Math.max(0, Math.trunc(fractionDigits)));\n return new Intl.NumberFormat(locale || undefined, {\n minimumFractionDigits: digits,\n maximumFractionDigits: digits\n }).format(value);\n}\n\nfunction spinnerDisplay(element: HTMLElement): HTMLElement | null {\n return element.querySelector<HTMLElement>(\n \"[data-flowpress-number-spinner-value]\"\n );\n}\n\nfunction spinnerTarget(element: HTMLElement): number {\n const target = Number(element.dataset.spinnerTarget);\n return Number.isFinite(target) ? target : 0;\n}\n\nfunction spinnerLocale(element: HTMLElement): string {\n return element.dataset.spinnerLocale\n || document.documentElement.lang\n || navigator.language\n || \"en\";\n}\n\nfunction renderSpinnerValue(\n element: HTMLElement,\n value: number,\n final = false\n): void {\n const display = spinnerDisplay(element);\n if (!display) {\n return;\n }\n const target = spinnerTarget(element);\n const digits = Number(element.dataset.spinnerFractionDigits) || 0;\n display.textContent = formatSpinnerValue(\n final ? target : value,\n spinnerLocale(element),\n digits\n );\n if (final) {\n element.dataset.spinnerState = \"complete\";\n }\n}\n\nfunction finishSpinner(element: HTMLElement): void {\n const frame = runningFrames.get(element);\n if (frame !== undefined) {\n cancelAnimationFrame(frame);\n runningFrames.delete(element);\n }\n renderSpinnerValue(element, spinnerTarget(element), true);\n}\n\nfunction animateSpinner(element: HTMLElement): void {\n const duration = clampSpinnerDuration(element.dataset.spinnerDuration);\n const target = spinnerTarget(element);\n if (duration === 0 || target === 0) {\n finishSpinner(element);\n return;\n }\n\n element.dataset.spinnerState = \"running\";\n renderSpinnerValue(element, 0);\n const startedAt = performance.now();\n\n const step = (now: number) => {\n const progress = Math.min(1, (now - startedAt) / duration);\n renderSpinnerValue(element, spinnerFrameValue(target, progress));\n if (progress >= 1) {\n finishSpinner(element);\n return;\n }\n runningFrames.set(element, requestAnimationFrame(step));\n };\n\n runningFrames.set(element, requestAnimationFrame(step));\n}\n\nexport function initializeNumberSpinners(root: ParentNode = document): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n const elements = Array.from(\n root.querySelectorAll<HTMLElement>(\"[data-flowpress-number-spinner]\")\n ).filter((element) => {\n if (initializedSpinners.has(element)) {\n return false;\n }\n initializedSpinners.add(element);\n return true;\n });\n if (!elements.length) {\n return;\n }\n\n const reducedMotion = window.matchMedia(REDUCED_MOTION_QUERY);\n if (reducedMotion.matches) {\n elements.forEach(finishSpinner);\n return;\n }\n\n if (!(\"IntersectionObserver\" in window)) {\n elements.forEach(animateSpinner);\n return;\n }\n\n const observer = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) {\n continue;\n }\n const element = entry.target as HTMLElement;\n observer.unobserve(element);\n animateSpinner(element);\n }\n }, {\n threshold: 0.15\n });\n\n elements.forEach((element) => observer.observe(element));\n\n const stopForReducedMotion = (event: MediaQueryListEvent) => {\n if (!event.matches) {\n return;\n }\n observer.disconnect();\n elements.forEach(finishSpinner);\n reducedMotion.removeEventListener(\"change\", stopForReducedMotion);\n };\n reducedMotion.addEventListener(\"change\", stopForReducedMotion);\n}\n"]}
@@ -1 +1 @@
1
- {"root":["../src/astro.d.ts","../src/client.ts","../src/env.ts","../src/index.ts","../src/style-classes.ts","../src/styleflow.ts","../src/template-hierarchy.ts","../src/templates.ts","../src/types.ts","../src/astro/blocks/helpers.ts"],"version":"5.9.3"}
1
+ {"root":["../src/astro.d.ts","../src/client.ts","../src/env.ts","../src/index.ts","../src/number-spinner.ts","../src/style-classes.ts","../src/styleflow.ts","../src/template-hierarchy.ts","../src/templates.ts","../src/types.ts","../src/astro/blocks/helpers.ts"],"version":"5.9.3"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@styleflow.app/flowpress-astro-core",
3
- "version": "0.2.0-beta.10",
3
+ "version": "0.2.0-beta.12",
4
4
  "description": "Project-agnostic WordPress routing, templates, SEO and GraphQL runtime for FlowPress Astro apps.",
5
5
  "type": "module",
6
6
  "private": false,