mnfst 0.5.135 → 0.5.137
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/lib/manifest.appwrite.auth.js +537 -47
- package/lib/manifest.chart.css +49 -0
- package/lib/manifest.charts.js +155 -3
- package/lib/manifest.css +272 -1
- package/lib/manifest.data.js +23 -3
- package/lib/manifest.dropdown.css +1 -1
- package/lib/manifest.integrity.json +8 -8
- package/lib/manifest.js +1 -1
- package/lib/manifest.localization.js +17 -0
- package/lib/manifest.min.css +1 -1
- package/lib/manifest.payments.js +11 -1
- package/lib/manifest.schema.json +11 -2
- package/lib/manifest.tailwind.js +87 -110
- package/lib/manifest.utilities.js +36 -0
- package/package.json +9 -9
package/lib/manifest.chart.css
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
--color-chart-grid: var(--color-line, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent));
|
|
6
6
|
--color-chart-label: var(--color-content-subtle, oklch(55.6% 0 0));
|
|
7
7
|
|
|
8
|
+
/* Heatmap ramp — single-hue sequential scale derived from --color-chart-1,
|
|
9
|
+
spanning a pale tint to a deep, saturated shade so values stay legible
|
|
10
|
+
even when the brand color itself is light. */
|
|
11
|
+
--color-chart-heat-low: oklch(from var(--color-chart-1) 0.95 calc(c * 0.3) h);
|
|
12
|
+
--color-chart-heat-high: oklch(from var(--color-chart-1) 0.55 calc(c * 1.15) h);
|
|
13
|
+
|
|
8
14
|
/* Chart data segments */
|
|
9
15
|
--color-chart-1: var(--color-yellow-400, oklch(85.2% 0.199 91.936));
|
|
10
16
|
--color-chart-2: var(--color-yellow-500, oklch(79.5% 0.184 86.047));
|
|
@@ -119,6 +125,23 @@
|
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
/* Gauge — track band + value arc */
|
|
129
|
+
& path.gauge-track {
|
|
130
|
+
fill: var(--color-chart-color, var(--color-line, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent)))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
& path.gauge-value {
|
|
134
|
+
fill: var(--color-chart-color, var(--color-chart-1))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Gauge centered readout */
|
|
138
|
+
& text.gauge-label {
|
|
139
|
+
fill: var(--color-content-stark, oklch(20.5% 0 0));
|
|
140
|
+
font-size: 1.75rem;
|
|
141
|
+
font-weight: 700;
|
|
142
|
+
pointer-events: none
|
|
143
|
+
}
|
|
144
|
+
|
|
122
145
|
/* Value labels drawn on/above segments */
|
|
123
146
|
& text.value {
|
|
124
147
|
fill: var(--color-content-stark, oklch(20.5% 0 0));
|
|
@@ -135,6 +158,17 @@
|
|
|
135
158
|
}
|
|
136
159
|
}
|
|
137
160
|
|
|
161
|
+
/* Heatmap cell — fill interpolates between the two heat tokens by the
|
|
162
|
+
per-cell --heat percentage (0% = low, 100% = high) */
|
|
163
|
+
& rect.heat-cell {
|
|
164
|
+
fill: color-mix(in oklch, var(--color-chart-heat-high, var(--color-chart-1)) var(--heat, 0%), var(--color-chart-heat-low, var(--color-surface-2)));
|
|
165
|
+
transition: opacity var(--transition-duration, .1s) ease;
|
|
166
|
+
|
|
167
|
+
&:hover {
|
|
168
|
+
opacity: 0.82
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
138
172
|
/* Legend — <footer> sibling below the SVG; <span> items, <i> swatches */
|
|
139
173
|
& footer {
|
|
140
174
|
display: flex;
|
|
@@ -160,6 +194,21 @@
|
|
|
160
194
|
}
|
|
161
195
|
}
|
|
162
196
|
|
|
197
|
+
/* Heatmap gradient legend — low label, ramp bar (spans the grid), high
|
|
198
|
+
label. Footer is padded to the chart margins in JS so the bar aligns
|
|
199
|
+
with the columns. */
|
|
200
|
+
& footer.heat-legend {
|
|
201
|
+
flex-wrap: nowrap;
|
|
202
|
+
gap: 0.5rem;
|
|
203
|
+
|
|
204
|
+
& i {
|
|
205
|
+
flex: 1;
|
|
206
|
+
height: 0.5rem;
|
|
207
|
+
border-radius: 1rem;
|
|
208
|
+
background: linear-gradient(to right, var(--color-chart-heat-low, var(--color-surface-2)), var(--color-chart-heat-high, var(--color-chart-1)))
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
163
212
|
/* Cursor-following tooltip — chrome comes from manifest.tooltip.css
|
|
164
213
|
(.tooltip); only the pointer-tracking positioning lives here */
|
|
165
214
|
& .tooltip {
|
package/lib/manifest.charts.js
CHANGED
|
@@ -147,6 +147,7 @@
|
|
|
147
147
|
cfg.grid = cfg.grid !== false;
|
|
148
148
|
cfg.tooltip = cfg.tooltip !== false; // hover tooltips on by default
|
|
149
149
|
cfg.dataLabels = !!cfg.dataLabels; // static value labels off by default
|
|
150
|
+
cfg.gap = num(cfg.gap, 1); // heatmap tile gutter in px
|
|
150
151
|
cfg.labels = Array.isArray(cfg.labels) ? cfg.labels : [];
|
|
151
152
|
|
|
152
153
|
// Series may be omitted in favor of a single `data` array.
|
|
@@ -161,6 +162,13 @@
|
|
|
161
162
|
s.data = s.data.map(d => num(d.value, 0));
|
|
162
163
|
}
|
|
163
164
|
}
|
|
165
|
+
|
|
166
|
+
// Gauge: a single value, from `value` or the first series datum.
|
|
167
|
+
if (cfg.type === 'gauge') {
|
|
168
|
+
if (cfg.value != null && !cfg.series.length) cfg.series = [{ data: [num(cfg.value, 0)] }];
|
|
169
|
+
cfg.min = num(cfg.min, 0);
|
|
170
|
+
cfg.max = num(cfg.max, 100);
|
|
171
|
+
}
|
|
164
172
|
return cfg;
|
|
165
173
|
}
|
|
166
174
|
|
|
@@ -176,8 +184,13 @@
|
|
|
176
184
|
t.appendChild(document.createTextNode(str == null ? '' : String(str))); // untrusted-safe
|
|
177
185
|
return t;
|
|
178
186
|
}
|
|
187
|
+
// Entry animations run only on a chart's first draw. Reactive redraws
|
|
188
|
+
// (a bound value changing, a resize) must paint the final state directly
|
|
189
|
+
// — otherwise dragging a slider replays the reveal every frame and the
|
|
190
|
+
// chart flickers. drawChart sets this before dispatching.
|
|
191
|
+
let _suppressAnim = false;
|
|
179
192
|
function animate(el, keyframes, opts) {
|
|
180
|
-
if (prefersReducedMotion() || typeof el.animate !== 'function') return;
|
|
193
|
+
if (_suppressAnim || prefersReducedMotion() || typeof el.animate !== 'function') return;
|
|
181
194
|
try { el.animate(keyframes, Object.assign({ duration: 600, easing: 'cubic-bezier(0.22,1,0.36,1)', fill: 'backwards' }, opts)); } catch (_) { }
|
|
182
195
|
}
|
|
183
196
|
// Cursor-following tooltip. Manifest's x-tooltip relies on CSS anchor
|
|
@@ -280,11 +293,17 @@
|
|
|
280
293
|
const hasData = cfg.series.some(s => Array.isArray(s.data) && s.data.length);
|
|
281
294
|
if (!hasData) { const d = document.createElement('small'); d.textContent = 'No data'; el.appendChild(d); return; }
|
|
282
295
|
|
|
296
|
+
// Animate the reveal only on the first paint; redraws snap to state.
|
|
297
|
+
_suppressAnim = !!state._drawn;
|
|
298
|
+
state._drawn = true;
|
|
299
|
+
|
|
283
300
|
// Label via aria-label (not an SVG <title>, which renders a native
|
|
284
301
|
// browser tooltip that conflicts with our cursor tooltip).
|
|
285
302
|
const root = svg('svg', { viewBox: `0 0 ${width} ${height}`, width: '100%', height: String(height), role: 'img', 'aria-label': cfg.title || (cfg.type + ' chart'), preserveAspectRatio: 'xMidYMid meet' }, el);
|
|
286
303
|
|
|
287
304
|
if (cfg.type === 'pie' || cfg.type === 'donut') drawPie(state, root, width, height);
|
|
305
|
+
else if (cfg.type === 'gauge') drawGauge(state, root, width, height);
|
|
306
|
+
else if (cfg.type === 'heatmap') drawHeatmap(state, root, width, height);
|
|
288
307
|
else drawCartesian(state, root, width, height);
|
|
289
308
|
}
|
|
290
309
|
|
|
@@ -303,6 +322,17 @@
|
|
|
303
322
|
}
|
|
304
323
|
function seriesColorVar(i, explicit) { return explicit || `var(--color-chart-${(i % _paletteN) + 1})`; }
|
|
305
324
|
|
|
325
|
+
// Resolve the theme --radius token to user-space px (the viewBox is 1:1
|
|
326
|
+
// with CSS px), so SVG corners match the rest of the UI's rounding.
|
|
327
|
+
function cssRadius(el) {
|
|
328
|
+
try {
|
|
329
|
+
const v = getComputedStyle(el).getPropertyValue('--radius').trim() || '0.5rem';
|
|
330
|
+
if (v.endsWith('rem')) return parseFloat(v) * (parseFloat(getComputedStyle(document.documentElement).fontSize) || 16);
|
|
331
|
+
if (v.endsWith('px')) return parseFloat(v);
|
|
332
|
+
return parseFloat(v) || 8;
|
|
333
|
+
} catch (_) { return 8; }
|
|
334
|
+
}
|
|
335
|
+
|
|
306
336
|
// Line interpolation: monotone (smooth, default) | linear | step | natural.
|
|
307
337
|
function curveFor(d3, name) {
|
|
308
338
|
switch (String(name || '').toLowerCase()) {
|
|
@@ -479,7 +509,7 @@
|
|
|
479
509
|
});
|
|
480
510
|
}
|
|
481
511
|
function animateBar(rect, ih) {
|
|
482
|
-
if (prefersReducedMotion() || typeof rect.animate !== 'function') return;
|
|
512
|
+
if (_suppressAnim || prefersReducedMotion() || typeof rect.animate !== 'function') return;
|
|
483
513
|
rect.style.transformBox = 'fill-box'; rect.style.transformOrigin = 'center bottom';
|
|
484
514
|
try { rect.animate([{ transform: 'scaleY(0)' }, { transform: 'scaleY(1)' }], { duration: 600, easing: 'cubic-bezier(0.22,1,0.36,1)', fill: 'backwards' }); } catch (_) { }
|
|
485
515
|
}
|
|
@@ -511,7 +541,7 @@
|
|
|
511
541
|
const path = svg('path', { class: 'slice', d: arc(slice), style: `--color-chart-color:${seriesColorVar(i)}` }, g);
|
|
512
542
|
applyTip(path, labels[i] + ': ' + data[i], cfg);
|
|
513
543
|
if (cfg.dataLabels) { const c = arc.centroid(slice); dataLabel(g, data[i], c[0], c[1], 'middle', 'central', 'inverse'); }
|
|
514
|
-
if (!prefersReducedMotion() && typeof path.animate === 'function') {
|
|
544
|
+
if (!_suppressAnim && !prefersReducedMotion() && typeof path.animate === 'function') {
|
|
515
545
|
path.style.transformBox = 'fill-box'; path.style.transformOrigin = 'center';
|
|
516
546
|
try { path.animate([{ opacity: 0, transform: 'scale(0.85)' }, { opacity: 1, transform: 'scale(1)' }], { duration: 450, delay: i * 60, easing: 'cubic-bezier(0.22,1,0.36,1)', fill: 'backwards' }); } catch (_) { }
|
|
517
547
|
}
|
|
@@ -519,6 +549,128 @@
|
|
|
519
549
|
if (cfg.legend) drawLegend(state, labels);
|
|
520
550
|
}
|
|
521
551
|
|
|
552
|
+
// Gauge — a single value swept across a 180° dome. Track + value arc
|
|
553
|
+
// reuse the pie/donut arc primitive; optional `zones` paint threshold
|
|
554
|
+
// bands (e.g. positive/warning/negative ranges). `unit` suffixes the
|
|
555
|
+
// centered readout; `min`/`max` default 0–100.
|
|
556
|
+
function drawGauge(state, root, width, height) {
|
|
557
|
+
const cfg = state.config, d3 = state.d3;
|
|
558
|
+
const value = num(cfg.series[0] && cfg.series[0].data[0], 0);
|
|
559
|
+
const min = cfg.min, max = cfg.max;
|
|
560
|
+
const START = -Math.PI / 2, END = Math.PI / 2;
|
|
561
|
+
const scale = d3.scaleLinear().domain([min, max]).range([START, END]).clamp(true);
|
|
562
|
+
const unit = cfg.unit || '';
|
|
563
|
+
|
|
564
|
+
const r = Math.min(width / 2, height) - 8;
|
|
565
|
+
const thickness = Math.max(8, r * 0.22);
|
|
566
|
+
const cx = width / 2, cy = 8 + r;
|
|
567
|
+
const g = svg('g', { transform: `translate(${cx},${cy})` }, root);
|
|
568
|
+
const arc = d3.arc().innerRadius(r - thickness).outerRadius(r).cornerRadius(cssRadius(state.el));
|
|
569
|
+
|
|
570
|
+
// Track, or threshold zone bands when `zones` is given.
|
|
571
|
+
if (Array.isArray(cfg.zones) && cfg.zones.length) {
|
|
572
|
+
let from = min;
|
|
573
|
+
cfg.zones.forEach((z, i) => {
|
|
574
|
+
const to = num(z.to, max);
|
|
575
|
+
svg('path', { class: 'gauge-track', d: arc({ startAngle: scale(from), endAngle: scale(to) }), style: `--color-chart-color:${z.color || seriesColorVar(i)}`, opacity: 0.35 }, g);
|
|
576
|
+
from = to;
|
|
577
|
+
});
|
|
578
|
+
} else {
|
|
579
|
+
svg('path', { class: 'gauge-track', d: arc({ startAngle: START, endAngle: END }) }, g);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Value arc — final geometry is set unconditionally so the gauge is
|
|
583
|
+
// correct even if the entry animation never runs (background tab);
|
|
584
|
+
// the sweep is a CSS reveal, never the source of the end state.
|
|
585
|
+
const color = (cfg.series[0] && cfg.series[0].color) || 'var(--color-chart-1)';
|
|
586
|
+
const valueAngle = scale(value);
|
|
587
|
+
const vArc = svg('path', { class: 'gauge-value', d: arc({ startAngle: START, endAngle: valueAngle }), style: `--color-chart-color:${color}` }, g);
|
|
588
|
+
applyTip(vArc, (cfg.title ? cfg.title + ': ' : '') + value + unit, cfg);
|
|
589
|
+
// Reveal via fade (WAAPI, origin-independent) — the final geometry above
|
|
590
|
+
// is unconditional, so the gauge is correct even if this never runs.
|
|
591
|
+
animate(vArc, [{ opacity: 0 }, { opacity: 1 }], { duration: 500 });
|
|
592
|
+
|
|
593
|
+
// Centered readout + range end labels.
|
|
594
|
+
text(g, value + unit, { class: 'gauge-label', x: 0, y: -r * 0.12, 'text-anchor': 'middle', 'dominant-baseline': 'central' });
|
|
595
|
+
if (cfg.axis) {
|
|
596
|
+
const lr = r - thickness / 2;
|
|
597
|
+
text(g, String(min), { x: -lr, y: 16, 'text-anchor': 'middle' });
|
|
598
|
+
text(g, String(max), { x: lr, y: 16, 'text-anchor': 'middle' });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Heatmap — a matrix of cells: each series is a row, each datum a column
|
|
603
|
+
// aligned to `labels`. Cell colour is a CSS color-mix between the two
|
|
604
|
+
// --color-chart-heat-* tokens (no JS colour-interpolation dep), driven
|
|
605
|
+
// by the per-cell `--heat` percentage.
|
|
606
|
+
function drawHeatmap(state, root, width, height) {
|
|
607
|
+
const cfg = state.config, d3 = state.d3;
|
|
608
|
+
const rows = cfg.series;
|
|
609
|
+
const cols = cfg.labels.length ? cfg.labels : (rows[0] && Array.isArray(rows[0].data) ? rows[0].data.map((_, i) => i + 1) : []);
|
|
610
|
+
const rowName = (r, i) => r.name || String(i + 1);
|
|
611
|
+
const showLabels = cfg.axis;
|
|
612
|
+
|
|
613
|
+
const m = { top: 4, right: 4, bottom: showLabels ? 24 : 4, left: showLabels ? 64 : 4 };
|
|
614
|
+
const iw = width - m.left - m.right;
|
|
615
|
+
const ih = height - m.top - m.bottom;
|
|
616
|
+
// Tiles abut (padding 0); the gutter comes from insetting each rect
|
|
617
|
+
// by `gap` px (config, default 1; set 0 for a seamless field).
|
|
618
|
+
const gap = Math.max(0, cfg.gap);
|
|
619
|
+
const x = d3.scaleBand().domain(cols.map(String)).range([0, iw]).padding(0);
|
|
620
|
+
const yb = d3.scaleBand().domain(rows.map(rowName)).range([0, ih]).padding(0);
|
|
621
|
+
const cw = Math.max(0, x.bandwidth() - gap), ch = Math.max(0, yb.bandwidth() - gap);
|
|
622
|
+
|
|
623
|
+
// Value domain across every cell.
|
|
624
|
+
let lo = Infinity, hi = -Infinity;
|
|
625
|
+
rows.forEach(r => (r.data || []).forEach(v => { const n = num(v, 0); if (n < lo) lo = n; if (n > hi) hi = n; }));
|
|
626
|
+
if (!isFinite(lo)) { lo = 0; hi = 1; }
|
|
627
|
+
if (lo === hi) hi = lo + 1;
|
|
628
|
+
|
|
629
|
+
const plot = svg('g', { transform: `translate(${m.left},${m.top})` }, root);
|
|
630
|
+
|
|
631
|
+
// Round only the grid's outer corners: square tiles clipped to a single
|
|
632
|
+
// rounded rect hugging the cell extent (right/bottom edge sits at the
|
|
633
|
+
// last tile's inner edge, so the radius isn't clipping empty gutter).
|
|
634
|
+
const clipId = 'mnfst-heat-' + (++_uid);
|
|
635
|
+
const cp = svg('clipPath', { id: clipId }, svg('defs', null, root));
|
|
636
|
+
svg('rect', { x: 0, y: 0, width: Math.max(0, iw - gap), height: Math.max(0, ih - gap), rx: cssRadius(state.el) }, cp);
|
|
637
|
+
const cellsG = svg('g', { 'clip-path': `url(#${clipId})` }, plot);
|
|
638
|
+
|
|
639
|
+
rows.forEach((r, ri) => {
|
|
640
|
+
const yy = yb(rowName(r, ri));
|
|
641
|
+
cols.forEach((c, ci) => {
|
|
642
|
+
const v = num(r.data[ci], 0);
|
|
643
|
+
const t = Math.round(((v - lo) / (hi - lo)) * 100);
|
|
644
|
+
const cell = svg('rect', { class: 'heat-cell', x: x(String(c)), y: yy, width: cw, height: ch, style: `--heat:${t}%` }, cellsG);
|
|
645
|
+
applyTip(cell, (r.name ? r.name + ' · ' : '') + c + ': ' + v, cfg);
|
|
646
|
+
animate(cell, [{ opacity: 0 }, { opacity: 1 }], { duration: 300, delay: (ri + ci) * 20 });
|
|
647
|
+
if (cfg.dataLabels) dataLabel(plot, v, x(String(c)) + x.bandwidth() / 2, yy + yb.bandwidth() / 2, 'middle', 'central');
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
if (showLabels) {
|
|
652
|
+
rows.forEach((r, ri) => text(plot, rowName(r, ri), { x: -8, y: yb(rowName(r, ri)) + yb.bandwidth() / 2, 'text-anchor': 'end', 'dominant-baseline': 'central' }));
|
|
653
|
+
cols.forEach(c => text(plot, c, { x: x(String(c)) + x.bandwidth() / 2, y: ih + 16, 'text-anchor': 'middle' }));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (cfg.legend) drawHeatLegend(state, lo, hi, m);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Continuous gradient legend for the heatmap: low label, ramp bar, high
|
|
660
|
+
// label. Padded to the chart's margins so the bar spans the grid width
|
|
661
|
+
// (bar flexes to fill — see .heat-legend in the CSS).
|
|
662
|
+
function drawHeatLegend(state, lo, hi, m) {
|
|
663
|
+
const footer = document.createElement('footer');
|
|
664
|
+
footer.className = 'heat-legend';
|
|
665
|
+
footer.style.paddingLeft = m.left + 'px';
|
|
666
|
+
footer.style.paddingRight = m.right + 'px';
|
|
667
|
+
const a = document.createElement('span'); a.textContent = lo;
|
|
668
|
+
const bar = document.createElement('i');
|
|
669
|
+
const b = document.createElement('span'); b.textContent = hi;
|
|
670
|
+
footer.append(a, bar, b);
|
|
671
|
+
state.el.appendChild(footer);
|
|
672
|
+
}
|
|
673
|
+
|
|
522
674
|
// Legend is a <footer> sibling below the SVG (inline flex), not an
|
|
523
675
|
// absolute overlay — so it never collides with axis labels. Each item is
|
|
524
676
|
// a <span> with an <i> swatch carrying the series colour.
|
package/lib/manifest.css
CHANGED
|
@@ -651,6 +651,12 @@
|
|
|
651
651
|
--color-chart-grid: var(--color-line, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent));
|
|
652
652
|
--color-chart-label: var(--color-content-subtle, oklch(55.6% 0 0));
|
|
653
653
|
|
|
654
|
+
/* Heatmap ramp — single-hue sequential scale derived from --color-chart-1,
|
|
655
|
+
spanning a pale tint to a deep, saturated shade so values stay legible
|
|
656
|
+
even when the brand color itself is light. */
|
|
657
|
+
--color-chart-heat-low: oklch(from var(--color-chart-1) 0.95 calc(c * 0.3) h);
|
|
658
|
+
--color-chart-heat-high: oklch(from var(--color-chart-1) 0.55 calc(c * 1.15) h);
|
|
659
|
+
|
|
654
660
|
/* Chart data segments */
|
|
655
661
|
--color-chart-1: var(--color-yellow-400, oklch(85.2% 0.199 91.936));
|
|
656
662
|
--color-chart-2: var(--color-yellow-500, oklch(79.5% 0.184 86.047));
|
|
@@ -765,6 +771,23 @@
|
|
|
765
771
|
}
|
|
766
772
|
}
|
|
767
773
|
|
|
774
|
+
/* Gauge — track band + value arc */
|
|
775
|
+
& path.gauge-track {
|
|
776
|
+
fill: var(--color-chart-color, var(--color-line, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent)))
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
& path.gauge-value {
|
|
780
|
+
fill: var(--color-chart-color, var(--color-chart-1))
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/* Gauge centered readout */
|
|
784
|
+
& text.gauge-label {
|
|
785
|
+
fill: var(--color-content-stark, oklch(20.5% 0 0));
|
|
786
|
+
font-size: 1.75rem;
|
|
787
|
+
font-weight: 700;
|
|
788
|
+
pointer-events: none
|
|
789
|
+
}
|
|
790
|
+
|
|
768
791
|
/* Value labels drawn on/above segments */
|
|
769
792
|
& text.value {
|
|
770
793
|
fill: var(--color-content-stark, oklch(20.5% 0 0));
|
|
@@ -781,6 +804,17 @@
|
|
|
781
804
|
}
|
|
782
805
|
}
|
|
783
806
|
|
|
807
|
+
/* Heatmap cell — fill interpolates between the two heat tokens by the
|
|
808
|
+
per-cell --heat percentage (0% = low, 100% = high) */
|
|
809
|
+
& rect.heat-cell {
|
|
810
|
+
fill: color-mix(in oklch, var(--color-chart-heat-high, var(--color-chart-1)) var(--heat, 0%), var(--color-chart-heat-low, var(--color-surface-2)));
|
|
811
|
+
transition: opacity var(--transition-duration, .1s) ease;
|
|
812
|
+
|
|
813
|
+
&:hover {
|
|
814
|
+
opacity: 0.82
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
784
818
|
/* Legend — <footer> sibling below the SVG; <span> items, <i> swatches */
|
|
785
819
|
& footer {
|
|
786
820
|
display: flex;
|
|
@@ -806,6 +840,21 @@
|
|
|
806
840
|
}
|
|
807
841
|
}
|
|
808
842
|
|
|
843
|
+
/* Heatmap gradient legend — low label, ramp bar (spans the grid), high
|
|
844
|
+
label. Footer is padded to the chart margins in JS so the bar aligns
|
|
845
|
+
with the columns. */
|
|
846
|
+
& footer.heat-legend {
|
|
847
|
+
flex-wrap: nowrap;
|
|
848
|
+
gap: 0.5rem;
|
|
849
|
+
|
|
850
|
+
& i {
|
|
851
|
+
flex: 1;
|
|
852
|
+
height: 0.5rem;
|
|
853
|
+
border-radius: 1rem;
|
|
854
|
+
background: linear-gradient(to right, var(--color-chart-heat-low, var(--color-surface-2)), var(--color-chart-heat-high, var(--color-chart-1)))
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
809
858
|
/* Cursor-following tooltip — chrome comes from manifest.tooltip.css
|
|
810
859
|
(.tooltip); only the pointer-tracking positioning lives here */
|
|
811
860
|
& .tooltip {
|
|
@@ -2212,7 +2261,7 @@
|
|
|
2212
2261
|
margin-left: auto;
|
|
2213
2262
|
margin-right: auto;
|
|
2214
2263
|
|
|
2215
|
-
& :where(a, button, [role=button])
|
|
2264
|
+
& :where(a, button, [role=button]) {
|
|
2216
2265
|
justify-content: center
|
|
2217
2266
|
}
|
|
2218
2267
|
}
|
|
@@ -2357,6 +2406,228 @@
|
|
|
2357
2406
|
}
|
|
2358
2407
|
}
|
|
2359
2408
|
|
|
2409
|
+
/* Manifest Edit */
|
|
2410
|
+
|
|
2411
|
+
/* Authors override any of this without specificity battles: every rule is wrapped
|
|
2412
|
+
in :where(), state lives on data-attributes, and injected affordances use plain
|
|
2413
|
+
semantic class names (.edit-handle, .edit-frame) — no framework-prefixed litter. */
|
|
2414
|
+
|
|
2415
|
+
@layer utilities {
|
|
2416
|
+
|
|
2417
|
+
/* ---- Editable area (armed) ---- */
|
|
2418
|
+
:where([data-edit-armed]) {
|
|
2419
|
+
position: relative;
|
|
2420
|
+
outline: 1px dashed var(--color-line);
|
|
2421
|
+
outline-offset: 6px;
|
|
2422
|
+
border-radius: var(--radius);
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
:where([data-edit-armed])::before {
|
|
2426
|
+
content: attr(data-edit-label);
|
|
2427
|
+
position: absolute;
|
|
2428
|
+
inset-block-start: -1.5rem;
|
|
2429
|
+
inset-inline-start: 0;
|
|
2430
|
+
font-size: 0.7rem;
|
|
2431
|
+
letter-spacing: 0.04em;
|
|
2432
|
+
text-transform: uppercase;
|
|
2433
|
+
color: var(--color-content-subtle);
|
|
2434
|
+
pointer-events: none;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
/* ---- Reorder (pointer + keyboard; touch-action:none lets touch-drag beat scroll) ---- */
|
|
2438
|
+
:where([data-edit-sortable]) {
|
|
2439
|
+
cursor: grab;
|
|
2440
|
+
touch-action: none;
|
|
2441
|
+
transition: opacity 0.12s ease, outline-color 0.12s ease;
|
|
2442
|
+
}
|
|
2443
|
+
:where([data-edit-sortable]):hover {
|
|
2444
|
+
outline: 1px solid var(--edit-accent, var(--color-brand-content));
|
|
2445
|
+
outline-offset: 3px;
|
|
2446
|
+
}
|
|
2447
|
+
:where([data-edit-sortable]):focus-visible,
|
|
2448
|
+
:where([data-edit-movable]):focus-visible {
|
|
2449
|
+
outline: 2px solid var(--edit-accent, var(--color-brand-content));
|
|
2450
|
+
outline-offset: 3px;
|
|
2451
|
+
}
|
|
2452
|
+
:where([data-edit-dragging]) { opacity: 0.4; }
|
|
2453
|
+
:where([data-edit-grabbed]) {
|
|
2454
|
+
outline: 2px dashed var(--edit-accent, var(--color-brand-content));
|
|
2455
|
+
outline-offset: 3px;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
/* Screen-reader announcement region (keyboard grab/move/drop) */
|
|
2459
|
+
:where(.edit-sr) {
|
|
2460
|
+
position: absolute;
|
|
2461
|
+
width: 1px; height: 1px;
|
|
2462
|
+
margin: -1px; padding: 0; border: 0;
|
|
2463
|
+
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
/* ---- Move (positioned children) ---- */
|
|
2467
|
+
:where([data-edit-movable]) { cursor: move; touch-action: none; }
|
|
2468
|
+
:where([data-edit-movable]):hover {
|
|
2469
|
+
outline: 1px solid var(--edit-accent, var(--color-brand-content));
|
|
2470
|
+
outline-offset: 2px;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
/* ---- Inline text ---- */
|
|
2474
|
+
:where([data-edit-armed]) [contenteditable="true"] {
|
|
2475
|
+
outline: 1px solid color-mix(in srgb, var(--edit-accent, var(--color-brand-content)) 45%, transparent);
|
|
2476
|
+
outline-offset: 2px;
|
|
2477
|
+
border-radius: 3px;
|
|
2478
|
+
}
|
|
2479
|
+
:where([data-edit-armed]) [contenteditable="true"]:focus {
|
|
2480
|
+
outline: 2px solid var(--edit-accent, var(--color-brand-content));
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
/* ---- Size (resize handles) — injected overlay children, fully stylable ---- */
|
|
2484
|
+
/* The element keeps its own markup untouched, so its native :hover/:focus still
|
|
2485
|
+
fire. Handles are separate elements with pointer-events; the body of the
|
|
2486
|
+
element stays interactive. */
|
|
2487
|
+
:where([data-edit-sizable]) {
|
|
2488
|
+
position: relative;
|
|
2489
|
+
|
|
2490
|
+
.edit-handle {
|
|
2491
|
+
position: absolute;
|
|
2492
|
+
z-index: 100;
|
|
2493
|
+
background: transparent;
|
|
2494
|
+
--h: var(--edit-size-handle, 1rem);
|
|
2495
|
+
|
|
2496
|
+
&::before {
|
|
2497
|
+
content: '';
|
|
2498
|
+
position: absolute;
|
|
2499
|
+
top: 50%;
|
|
2500
|
+
left: 50%;
|
|
2501
|
+
transform: translate(-50%, -50%);
|
|
2502
|
+
width: 1px;
|
|
2503
|
+
height: 1px;
|
|
2504
|
+
background: transparent;
|
|
2505
|
+
transition: background-color 0.15s ease;
|
|
2506
|
+
}
|
|
2507
|
+
&:hover::before, &:active::before {
|
|
2508
|
+
background-color: var(--edit-accent, var(--color-brand-content));
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
.edit-handle-top, .edit-handle-bottom { left: 0; width: 100%; height: var(--h); cursor: ns-resize; &::before { width: 100%; } }
|
|
2513
|
+
.edit-handle-left, .edit-handle-right, .edit-handle-start, .edit-handle-end { top: 0; height: 100%; width: var(--h); cursor: ew-resize; &::before { height: 100%; } }
|
|
2514
|
+
.edit-handle-top { top: calc(var(--h) * -0.5); }
|
|
2515
|
+
.edit-handle-bottom { bottom: calc(var(--h) * -0.5); }
|
|
2516
|
+
.edit-handle-left { left: calc(var(--h) * -0.5); }
|
|
2517
|
+
.edit-handle-right { right: calc(var(--h) * -0.5); }
|
|
2518
|
+
/* Logical edges — inset-inline-* auto-flips for RTL */
|
|
2519
|
+
.edit-handle-start { inset-inline-start: calc(var(--h) * -0.5); }
|
|
2520
|
+
.edit-handle-end { inset-inline-end: calc(var(--h) * -0.5); }
|
|
2521
|
+
|
|
2522
|
+
.edit-handle-top-left, .edit-handle-top-right, .edit-handle-bottom-left, .edit-handle-bottom-right,
|
|
2523
|
+
.edit-handle-top-start, .edit-handle-top-end, .edit-handle-bottom-start, .edit-handle-bottom-end {
|
|
2524
|
+
width: var(--h); height: var(--h);
|
|
2525
|
+
}
|
|
2526
|
+
.edit-handle-top-left { top: calc(var(--h) * -0.5); left: calc(var(--h) * -0.5); cursor: nwse-resize; }
|
|
2527
|
+
.edit-handle-top-right { top: calc(var(--h) * -0.5); right: calc(var(--h) * -0.5); cursor: nesw-resize; }
|
|
2528
|
+
.edit-handle-bottom-left { bottom: calc(var(--h) * -0.5); left: calc(var(--h) * -0.5); cursor: nesw-resize; }
|
|
2529
|
+
.edit-handle-bottom-right { bottom: calc(var(--h) * -0.5); right: calc(var(--h) * -0.5); cursor: nwse-resize; }
|
|
2530
|
+
.edit-handle-top-start { top: calc(var(--h) * -0.5); inset-inline-start: calc(var(--h) * -0.5); cursor: nwse-resize; }
|
|
2531
|
+
.edit-handle-top-end { top: calc(var(--h) * -0.5); inset-inline-end: calc(var(--h) * -0.5); cursor: nesw-resize; }
|
|
2532
|
+
.edit-handle-bottom-start { bottom: calc(var(--h) * -0.5); inset-inline-start: calc(var(--h) * -0.5); cursor: nesw-resize; }
|
|
2533
|
+
.edit-handle-bottom-end { bottom: calc(var(--h) * -0.5); inset-inline-end: calc(var(--h) * -0.5); cursor: nwse-resize; }
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
/* Collapsed state (size dropped below --edit-size-collapse-*) — authors restyle freely */
|
|
2537
|
+
:where([data-edit-collapsed]) { opacity: 0.45; }
|
|
2538
|
+
|
|
2539
|
+
/* Full-viewport capture layer during a size/move drag (mirrors resize overlay) */
|
|
2540
|
+
:where(.edit-overlay) {
|
|
2541
|
+
position: fixed;
|
|
2542
|
+
inset: 0;
|
|
2543
|
+
z-index: 9999;
|
|
2544
|
+
background: transparent;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2548
|
+
:where([data-edit-armed]) > [draggable="true"], :where([data-edit-sizable]) .edit-handle::before { transition: none; }
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
/* ---- Plugin dev chrome (spike-only: toolbar + inspector + class menu) ---- */
|
|
2553
|
+
@layer components {
|
|
2554
|
+
|
|
2555
|
+
:where(.edit-toolbar) {
|
|
2556
|
+
position: fixed;
|
|
2557
|
+
inset-block-end: 1rem;
|
|
2558
|
+
inset-inline-end: 1rem;
|
|
2559
|
+
z-index: 9999;
|
|
2560
|
+
display: flex;
|
|
2561
|
+
gap: 0.5rem;
|
|
2562
|
+
align-items: center;
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
:where(.edit-classes) {
|
|
2566
|
+
position: fixed;
|
|
2567
|
+
z-index: 10000;
|
|
2568
|
+
width: 260px;
|
|
2569
|
+
padding: 0.6rem;
|
|
2570
|
+
display: flex;
|
|
2571
|
+
flex-direction: column;
|
|
2572
|
+
gap: 0.4rem;
|
|
2573
|
+
background: var(--color-surface-1);
|
|
2574
|
+
border: 1px solid var(--color-line);
|
|
2575
|
+
border-radius: var(--radius);
|
|
2576
|
+
box-shadow: 0 8px 30px rgb(0 0 0 / 0.2);
|
|
2577
|
+
}
|
|
2578
|
+
:where(.edit-classes)[hidden] { display: none; }
|
|
2579
|
+
:where(.edit-classes) input { width: 100%; font-family: ui-monospace, Menlo, monospace; font-size: 0.75rem; }
|
|
2580
|
+
:where(.edit-classes) .muted { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--color-content-subtle); }
|
|
2581
|
+
|
|
2582
|
+
:where(.edit-inspector) {
|
|
2583
|
+
position: fixed;
|
|
2584
|
+
inset-block: 1rem 5rem;
|
|
2585
|
+
inset-inline-end: 1rem;
|
|
2586
|
+
width: 380px;
|
|
2587
|
+
max-width: 42vw;
|
|
2588
|
+
z-index: 9998;
|
|
2589
|
+
overflow: auto;
|
|
2590
|
+
padding: 1rem;
|
|
2591
|
+
background: var(--color-surface-1);
|
|
2592
|
+
border: 1px solid var(--color-line);
|
|
2593
|
+
border-radius: var(--radius);
|
|
2594
|
+
box-shadow: 0 8px 30px rgb(0 0 0 / 0.18);
|
|
2595
|
+
font-size: 0.78rem;
|
|
2596
|
+
}
|
|
2597
|
+
:where(.edit-inspector)[hidden] { display: none; }
|
|
2598
|
+
:where(.edit-inspector) h4 { margin: 0 0 0.25rem; }
|
|
2599
|
+
:where(.edit-inspector) section { margin-bottom: 1rem; }
|
|
2600
|
+
:where(.edit-inspector) pre {
|
|
2601
|
+
margin: 0.25rem 0 0;
|
|
2602
|
+
padding: 0.6rem;
|
|
2603
|
+
background: var(--color-surface-3);
|
|
2604
|
+
border-radius: 6px;
|
|
2605
|
+
white-space: pre-wrap;
|
|
2606
|
+
word-break: break-word;
|
|
2607
|
+
font-family: ui-monospace, Menlo, monospace;
|
|
2608
|
+
font-size: 0.72rem;
|
|
2609
|
+
line-height: 1.45;
|
|
2610
|
+
}
|
|
2611
|
+
:where(.edit-inspector) .muted { color: var(--color-content-subtle); }
|
|
2612
|
+
:where(.edit-inspector) .tag {
|
|
2613
|
+
display: inline-block;
|
|
2614
|
+
font-size: 0.64rem;
|
|
2615
|
+
text-transform: uppercase;
|
|
2616
|
+
letter-spacing: 0.05em;
|
|
2617
|
+
padding: 0.05rem 0.4rem;
|
|
2618
|
+
border-radius: 999px;
|
|
2619
|
+
background: var(--color-surface-3);
|
|
2620
|
+
color: var(--color-content-subtle);
|
|
2621
|
+
margin-inline-start: 0.4rem;
|
|
2622
|
+
}
|
|
2623
|
+
:where(.edit-inspector) .tag.static { background: color-mix(in srgb, var(--color-brand-content) 18%, transparent); }
|
|
2624
|
+
:where(.edit-inspector) .tag.data { background: color-mix(in srgb, var(--color-positive-content) 22%, transparent); }
|
|
2625
|
+
:where(.edit-inspector) .tag.component { background: color-mix(in srgb, var(--color-negative-content) 20%, transparent); }
|
|
2626
|
+
:where(.edit-inspector) .delta { padding: 0.2rem 0.3rem; border-radius: 4px; font-family: ui-monospace, Menlo, monospace; font-size: 0.7rem; line-height: 1.5; }
|
|
2627
|
+
:where(.edit-inspector) .delta.undone { opacity: 0.4; text-decoration: line-through; }
|
|
2628
|
+
:where(.edit-toolbar) button[disabled] { opacity: 0.35; pointer-events: none; }
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2360
2631
|
/* Manifest Forms */
|
|
2361
2632
|
|
|
2362
2633
|
@layer components {
|
package/lib/manifest.data.js
CHANGED
|
@@ -1497,6 +1497,15 @@ function numericKeyObjectToArray(obj) {
|
|
|
1497
1497
|
return sorted.map(i => obj[String(i)]);
|
|
1498
1498
|
}
|
|
1499
1499
|
|
|
1500
|
+
// Empty value for a header-only CSV (declares columns but has no data rows yet —
|
|
1501
|
+
// e.g. a locales file used only to declare available languages). Shapes the
|
|
1502
|
+
// result like a populated parse would: tabular ('id' + 3+ columns) → [], else
|
|
1503
|
+
// key-value → {}. A header-only CSV is a valid empty source, not an error.
|
|
1504
|
+
function emptyCsvResult(headers) {
|
|
1505
|
+
const first = (headers[0] || '').toLowerCase();
|
|
1506
|
+
return (headers.length > 2 && first === 'id') ? [] : {};
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1500
1509
|
// Parse CSV text to nested object structure
|
|
1501
1510
|
function parseCSVToNestedObject(csvText, options = {}) {
|
|
1502
1511
|
const {
|
|
@@ -1520,7 +1529,13 @@ function parseCSVToNestedObject(csvText, options = {}) {
|
|
|
1520
1529
|
}
|
|
1521
1530
|
|
|
1522
1531
|
if (!parsed.data || parsed.data.length === 0) {
|
|
1523
|
-
|
|
1532
|
+
// Header-only CSV is a valid empty source — return empty rather than
|
|
1533
|
+
// throw. Only a file with no header row at all is treated as invalid.
|
|
1534
|
+
const fields = parsed.meta?.fields || [];
|
|
1535
|
+
if (fields.length === 0) {
|
|
1536
|
+
throw new Error('[Manifest Data] CSV file is empty or has no headers');
|
|
1537
|
+
}
|
|
1538
|
+
return emptyCsvResult(fields);
|
|
1524
1539
|
}
|
|
1525
1540
|
|
|
1526
1541
|
const result = {};
|
|
@@ -1608,8 +1623,8 @@ function parseCSVToNestedObject(csvText, options = {}) {
|
|
|
1608
1623
|
} else {
|
|
1609
1624
|
// Fallback simple parser (if PapaParse not loaded)
|
|
1610
1625
|
const lines = csvText.split('\n').filter(line => line.trim());
|
|
1611
|
-
if (lines.length
|
|
1612
|
-
throw new Error('[Manifest Data] CSV file
|
|
1626
|
+
if (lines.length === 0) {
|
|
1627
|
+
throw new Error('[Manifest Data] CSV file is empty or has no headers');
|
|
1613
1628
|
}
|
|
1614
1629
|
|
|
1615
1630
|
// Simple CSV line parser (handles quoted values)
|
|
@@ -1638,6 +1653,11 @@ function parseCSVToNestedObject(csvText, options = {}) {
|
|
|
1638
1653
|
throw new Error('[Manifest Data] CSV file must have at least two columns');
|
|
1639
1654
|
}
|
|
1640
1655
|
|
|
1656
|
+
if (lines.length === 1) {
|
|
1657
|
+
// Header-only CSV → valid empty source (see PapaParse path above).
|
|
1658
|
+
return emptyCsvResult(headers);
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1641
1661
|
// First column is always the key
|
|
1642
1662
|
const keyColumn = headers[0];
|
|
1643
1663
|
|