@refrakt-md/lumina 0.20.2 → 0.21.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.
@@ -1162,6 +1162,49 @@
1162
1162
  "{content}"
1163
1163
  ]
1164
1164
  },
1165
+ "Section": {
1166
+ "block": "section",
1167
+ "root": ".rf-section",
1168
+ "dataRune": "section",
1169
+ "childOrder": [
1170
+ "{content}"
1171
+ ],
1172
+ "modifiers": {
1173
+ "align": {
1174
+ "source": "meta",
1175
+ "default": "start",
1176
+ "classPattern": ".rf-section--{value}",
1177
+ "dataAttribute": "data-align"
1178
+ }
1179
+ },
1180
+ "elements": {
1181
+ "preamble": {
1182
+ "tag": "header",
1183
+ "selector": ".rf-section__preamble",
1184
+ "source": "autoLabel"
1185
+ },
1186
+ "eyebrow": {
1187
+ "tag": "eyebrow",
1188
+ "selector": ".rf-section__eyebrow",
1189
+ "source": "autoLabel"
1190
+ },
1191
+ "headline": {
1192
+ "tag": "headline",
1193
+ "selector": ".rf-section__headline",
1194
+ "source": "autoLabel"
1195
+ },
1196
+ "blurb": {
1197
+ "tag": "blurb",
1198
+ "selector": ".rf-section__blurb",
1199
+ "source": "autoLabel"
1200
+ },
1201
+ "image": {
1202
+ "tag": "image",
1203
+ "selector": ".rf-section__image",
1204
+ "source": "autoLabel"
1205
+ }
1206
+ }
1207
+ },
1165
1208
  "Juxtapose": {
1166
1209
  "block": "juxtapose",
1167
1210
  "root": ".rf-juxtapose",
@@ -1266,7 +1309,7 @@
1266
1309
  "modifiers": {
1267
1310
  "media-position": {
1268
1311
  "source": "meta",
1269
- "default": "top",
1312
+ "default": "bottom",
1270
1313
  "dataAttribute": "data-media-position"
1271
1314
  },
1272
1315
  "align": {
@@ -1286,6 +1329,18 @@
1286
1329
  "collapse": {
1287
1330
  "source": "meta",
1288
1331
  "dataAttribute": "data-collapse"
1332
+ },
1333
+ "content-place": {
1334
+ "source": "meta",
1335
+ "dataAttribute": "data-content-place"
1336
+ },
1337
+ "height": {
1338
+ "source": "meta",
1339
+ "dataAttribute": "data-height"
1340
+ },
1341
+ "aspect": {
1342
+ "source": "meta",
1343
+ "dataAttribute": "data-aspect"
1289
1344
  }
1290
1345
  },
1291
1346
  "elements": {
@@ -1323,6 +1378,100 @@
1323
1378
  "inlineStyles": {
1324
1379
  "valign": {
1325
1380
  "prop": "--split-valign"
1381
+ },
1382
+ "aspect": "aspect-ratio"
1383
+ },
1384
+ "variants": {
1385
+ "media-position": {
1386
+ "cover": {
1387
+ "block": "hero",
1388
+ "root": ".rf-hero",
1389
+ "dataRune": "hero",
1390
+ "childOrder": [
1391
+ "{content}"
1392
+ ],
1393
+ "modifiers": {
1394
+ "media-position": {
1395
+ "source": "meta",
1396
+ "default": "bottom",
1397
+ "dataAttribute": "data-media-position"
1398
+ },
1399
+ "align": {
1400
+ "source": "meta",
1401
+ "default": "center",
1402
+ "classPattern": ".rf-hero--{value}",
1403
+ "dataAttribute": "data-align"
1404
+ },
1405
+ "media-ratio": {
1406
+ "source": "meta",
1407
+ "dataAttribute": "data-media-ratio"
1408
+ },
1409
+ "valign": {
1410
+ "source": "meta",
1411
+ "dataAttribute": "data-valign"
1412
+ },
1413
+ "collapse": {
1414
+ "source": "meta",
1415
+ "dataAttribute": "data-collapse"
1416
+ },
1417
+ "content-place": {
1418
+ "source": "meta",
1419
+ "dataAttribute": "data-content-place"
1420
+ },
1421
+ "height": {
1422
+ "source": "meta",
1423
+ "dataAttribute": "data-height"
1424
+ },
1425
+ "aspect": {
1426
+ "source": "meta",
1427
+ "dataAttribute": "data-aspect"
1428
+ }
1429
+ },
1430
+ "staticModifiers": [
1431
+ {
1432
+ "name": "cover",
1433
+ "selector": ".rf-hero--cover"
1434
+ }
1435
+ ],
1436
+ "elements": {
1437
+ "preamble": {
1438
+ "tag": "header",
1439
+ "selector": ".rf-hero__preamble",
1440
+ "source": "autoLabel"
1441
+ },
1442
+ "eyebrow": {
1443
+ "tag": "eyebrow",
1444
+ "selector": ".rf-hero__eyebrow",
1445
+ "source": "autoLabel"
1446
+ },
1447
+ "headline": {
1448
+ "tag": "headline",
1449
+ "selector": ".rf-hero__headline",
1450
+ "source": "autoLabel"
1451
+ },
1452
+ "blurb": {
1453
+ "tag": "blurb",
1454
+ "selector": ".rf-hero__blurb",
1455
+ "source": "autoLabel"
1456
+ },
1457
+ "image": {
1458
+ "tag": "image",
1459
+ "selector": ".rf-hero__image",
1460
+ "source": "autoLabel"
1461
+ },
1462
+ "media": {
1463
+ "tag": "media",
1464
+ "selector": ".rf-hero__media",
1465
+ "source": "autoLabel"
1466
+ }
1467
+ },
1468
+ "inlineStyles": {
1469
+ "valign": {
1470
+ "prop": "--split-valign"
1471
+ },
1472
+ "aspect": "aspect-ratio"
1473
+ }
1474
+ }
1326
1475
  }
1327
1476
  }
1328
1477
  },
@@ -1472,7 +1621,7 @@
1472
1621
  "modifiers": {
1473
1622
  "media-position": {
1474
1623
  "source": "meta",
1475
- "default": "top",
1624
+ "default": "bottom",
1476
1625
  "dataAttribute": "data-media-position"
1477
1626
  },
1478
1627
  "align": {
@@ -1553,7 +1702,7 @@
1553
1702
  "modifiers": {
1554
1703
  "media-position": {
1555
1704
  "source": "meta",
1556
- "default": "top",
1705
+ "default": "bottom",
1557
1706
  "dataAttribute": "data-media-position"
1558
1707
  },
1559
1708
  "align": {
@@ -1639,7 +1788,7 @@
1639
1788
  "modifiers": {
1640
1789
  "media-position": {
1641
1790
  "source": "meta",
1642
- "default": "top",
1791
+ "default": "bottom",
1643
1792
  "dataAttribute": "data-media-position"
1644
1793
  },
1645
1794
  "align": {
@@ -1773,7 +1922,7 @@
1773
1922
  "modifiers": {
1774
1923
  "media-position": {
1775
1924
  "source": "meta",
1776
- "default": "top",
1925
+ "default": "bottom",
1777
1926
  "dataAttribute": "data-media-position"
1778
1927
  },
1779
1928
  "media-ratio": {
package/index.css CHANGED
@@ -103,6 +103,7 @@
103
103
  @import './styles/runes/realm.css';
104
104
  @import './styles/runes/recipe.css';
105
105
  @import './styles/runes/reveal.css';
106
+ @import './styles/runes/section.css';
106
107
  @import './styles/runes/sidenote.css';
107
108
  @import './styles/runes/steps.css';
108
109
  @import './styles/runes/storyboard.css';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@refrakt-md/lumina",
3
3
  "description": "Lumina theme for refrakt.md — design tokens, CSS, identity transform, and layout configs",
4
- "version": "0.20.2",
4
+ "version": "0.21.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -83,9 +83,9 @@
83
83
  "build": "tsc"
84
84
  },
85
85
  "dependencies": {
86
- "@refrakt-md/runes": "0.20.2",
87
- "@refrakt-md/transform": "0.20.2",
88
- "@refrakt-md/types": "0.20.2"
86
+ "@refrakt-md/runes": "0.21.0",
87
+ "@refrakt-md/transform": "0.21.0",
88
+ "@refrakt-md/types": "0.21.0"
89
89
  },
90
90
  "devDependencies": {
91
91
  "postcss": "^8.4.0"
@@ -63,6 +63,24 @@
63
63
  height: 100%;
64
64
  object-fit: cover;
65
65
  }
66
+ /* SPEC-101 — non-img/video guests (a sandbox, an embed) fill the well too.
67
+ * They can't be object-fit-cropped, so they get the well's box outright;
68
+ * display:block covers undefined custom elements (inline by default). */
69
+ [data-media-position="cover"] [data-section="media"] > :not(img, video) {
70
+ display: block;
71
+ width: 100%;
72
+ height: 100%;
73
+ }
74
+ /* A backdrop sandbox sits flush in the well — the well's own radius/overflow
75
+ * does the clipping. The iframe height is pinned inline by the element in
76
+ * `fill` mode; this carries the fixed-height case visually until then. */
77
+ [data-media-position="cover"] [data-section="media"] .rf-sandbox {
78
+ margin: 0;
79
+ border-radius: 0;
80
+ }
81
+ [data-media-position="cover"] [data-section="media"] .rf-sandbox iframe {
82
+ height: 100%;
83
+ }
66
84
  /* The overlaid box (full: content; header: preamble) anchors via content-place. */
67
85
  [data-media-position="cover"]:not([data-cover-scope="header"]) > [data-name="content"],
68
86
  [data-cover-scope="header"] > [data-name="cover-band"] > [data-name="preamble"] {
@@ -9,12 +9,14 @@
9
9
 
10
10
  [data-density="full"] {
11
11
  --rune-padding: var(--rf-spacing-md);
12
+ --rf-title-size: 1.5rem;
12
13
  }
13
14
 
14
15
  /* ─── Compact: truncated descriptions, tight spacing ──────────────── */
15
16
 
16
17
  [data-density="compact"] {
17
18
  --rune-padding: var(--rf-spacing-sm);
19
+ --rf-title-size: 1.25rem;
18
20
  }
19
21
 
20
22
  [data-density="compact"] [data-section="description"] {
@@ -32,6 +34,7 @@
32
34
 
33
35
  [data-density="minimal"] {
34
36
  --rune-padding: var(--rf-spacing-xs);
37
+ --rf-title-size: 1rem;
35
38
  }
36
39
 
37
40
  [data-density="minimal"] [data-section="description"] {
@@ -25,24 +25,21 @@
25
25
 
26
26
  /* ─── Title: primary heading ──────────────────────────────────────── */
27
27
 
28
+ /* The size rides --rf-title-size (set per density root in density.css) so a
29
+ * nested rune's own density re-declares it — a full-density hero inside a
30
+ * compact preview keeps its full-size title — and a rune's own title CSS
31
+ * (e.g. .rf-hero__headline) outranks the bare attribute selector. The old
32
+ * `[data-density="compact"] [data-section="title"]` descendant rule leaked
33
+ * through nested densities AND beat per-rune title sizes. */
28
34
  [data-section="title"] {
29
35
  font-family: var(--rf-font-sans);
30
- font-size: 1.5rem;
36
+ font-size: var(--rf-title-size, 1.5rem);
31
37
  font-weight: 700;
32
38
  line-height: 1.2;
33
39
  color: var(--rf-color-text);
34
40
  margin: 0;
35
41
  }
36
42
 
37
- /* Scale title with density */
38
- [data-density="compact"] [data-section="title"] {
39
- font-size: 1.25rem;
40
- }
41
-
42
- [data-density="minimal"] [data-section="title"] {
43
- font-size: 1rem;
44
- }
45
-
46
43
  /* ─── Description: secondary text ─────────────────────────────────── */
47
44
 
48
45
  [data-section="description"] {
@@ -132,3 +132,15 @@
132
132
  .rf-feature--in-grid {
133
133
  padding: 0;
134
134
  }
135
+
136
+ /* BUG-001 — content-first DOM inverts the shared media-first stacked contract
137
+ * (layouts/split.css): counter it so the labels are truthful. `bottom` (the
138
+ * default) is plain block flow — media after the content, the historical
139
+ * look; an explicit `top` flips the visual order without touching the DOM. */
140
+ .rf-feature[data-media-position="bottom"] {
141
+ display: block;
142
+ }
143
+ .rf-feature[data-media-position="top"] {
144
+ display: flex;
145
+ flex-direction: column-reverse;
146
+ }
@@ -181,6 +181,23 @@
181
181
  margin-top: 3rem;
182
182
  border-radius: var(--rf-radius-lg);
183
183
  }
184
+ /* BUG-001 — hero DOM is content-first (headline before media: the right
185
+ * reading order for the classic hero), inverting the shared media-first
186
+ * stacked contract in layouts/split.css. Counter it so the labels are
187
+ * truthful: `bottom` (the hero default) is plain block flow — media renders
188
+ * after the content, exactly the historical look — and an explicit `top`
189
+ * flips the visual order without touching the DOM. */
190
+ .rf-hero[data-media-position="bottom"] {
191
+ display: block;
192
+ }
193
+ .rf-hero[data-media-position="top"] {
194
+ display: flex;
195
+ flex-direction: column-reverse;
196
+ }
197
+ .rf-hero[data-media-position="top"] > .rf-hero__media {
198
+ margin-top: 0;
199
+ margin-bottom: 3rem;
200
+ }
184
201
  .rf-hero__media img {
185
202
  display: block;
186
203
  width: 100%;
@@ -190,6 +207,90 @@
190
207
  margin: 0;
191
208
  }
192
209
 
210
+ /* ── Cover mode (SPEC-101) — `media-position="cover"` ───────────────────
211
+ * The shared cover layer (dimensions/cover.css) does the grid stacking, the
212
+ * default scrim, and the overlay anchoring; these rules supply the hero-band
213
+ * specifics. This file imports after cover.css, so equal-specificity rules
214
+ * here win. */
215
+
216
+ /* The section's generous padding moves to the overlaid content — on the root
217
+ * it would inset the media well (the backdrop must be flush with the band). */
218
+ .rf-hero--cover {
219
+ padding: 0;
220
+ }
221
+ .rf-hero--cover > [data-name="content"] {
222
+ --rune-padding: 7rem 2rem 6.5rem;
223
+ }
224
+
225
+ /* Band height authority: a hero is a full-width band, not a portrait tile.
226
+ * Height comes from a viewport-aware floor — deliberately NOT a default
227
+ * aspect-ratio: with `aspect-ratio` + `min-height`, the ratio transfers a
228
+ * winning min-height back into *width* on narrow screens (a 22rem-tall 21/9
229
+ * band computes ~820px wide on a 375px phone — the band marches off-screen).
230
+ * `--cover-aspect: auto` releases the shared 3/4 tile default; an explicit
231
+ * `aspect` knob still lands as an inline style and owns the shape. */
232
+ .rf-hero--cover {
233
+ --cover-aspect: auto;
234
+ min-height: clamp(22rem, 55vh, 40rem);
235
+ }
236
+ /* An explicit aspect owns the shape — drop the floor so the same
237
+ * ratio-vs-min-height transfer can't reintroduce the overflow. */
238
+ .rf-hero--cover[data-aspect] {
239
+ min-height: 0;
240
+ }
241
+
242
+ /* The shared cover layer applies `container-type: size` (size containment) for
243
+ * its orientation query — under it the band cannot grow with its content, so
244
+ * tall content clips at min-height. The hero band doesn't use the orientation
245
+ * query (the centred-band default below covers `auto` placement), so release
246
+ * the containment and let the band stretch to fit. */
247
+ .rf-hero--cover[data-media-position="cover"][data-cover-scope="full"] {
248
+ container-type: normal;
249
+ }
250
+
251
+ /* Named height scale (`height="sm…xl"`): an explicit band height. */
252
+ .rf-hero--cover[data-height="sm"] { min-height: 20rem; }
253
+ .rf-hero--cover[data-height="md"] { min-height: 28rem; }
254
+ .rf-hero--cover[data-height="lg"] { min-height: 36rem; }
255
+ .rf-hero--cover[data-height="xl"] { min-height: 44rem; }
256
+
257
+ /* Mobile: the desktop overlay padding (7rem bands) would eat most of the well
258
+ * on a phone — tighten it so the content fits the band. */
259
+ @media (max-width: 640px) {
260
+ .rf-hero--cover > [data-name="content"] {
261
+ --rune-padding: 3.5rem 1.25rem 3rem;
262
+ }
263
+ }
264
+
265
+ /* A full-bleed band sits flush — no media radius on the well. */
266
+ .rf-hero--cover > [data-section="media"] {
267
+ border-radius: 0;
268
+ }
269
+
270
+ /* Legibility over the scrim: the primary action hard-codes white text above,
271
+ * which inverts to white-on-near-white once the cover scheme flips
272
+ * --rf-color-primary on the dark overlay. Use the scheme's tokens instead. */
273
+ .rf-hero--cover li:first-child a {
274
+ color: var(--rf-color-on-accent, var(--rf-color-bg));
275
+ }
276
+
277
+ /* Centre-band default: a hero overlay reads as a centred band, not the
278
+ * cover-card caption strip — seat it mid-well unless the author anchors it
279
+ * with an explicit `content-place`. */
280
+ .rf-hero--cover[data-media-position="cover"]:is([data-content-place="auto"], :not([data-content-place])) > [data-name="content"] {
281
+ align-self: center;
282
+ justify-self: stretch;
283
+ }
284
+
285
+ /* The default cover scrim is edge-weighted (a caption-strip treatment); under
286
+ * the centred band it would leave the headline on undarkened media. With no
287
+ * explicit `content-place`, darken the well evenly (slightly bottom-weighted).
288
+ * An explicit `content-place` re-enters the shared content-edge scrim contract,
289
+ * and `scrim="none"` still opts out. */
290
+ .rf-hero--cover:is([data-content-place="auto"], :not([data-content-place])):not([data-scrim="none"]) > [data-section="media"]::after {
291
+ background-image: var(--cover-scrim-image, linear-gradient(rgb(0 0 0 / 0.45), rgb(0 0 0 / 0.35) 50%, rgb(0 0 0 / 0.55)));
292
+ }
293
+
193
294
  /* Mobile: stack actions full-width so the code block and button line up */
194
295
  @media (max-width: 640px) {
195
296
  .rf-hero__actions {
@@ -37,3 +37,53 @@
37
37
  background: var(--rf-color-warning-bg, #fef3c7);
38
38
  border-bottom: 1px solid var(--rf-color-warning, #b45309);
39
39
  }
40
+
41
+ /* Deferred activation (WORK-381): poster shown until a non-eager sandbox
42
+ mounts. Rendered by the RfSandbox custom element from `data-poster`. */
43
+ .rf-sandbox__poster {
44
+ position: relative;
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ min-height: 150px;
49
+ background: var(--rf-color-surface-2, var(--rf-color-surface, #f4f4f5));
50
+ }
51
+
52
+ .rf-sandbox__poster-image {
53
+ position: absolute;
54
+ inset: 0;
55
+ width: 100%;
56
+ height: 100%;
57
+ object-fit: cover;
58
+ }
59
+
60
+ .rf-sandbox__activate {
61
+ position: relative;
62
+ z-index: 1;
63
+ display: inline-flex;
64
+ align-items: center;
65
+ gap: 0.5ch;
66
+ padding: 0.5rem 1rem;
67
+ font: inherit;
68
+ font-weight: 600;
69
+ color: var(--rf-color-on-accent, #fff);
70
+ background: var(--rf-color-accent, #2563eb);
71
+ border: none;
72
+ border-radius: var(--rf-radius-md);
73
+ cursor: pointer;
74
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
75
+ }
76
+
77
+ .rf-sandbox__activate::before {
78
+ content: "▶";
79
+ font-size: 0.85em;
80
+ }
81
+
82
+ .rf-sandbox__activate:hover {
83
+ background: var(--rf-color-accent-hover, var(--rf-color-accent, #1d4ed8));
84
+ }
85
+
86
+ .rf-sandbox__activate:focus-visible {
87
+ outline: 2px solid var(--rf-color-accent, #2563eb);
88
+ outline-offset: 2px;
89
+ }
@@ -0,0 +1,70 @@
1
+ /* Section — a generic page section: an eyebrow/headline/blurb header above
2
+ arbitrary body content. Header styling mirrors the shared page-section look
3
+ (reveal/feature); the body is content-agnostic and full width. */
4
+ .rf-section {
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: 1.5rem;
8
+ }
9
+
10
+ /* Header */
11
+ .rf-section__preamble {
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: 0.5rem;
15
+ max-width: var(--rf-measure, 65ch);
16
+ margin: 0;
17
+ }
18
+ .rf-section__eyebrow {
19
+ font-size: 0.8125rem;
20
+ font-weight: 600;
21
+ letter-spacing: 0.05em;
22
+ text-transform: uppercase;
23
+ color: var(--rf-color-primary);
24
+ margin: 0;
25
+ }
26
+ .rf-section__eyebrow:has(a) {
27
+ display: inline-block;
28
+ position: relative;
29
+ padding: 0.25rem 0.875rem;
30
+ border: 1px solid var(--rf-color-border);
31
+ border-radius: var(--rf-radius-full);
32
+ color: var(--rf-color-text);
33
+ font-weight: 400;
34
+ text-transform: none;
35
+ letter-spacing: 0;
36
+ transition: border-color 150ms ease;
37
+ }
38
+ .rf-section__eyebrow:has(a):hover { border-color: var(--rf-color-muted); }
39
+ .rf-section__eyebrow:has(a) a { color: var(--rf-color-primary); font-weight: 600; text-decoration: none; }
40
+ .rf-section__eyebrow:has(a) a::before { content: ''; position: absolute; inset: 0; border-radius: inherit; }
41
+ .rf-section__headline {
42
+ margin: 0;
43
+ }
44
+ .rf-section__blurb {
45
+ color: var(--rf-color-muted);
46
+ margin: 0;
47
+ }
48
+ .rf-section__image {
49
+ margin: 0;
50
+ border-radius: var(--rf-radius-md);
51
+ max-width: 100%;
52
+ height: auto;
53
+ }
54
+
55
+ /* Body — content-agnostic; fills the section width regardless of header align. */
56
+ .rf-section__body {
57
+ width: 100%;
58
+ }
59
+
60
+ /* Header alignment (the body stays full width). */
61
+ .rf-section[data-align="center"] .rf-section__preamble {
62
+ align-items: center;
63
+ text-align: center;
64
+ margin-inline: auto;
65
+ }
66
+ .rf-section[data-align="end"] .rf-section__preamble {
67
+ align-items: flex-end;
68
+ text-align: end;
69
+ margin-inline-start: auto;
70
+ }
@@ -101,3 +101,15 @@
101
101
  border-radius: var(--rf-radius-md);
102
102
  overflow: hidden;
103
103
  }
104
+
105
+ /* BUG-001 — content-first DOM inverts the shared media-first stacked contract
106
+ * (layouts/split.css): counter it so the labels are truthful. `bottom` (the
107
+ * default) is plain block flow — media after the content, the historical
108
+ * look; an explicit `top` flips the visual order without touching the DOM. */
109
+ .rf-step[data-media-position="bottom"] {
110
+ display: block;
111
+ }
112
+ .rf-step[data-media-position="top"] {
113
+ display: flex;
114
+ flex-direction: column-reverse;
115
+ }