@contentrain/astro-kit 0.44.2 → 0.45.0

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
@@ -148,6 +148,17 @@
148
148
  "default": "default",
149
149
  "description": "Section background when nothing is measured."
150
150
  },
151
+ "inset": {
152
+ "options": [
153
+ "none",
154
+ "15",
155
+ "27",
156
+ "34",
157
+ "42"
158
+ ],
159
+ "default": "none",
160
+ "description": "Layout stack only (split ignores it). From md (768px) up, the band's content (copy, image and items) starts at this share of the frame's width and runs to its end, its text at that edge (a centred band's too): a narrow talk beside a wide margin. Below md it is the full column. none: as before."
161
+ },
151
162
  "headingLines": {
152
163
  "options": [
153
164
  "flow",
@@ -3822,6 +3833,33 @@
3822
3833
  ],
3823
3834
  "default": "none",
3824
3835
  "description": "Split and centered: edge runs the image on to the window's edge on its side (both sides when centered); overlap also lets it cross the section's bottom edge by --hero-overlap (3rem), over the next section's top padding, when the image comes last: with stackOrder media-first it draws as edge, so it never covers the text. A cover ignores it."
3836
+ },
3837
+ "inset": {
3838
+ "options": [
3839
+ "none",
3840
+ "15",
3841
+ "27",
3842
+ "34",
3843
+ "42"
3844
+ ],
3845
+ "default": "none",
3846
+ "description": "Layout centered only (split and cover ignore it). From md (768px) up, the copy's start edge sits at this share of the frame's width and its text is set at that edge, not centred: a statement on an axis, the void before it part of the page. Below md the hero is centred as without it. none: as before."
3847
+ },
3848
+ "height": {
3849
+ "options": [
3850
+ "auto",
3851
+ "screen"
3852
+ ],
3853
+ "default": "auto",
3854
+ "description": "The hero's height. screen: from sm (640px) up, at least the window's height (100svh, 100vh where unknown) less --header-h, the header's height where the site sets it (0 without, so a static header above adds to it): the page opens on one full screen, the statement placed in it by contentAt. Below sm the hero is as tall as its content, as with auto: no empty screen on a phone. A boxed cover (width content) ignores it. auto: as before."
3855
+ },
3856
+ "contentAt": {
3857
+ "options": [
3858
+ "start",
3859
+ "end"
3860
+ ],
3861
+ "default": "start",
3862
+ "description": "Where the content sits in a hero taller than it (height screen, or a cover with its own height). end: on the bottom edge, the section's bottom padding under it (a title on the image's lower edge, a statement after a void). start: where the layout puts it, as before."
3825
3863
  }
3826
3864
  },
3827
3865
  "props": {
@@ -5783,6 +5821,17 @@
5783
5821
  ],
5784
5822
  "default": "lg",
5785
5823
  "description": "Type scale of the body."
5824
+ },
5825
+ "inset": {
5826
+ "options": [
5827
+ "none",
5828
+ "15",
5829
+ "27",
5830
+ "34",
5831
+ "42"
5832
+ ],
5833
+ "default": "none",
5834
+ "description": "From md (768px) up, the body's start edge sits at this share of its container's width and the body runs to the container's end: the axis an editorial page sets its text on (an intro in a narrow column, a column set off the page's start). Below md it is the full column. none: as before. The share is of the container the body sits in (in the starter's pages, the reading column)."
5786
5835
  }
5787
5836
  },
5788
5837
  "props": {
@@ -15,6 +15,7 @@ import { muted } from '../_shared/muted'
15
15
 
16
16
  const band = tv({
17
17
  slots: {
18
+ column: 'flex flex-col gap-10',
18
19
  grid: 'grid gap-10',
19
20
  copy: 'flex max-w-3xl flex-col gap-4',
20
21
  eyebrow: 'text-sm font-semibold tracking-wide text-accent-text uppercase',
@@ -46,8 +47,18 @@ const band = tv({
46
47
  },
47
48
  // Accent backgrounds have the least contrast headroom: text on them is never dimmed.
48
49
  tone: { default: {}, muted: {}, inverse: {}, accent: { eyebrow: 'text-current' } },
50
+ // Stack only (split ignores it): from md the band's content starts at this share of the frame's width and runs to
51
+ // its end, its text at that edge; below md it is the full column, as without it.
52
+ inset: { none: {}, '15': {}, '27': {}, '34': {}, '42': {} },
49
53
  },
50
- defaultVariants: { layout: 'stack', columns: '3', align: 'start', tone: 'default' },
54
+ compoundVariants: [
55
+ { layout: 'stack', inset: '15', class: { column: 'md:ms-[15%]' } },
56
+ { layout: 'stack', inset: '27', class: { column: 'md:ms-[27%]' } },
57
+ { layout: 'stack', inset: '34', class: { column: 'md:ms-[34%]' } },
58
+ { layout: 'stack', inset: '42', class: { column: 'md:ms-[42%]' } },
59
+ { layout: 'stack', align: 'center', inset: ['15', '27', '34', '42'], class: { copy: 'md:mx-0 md:items-start md:text-start', actions: 'md:justify-start', item: 'md:items-start md:text-start' } },
60
+ ],
61
+ defaultVariants: { layout: 'stack', columns: '3', align: 'start', tone: 'default', inset: 'none' },
51
62
  })
52
63
 
53
64
  interface Item {
@@ -88,12 +99,14 @@ interface Props {
88
99
  headingLines?: 'flow' | 'lines' | undefined
89
100
  /** The same for the lead: `lines` draws each of its lines as a block, still one paragraph. */
90
101
  leadLines?: 'flow' | 'lines' | undefined
102
+ /** Stack only: from md the content starts at this share of the frame's width (`15` … `42` %), its text at that edge. `none` (absent): as before. */
103
+ inset?: 'none' | '15' | '27' | '34' | '42' | undefined
91
104
  }
92
105
 
93
- const { heading, eyebrow, lead, media, items = [], actions = [], headingLevel = 2, layout, columns, align, tone, measured, id, headingLines, leadLines } = Astro.props
106
+ const { heading, eyebrow, lead, media, items = [], actions = [], headingLevel = 2, layout, columns, align, tone, measured, id, headingLines, leadLines, inset } = Astro.props
94
107
  // The measured alignment when none is given (`end` has no band layout: it reads as `start`).
95
108
  const aligned = align ?? (measured?.align === 'center' ? 'center' : undefined)
96
- const s = band({ layout: media ? layout : 'stack', columns, align: aligned, tone })
109
+ const s = band({ layout: media ? layout : 'stack', columns, align: aligned, tone, inset })
97
110
  const Heading = `h${headingLevel}` as 'h2' | 'h3'
98
111
  const ItemHeading = `h${heading ? headingLevel + 1 : headingLevel}` as 'h2' | 'h3' | 'h4'
99
112
  // Two bands with the same heading on one page get two ids (`band-<heading>`, then `band-<heading>-2`).
@@ -101,7 +114,7 @@ const headingId = heading ? (id ? `${id}-title` : instanceId(Astro.locals, undef
101
114
  const hasCopy = Boolean(heading || eyebrow || lead || actions.length)
102
115
  ---
103
116
  <Section kit="band" tone={tone} id={id} aria-labelledby={headingId} measured={measured}>
104
- <div class="flex flex-col gap-10">
117
+ <div class={s.column()}>
105
118
  {(hasCopy || media) && (
106
119
  <div class={s.grid()}>
107
120
  {hasCopy && (
@@ -5,5 +5,129 @@
5
5
  {"name": "measured-image-overlay", "props": {"heading": "Visit the farm", "lead": "Open every weekend.", "actions": [{"label": "Plan a visit", "href": "/visit/"}], "measured": {"padY": 120, "minHeight": 480, "tone": "dark", "bg": {"image": "/kit-fixtures/photo-2.svg", "overlay": "rgba(0, 0, 0, 0.55)", "size": "cover", "position": "center"}}, "columns": "1"}},
6
6
  {"name": "items-links", "props": {"heading": "Services", "columns": "2", "tone": "muted", "items": [{"title": "Audits", "text": "Where a site loses readers.", "href": "/audits/", "linkLabel": "Read more", "media": "/kit-fixtures/photo-3.svg"}, {"title": "Hosting", "text": "Static hosting, managed.", "href": "/hosting/", "icon": "/kit-fixtures/logo.svg"}]}},
7
7
  {"name": "values-points", "props": {"heading": "Plans", "columns": "3", "items": [{"value": "$9", "title": "Starter", "meta": "per month", "points": ["One site", "Email support"]}, {"value": "$29", "title": "Team", "meta": "per month", "points": ["Five sites", "Shared components"], "href": "/pricing/team/", "linkLabel": "Choose Team"}, {"value": "250+", "title": "Sites moved", "text": "Since 2019."}]}},
8
- {"name": "heading-lines", "props": {"heading": "Fewer tools.\nMore writing.", "headingLines": "lines", "lead": "Your posts, pages and forms in one place.\nNothing to update, nothing to patch.", "leadLines": "lines", "actions": [{"label": "See how", "href": "/how/"}]}}
8
+ {"name": "heading-lines", "props": {"heading": "Fewer tools.\nMore writing.", "headingLines": "lines", "lead": "Your posts, pages and forms in one place.\nNothing to update, nothing to patch.", "leadLines": "lines", "actions": [{"label": "See how", "href": "/how/"}]}},
9
+ {
10
+ "name": "inset-27",
11
+ "props": {
12
+ "heading": "What we do",
13
+ "lead": "Three things, done carefully.",
14
+ "items": [
15
+ {
16
+ "title": "Design",
17
+ "text": "Tokens and components."
18
+ },
19
+ {
20
+ "title": "Migration",
21
+ "text": "Every address kept."
22
+ },
23
+ {
24
+ "title": "Speed",
25
+ "text": "Static and fast."
26
+ }
27
+ ],
28
+ "tone": "inverse",
29
+ "inset": "27"
30
+ }
31
+ },
32
+ {
33
+ "name": "center-inset-34",
34
+ "props": {
35
+ "heading": "Our numbers",
36
+ "align": "center",
37
+ "measured": {
38
+ "containerPx": 1140,
39
+ "padY": 96,
40
+ "padX": 24,
41
+ "bg": {
42
+ "color": "#0f172a"
43
+ },
44
+ "columns": {
45
+ "390": 1,
46
+ "768": 2,
47
+ "1280": 4
48
+ },
49
+ "gap": 32,
50
+ "radius": 8
51
+ },
52
+ "items": [
53
+ {
54
+ "title": "12",
55
+ "text": "years"
56
+ },
57
+ {
58
+ "title": "340",
59
+ "text": "sites"
60
+ },
61
+ {
62
+ "title": "9",
63
+ "text": "countries"
64
+ },
65
+ {
66
+ "title": "1",
67
+ "text": "team"
68
+ }
69
+ ],
70
+ "columns": "4",
71
+ "inset": "34"
72
+ }
73
+ },
74
+ {
75
+ "name": "items-links-inset-15",
76
+ "props": {
77
+ "heading": "Services",
78
+ "columns": "2",
79
+ "tone": "muted",
80
+ "items": [
81
+ {
82
+ "title": "Audits",
83
+ "text": "Where a site loses readers.",
84
+ "href": "/audits/",
85
+ "linkLabel": "Read more",
86
+ "media": "/kit-fixtures/photo-3.svg"
87
+ },
88
+ {
89
+ "title": "Hosting",
90
+ "text": "Static hosting, managed.",
91
+ "href": "/hosting/",
92
+ "icon": "/kit-fixtures/logo.svg"
93
+ }
94
+ ],
95
+ "inset": "15"
96
+ }
97
+ },
98
+ {
99
+ "name": "values-points-inset-42",
100
+ "props": {
101
+ "heading": "Plans",
102
+ "columns": "3",
103
+ "items": [
104
+ {
105
+ "value": "$9",
106
+ "title": "Starter",
107
+ "meta": "per month",
108
+ "points": [
109
+ "One site",
110
+ "Email support"
111
+ ]
112
+ },
113
+ {
114
+ "value": "$29",
115
+ "title": "Team",
116
+ "meta": "per month",
117
+ "points": [
118
+ "Five sites",
119
+ "Shared components"
120
+ ],
121
+ "href": "/pricing/team/",
122
+ "linkLabel": "Choose Team"
123
+ },
124
+ {
125
+ "value": "250+",
126
+ "title": "Sites moved",
127
+ "text": "Since 2019."
128
+ }
129
+ ],
130
+ "inset": "42"
131
+ }
132
+ }
9
133
  ]
@@ -52,6 +52,17 @@
52
52
  "default": "default",
53
53
  "description": "Section background when nothing is measured."
54
54
  },
55
+ "inset": {
56
+ "options": [
57
+ "none",
58
+ "15",
59
+ "27",
60
+ "34",
61
+ "42"
62
+ ],
63
+ "default": "none",
64
+ "description": "Layout stack only (split ignores it). From md (768px) up, the band's content (copy, image and items) starts at this share of the frame's width and runs to its end, its text at that edge (a centred band's too): a narrow talk beside a wide margin. Below md it is the full column. none: as before."
65
+ },
55
66
  "headingLines": {
56
67
  "options": [
57
68
  "flow",
@@ -13,6 +13,8 @@ import { coverHeight as heightOf, headingStyle as styleOf } from './hero'
13
13
  import { muted, mutedInverse } from '../_shared/muted'
14
14
  import { BLEED } from '../_shared/bleed'
15
15
 
16
+ const SCREEN = 'sm:flex sm:flex-col sm:min-h-[calc(100vh-var(--header-h,0px))] sm:supports-[height:100svh]:min-h-[calc(100svh-var(--header-h,0px))] sm:[&>.container-page]:flex sm:[&>.container-page]:flex-1 sm:[&>.container-page]:flex-col sm:[&>.container-page>.grid]:flex-1'
17
+
16
18
  const hero = tv({
17
19
  slots: {
18
20
  grid: 'grid items-center gap-10',
@@ -60,6 +62,12 @@ const hero = tv({
60
62
  // padding: set at the grid's bottom and moved past the hero's own bottom padding (`spacing="lg"`: 4rem, 7rem from md),
61
63
  // not laid out there, so the next section stays where it was. Only an image that comes last can cross the bottom: one
62
64
  // that comes first (`media-first`) would be moved down over the text, so there overlap draws as edge.
65
+ // Centered only (split and cover ignore it): from md the copy's start edge sits at this share of the frame's width,
66
+ // its text at that edge; below md it is centred, as without it.
67
+ inset: { none: {}, '15': {}, '27': {}, '34': {}, '42': {} },
68
+ // Where the content sits in a hero taller than it (a screen-high one, a cover): where the layout puts it, or on the
69
+ // bottom edge, the section's bottom padding under it.
70
+ contentAt: { start: {}, end: { grid: 'content-end' } },
63
71
  imageBleed: { none: {}, edge: { media: 'rounded-none' }, overlap: { media: 'relative z-10 self-end translate-y-[calc(4rem+var(--hero-overlap,3rem))] rounded-none md:translate-y-[calc(7rem+var(--hero-overlap,3rem))]' } },
64
72
  },
65
73
  compoundVariants: [
@@ -69,8 +77,14 @@ const hero = tv({
69
77
  // After the centring rules, so the lead leaves them.
70
78
  { leadAlign: 'start', class: { lead: 'mx-0 max-w-none self-stretch text-start' } },
71
79
  { actionsAlign: 'start', class: { actions: 'justify-start self-stretch' } },
80
+ // After the centring rules, so the copy leaves them from md.
81
+ { layout: 'centered', inset: ['15', '27', '34', '42'], class: { copy: 'md:items-start md:text-start', lead: 'md:mx-0', points: 'md:justify-start', actions: 'md:justify-start' } },
82
+ { layout: 'centered', inset: '15', class: { copy: 'md:ms-[15%]' } },
83
+ { layout: 'centered', inset: '27', class: { copy: 'md:ms-[27%]' } },
84
+ { layout: 'centered', inset: '34', class: { copy: 'md:ms-[34%]' } },
85
+ { layout: 'centered', inset: '42', class: { copy: 'md:ms-[42%]' } },
72
86
  ],
73
- defaultVariants: { layout: 'split', tone: 'default', align: 'center', leadAlign: 'inherit', actionsAlign: 'inherit', width: 'full', headingScale: 'default', headingSize: 'own', headingLines: 'flow', stackOrder: 'text-first', imageBleed: 'none' },
87
+ defaultVariants: { layout: 'split', tone: 'default', align: 'center', leadAlign: 'inherit', actionsAlign: 'inherit', width: 'full', headingScale: 'default', headingSize: 'own', headingLines: 'flow', stackOrder: 'text-first', inset: 'none', contentAt: 'start', imageBleed: 'none' },
74
88
  })
75
89
 
76
90
  interface Props {
@@ -106,17 +120,23 @@ interface Props {
106
120
  stackOrder?: 'text-first' | 'media-first' | undefined
107
121
  /** Split and centered: `edge` runs the image on to the window's edge on its side (both sides when centered); `overlap` also lets it cross the section's bottom edge by `--hero-overlap` (3rem), when the image comes last (with `media-first` it draws as `edge`). `none` (absent): as before. */
108
122
  imageBleed?: 'none' | 'edge' | 'overlap' | undefined
123
+ /** Centered only: from md the copy starts at this share of the frame's width (`15` … `42` %), its text at that edge. `none` (absent): as before. */
124
+ inset?: 'none' | '15' | '27' | '34' | '42' | undefined
125
+ /** `screen`: from sm the hero is at least the window's height less `--header-h` (a site that sets it; 0 without), on phones as before. A boxed cover (`width: content`) ignores it. `auto` (absent): as before. */
126
+ height?: 'auto' | 'screen' | undefined
127
+ /** `end`: the content sits on the hero's bottom edge, in a hero taller than it (`height: screen`, a cover). `start` (absent): where the layout puts it, as before. */
128
+ contentAt?: 'start' | 'end' | undefined
109
129
  /** The source section's measured style (`MeasuredStyle`): frame, padding, fill, background image, columns per width, gap. Absent, the site's tokens; present, it wins over tone and columns. */
110
130
  measured?: MeasuredStyle | undefined
111
131
  }
112
132
 
113
133
 
114
- const { heading, level = 1, eyebrow, lead, items = [], actions = [], image, layout = 'split', align, leadAlign, actionsAlign, width = 'full', tone, id: givenId, minHeight, headingScale, headingSize, headingLines, stackOrder, imageBleed, measured } = Astro.props
134
+ const { heading, level = 1, eyebrow, lead, items = [], actions = [], image, layout = 'split', align, leadAlign, actionsAlign, width = 'full', tone, id: givenId, minHeight, headingScale, headingSize, headingLines, stackOrder, imageBleed, inset, height, contentAt, measured } = Astro.props
115
135
  // Every section has an id, so a theme or a design layer can address this one: the page's, or `hero`, `hero-2`, … in render order.
116
136
  const id = instanceId(Astro.locals, givenId, 'hero')
117
137
  // Overlap moves the image down past the section's bottom, so only an image that comes last may: first, it is edge.
118
138
  const bleedAs = imageBleed === 'overlap' && stackOrder === 'media-first' ? 'edge' : imageBleed
119
- const s = hero({ layout, align, leadAlign, actionsAlign, width, tone, headingScale, headingSize, headingLines, stackOrder, imageBleed: bleedAs })
139
+ const s = hero({ layout, align, leadAlign, actionsAlign, width, tone, headingScale, headingSize, headingLines, stackOrder, inset, contentAt, imageBleed: bleedAs })
120
140
  const Heading = level === 1 ? 'h1' : 'h2'
121
141
  const headingId = `${id}-title`
122
142
  const cover = layout === 'cover' && image
@@ -127,6 +147,10 @@ const sectionMeasured = cover && measured ? { ...measured, bg: measured.bg ? { .
127
147
  const scrim = cover ? measuredFrame(measured).overlay : undefined
128
148
  // The window's edge for a bleeding image: its own side in split (the start side when it comes first), both when centered.
129
149
  const bleeds = !cover && image && imageBleed !== undefined && imageBleed !== 'none'
150
+ // A screen-high hero from sm: the section is a column at least the window's height (less a header the site measures as
151
+ // `--header-h`), its frame and grid stretched to fill it, so `contentAt` has the room to place the content in. 100vh
152
+ // where the small viewport unit is unknown. On phones the hero is as tall as its content.
153
+ const screen = height === 'screen' && !(cover && width === 'content') ? SCREEN : undefined
130
154
  const bleed = bleeds ? (layout === 'centered' ? `max-w-none md:w-auto ${BLEED.start} ${BLEED.end}` : BLEED[stackOrder === 'media-first' ? 'start' : 'end']) : undefined
131
155
  ---
132
156
  {cover && width === 'content'
@@ -151,7 +175,7 @@ const bleed = bleeds ? (layout === 'centered' ? `max-w-none md:w-auto ${BLEED.st
151
175
  )
152
176
  : cover
153
177
  ? (
154
- <Section kit="hero" measured={sectionMeasured} tone="inverse" spacing="none" width="none" id={id} aria-labelledby={headingId} class="relative isolate overflow-hidden">
178
+ <Section kit="hero" measured={sectionMeasured} tone="inverse" spacing="none" width="none" id={id} aria-labelledby={headingId} class={screen ? `relative isolate overflow-hidden ${screen}` : 'relative isolate overflow-hidden'}>
155
179
  <KitImage image={image} priority sizes="100vw" class="absolute inset-0 -z-10 h-full w-full object-cover" />
156
180
  <div class:list={['absolute inset-0 -z-10', !scrim && 'bg-ink/60']} style={scrim ? `background: ${scrim}` : undefined} aria-hidden="true" />
157
181
  <div class:list={['container-page', measured?.padY === undefined && 'py-16 md:py-28']}>
@@ -168,7 +192,7 @@ const bleed = bleeds ? (layout === 'centered' ? `max-w-none md:w-auto ${BLEED.st
168
192
  </Section>
169
193
  )
170
194
  : (
171
- <Section kit="hero" measured={sectionMeasured} tone={tone} spacing="lg" id={id} aria-labelledby={headingId} class={bleeds ? 'overflow-x-clip' : undefined}>
195
+ <Section kit="hero" measured={sectionMeasured} tone={tone} spacing="lg" id={id} aria-labelledby={headingId} class={[bleeds && 'overflow-x-clip', screen].filter(Boolean).join(' ') || undefined}>
172
196
  {/* Without an image a split has nothing beside the text: the text takes the whole row, not half of it. */}
173
197
  <div class={s.grid({ class: image ? undefined : 'md:grid-cols-1' })}>
174
198
  <div class={s.copy()}>
@@ -248,5 +248,122 @@
248
248
  {"name": "split-overlap", "props": {"heading": "Your site, moved and checked", "lead": "Every page, address and form carried over.", "imageBleed": "overlap", "tone": "muted", "image": {"src": "/kit-fixtures/photo-1.svg", "alt": "A desk by a window", "width": 1200, "height": 800}}},
249
249
  {"name": "centered-media-first-overlap", "props": {"layout": "centered", "heading": "Your site, moved and checked", "lead": "Every page, address and form carried over.", "stackOrder": "media-first", "imageBleed": "overlap", "tone": "muted", "image": {"src": "/kit-fixtures/photo-1.svg", "alt": "A desk by a window", "width": 1200, "height": 800}}},
250
250
  {"name": "split-media-first-overlap", "props": {"heading": "Your site, moved and checked", "lead": "Every page, address and form carried over.", "stackOrder": "media-first", "imageBleed": "overlap", "tone": "muted", "image": {"src": "/kit-fixtures/photo-1.svg", "alt": "A desk by a window", "width": 1200, "height": 800}}},
251
- {"name": "heading-hooks", "theme": {"--font-heading": "var(--font-serif)", "--tracking-heading": "0.04em"}, "props": {"layout": "centered", "eyebrow": "A design direction", "heading": "Headings in a face of their own", "lead": "The heading face and track come from the theme; the lead keeps the body face."}}
251
+ {"name": "heading-hooks", "theme": {"--font-heading": "var(--font-serif)", "--tracking-heading": "0.04em"}, "props": {"layout": "centered", "eyebrow": "A design direction", "heading": "Headings in a face of their own", "lead": "The heading face and track come from the theme; the lead keeps the body face."}},
252
+ {
253
+ "name": "centered-inset-42",
254
+ "props": {
255
+ "heading": "Plan a calmer trip",
256
+ "lead": "Our guides pick fewer places and give each one more time.",
257
+ "layout": "centered",
258
+ "tone": "muted",
259
+ "level": 2,
260
+ "actions": [
261
+ {
262
+ "label": "Browse guides",
263
+ "href": "/category/guides/"
264
+ }
265
+ ],
266
+ "image": {
267
+ "src": "/kit-fixtures/photo-2.svg",
268
+ "alt": "",
269
+ "width": 1600,
270
+ "height": 700
271
+ },
272
+ "inset": "42"
273
+ }
274
+ },
275
+ {
276
+ "name": "centered-lead-start-inset-15",
277
+ "props": {
278
+ "heading": "About the studio",
279
+ "lead": "We are a small team of editors and photographers. This page tells you who we are, how we work and what we will never do with your data.",
280
+ "layout": "centered",
281
+ "leadAlign": "start",
282
+ "level": 2,
283
+ "inset": "15"
284
+ }
285
+ },
286
+ {
287
+ "name": "heading-lines-inset-27",
288
+ "props": {
289
+ "layout": "centered",
290
+ "tone": "accent",
291
+ "headingLines": "lines",
292
+ "eyebrow": "Northwind Journal",
293
+ "heading": "Slow travel.\nGood coffee.\nTwelve years on the road.",
294
+ "lead": "Essays, guides and photographs, now in one place.",
295
+ "actions": [
296
+ {
297
+ "label": "Start reading",
298
+ "href": "/journal/"
299
+ }
300
+ ],
301
+ "inset": "27"
302
+ }
303
+ },
304
+ {
305
+ "name": "centered-size-h2-inset-34",
306
+ "props": {
307
+ "heading": "Plan a calmer trip",
308
+ "lead": "Our guides pick fewer places and give each one more time.",
309
+ "layout": "centered",
310
+ "tone": "muted",
311
+ "level": 2,
312
+ "actions": [
313
+ {
314
+ "label": "Browse guides",
315
+ "href": "/category/guides/"
316
+ }
317
+ ],
318
+ "image": {
319
+ "src": "/kit-fixtures/photo-2.svg",
320
+ "alt": "",
321
+ "width": 1600,
322
+ "height": 700
323
+ },
324
+ "headingSize": "h2",
325
+ "inset": "34"
326
+ }
327
+ },
328
+ {
329
+ "name": "heading-lines-screen-end-inset-42",
330
+ "props": {
331
+ "layout": "centered",
332
+ "tone": "accent",
333
+ "headingLines": "lines",
334
+ "eyebrow": "Northwind Journal",
335
+ "heading": "Slow travel.\nGood coffee.\nTwelve years on the road.",
336
+ "lead": "Essays, guides and photographs, now in one place.",
337
+ "actions": [
338
+ {
339
+ "label": "Start reading",
340
+ "href": "/journal/"
341
+ }
342
+ ],
343
+ "height": "screen",
344
+ "contentAt": "end",
345
+ "inset": "42"
346
+ }
347
+ },
348
+ {
349
+ "name": "cover-content-at-end",
350
+ "props": {
351
+ "heading": "The long way round",
352
+ "lead": "A year of trains across Europe, one station at a time.",
353
+ "layout": "cover",
354
+ "actions": [
355
+ {
356
+ "label": "Read the series",
357
+ "href": "/series/long-way/"
358
+ }
359
+ ],
360
+ "image": {
361
+ "src": "/kit-fixtures/photo-3.svg",
362
+ "alt": "",
363
+ "width": 1600,
364
+ "height": 900
365
+ },
366
+ "contentAt": "end"
367
+ }
368
+ }
252
369
  ]
@@ -109,6 +109,33 @@
109
109
  ],
110
110
  "default": "none",
111
111
  "description": "Split and centered: edge runs the image on to the window's edge on its side (both sides when centered); overlap also lets it cross the section's bottom edge by --hero-overlap (3rem), over the next section's top padding, when the image comes last: with stackOrder media-first it draws as edge, so it never covers the text. A cover ignores it."
112
+ },
113
+ "inset": {
114
+ "options": [
115
+ "none",
116
+ "15",
117
+ "27",
118
+ "34",
119
+ "42"
120
+ ],
121
+ "default": "none",
122
+ "description": "Layout centered only (split and cover ignore it). From md (768px) up, the copy's start edge sits at this share of the frame's width and its text is set at that edge, not centred: a statement on an axis, the void before it part of the page. Below md the hero is centred as without it. none: as before."
123
+ },
124
+ "height": {
125
+ "options": [
126
+ "auto",
127
+ "screen"
128
+ ],
129
+ "default": "auto",
130
+ "description": "The hero's height. screen: from sm (640px) up, at least the window's height (100svh, 100vh where unknown) less --header-h, the header's height where the site sets it (0 without, so a static header above adds to it): the page opens on one full screen, the statement placed in it by contentAt. Below sm the hero is as tall as its content, as with auto: no empty screen on a phone. A boxed cover (width content) ignores it. auto: as before."
131
+ },
132
+ "contentAt": {
133
+ "options": [
134
+ "start",
135
+ "end"
136
+ ],
137
+ "default": "start",
138
+ "description": "Where the content sits in a hero taller than it (height screen, or a cover with its own height). end: on the bottom edge, the section's bottom padding under it (a title on the image's lower edge, a statement after a void). start: where the layout puts it, as before."
112
139
  }
113
140
  },
114
141
  "props": {
@@ -13,8 +13,11 @@ const prose = tv({
13
13
  base: 'prose prose-site max-w-none',
14
14
  variants: {
15
15
  size: { base: 'prose-base', lg: 'prose-lg' },
16
+ // From md, the body's start edge sits at this share of its container's width (the axis an editorial layout sets
17
+ // its text on); it runs to the container's end. Below md it is the full column, as without it.
18
+ inset: { none: '', '15': 'md:ms-[15%]', '27': 'md:ms-[27%]', '34': 'md:ms-[34%]', '42': 'md:ms-[42%]' },
16
19
  },
17
- defaultVariants: { size: 'lg' },
20
+ defaultVariants: { size: 'lg', inset: 'none' },
18
21
  })
19
22
 
20
23
  interface Props {
@@ -26,9 +29,11 @@ interface Props {
26
29
  searchable?: boolean | undefined
27
30
  class?: string | undefined
28
31
  size?: 'base' | 'lg' | undefined
32
+ /** From md, the body starts at this share of its container's width (`15` … `42` %). `none` (absent): as before. */
33
+ inset?: 'none' | '15' | '27' | '34' | '42' | undefined
29
34
  }
30
35
 
31
- const { html, markdown, searchable = true, class: className, size } = Astro.props
36
+ const { html, markdown, searchable = true, class: className, size, inset } = Astro.props
32
37
  const body = html ?? await renderMarkdown(markdown)
33
38
  ---
34
- {body && <div class={prose({ size, class: className })} data-pagefind-body={searchable ? '' : undefined} set:html={body} />}
39
+ {body && <div class={prose({ size, inset, class: className })} data-pagefind-body={searchable ? '' : undefined} set:html={body} />}
@@ -1,5 +1,9 @@
1
1
  [
2
2
  { "name": "lg", "frame": "narrow", "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>" } },
3
3
  { "name": "base", "frame": "narrow", "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>" } },
4
- { "name": "markdown", "frame": "narrow", "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" } }
4
+ { "name": "markdown", "frame": "narrow", "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" } },
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
+ { "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
+ { "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" } }
5
9
  ]
@@ -13,6 +13,17 @@
13
13
  ],
14
14
  "default": "lg",
15
15
  "description": "Type scale of the body."
16
+ },
17
+ "inset": {
18
+ "options": [
19
+ "none",
20
+ "15",
21
+ "27",
22
+ "34",
23
+ "42"
24
+ ],
25
+ "default": "none",
26
+ "description": "From md (768px) up, the body's start edge sits at this share of its container's width and the body runs to the container's end: the axis an editorial page sets its text on (an intro in a narrow column, a column set off the page's start). Below md it is the full column. none: as before. The share is of the container the body sits in (in the starter's pages, the reading column)."
16
27
  }
17
28
  },
18
29
  "props": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/astro-kit",
3
- "version": "0.44.2",
3
+ "version": "0.45.0",
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",