@contentrain/astro-kit 0.11.0 → 0.12.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.
package/catalog.json CHANGED
@@ -2902,6 +2902,14 @@
2902
2902
  ],
2903
2903
  "default": "sm",
2904
2904
  "description": "The label under each figure: sm (small), base (body size) or lg (a counter title set like a subheading)."
2905
+ },
2906
+ "order": {
2907
+ "options": [
2908
+ "label-first",
2909
+ "value-first"
2910
+ ],
2911
+ "default": "label-first",
2912
+ "description": "Which the page's text reads first, as the source does: label-first (an Elementor counter) or value-first (a Gutenberg figure heading over its label). The figure is drawn on top either way."
2905
2913
  }
2906
2914
  },
2907
2915
  "props": {
@@ -3039,6 +3047,11 @@
3039
3047
  "items": {
3040
3048
  "type": "object",
3041
3049
  "fields": {
3050
+ "number": {
3051
+ "type": "string",
3052
+ "label": "Number",
3053
+ "description": "The step's number as the source writes it (01, Step 2); empty: the list counts 1, 2, 3."
3054
+ },
3042
3055
  "title": {
3043
3056
  "type": "string",
3044
3057
  "label": "Title",
@@ -3102,6 +3115,7 @@
3102
3115
  "heading": "heading",
3103
3116
  "intro": "intro",
3104
3117
  "items": "items",
3118
+ "items[].number": "items[].number",
3105
3119
  "items[].title": "items[].title",
3106
3120
  "items[].text": "items[].text",
3107
3121
  "items[].image": "items[].image",
@@ -7,7 +7,7 @@ import Section, { type Tone } from '../_shared/Section.astro'
7
7
  const stats = tv({
8
8
  slots: {
9
9
  list: 'grid grid-cols-2 gap-6 md:gap-8',
10
- item: 'flex flex-col-reverse gap-1',
10
+ item: 'flex gap-1',
11
11
  value: 'text-4xl [font-weight:var(--font-weight-heading,700)] tracking-tight tabular-nums md:text-5xl',
12
12
  label: 'text-sm opacity-80',
13
13
  },
@@ -25,8 +25,11 @@ const stats = tv({
25
25
  tone: { default: {}, muted: {}, inverse: {}, accent: { label: 'opacity-100' } },
26
26
  // The label under each figure: small, body size, or large (a counter title set like a subheading).
27
27
  labelSize: { sm: {}, base: { label: 'text-base' }, lg: { label: 'text-lg' } },
28
+ // Which comes first in the page's text: the source's own order (a Gutenberg figure is a heading over its label, an
29
+ // Elementor counter names itself first). The figure is drawn on top either way.
30
+ order: { 'label-first': { item: 'flex-col-reverse' }, 'value-first': { item: 'flex-col' } },
28
31
  },
29
- defaultVariants: { columns: '3', style: 'plain', tone: 'default', labelSize: 'sm' },
32
+ defaultVariants: { columns: '3', style: 'plain', tone: 'default', labelSize: 'sm', order: 'label-first' },
30
33
  })
31
34
 
32
35
  interface Item {
@@ -43,13 +46,15 @@ interface Props {
43
46
  columns?: '2' | '3' | '4' | undefined
44
47
  style?: 'plain' | 'card' | undefined
45
48
  labelSize?: 'sm' | 'base' | 'lg' | undefined
49
+ order?: 'label-first' | 'value-first' | undefined
46
50
  tone?: Tone | undefined
47
51
  id?: string | undefined
48
52
  }
49
53
 
50
- const { heading, intro, items, columns, style, labelSize, tone, id } = Astro.props
51
- const s = stats({ columns, style, labelSize, tone })
54
+ const { heading, intro, items, columns, style, labelSize, order, tone, id } = Astro.props
55
+ const s = stats({ columns, style, labelSize, order, tone })
52
56
  const headingId = heading && id ? `${id}-title` : undefined
57
+ // A <dl> names its term first, so figure-first is a plain list, read in the order it is drawn (WCAG 1.3.2).
53
58
  ---
54
59
  <Section tone={tone} id={id} aria-labelledby={headingId}>
55
60
  {(heading || intro) && (
@@ -58,12 +63,23 @@ const headingId = heading && id ? `${id}-title` : undefined
58
63
  {intro && <p class:list={['text-lg', tone === 'accent' ? undefined : 'opacity-80']} data-kit-text>{intro}</p>}
59
64
  </header>
60
65
  )}
61
- <dl class={s.list()}>
62
- {items.map(item => (
63
- <div class={s.item()}>
64
- <dt class={s.label()}>{item.label}</dt>
65
- <dd class={s.value()}>{item.prefix}{item.value}{item.suffix}</dd>
66
- </div>
67
- ))}
68
- </dl>
66
+ {order === 'value-first' ? (
67
+ <ul class={s.list()} role="list">
68
+ {items.map(item => (
69
+ <li class={s.item()}>
70
+ <p class={s.value()}>{item.prefix}{item.value}{item.suffix}</p>
71
+ <p class={s.label()}>{item.label}</p>
72
+ </li>
73
+ ))}
74
+ </ul>
75
+ ) : (
76
+ <dl class={s.list()}>
77
+ {items.map(item => (
78
+ <div class={s.item()}>
79
+ <dt class={s.label()}>{item.label}</dt>
80
+ <dd class={s.value()}>{item.prefix}{item.value}{item.suffix}</dd>
81
+ </div>
82
+ ))}
83
+ </dl>
84
+ )}
69
85
  </Section>
@@ -133,5 +133,28 @@
133
133
  }
134
134
  ]
135
135
  }
136
+ },
137
+ {
138
+ "name": "value-first",
139
+ "props": {
140
+ "heading": "Launch in numbers",
141
+ "order": "value-first",
142
+ "items": [
143
+ {
144
+ "value": "1,200",
145
+ "label": "Teams using Calm Status"
146
+ },
147
+ {
148
+ "value": "99.99",
149
+ "suffix": "%",
150
+ "label": "Uptime last year"
151
+ },
152
+ {
153
+ "value": "4",
154
+ "suffix": " min",
155
+ "label": "Median time to first update"
156
+ }
157
+ ]
158
+ }
136
159
  }
137
160
  ]
@@ -43,6 +43,14 @@
43
43
  ],
44
44
  "default": "sm",
45
45
  "description": "The label under each figure: sm (small), base (body size) or lg (a counter title set like a subheading)."
46
+ },
47
+ "order": {
48
+ "options": [
49
+ "label-first",
50
+ "value-first"
51
+ ],
52
+ "default": "label-first",
53
+ "description": "Which the page's text reads first, as the source does: label-first (an Elementor counter) or value-first (a Gutenberg figure heading over its label). The figure is drawn on top either way."
46
54
  }
47
55
  },
48
56
  "props": {
@@ -15,6 +15,7 @@ const steps = tv({
15
15
  list: 'grid gap-8 [counter-reset:kit-step]',
16
16
  step: 'flex gap-4 [counter-increment:kit-step]',
17
17
  body: 'flex flex-col gap-2',
18
+ badge: 'shrink-0',
18
19
  media: 'mb-2 overflow-hidden rounded-[var(--radius-image,var(--radius-card))]',
19
20
  title: 'text-lg leading-snug [font-weight:var(--font-weight-heading,600)] text-balance',
20
21
  text: 'text-pretty opacity-80',
@@ -24,16 +25,22 @@ const steps = tv({
24
25
  list: { list: 'max-w-3xl' },
25
26
  row: { list: 'sm:grid-cols-2 lg:auto-cols-fr lg:grid-flow-col lg:grid-cols-none', step: 'flex-col' },
26
27
  },
27
- // The number is drawn from a CSS counter, so the list stays one <ol> a screen reader counts itself.
28
+ // The number is drawn from a CSS counter, so the list stays one <ol> a screen reader counts itself. A number the
29
+ // source wrote (`01`) is the page's text and is shown as written, hidden from the screen reader that already counts.
28
30
  style: {
29
- numbered: { step: "before:flex before:size-10 before:shrink-0 before:items-center before:justify-center before:rounded-full before:bg-accent before:font-semibold before:text-accent-ink before:content-[counter(kit-step)]" },
30
- plain: { step: 'border-l-2 border-accent pl-4' },
31
+ numbered: {
32
+ step: "before:flex before:size-10 before:shrink-0 before:items-center before:justify-center before:rounded-full before:bg-accent before:font-semibold before:text-accent-ink before:content-[counter(kit-step)]",
33
+ badge: 'flex h-10 min-w-10 items-center justify-center self-start rounded-full bg-accent px-2 font-semibold text-accent-ink',
34
+ },
35
+ plain: { step: 'border-l-2 border-accent pl-4', badge: 'font-semibold tabular-nums' },
31
36
  },
32
37
  },
33
38
  defaultVariants: { layout: 'list', style: 'numbered' },
34
39
  })
35
40
 
36
41
  interface Step {
42
+ /** The number as the source writes it (`01`, `Step 2`); without it the list counts itself. */
43
+ number?: string | undefined
37
44
  title: string
38
45
  text: string
39
46
  /** The image's address. */
@@ -64,7 +71,8 @@ const headingId = heading && id ? `${id}-title` : undefined
64
71
  )}
65
72
  <ol class={s.list()}>
66
73
  {items.map(step => (
67
- <li class={s.step()}>
74
+ <li class={s.step({ class: step.number ? 'before:hidden' : undefined })}>
75
+ {step.number && <span class={s.badge()} aria-hidden="true">{step.number}</span>}
68
76
  <div class={s.body()}>
69
77
  {imageOf(step.image, step.imageAlt) && <div class={s.media()}><KitImage image={imageOf(step.image, step.imageAlt)!} sizes="(min-width: 1024px) 25vw, (min-width: 640px) 50vw, 100vw" class="aspect-[3/2] w-full object-cover" /></div>}
70
78
  <h3 class={s.title()}>{step.title}</h3>
@@ -1,5 +1,7 @@
1
1
  [
2
2
  {"name": "list-numbered", "props": {"id": "how", "heading": "How it works", "intro": "Three steps from WordPress to a static site.", "items": [{"title": "Connect", "text": "Point us at your site; we read everything it publishes."}, {"title": "Review", "text": "Check the plan: every page, model and redirect."}, {"title": "Launch", "text": "Deploy the new site; every old address still works."}]}},
3
3
  {"name": "row-plain-images", "props": {"layout": "row", "style": "plain", "heading": "Our process", "items": [{"title": "Discover", "text": "We learn the site and its readers.", "image": "/kit-fixtures/photo-1.svg"}, {"title": "Design", "text": "We draw the pages and the system.", "image": "/kit-fixtures/photo-2.svg"}, {"title": "Build", "text": "We ship it, fast and accessible.", "image": "/kit-fixtures/photo-3.svg", "imageAlt": "A laptop showing the finished site"}]}},
4
- {"name": "row-numbered", "props": {"layout": "row", "items": [{"title": "Sign up", "text": "One minute, no card."}, {"title": "Import", "text": "Bring your content."}, {"title": "Edit", "text": "Change it in Studio."}, {"title": "Publish", "text": "Push and it is live."}]}}
4
+ {"name": "row-numbered", "props": {"layout": "row", "items": [{"title": "Sign up", "text": "One minute, no card."}, {"title": "Import", "text": "Bring your content."}, {"title": "Edit", "text": "Change it in Studio."}, {"title": "Publish", "text": "Push and it is live."}]}},
5
+ {"name": "source-numbers", "props": {"heading": "Getting started", "items": [{"number": "01", "title": "Connect", "text": "Point Calm Status at the services you already monitor."}, {"number": "02", "title": "Write", "text": "Pick the words customers see for each kind of problem."}, {"number": "03", "title": "Publish", "text": "Put the page live on your own domain."}]}},
6
+ {"name": "row-plain-source-numbers", "props": {"layout": "row", "style": "plain", "items": [{"number": "Step 1", "title": "Call", "text": "We learn what you make and who buys it."}, {"number": "Step 2", "title": "Design and build", "text": "Four weeks of weekly previews."}, {"number": "Step 3", "title": "Launch", "text": "We switch it on together."}]}}
5
7
  ]
@@ -46,6 +46,11 @@
46
46
  "items": {
47
47
  "type": "object",
48
48
  "fields": {
49
+ "number": {
50
+ "type": "string",
51
+ "label": "Number",
52
+ "description": "The step's number as the source writes it (01, Step 2); empty: the list counts 1, 2, 3."
53
+ },
49
54
  "title": {
50
55
  "type": "string",
51
56
  "label": "Title",
@@ -58,6 +58,9 @@ interface Props {
58
58
  const { items, heading, layout, quoteSize, tone, id } = Astro.props
59
59
  const s = testimonial({ layout, quoteSize, tone })
60
60
  const headingId = heading && id ? `${id}-title` : undefined
61
+ // The quotation marks are drawn, not written: the quote's text is the source's words, read once. A quote the source
62
+ // already wrote inside marks keeps its own.
63
+ const quoted = (text: string) => /^\s*["'“‘„«‹「『]/.test(text)
61
64
  ---
62
65
  {items.length > 0 && (
63
66
  <Section tone={tone} spacing="md" id={id} aria-labelledby={headingId}>
@@ -65,7 +68,7 @@ const headingId = heading && id ? `${id}-title` : undefined
65
68
  <div class={s.list()}>
66
69
  {items.map(item => (
67
70
  <figure class={s.card()}>
68
- <blockquote class={s.quote()}><p>“{item.quote}”</p></blockquote>
71
+ <blockquote class={s.quote()}><p class:list={[!quoted(item.quote) && "before:content-[open-quote] after:content-[close-quote]"]}>{item.quote}</p></blockquote>
69
72
  <figcaption class={s.person()}>
70
73
  {imageOf(item.avatar, item.avatarAlt) && <KitImage image={imageOf(item.avatar, item.avatarAlt)!} sizes="48px" class="size-12 shrink-0 rounded-full object-cover" />}
71
74
  <span class="flex flex-col text-left">
@@ -34,5 +34,9 @@
34
34
  {
35
35
  "name": "single-quote-lg",
36
36
  "props": { "layout": "single", "quoteSize": "lg", "items": [{ "quote": "We planned less and saw more.", "name": "Jonas Weber", "role": "Reader" }] }
37
+ },
38
+ {
39
+ "name": "grid-source-quoted",
40
+ "props": { "items": [{ "quote": "“We set it up before lunch.”", "name": "Grace Lin", "role": "Head of Support, Fernhill" }, { "quote": "People just read the page.", "name": "Omar Haddad", "role": "Support lead" }] }
37
41
  }
38
42
  ]
package/mapping/divi.json CHANGED
@@ -413,6 +413,9 @@
413
413
  {
414
414
  "id": "stats.counter",
415
415
  "component": "stats",
416
+ "variant": {
417
+ "order": "value-first"
418
+ },
416
419
  "slots": {
417
420
  "intro": {
418
421
  "match": "divi/et_pb_text",
@@ -533,6 +533,7 @@
533
533
  "into": "items",
534
534
  "each": "column",
535
535
  "item": {
536
+ "number": "@number attr:title || @number dom:",
536
537
  "title": "@title attr:title || @title dom:",
537
538
  "text": "@text dom:"
538
539
  }
@@ -662,6 +662,9 @@
662
662
  {
663
663
  "id": "stats.columns",
664
664
  "component": "stats",
665
+ "variant": {
666
+ "order": "value-first"
667
+ },
665
668
  "when": {
666
669
  "columns": [
667
670
  2,
@@ -829,6 +832,7 @@
829
832
  "into": "items",
830
833
  "each": "column",
831
834
  "item": {
835
+ "number": "@number dom:",
832
836
  "title": "@title dom:",
833
837
  "text": "@text dom:"
834
838
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/astro-kit",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
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.33.0"
45
+ "@contentrain/types": "1.35.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^22.10.0",