@contentrain/astro-kit 0.45.0 → 0.45.2

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/catalog.json CHANGED
@@ -2482,7 +2482,7 @@
2482
2482
  "type": "richtext",
2483
2483
  "label": "Answer",
2484
2484
  "required": true,
2485
- "description": "The answer; links and lists allowed."
2485
+ "description": "The answer; links and lists allowed. One with no text and nothing to see (no image, video or frame) draws its question alone, in the item's style, with no disclosure to press, and stays out of the FAQPage JSON-LD."
2486
2486
  }
2487
2487
  }
2488
2488
  },
@@ -2659,7 +2659,7 @@
2659
2659
  "classic/details"
2660
2660
  ]
2661
2661
  },
2662
- "a11y": "Native <details>/<summary>: keyboard operable and announced as expandable by every screen reader, with no script to fail. The plus icon is decorative. The JSON-LD answer text is the answer with its markup removed.",
2662
+ "a11y": "Native <details>/<summary>: keyboard operable and announced as expandable by every screen reader, with no script to fail. The plus icon is decorative. The JSON-LD answer text is the answer with its markup removed; a question whose answer has no text is left out of it, and an empty answer gets no control that opens onto nothing.",
2663
2663
  "files": [
2664
2664
  "Faq.astro"
2665
2665
  ],
@@ -97,12 +97,19 @@ const wide = layout === 'split' || columns === '2' || measuredCols !== undefined
97
97
  const listClass = measuredCols && (style === 'cards' || style === 'plain' ? measuredCols : `${measuredCols} divide-y-0 border-b-0`)
98
98
  const itemClass = measuredCols ? (style === 'cards' ? 'break-inside-avoid mb-3' : style === 'plain' ? 'break-inside-avoid' : 'break-inside-avoid border-b border-line') : undefined
99
99
  const headingId = heading ? `${id}-title` : undefined
100
- const plain = (html: string) => html.replace(/<[^>]*>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim()
101
- const structured = schema && items.length > 0
100
+ const plain = (html: string) => html.replace(/<[^>]*>/g, ' ').replace(/&(?:nbsp|#160|#xa0);/gi, ' ').replace(/\s+/g, ' ').trim()
101
+ // An answer with no text and nothing to see (an empty paragraph, a stray break) opens onto nothing: its question is
102
+ // drawn alone, in the item's style, with no disclosure to press. An image or a frame alone is still an answer.
103
+ const MEDIA = /<(?:img|picture|video|audio|iframe|svg|embed|object|canvas)\b/i
104
+ const answered = items.map(item => plain(item.answer) !== '' || MEDIA.test(item.answer))
105
+ const firstAnswered = answered.indexOf(true)
106
+ // FAQPage needs each question's acceptedAnswer text, so a question without one is left out of it.
107
+ const withText = items.filter(item => plain(item.answer) !== '')
108
+ const structured = schema && withText.length > 0
102
109
  ? JSON.stringify({
103
110
  '@context': 'https://schema.org',
104
111
  '@type': 'FAQPage',
105
- mainEntity: items.map(item => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: plain(item.answer) } })),
112
+ mainEntity: withText.map(item => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: plain(item.answer) } })),
106
113
  }).replace(/</g, '\\u003c')
107
114
  : null
108
115
  ---
@@ -116,8 +123,8 @@ const structured = schema && items.length > 0
116
123
  </header>
117
124
  )}
118
125
  <div class={s.list({ class: listClass })}>
119
- {items.map((item, i) => (
120
- <details class={s.item({ class: itemClass })} name={exclusive ? id : undefined} open={openFirst && i === 0}>
126
+ {items.map((item, i) => answered[i] ? (
127
+ <details class={s.item({ class: itemClass })} name={exclusive ? id : undefined} open={openFirst && i === firstAnswered}>
121
128
  <summary class={s.summary()}>
122
129
  <span class={s.question()}>{item.question}</span>
123
130
  {icon === 'chevron'
@@ -126,6 +133,12 @@ const structured = schema && items.length > 0
126
133
  </summary>
127
134
  <div class={s.answer()} set:html={item.answer} />
128
135
  </details>
136
+ ) : (
137
+ <div class={s.item({ class: itemClass })}>
138
+ <div class={s.summary({ class: style === 'plain' ? 'block cursor-auto' : 'cursor-auto' })}>
139
+ <span class={s.question()}>{item.question}</span>
140
+ </div>
141
+ </div>
129
142
  ))}
130
143
  </div>
131
144
  </div>
@@ -14,7 +14,7 @@
14
14
  "questionStyle": { "options": ["heading", "bold"], "default": "heading", "description": "How the questions read. heading: a small heading (lg, heading weight). bold: body size in bold, as accordion and toggle titles often are. The plain style sets its own and ignores this." }
15
15
  },
16
16
  "props": {
17
- "items": { "type": "array", "label": "Questions", "required": true, "items": { "type": "object", "fields": { "question": { "type": "string", "label": "Question", "required": true, "description": "The question, as the visitor would ask it." }, "answer": { "type": "richtext", "label": "Answer", "required": true, "description": "The answer; links and lists allowed." } } }, "description": "The questions in order (core/details summary + content, Elementor accordion/toggle tabs, Divi accordion/toggle items)." },
17
+ "items": { "type": "array", "label": "Questions", "required": true, "items": { "type": "object", "fields": { "question": { "type": "string", "label": "Question", "required": true, "description": "The question, as the visitor would ask it." }, "answer": { "type": "richtext", "label": "Answer", "required": true, "description": "The answer; links and lists allowed. One with no text and nothing to see (no image, video or frame) draws its question alone, in the item's style, with no disclosure to press, and stays out of the FAQPage JSON-LD." } } }, "description": "The questions in order (core/details summary + content, Elementor accordion/toggle tabs, Divi accordion/toggle items)." },
18
18
  "heading": { "type": "string", "label": "Heading", "description": "Section heading." },
19
19
  "intro": { "type": "text", "label": "Introduction", "description": "A sentence under the heading." },
20
20
  "exclusive": { "type": "boolean", "default": false, "content": false, "description": "One answer open at a time (Elementor accordion, Divi accordion). Off for toggles." },
@@ -33,5 +33,5 @@
33
33
  "divi": ["divi/et_pb_accordion", "divi/et_pb_toggle"],
34
34
  "classic": ["classic/faq", "classic/details"]
35
35
  },
36
- "a11y": "Native <details>/<summary>: keyboard operable and announced as expandable by every screen reader, with no script to fail. The plus icon is decorative. The JSON-LD answer text is the answer with its markup removed."
36
+ "a11y": "Native <details>/<summary>: keyboard operable and announced as expandable by every screen reader, with no script to fail. The plus icon is decorative. The JSON-LD answer text is the answer with its markup removed; a question whose answer has no text is left out of it, and an empty answer gets no control that opens onto nothing."
37
37
  }
@@ -5,5 +5,7 @@
5
5
  { "name": "inset-15", "frame": "page", "props": { "html": "<h2 class=\"wp-block-heading\">Keeping what works</h2><p>A careful migration keeps <strong>addresses</strong>, <a href=\"/about/\">formatting</a> and rankings.</p><ul class=\"wp-block-list\"><li>Permalinks stay put.</li><li>Archives survive.</li></ul><blockquote class=\"wp-block-quote\"><p>The fastest request is the one the server never has to think about.</p><cite>Every performance engineer</cite></blockquote><div class=\"wp-block-buttons is-layout-flex\"><div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"/contact/\">Get in touch</a></div></div>", "inset": "15" } },
6
6
  { "name": "inset-27", "frame": "page", "props": { "size": "base", "searchable": false, "html": "<p class=\"has-drop-cap\">Moving a WordPress site to a static stack is mostly about keeping what already works.</p><figure class=\"wp-block-table is-style-stripes\"><table><thead><tr><th>Metric</th><th>Before</th><th>After</th></tr></thead><tbody><tr><td>Requests</td><td>84</td><td>9</td></tr><tr><td>LCP</td><td>3.1 s</td><td>0.8 s</td></tr></tbody></table></figure>", "inset": "27" } },
7
7
  { "name": "inset-34", "frame": "page", "props": { "markdown": "## From a page section\n\nA section's text is **markdown** in the page model, with [a link](/about/) and a list:\n\n- Written once\n- Rendered at build time", "size": "base", "inset": "34" } },
8
- { "name": "inset-42", "frame": "page", "props": { "html": "<h2 class=\"wp-block-heading\">Keeping what works</h2><p>A careful migration keeps <strong>addresses</strong>, <a href=\"/about/\">formatting</a> and rankings.</p><ul class=\"wp-block-list\"><li>Permalinks stay put.</li><li>Archives survive.</li></ul><blockquote class=\"wp-block-quote\"><p>The fastest request is the one the server never has to think about.</p><cite>Every performance engineer</cite></blockquote><div class=\"wp-block-buttons is-layout-flex\"><div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"/contact/\">Get in touch</a></div></div>", "inset": "42" } }
8
+ { "name": "inset-42", "frame": "page", "props": { "html": "<h2 class=\"wp-block-heading\">Keeping what works</h2><p>A careful migration keeps <strong>addresses</strong>, <a href=\"/about/\">formatting</a> and rankings.</p><ul class=\"wp-block-list\"><li>Permalinks stay put.</li><li>Archives survive.</li></ul><blockquote class=\"wp-block-quote\"><p>The fastest request is the one the server never has to think about.</p><cite>Every performance engineer</cite></blockquote><div class=\"wp-block-buttons is-layout-flex\"><div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"/contact/\">Get in touch</a></div></div>", "inset": "42" } },
9
+ { "name": "table-wide", "frame": "page", "props": { "html": "<h2 class=\"wp-block-heading\">Compare plans</h2><p>Every plan side by side.</p><table><thead><tr><th>Plan</th><th>Monthly price</th><th>Yearly price</th><th>Sites included</th><th>Storage per site</th><th>Bandwidth per month</th><th>Staging environments</th><th>Daily backups kept</th><th>Edge caching</th><th>Malware scanning</th><th>Priority support</th><th>Migration help</th></tr></thead><tbody><tr><td>Starter</td><td>$35</td><td>$350</td><td>1</td><td>10 GB</td><td>50 GB</td><td>1</td><td>14 days</td><td>Included</td><td>Included</td><td>Email</td><td>One free</td></tr><tr><td>Business</td><td>$115</td><td>$1,150</td><td>5</td><td>40 GB</td><td>250 GB</td><td>5</td><td>30 days</td><td>Included</td><td>Included</td><td>Chat and email</td><td>Unlimited</td></tr></tbody></table><p>Prices exclude tax.</p>" } },
10
+ { "name": "table-narrow", "frame": "page", "props": { "html": "<h2 class=\"wp-block-heading\">Opening hours</h2><table><tbody><tr><td>Weekdays</td><td>9–18</td></tr><tr><td>Saturday</td><td>10–14</td></tr></tbody></table><p>Closed on public holidays.</p>" } }
9
11
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/astro-kit",
3
- "version": "0.45.0",
3
+ "version": "0.45.2",
4
4
  "license": "MIT",
5
5
  "description": "Copy-in Astro components for Contentrain sites (shadcn model): typed props, tailwind-variants, zero JavaScript by default — with a machine-readable catalog",
6
6
  "type": "module",
@@ -42,7 +42,7 @@
42
42
  "mapping"
43
43
  ],
44
44
  "dependencies": {
45
- "@contentrain/types": "1.65.0"
45
+ "@contentrain/types": "1.66.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^22.10.0",