@refrakt-md/lumina 0.20.0 → 0.20.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/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.0",
4
+ "version": "0.20.2",
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.0",
87
- "@refrakt-md/transform": "0.20.0",
88
- "@refrakt-md/types": "0.20.0"
86
+ "@refrakt-md/runes": "0.20.2",
87
+ "@refrakt-md/transform": "0.20.2",
88
+ "@refrakt-md/types": "0.20.2"
89
89
  },
90
90
  "devDependencies": {
91
91
  "postcss": "^8.4.0"
@@ -103,14 +103,27 @@
103
103
  position: absolute;
104
104
  inset: 0;
105
105
  pointer-events: none;
106
- background-image: linear-gradient(var(--cover-scrim-dir, to top), rgb(0 0 0 / 0.55), transparent 62%);
106
+ /* `inherit` matches the media zone's `--rf-radius-media`, so a frost scrim's
107
+ * `backdrop-filter` region clips at the same rounded corners — `overflow:
108
+ * hidden` on the media zone doesn't reliably clip backdrop-filter past the
109
+ * rounded edge in WebKit, so the pseudo carries its own radius. (The
110
+ * gradient variant doesn't strictly need this, but it's harmless and keeps
111
+ * the two scrim variants visually congruent.) */
112
+ border-radius: inherit;
113
+ /* The scrim shape is `--cover-scrim-image` (custom property override) when
114
+ * the engine emits one — for `content-place="center …"` it emits a radial
115
+ * ellipse so the dark falls under the centred overlay. For the directional
116
+ * cases (`start`/`end` block-axis) the engine sets `--cover-scrim-dir` and
117
+ * the linear fallback resolves to the right edge. */
118
+ background-image: var(--cover-scrim-image, linear-gradient(var(--cover-scrim-dir, to top), rgb(0 0 0 / 0.55), transparent 62%));
107
119
  }
108
120
 
109
121
  /* Frost treatment (`scrim-type="frost"`) — a frosted-glass blur over the media
110
122
  * instead of a gradient. `scrim-blur` (named scale) sets the blur radius; the
111
123
  * tint follows the cover scheme (a dark scheme → a dark frost for light text).
112
124
  * The frost is masked to the content edge (following `--cover-scrim-dir`, default
113
- * bottom) so it reads as a band behind the text, not a blur over the whole image. */
125
+ * bottom) so it reads as a band behind the text, not a blur over the whole
126
+ * image. `--cover-scrim-mask` is the radial equivalent for centred content. */
114
127
  [data-media-position="cover"][data-scrim-blur="none"] { --cover-scrim-blur: 0px; }
115
128
  [data-media-position="cover"][data-scrim-blur="sm"] { --cover-scrim-blur: 4px; }
116
129
  [data-media-position="cover"][data-scrim-blur="md"] { --cover-scrim-blur: 8px; }
@@ -120,8 +133,8 @@
120
133
  background-color: rgb(0 0 0 / 0.18);
121
134
  -webkit-backdrop-filter: blur(var(--cover-scrim-blur, 8px));
122
135
  backdrop-filter: blur(var(--cover-scrim-blur, 8px));
123
- -webkit-mask-image: linear-gradient(var(--cover-scrim-dir, to top), #000 30%, transparent 72%);
124
- mask-image: linear-gradient(var(--cover-scrim-dir, to top), #000 30%, transparent 72%);
136
+ -webkit-mask-image: var(--cover-scrim-mask, linear-gradient(var(--cover-scrim-dir, to top), #000 30%, transparent 72%));
137
+ mask-image: var(--cover-scrim-mask, linear-gradient(var(--cover-scrim-dir, to top), #000 30%, transparent 72%));
125
138
  }
126
139
  [data-media-position="cover"][data-scrim-type="frost"][data-color-scheme="light"]:not([data-scrim="none"]) [data-section="media"]::after {
127
140
  background-color: rgb(255 255 255 / 0.25);
@@ -20,11 +20,31 @@
20
20
  * `frame-shadow` (drop-shadow silhouette here) never collide — different property,
21
21
  * different surface. */
22
22
 
23
- /* ── Silhouette drop-shadow ──────────────────────────────────────────── */
24
- [data-frame-shadow="none"] { filter: none; }
25
- [data-frame-shadow="sm"] { filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.10)); }
26
- [data-frame-shadow="md"] { filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.25)); }
27
- [data-frame-shadow="lg"] { filter: drop-shadow(0 12px 40px rgba(0, 0, 0, 0.20)); }
23
+ /* ── Silhouette drop-shadow ────────────────────────────────────────────
24
+ * Same media-target / self-target split as `frame-displace`. Media-target
25
+ * frames land `data-frame-shadow` on the media zone wrapper, but the shadow
26
+ * should ride the inner guest so it (a) shifts with `frame-displace` and
27
+ * (b) actually casts onto exposed slot interior when the guest is inset
28
+ * from the wrapper. Self-target frames (showcase, figure) keep the filter
29
+ * on the root itself, where it traces the rune's own painted silhouette. */
30
+ [data-frame-shadow="none"] { --frame-shadow: none; }
31
+ [data-frame-shadow="sm"] { --frame-shadow: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.10)); }
32
+ [data-frame-shadow="md"] { --frame-shadow: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.25)); }
33
+ [data-frame-shadow="lg"] { --frame-shadow: drop-shadow(0 12px 40px rgba(0, 0, 0, 0.20)); }
34
+
35
+ /* Self-target — figure, showcase, anything else where the rune root is the
36
+ * frame target. The filter applies to the painted root. */
37
+ [data-frame-shadow]:not([data-section="media"]) {
38
+ filter: var(--frame-shadow);
39
+ }
40
+
41
+ /* Media-target — card, bento-cell, recipe, etc. The filter rides the inner
42
+ * guest so a displaced image's shadow falls into the slot's exposed
43
+ * interior (clipped at the slot's `overflow: hidden` boundary like any
44
+ * other in-slot content). */
45
+ [data-section="media"][data-frame-shadow] > :is(img, video, [data-rune]) {
46
+ filter: var(--frame-shadow);
47
+ }
28
48
 
29
49
  /* ── Aspect ratio ────────────────────────────────────────────────────── */
30
50
  [style*="--frame-aspect"] { aspect-ratio: var(--frame-aspect); }
@@ -36,19 +56,49 @@
36
56
  [style*="--frame-place-x"] { justify-self: var(--frame-place-x, auto); align-self: var(--frame-place-y, auto); }
37
57
 
38
58
  /* ── Oversize — guest exceeds its slot (clipping hosts crop it) ──────── */
39
- [style*="--frame-oversize"] > :is(img, video) { width: calc(100% * var(--frame-oversize, 1)); max-width: none; }
40
-
41
- /* ── Displacement (bleed / peek) — move the guest toward an edge/corner.
42
- * The host decides whether it spills or is cropped; this only moves it. ─ */
43
- [data-displace] { position: relative; z-index: 1; }
44
- [data-displace="top"] { margin-top: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
45
- [data-displace="bottom"] { margin-bottom: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
46
- [data-displace="both"] { margin-block: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
47
- [data-displace="end"] { margin-inline-end: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
48
- [data-displace="bottom-end"] { margin-bottom: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); margin-inline-end: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
49
- [data-displace="top-end"] { margin-top: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); margin-inline-end: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
50
-
51
- /* Collapse displacement on mobile, regardless of host. */
52
- @media (max-width: 768px) {
53
- [data-displace] { margin: 0; }
59
+ /* `[data-rune]` extends the rule to nested rune guests (codegroup, mockup,
60
+ * chart, etc.), so frame chrome stays guest-agnostic — the same selector
61
+ * pattern the displace rule uses. */
62
+ [style*="--frame-oversize"] > :is(img, video, [data-rune]) { width: calc(100% * var(--frame-oversize, 1)); max-width: none; }
63
+
64
+ /* ── Displacement (peek / spill) — move the guest toward an edge/corner.
65
+ * The host decides whether the guest is cropped or spills; this only moves it.
66
+ * Two cases, distinguished by where the engine landed `data-displace`:
67
+ *
68
+ * 1. Media-target frame (`frameTarget: 'media'`, e.g. card / bento-cell /
69
+ * figure media well). `data-displace` lands on the media zone wrapper
70
+ * (`[data-section="media"]`), but the wrapper itself is the *host* and
71
+ * already has `overflow: hidden`. We translate the inner guest with
72
+ * `transform: translate(...)` so the zone clips the peek at its own edge,
73
+ * not at the outer card/cell edge. The wrapper stays put — the card's
74
+ * layout is unchanged.
75
+ *
76
+ * 2. Self-target frame (`frameTarget: 'self'`, e.g. showcase, where the rune
77
+ * root is the host). `data-displace` lands on the root, and the root's
78
+ * parent (a page section, the page itself) is the clip-or-spill decider.
79
+ * We push the root with negative margins so it can spill past its
80
+ * parent's edge for a true breakout. */
81
+
82
+ /* — Case 1: media-zone displace → translate the inner guest, zone clips. */
83
+ [data-section="media"][data-displace] > :is(img, video, [data-rune]) {
84
+ transform: translate(var(--displace-x, 0), var(--displace-y, 0));
54
85
  }
86
+ [data-section="media"][data-displace="top"] { --displace-y: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
87
+ [data-section="media"][data-displace="bottom"] { --displace-y: var(--frame-offset, var(--rf-spacing-lg)); }
88
+ [data-section="media"][data-displace="end"] { --displace-x: var(--frame-offset, var(--rf-spacing-lg)); }
89
+ [data-section="media"][data-displace="top-end"] { --displace-x: var(--frame-offset, var(--rf-spacing-lg)); --displace-y: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
90
+ [data-section="media"][data-displace="bottom-end"] { --displace-x: var(--frame-offset, var(--rf-spacing-lg)); --displace-y: var(--frame-offset, var(--rf-spacing-lg)); }
91
+
92
+ /* — Case 2: self-target (showcase, etc.) → root moves; parent decides. */
93
+ [data-displace]:not([data-section="media"]) { position: relative; z-index: 1; }
94
+ [data-displace="top"]:not([data-section="media"]) { margin-top: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
95
+ [data-displace="bottom"]:not([data-section="media"]) { margin-bottom: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
96
+ [data-displace="both"]:not([data-section="media"]) { margin-block: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
97
+ [data-displace="end"]:not([data-section="media"]) { margin-inline-end: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
98
+ [data-displace="bottom-end"]:not([data-section="media"]) { margin-bottom: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); margin-inline-end: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
99
+ [data-displace="top-end"]:not([data-section="media"]) { margin-top: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); margin-inline-end: calc(-1 * var(--frame-offset, var(--rf-spacing-lg))); }
100
+
101
+ /* Displacement carries across breakpoints. For media-target the guest is
102
+ * clipped by the zone's `overflow: hidden` regardless of width, so the peek
103
+ * reads consistently from desktop to mobile. For self-target (showcase) the
104
+ * negative-margin spill stays on too — that's the intended breakout. */
@@ -55,16 +55,23 @@
55
55
  --substrate-tile: auto;
56
56
  }
57
57
 
58
- /* cross — plus marks on a grid (a grid masked down to short ticks at each node) */
58
+ /* cross — plus marks at the centre of each cell, drawn as two perpendicular
59
+ * thin ellipses (no `mask` — an element-level `mask` would clip the rune's
60
+ * own text content along with the pattern, which is what we don't want). A
61
+ * small plus reads fainter than a 1px line at the same ink opacity, so the
62
+ * pattern carries its own ink boost — same idea as dots.
63
+ *
64
+ * The stop syntax mirrors dots: `ink <r>, transparent 0` produces a sharp-
65
+ * edged shape filled with ink (the explicit length on ink, plus `0` after
66
+ * transparent for a zero-length transition, beats the `ink 100%, transparent
67
+ * 100%` form which some renderers collapse to nothing). Ellipse radii at
68
+ * 1px (rather than sub-pixel 0.5px) so the stroke survives DPR rounding. */
59
69
  [data-substrate="cross"] {
60
70
  --substrate-image:
61
- linear-gradient(var(--substrate-ink) 1px, transparent 1px),
62
- linear-gradient(90deg, var(--substrate-ink) 1px, transparent 1px);
71
+ radial-gradient(ellipse 3px 1px at center, var(--substrate-ink) 100%, transparent 0),
72
+ radial-gradient(ellipse 1px 3px at center, var(--substrate-ink) 100%, transparent 0);
63
73
  --substrate-pos: center;
64
- -webkit-mask-image: radial-gradient(circle at center, #000 0 2px, transparent 2px);
65
- -webkit-mask-size: var(--substrate-cell) var(--substrate-cell);
66
- mask-image: radial-gradient(circle at center, #000 0 2px, transparent 2px);
67
- mask-size: var(--substrate-cell) var(--substrate-cell);
74
+ --substrate-boost: 1.6;
68
75
  }
69
76
 
70
77
  /* checker — alternating filled cells */
@@ -103,6 +103,17 @@
103
103
  padding: var(--rune-padding, var(--rf-spacing-md));
104
104
  }
105
105
 
106
+ /* ─── Media slot inherits the surface border ─────────────────────────
107
+ * When a surface-bearing rune wraps a `[data-section="media"]` slot, the slot
108
+ * shares the surface's 1px border so the framed media reads as part of the
109
+ * same surface rather than a separate inner box. Scoped via `:where()` for
110
+ * zero specificity — per-rune CSS can still override if a particular surface
111
+ * wants a different chrome (e.g. character's circular portrait avoids this
112
+ * because it's not a `[data-section="media"]` slot). */
113
+ :where(.rf-card, .rf-bento-cell, .rf-recipe, .rf-realm, .rf-faction, .rf-playlist) [data-section="media"] {
114
+ border: 1px solid var(--rf-color-border);
115
+ }
116
+
106
117
  /* ─── Inset surface (SPEC-087) — tint-tracking recessed fill ──────────
107
118
  * Derived at use-site via relative-color (lower L, keep C+H) so it recomputes from a tinted
108
119
  * `--rf-color-surface` automatically (a static inset-colour token would
@@ -116,6 +127,28 @@
116
127
  background: oklch(from var(--rf-color-surface) calc(l - var(--rf-surface-inset-shift)) c h);
117
128
  }
118
129
 
130
+ /* …but when they're a media-zone guest the slot already provides the recessed
131
+ * surface (border, radius, inset fill), so the inner chrome would just stack
132
+ * as double-chrome. Drop it; keep padding so axis labels / SVG keep breathing
133
+ * room. */
134
+ [data-section="media"] > :is(.rf-chart, .rf-diagram) {
135
+ background: transparent;
136
+ border: 0;
137
+ }
138
+
139
+ /* Same treatment for `juxtapose` as a media guest — its `__panels` container
140
+ * has its own border + background + rounded corners (juxtapose stands alone
141
+ * outside a card), but inside a card slot they double up with the slot's own
142
+ * chrome. Drop the inner chrome; round to match the slot's media radius so the
143
+ * comparison reads as part of the card surface. The slot's overflow:visible
144
+ * and container-type:normal still come from `split.css` (juxtapose manages
145
+ * its own clip inside `__panels`). */
146
+ [data-section="media"] > .rf-juxtapose .rf-juxtapose__panels {
147
+ background: transparent;
148
+ border: 0;
149
+ border-radius: var(--rf-radius-media);
150
+ }
151
+
119
152
  /* Media wells of media-bearing runes: a recessed sub-surface that tracks the
120
153
  * (possibly tinted) container colour — invisible under a full-bleed guest,
121
154
  * visible in the gaps (transparent, displaced, or absent guest). */
@@ -106,6 +106,16 @@
106
106
  ) {
107
107
  overflow: visible;
108
108
  container-type: normal;
109
+ }
110
+ /* `preview` (negative-margin breakout) and a displaced `showcase` (peek
111
+ * spill) want the slot fully de-chromed so the bleed reads as edge-to-edge.
112
+ * Juxtapose just opted out of the clip — it still wants to read as part of
113
+ * the card surface, so it keeps the slot's rounded corners (juxtapose's
114
+ * own `__panels` chrome is dropped instead; see surfaces.css). */
115
+ [data-section="media"]:is(
116
+ :has(> [data-rune="preview"]),
117
+ :has(> .rf-showcase[data-displace]:not(.rf-showcase--in-bento-cell))
118
+ ) {
109
119
  border-radius: 0;
110
120
  }
111
121
  /* …and let those self-managing guests keep their intrinsic width/sizing: the
@@ -1,11 +1,16 @@
1
1
  /* Background — Directive rune for background images, video, overlays, blur */
2
2
 
3
- /* Background layer — absolute positioned behind content */
3
+ /* Background layer — absolute positioned behind content. `border-radius:
4
+ * inherit` rounds the gradient/image to whatever shape the parent surface has,
5
+ * so a card / bento-cell / any rounded host clips the bg to its own corners
6
+ * without needing `overflow: hidden` (though hosts with thin-edge padding
7
+ * still need it so the bg can't poke past the rounded outer edge). */
4
8
  [data-name="bg"] {
5
9
  position: absolute;
6
10
  inset: 0;
7
11
  z-index: 0;
8
12
  overflow: hidden;
13
+ border-radius: inherit;
9
14
  background-image: var(--bg-image);
10
15
  background-size: var(--bg-fit, cover);
11
16
  background-position: var(--bg-position, center);
@@ -22,8 +27,14 @@
22
27
  [data-name="bg"][data-bg-fixed] {
23
28
  background-attachment: fixed;
24
29
  }
25
- /* Content above background — any sibling of bg layer gets z-index */
26
- .rf-has-bg > :not([data-name="bg"]) {
30
+ /* Content above background — any sibling of the bg layer gets `z-index: 1`
31
+ * so it stacks over the `data-name="bg"` layer (which sits at `z-index: 0`).
32
+ * Keyed off the `data-bg` attribute the engine emits on the bg-owning rune
33
+ * (`engine.ts` → `bgDataAttrs['data-bg'] = ''`), not the per-block BEM
34
+ * modifier class `rf-<block>--has-bg` — the attribute is rune-agnostic so
35
+ * one rule covers card, bento-cell, hero, feature, and anything else that
36
+ * raises a bg layer. */
37
+ [data-bg] > :not([data-name="bg"]) {
27
38
  position: relative;
28
39
  z-index: 1;
29
40
  }
@@ -27,6 +27,13 @@
27
27
  border-radius: var(--rf-radius-container);
28
28
  border: 1px solid var(--rf-color-border);
29
29
  background: var(--rf-color-surface);
30
+ /* Clip non-media content (substrate patterns, bg gradient/image layers) to
31
+ * the card's rounded outer corners. The displaced-peek case is unaffected:
32
+ * `frame-displace` translates the guest *inside* the media zone, which has
33
+ * its own `overflow: hidden`, so no peek ever crosses this boundary.
34
+ * `box-shadow` from `elevation` paints outside the element box and is
35
+ * unaffected by `overflow`. */
36
+ overflow: hidden;
30
37
  }
31
38
 
32
39
  /* Content zone fills the remaining inset so total text padding still equals
@@ -1,9 +1,23 @@
1
1
  /* Figure */
2
2
  .rf-figure img {
3
+ display: block;
3
4
  max-width: 100%;
4
5
  height: auto;
6
+ margin-inline: auto;
5
7
  border-radius: var(--rf-radius-media);
6
8
  }
9
+
10
+ /* When `frame-aspect` sets an explicit surface shape, the image fills the
11
+ * shape rather than sitting at its natural size with whitespace on the
12
+ * inline-end. `object-fit: cover` crops only if the natural aspect doesn't
13
+ * match — and `frame-anchor` already picks the focal point of that crop
14
+ * (the same contract as a card's media slot). */
15
+ .rf-figure[style*="--frame-aspect"] img {
16
+ width: 100%;
17
+ height: 100%;
18
+ object-fit: cover;
19
+ margin: 0;
20
+ }
7
21
  .rf-figure figcaption {
8
22
  margin-top: 0.625rem;
9
23
  font-size: 0.825rem;
@@ -22,6 +22,13 @@
22
22
  width: 100%;
23
23
  position: relative;
24
24
  background: var(--rf-color-surface, #f8f9fa);
25
+ /* Establish a stacking context so Leaflet's internal pane z-indices
26
+ * (tile-pane 200, overlay-pane 400, marker-pane 600, popup-pane 700,
27
+ * etc.) stay contained instead of escaping past siblings — otherwise
28
+ * a map dropped into a cover card's media slot punches through the
29
+ * cover scrim (z-index auto) and overlaid content (z-index 1).
30
+ * `position: relative` alone is not enough; `isolation: isolate` is. */
31
+ isolation: isolate;
25
32
  }
26
33
 
27
34
  /* Fallback list shown while Leaflet loads */
@@ -80,6 +87,28 @@
80
87
  display: none;
81
88
  }
82
89
 
90
+ /* ── Map as a media guest ──
91
+ * When dropped into a `[data-section="media"]` slot (e.g. a card or
92
+ * bento-cell in cover mode), the map yields its standalone presentation to
93
+ * the slot: no outer margin or padding (map is in the Banner surface group
94
+ * which gives it vertical `--rune-padding`; see `surfaces.css`), no rounding
95
+ * (the slot already clips), and the Leaflet container stretches to fill the
96
+ * slot height instead of capping at the per-size pixel height. The fallback
97
+ * list (rendered before Leaflet hydrates) drops its outer padding too so it
98
+ * doesn't bleed past the slot edge during the brief flash before tiles load. */
99
+ [data-section="media"] > .rf-map {
100
+ margin: 0;
101
+ padding: 0;
102
+ height: 100%;
103
+ border-radius: inherit;
104
+ }
105
+ [data-section="media"] > .rf-map > .rf-map__container {
106
+ height: 100%;
107
+ }
108
+ [data-section="media"] .rf-map__fallback {
109
+ padding: 0.75rem 1rem;
110
+ }
111
+
83
112
  /* Leaflet popup overrides to match theme */
84
113
  .rf-map .leaflet-popup-content-wrapper {
85
114
  border-radius: var(--rf-radius-md, 0.5rem);
@@ -75,7 +75,11 @@
75
75
  border-radius: var(--rf-radius-lg);
76
76
  overflow: hidden;
77
77
  }
78
- .rf-preview__canvas:has(.rf-mockup) {
78
+ /* Centre a standalone mockup whose `data-fit="none"` makes it inline-block
79
+ * margin-inline auto can't centre an inline-block, only its parent's text
80
+ * alignment can. Scoped to a *direct-child* mockup so a card or bento cell
81
+ * that happens to hold a mockup doesn't get its body text centred too. */
82
+ .rf-preview__canvas:has(> .rf-mockup) {
79
83
  text-align: center;
80
84
  }
81
85
  .rf-preview__canvas > *:first-child {