@chromamark/renderer 0.1.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/src/tones.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The ChromaMark color vocabulary: semantic tones plus a hex/name escape hatch.
3
+ * Shared by containers, pills, colored text, and meters.
4
+ */
5
+
6
+ /** Canonical semantic tones, in vocabulary order. */
7
+ export const TONES = ['success', 'danger', 'warning', 'info', 'tip', 'muted'];
8
+
9
+ /** Aliases mapping to a canonical tone. */
10
+ export const ALIASES = {
11
+ ok: 'success',
12
+ pass: 'success',
13
+ error: 'danger',
14
+ fail: 'danger',
15
+ warn: 'warning',
16
+ note: 'info',
17
+ hint: 'tip',
18
+ skip: 'muted',
19
+ };
20
+
21
+ const HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
22
+ const COLOR_NAME = /^[a-zA-Z][a-zA-Z0-9]*$/;
23
+
24
+ /**
25
+ * True for a color value safe to emit inside a `style="--fg:…"` attribute:
26
+ * a hex literal or a plain CSS color name. Functional forms are rejected to
27
+ * keep untrusted input from injecting arbitrary CSS.
28
+ */
29
+ export function isSafeColor(value) {
30
+ return isHex(value) || COLOR_NAME.test(value);
31
+ }
32
+
33
+ /**
34
+ * Resolve a token to a canonical tone, or null if it is not a known tone/alias.
35
+ * Case-insensitive.
36
+ */
37
+ export function resolveTone(name) {
38
+ if (typeof name !== 'string') return null;
39
+ const key = name.toLowerCase();
40
+ if (TONES.includes(key)) return key;
41
+ if (Object.prototype.hasOwnProperty.call(ALIASES, key)) return ALIASES[key];
42
+ return null;
43
+ }
44
+
45
+ /** True for a valid `#rgb` / `#rrggbb` literal. */
46
+ export function isHex(value) {
47
+ return typeof value === 'string' && HEX.test(value);
48
+ }
49
+
50
+ /**
51
+ * Parse the leading spec token of an inline construct or container attr.
52
+ * Accepts a tone/alias, `color=<hex|name>`, or a bare `#hex`.
53
+ * Returns `{ tone, color }` (exactly one non-null) or null when unrecognized.
54
+ * Bare CSS color names are intentionally rejected here to avoid swallowing
55
+ * ordinary label words; use the explicit `color=<name>` escape hatch instead.
56
+ */
57
+ export function parseSpec(token) {
58
+ if (typeof token !== 'string' || token.length === 0) return null;
59
+
60
+ if (token.toLowerCase().startsWith('color=')) {
61
+ const value = token.slice('color='.length);
62
+ if (!isSafeColor(value)) return null;
63
+ return { tone: null, color: value };
64
+ }
65
+
66
+ if (isHex(token)) return { tone: null, color: token };
67
+
68
+ const tone = resolveTone(token);
69
+ if (tone) return { tone, color: null };
70
+
71
+ return null;
72
+ }
@@ -0,0 +1,113 @@
1
+ /*
2
+ * ChromaMark theme — maps semantic tones to real colors and styles every
3
+ * rendered construct. Tones are theme-owned: switch light/dark and colors
4
+ * follow. Load alongside HTML produced by @chromamark/renderer.
5
+ *
6
+ * Theme resolution order: explicit [data-theme] wins; otherwise the reader's
7
+ * prefers-color-scheme is honored.
8
+ */
9
+
10
+ :root {
11
+ --cm-success-fg:#1a7f37; --cm-success-bg:#dafbe1; --cm-success-bd:#2da44e;
12
+ --cm-danger-fg:#cf222e; --cm-danger-bg:#ffebe9; --cm-danger-bd:#ff8182;
13
+ --cm-warning-fg:#9a6700; --cm-warning-bg:#fff8c5; --cm-warning-bd:#d4a72c;
14
+ --cm-info-fg:#0969da; --cm-info-bg:#ddf4ff; --cm-info-bd:#54aeff;
15
+ --cm-tip-fg:#0f7b6c; --cm-tip-bg:#d3f5f0; --cm-tip-bd:#3bc4b0;
16
+ --cm-muted-fg:#656d76; --cm-muted-bg:#f6f8fa; --cm-muted-bd:#d0d7de;
17
+ --cm-neutral-bg:#f6f8fa; --cm-neutral-bd:#d0d7de;
18
+ }
19
+
20
+ @media (prefers-color-scheme: dark) {
21
+ :root:not([data-theme="light"]) {
22
+ --cm-success-fg:#3fb950; --cm-success-bg:#12261e; --cm-success-bd:#238636;
23
+ --cm-danger-fg:#f85149; --cm-danger-bg:#25171c; --cm-danger-bd:#da3633;
24
+ --cm-warning-fg:#d29922; --cm-warning-bg:#272115; --cm-warning-bd:#9e6a03;
25
+ --cm-info-fg:#58a6ff; --cm-info-bg:#0d2233; --cm-info-bd:#1f6feb;
26
+ --cm-tip-fg:#56d4c3; --cm-tip-bg:#0c2620; --cm-tip-bd:#1c6f63;
27
+ --cm-muted-fg:#8b949e; --cm-muted-bg:#161b22; --cm-muted-bd:#30363d;
28
+ --cm-neutral-bg:#161b22; --cm-neutral-bd:#30363d;
29
+ }
30
+ }
31
+
32
+ [data-theme="dark"] {
33
+ --cm-success-fg:#3fb950; --cm-success-bg:#12261e; --cm-success-bd:#238636;
34
+ --cm-danger-fg:#f85149; --cm-danger-bg:#25171c; --cm-danger-bd:#da3633;
35
+ --cm-warning-fg:#d29922; --cm-warning-bg:#272115; --cm-warning-bd:#9e6a03;
36
+ --cm-info-fg:#58a6ff; --cm-info-bg:#0d2233; --cm-info-bd:#1f6feb;
37
+ --cm-tip-fg:#56d4c3; --cm-tip-bg:#0c2620; --cm-tip-bd:#1c6f63;
38
+ --cm-muted-fg:#8b949e; --cm-muted-bg:#161b22; --cm-muted-bd:#30363d;
39
+ --cm-neutral-bg:#161b22; --cm-neutral-bd:#30363d;
40
+ }
41
+
42
+ /* Map a tone onto the generic --fg/--bg/--bd consumed by every component. */
43
+ [data-tone="success"]{--fg:var(--cm-success-fg);--bg:var(--cm-success-bg);--bd:var(--cm-success-bd);}
44
+ [data-tone="danger"] {--fg:var(--cm-danger-fg); --bg:var(--cm-danger-bg); --bd:var(--cm-danger-bd);}
45
+ [data-tone="warning"]{--fg:var(--cm-warning-fg);--bg:var(--cm-warning-bg);--bd:var(--cm-warning-bd);}
46
+ [data-tone="info"] {--fg:var(--cm-info-fg); --bg:var(--cm-info-bg); --bd:var(--cm-info-bd);}
47
+ [data-tone="tip"] {--fg:var(--cm-tip-fg); --bg:var(--cm-tip-bg); --bd:var(--cm-tip-bd);}
48
+ [data-tone="muted"] {--fg:var(--cm-muted-fg); --bg:var(--cm-muted-bg); --bd:var(--cm-muted-bd);}
49
+
50
+ /* Custom color (color=…): derive a tinted bg/border from the chosen --fg. */
51
+ .cm-custom {
52
+ --bg:color-mix(in srgb, var(--fg) 12%, transparent);
53
+ --bd:color-mix(in srgb, var(--fg) 45%, transparent);
54
+ }
55
+
56
+ /* ---- Colored block / callout ---- */
57
+ .cm-block {
58
+ border:1px solid var(--bd, var(--cm-neutral-bd)); border-left-width:4px;
59
+ background:var(--bg, var(--cm-neutral-bg));
60
+ border-radius:8px; padding:10px 14px; margin:12px 0;
61
+ }
62
+ .cm-block > .cm-title { font-weight:700; color:var(--fg, inherit); }
63
+ .cm-block > .cm-body > :first-child { margin-top:0; }
64
+ .cm-block > .cm-body > :last-child { margin-bottom:0; }
65
+
66
+ /* ---- Pill / badge ---- */
67
+ .cm-pill {
68
+ display:inline-block; color:var(--fg); background:var(--bg); border:1px solid var(--bd);
69
+ border-radius:999px; padding:0 .55em; font-size:.82em; font-weight:600; line-height:1.7;
70
+ white-space:nowrap;
71
+ }
72
+
73
+ /* ---- Inline colored text (tint, no fill) ---- */
74
+ .cm-text { color:var(--fg); font-weight:600; }
75
+
76
+ /* ---- Progress meter ---- */
77
+ .cm-meter { display:inline-flex; align-items:center; gap:8px; vertical-align:middle; }
78
+ .cm-meter .cm-track {
79
+ width:120px; height:8px; border-radius:999px; background:var(--cm-neutral-bg);
80
+ border:1px solid var(--cm-neutral-bd); overflow:hidden;
81
+ }
82
+ .cm-meter .cm-fill { display:block; height:100%; background:var(--fg, var(--cm-info-fg)); }
83
+ .cm-meter .cm-val { font-size:.82em; font-weight:600; color:var(--fg); }
84
+
85
+ /* ---- Fields (key/value) ---- */
86
+ .cm-fields {
87
+ display:grid; grid-template-columns:auto 1fr; gap:4px 16px; margin:12px 0;
88
+ border:1px solid var(--cm-neutral-bd); border-radius:8px; padding:12px 14px;
89
+ background:var(--cm-neutral-bg);
90
+ }
91
+ .cm-fields dt { font-weight:600; opacity:.75; }
92
+ .cm-fields dd { margin:0; }
93
+
94
+ /* ---- Collapsible ---- */
95
+ .cm-details { border:1px solid var(--bd); border-radius:8px; margin:12px 0; overflow:hidden; }
96
+ .cm-details > summary {
97
+ cursor:pointer; padding:8px 14px; font-weight:600; list-style:none; background:var(--cm-neutral-bg);
98
+ }
99
+ .cm-details[data-tone] > summary, .cm-details.cm-custom > summary { color:var(--fg); background:var(--bg); }
100
+ .cm-details > summary::-webkit-details-marker { display:none; }
101
+ .cm-details > summary::before {
102
+ content:"\25B8"; display:inline-block; margin-right:8px; transition:transform .15s ease;
103
+ }
104
+ .cm-details[open] > summary::before { transform:rotate(90deg); }
105
+ .cm-details > .cm-body { padding:10px 14px; }
106
+ .cm-details > .cm-body > :first-child { margin-top:0; }
107
+ .cm-details > .cm-body > :last-child { margin-bottom:0; }
108
+
109
+ /* ---- Inline diff (CriticMarkup) ---- */
110
+ .crit-add { background:var(--cm-success-bg); color:var(--cm-success-fg); text-decoration:none; border-radius:3px; padding:0 2px; }
111
+ .crit-del { background:var(--cm-danger-bg); color:var(--cm-danger-fg); text-decoration:line-through; border-radius:3px; padding:0 2px; }
112
+ .crit-mark { background:var(--cm-warning-bg); border-radius:3px; padding:0 2px; }
113
+ .crit-comment { color:var(--cm-muted-fg); font-style:italic; }