thibaud frere commited on
Commit
2da6ea7
·
1 Parent(s): 8d0d788

update charts and colors

Browse files
README.md CHANGED
@@ -9,3 +9,42 @@ header: mini
9
  app_port: 8080
10
  thumbnail: https://huggingface.co/spaces/tfrere/research-paper-template/thumb.jpg
11
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  app_port: 8080
10
  thumbnail: https://huggingface.co/spaces/tfrere/research-paper-template/thumb.jpg
11
  ---
12
+
13
+ ## Find recent duplicated Spaces
14
+
15
+ This repository includes a small utility to list public Spaces created in the last N days that were duplicated from a given source Space.
16
+
17
+ Prerequisites:
18
+
19
+ ```bash
20
+ pip install huggingface_hub requests
21
+ ```
22
+
23
+ Usage:
24
+
25
+ ```bash
26
+ python app/scripts/find_duplicated_spaces.py --source owner/space-name --days 14
27
+ ```
28
+
29
+ Options:
30
+
31
+ - `--source`: required. The source Space in the form `owner/space-name`.
32
+ - `--days`: optional. Time window in days (default: 14).
33
+ - `--token`: optional. Your HF token. Defaults to `HF_TOKEN` env var if set.
34
+ - `--no-deep`: optional. Disable README/frontmatter fallback detection.
35
+
36
+ Examples:
37
+
38
+ ```bash
39
+ # Using env var for the token (optional)
40
+ export HF_TOKEN=hf_xxx
41
+
42
+ # Find Spaces duplicated from tfrere/my-space in the last 14 days
43
+ python app/scripts/find_duplicated_spaces.py --source tfrere/my-space
44
+
45
+ # Use a 7-day window and explicit token
46
+ python app/scripts/find_duplicated_spaces.py --source tfrere/my-space --days 7 --token $HF_TOKEN
47
+ ```
48
+
49
+ The script first checks card metadata (e.g., `duplicated_from`) and optionally falls back to parsing the README frontmatter for robustness.
50
+
app/scripts/find_duplicated_spaces.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Find Spaces created in the last N days that were duplicated from a given source Space.
4
+
5
+ This script uses the public Hugging Face Hub APIs via `huggingface_hub` and optionally
6
+ falls back to reading README frontmatter for robustness.
7
+
8
+ Usage:
9
+ python app/scripts/find_duplicated_spaces.py --source owner/space-name [--days 14] [--token <hf_token>] [--no-deep]
10
+
11
+ Notes:
12
+ - Comments are in English as requested.
13
+ - Chat responses remain in French.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import os
20
+ from datetime import datetime, timedelta, timezone
21
+ from typing import Iterable, List, Optional
22
+
23
+ import requests
24
+ from huggingface_hub import HfApi
25
+
26
+
27
+ def parse_args() -> argparse.Namespace:
28
+ parser = argparse.ArgumentParser(
29
+ description="List recent Spaces duplicated from a given Space"
30
+ )
31
+ parser.add_argument(
32
+ "--source",
33
+ required=True,
34
+ help="Source Space in the form 'owner/space-name'",
35
+ )
36
+ parser.add_argument(
37
+ "--days",
38
+ type=int,
39
+ default=14,
40
+ help="Time window in days (default: 14)",
41
+ )
42
+ parser.add_argument(
43
+ "--token",
44
+ default=os.environ.get("HF_TOKEN"),
45
+ help="Hugging Face token (optional). Defaults to HF_TOKEN env var if set.",
46
+ )
47
+ parser.add_argument(
48
+ "--no-deep",
49
+ action="store_true",
50
+ help=(
51
+ "Disable deep detection (README/frontmatter fetch) when card metadata is missing."
52
+ ),
53
+ )
54
+ return parser.parse_args()
55
+
56
+
57
+ def iso_to_datetime(value: str) -> datetime:
58
+ """Parse ISO 8601 timestamps returned by the Hub to aware datetime in UTC."""
59
+ # Examples: "2024-09-01T12:34:56.789Z" or without microseconds
60
+ try:
61
+ dt = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")
62
+ except ValueError:
63
+ dt = datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
64
+ return dt.replace(tzinfo=timezone.utc)
65
+
66
+
67
+ def readme_frontmatter_duplicated_from(space_id: str) -> Optional[str]:
68
+ """Fetch README raw and try to extract duplicated_from from YAML frontmatter."""
69
+ # Raw README for Spaces is accessible at /spaces/{id}/raw/README.md
70
+ url = f"https://huggingface.co/spaces/{space_id}/raw/README.md"
71
+ try:
72
+ resp = requests.get(url, timeout=10)
73
+ if resp.status_code != 200:
74
+ return None
75
+ text = resp.text
76
+ except requests.RequestException:
77
+ return None
78
+
79
+ # Very light-weight frontmatter scan to find a line like: duplicated_from: owner/space
80
+ # Do not parse full YAML to avoid extra deps.
81
+ lines = text.splitlines()
82
+ in_frontmatter = False
83
+ for line in lines:
84
+ if line.strip() == "---":
85
+ in_frontmatter = not in_frontmatter
86
+ # Stop if we closed the frontmatter without finding the key.
87
+ if not in_frontmatter:
88
+ break
89
+ continue
90
+ if in_frontmatter and line.strip().startswith("duplicated_from:"):
91
+ # Extract value after colon, trim quotes/spaces
92
+ value = line.split(":", 1)[1].strip().strip("'\"")
93
+ return value or None
94
+ return None
95
+
96
+
97
+ def get_recent_spaces(api: HfApi, days: int) -> Iterable:
98
+ """Yield Spaces created within the last `days` days, iterating newest first.
99
+
100
+ Tries to sort by creation date descending; falls back gracefully if not supported.
101
+ """
102
+ cutoff = datetime.now(timezone.utc) - timedelta(days=days)
103
+
104
+ # Primary attempt: request spaces sorted by creation date (newest first)
105
+ try:
106
+ spaces_iter = api.list_spaces(full=True, sort="created", direction=-1)
107
+ except TypeError:
108
+ # Fallback: no sort support in current huggingface_hub; get a reasonably large list
109
+ # Note: This may include items older than the cutoff; we'll filter below.
110
+ spaces_iter = api.list_spaces(full=True)
111
+
112
+ for space in spaces_iter:
113
+ created_at_raw = getattr(space, "created_at", None) or getattr(space, "createdAt", None)
114
+ if not created_at_raw:
115
+ # If missing, include conservatively
116
+ yield space
117
+ continue
118
+ created_at = (
119
+ created_at_raw if isinstance(created_at_raw, datetime) else iso_to_datetime(str(created_at_raw))
120
+ )
121
+ if created_at >= cutoff:
122
+ yield space
123
+ else:
124
+ # If we know the iteration is sorted by creation desc, we can break early
125
+ # Only do that when we explicitly asked for sort="created"
126
+ if "spaces_iter" in locals():
127
+ try:
128
+ # If we reached here under the sorted branch, short-circuit
129
+ # by checking if the generator came from the sorted call
130
+ _ = api # keep linter calm
131
+ except Exception:
132
+ pass
133
+ # We can't be certain the iterator is sorted in fallback; just continue
134
+ # without breaking to avoid missing any items.
135
+ continue
136
+
137
+
138
+ def find_duplicated_spaces(
139
+ api: HfApi, source: str, days: int, deep_detection: bool
140
+ ) -> List[str]:
141
+ """Return list of Space IDs that were duplicated from `source` within `days`."""
142
+ source = source.strip().strip("/ ")
143
+ results: List[str] = []
144
+ for space in get_recent_spaces(api, days=days):
145
+ space_id = getattr(space, "id", None) or getattr(space, "repo_id", None)
146
+ if not space_id:
147
+ continue
148
+
149
+ # Check card metadata first
150
+ card = getattr(space, "cardData", None) or getattr(space, "card_data", None)
151
+ duplicated_from_value: Optional[str] = None
152
+ if isinstance(card, dict):
153
+ for key in ("duplicated_from", "duplicatedFrom", "duplicated-from"):
154
+ if key in card and isinstance(card[key], str):
155
+ duplicated_from_value = card[key].strip().strip("/ ")
156
+ break
157
+
158
+ # Optional deep detection via README frontmatter
159
+ if not duplicated_from_value and deep_detection:
160
+ duplicated_from_value = readme_frontmatter_duplicated_from(space_id)
161
+ if duplicated_from_value:
162
+ duplicated_from_value = duplicated_from_value.strip().strip("/ ")
163
+
164
+ if duplicated_from_value and duplicated_from_value.lower() == source.lower():
165
+ results.append(space_id)
166
+
167
+ return results
168
+
169
+
170
+ def main() -> None:
171
+ args = parse_args()
172
+ api = HfApi(token=args.token)
173
+
174
+ duplicated = find_duplicated_spaces(
175
+ api=api,
176
+ source=args.source,
177
+ days=args.days,
178
+ deep_detection=not args.no_deep,
179
+ )
180
+
181
+ if duplicated:
182
+ print(
183
+ f"Found {len(duplicated)} Space(s) duplicated from {args.source} in the last {args.days} days:\n"
184
+ )
185
+ for sid in duplicated:
186
+ print(f"https://huggingface.co/spaces/{sid}")
187
+ else:
188
+ print(
189
+ f"No public Spaces duplicated from {args.source} in the last {args.days} days."
190
+ )
191
+
192
+
193
+ if __name__ == "__main__":
194
+ main()
195
+
196
+
app/src/components/ColorPicker.astro ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ ---
3
+ <div class="color-picker" style="width:100%; margin: 10px 0;">
4
+ <style>
5
+ .color-picker .picker__stack { display:flex; flex-direction:column; gap:12px; }
6
+ .color-picker .current-card { display:grid; grid-template-columns: 30% 70%; align-items: center; gap:14px; padding:14px 32px 14px 16px; border:1px solid var(--border-color); background: var(--surface-bg); border-radius: 12px; }
7
+ .color-picker .current-left { display:flex; flex-direction: column; gap:8px; min-width: 0; }
8
+ .color-picker .current-right { display:flex; flex-direction: column; gap:8px; padding-left: 14px; border-left: 1px solid var(--border-color); }
9
+ .color-picker .current-main { display:flex; align-items:center; gap:12px; min-width: 0; }
10
+ .color-picker .current-swatch { width: 64px; height: 64px; border-radius: 8px; border: 1px solid var(--border-color); }
11
+ .color-picker .current-text { display:flex; flex-direction: column; line-height: 1.2; min-width: 0; }
12
+ .color-picker .current-name { font-size: 14px; font-weight: 800; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: clamp(140px, 28vw, 260px); }
13
+ .color-picker .current-hex, .color-picker .current-extra { font-size: 11px; color: var(--muted-color); letter-spacing: .02em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: clamp(140px, 28vw, 260px); }
14
+ .color-picker .picker__label { font-weight:700; font-size: 12px; color: var(--muted-color); text-transform: uppercase; letter-spacing: .02em; }
15
+ .color-picker .hue-slider { position:relative; height:16px; border-radius:10px; border:1px solid var(--border-color); background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%); cursor: ew-resize; touch-action: none; flex: 1 1 auto; min-width: 200px; }
16
+ .color-picker .hue-knob { position:absolute; top:50%; left:93.6%; width:14px; height:14px; border-radius:50%; border:2px solid #fff; transform:translate(-50%, -50%); background: var(--surface-bg); z-index: 2; box-shadow: 0 0 0 1px rgba(0,0,0,.05); }
17
+ .color-picker .hue-slider:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; }
18
+ .color-picker .hue-value { font-variant-numeric: tabular-nums; color: var(--muted-color); font-size: 12px; }
19
+ @media (max-width: 720px) { .color-picker .current-card { grid-template-columns: 1fr; } .color-picker .current-right { padding-left: 0; border-left: none; } }
20
+ </style>
21
+ <div class="picker__stack">
22
+ <div class="current-card">
23
+ <div class="current-left">
24
+ <div class="current-main">
25
+ <div class="current-swatch" aria-label="Current color" title="Current color"></div>
26
+ <div class="current-text">
27
+ <div class="current-name">—</div>
28
+ <div class="current-hex">—</div>
29
+ <div class="current-extra current-lch">—</div>
30
+ <div class="current-extra current-rgb">—</div>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ <div class="current-right">
35
+ <div class="picker__label">Hue</div>
36
+ <div class="hue-slider" role="slider" aria-label="Hue" aria-valuemin="0" aria-valuemax="360" aria-valuenow="337" tabindex="0">
37
+ <div class="hue-knob"></div>
38
+ </div>
39
+ <div class="hue-value">337°</div>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ <script>
45
+ (() => {
46
+ const COLOR_NAMES = [{"name":"Candy Apple Red","hex":"#ff0800"},{"name":"Boiling Magma","hex":"#ff3300"},{"name":"Aerospace Orange","hex":"#ff4f00"},{"name":"Burtuqali Orange","hex":"#ff6700"},{"name":"American Orange","hex":"#ff8b00"},{"name":"Cheese","hex":"#ffa600"},{"name":"Amber","hex":"#ffbf00"},{"name":"Demonic Yellow","hex":"#ffe700"},{"name":"Bat-Signal","hex":"#feff00"},{"name":"Bitter Lime","hex":"#cfff00"},{"name":"Electric Lime","hex":"#ccff00"},{"name":"Bright Yellow Green","hex":"#9dff00"},{"name":"Lasting Lime","hex":"#88ff00"},{"name":"Bright Green","hex":"#66ff00"},{"name":"Chlorophyll Green","hex":"#4aff00"},{"name":"Green Screen","hex":"#22ff00"},{"name":"Electric Pickle","hex":"#00ff04"},{"name":"Acid","hex":"#00ff22"},{"name":"Lucent Lime","hex":"#00ff33"},{"name":"Cathode Green","hex":"#00ff55"},{"name":"Booger Buster","hex":"#00ff77"},{"name":"Green Gas","hex":"#00ff99"},{"name":"Enthusiasm","hex":"#00ffaa"},{"name":"Ice Ice Baby","hex":"#00ffdd"},{"name":"Master Sword Blue","hex":"#00ffee"},{"name":"Agressive Aqua","hex":"#00fbff"},{"name":"Vivid Sky Blue","hex":"#00ccff"},{"name":"Capri","hex":"#00bfff"},{"name":"Sky of Magritte","hex":"#0099ff"},{"name":"Azure","hex":"#007fff"},{"name":"Blue Ribbon","hex":"#0066ff"},{"name":"Blinking Blue","hex":"#0033ff"},{"name":"Icelandic Water","hex":"#0011ff"},{"name":"Blue","hex":"#0000ff"},{"name":"Blue Pencil","hex":"#2200ff"},{"name":"Electric Ultramarine","hex":"#3f00ff"},{"name":"Aladdin's Feather","hex":"#5500ff"},{"name":"Purple Climax","hex":"#8800ff"},{"name":"Amethyst Ganzstar","hex":"#8f00ff"},{"name":"Electric Purple","hex":"#bf00ff"},{"name":"Phlox","hex":"#df00ff"},{"name":"Brusque Pink","hex":"#ee00ff"},{"name":"Bright Magenta","hex":"#ff08e8"},{"name":"Big bang Pink","hex":"#ff00bb"},{"name":"Mean Girls Lipstick","hex":"#ff00ae"},{"name":"Pink","hex":"#ff0099"},{"name":"Hot Flamingoes","hex":"#ff005d"},{"name":"Blazing Dragonfruit","hex":"#ff0054"},{"name":"Carmine Red","hex":"#ff0038"},{"name":"Bright Red","hex":"#ff000d"}];
47
+ if (!window.__colorNames) window.__colorNames = COLOR_NAMES;
48
+
49
+ if (!window.__colorPickerBus) {
50
+ window.__colorPickerBus = (() => {
51
+ let hue = 337; let adjusting=false; const listeners = new Set();
52
+ return { get: () => ({ hue, adjusting }), publish: (sourceId, nextHue, isAdj) => { hue=((nextHue%360)+360)%360; adjusting=!!isAdj; listeners.forEach(fn => { try { fn({ sourceId, hue, adjusting }); } catch{} }); }, subscribe: (fn) => { listeners.add(fn); return () => listeners.delete(fn); } };
53
+ })();
54
+ }
55
+
56
+ const bootstrap = () => {
57
+ const root = document.querySelector('.color-picker'); if (!root || root.dataset.mounted) return; root.dataset.mounted='true';
58
+ const slider = root.querySelector('.hue-slider'); const knob = root.querySelector('.hue-knob'); const hueValue = root.querySelector('.hue-value'); const currentSwatch = root.querySelector('.current-swatch'); const currentName = root.querySelector('.current-name'); const currentHex = root.querySelector('.current-hex'); const currentLch = root.querySelector('.current-lch'); const currentRgb = root.querySelector('.current-rgb');
59
+ const bus = window.__colorPickerBus; const instanceId = Math.random().toString(36).slice(2);
60
+ const getKnobRadius = () => { try { const w = knob ? knob.getBoundingClientRect().width : 0; return w ? w/2 : 8; } catch { return 8; } };
61
+ const hexToHsl = (H) => {
62
+ const s = H.replace('#','');
63
+ const v = s.length===3 ? s.split('').map(ch=>ch+ch).join('') : s;
64
+ const bigint = parseInt(v, 16);
65
+ let r = (bigint >> 16) & 255, g = (bigint >> 8) & 255, b = bigint & 255;
66
+ r /= 255; g /= 255; b /= 255;
67
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
68
+ let h = 0, s2 = 0, l = (max + min) / 2;
69
+ if (max !== min) {
70
+ const d = max - min;
71
+ s2 = l > 0.5 ? d / (2 - max - min) : d / (max + min);
72
+ switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; default: h = (r - g) / d + 4; }
73
+ h /= 6;
74
+ }
75
+ return { h: Math.round(h*360), s: Math.round(s2*100), l: Math.round(l*100) };
76
+ };
77
+ const hslToHex = (h, s, l) => {
78
+ s /= 100; l /= 100;
79
+ const k = n => (n + h/30) % 12;
80
+ const a = s * Math.min(l, 1 - l);
81
+ const f = n => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
82
+ const toHex = x => Math.round(255 * x).toString(16).padStart(2, '0');
83
+ return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`.toUpperCase();
84
+ };
85
+ const getName = (hex) => { const h = hexToHsl(hex).h || 0; const labels=['Red','Orange','Yellow','Lime','Green','Cyan','Blue','Indigo','Violet','Magenta']; const idx=Math.round(((h%360)/360)*(labels.length-1)); return labels[idx]; };
86
+ const updateUI = (h, adjusting) => { const rect = slider.getBoundingClientRect(); const r=Math.min(getKnobRadius(), Math.max(0, rect.width/2 - 1)); const t=Math.max(0, Math.min(1, (h/360))); const leftPx = r + t * Math.max(0, (rect.width - 2*r)); if (knob) knob.style.left = (leftPx/rect.width*100) + '%'; if (hueValue) hueValue.textContent=`${Math.round(h)}°`; if (slider) slider.setAttribute('aria-valuenow', String(Math.round(h))); const L=62, S=72; const baseHex=hslToHex(h,S,L); if (currentSwatch) currentSwatch.style.background=baseHex; if (currentName) currentName.textContent=getName(baseHex); if (currentHex) currentHex.textContent=baseHex; if (currentLch) currentLch.textContent = `HSL ${L}, ${S}, ${Math.round(h)}°`; if (currentRgb){ const hex=baseHex.replace('#',''); const R=parseInt(hex.slice(0,2),16), G=parseInt(hex.slice(2,4),16), B=parseInt(hex.slice(4,6),16); currentRgb.textContent=`RGB ${R}, ${G}, ${B}`; } const hoverHex=hslToHex(h, Math.max(0,S-10), Math.max(0, L-8)); const rootEl=document.documentElement; rootEl.style.setProperty('--primary-color', baseHex); rootEl.style.setProperty('--primary-color-hover', hoverHex); };
87
+ const getHueFromEvent = (ev) => { const rect=slider.getBoundingClientRect(); const clientX=ev.touches ? ev.touches[0].clientX : ev.clientX; const x = clientX - rect.left; const r=Math.min(getKnobRadius(), Math.max(0, rect.width/2 - 1)); const effX=Math.max(r, Math.min(rect.width - r, x)); const denom=Math.max(1, rect.width - 2*r); const t=(effX - r) / denom; return t*360; };
88
+ const unsubscribe = bus.subscribe(({ sourceId, hue, adjusting }) => { if (sourceId === instanceId) return; updateUI(hue, adjusting); });
89
+ try { let initH=337; if (window.ColorPalettes && typeof window.ColorPalettes.getPrimary==='function'){ const hex=window.ColorPalettes.getPrimary(); initH = hexToHsl(hex).h || initH; } else { const cssPrimary=getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim(); if (cssPrimary) { initH = hexToHsl(cssPrimary).h || initH; } } const { hue: sharedHue } = bus.get(); updateUI(initH ?? sharedHue, false); bus.publish(instanceId, initH ?? sharedHue, false); } catch { const { hue: sharedHue } = bus.get(); updateUI(sharedHue, false); }
90
+ const onDown = (ev) => { ev.preventDefault(); const h=getHueFromEvent(ev); updateUI(h, true); bus.publish(instanceId, h, true); const move=(e)=>{ e.preventDefault && e.preventDefault(); const hh=getHueFromEvent(e); updateUI(hh, true); bus.publish(instanceId, hh, true); }; const up=()=>{ bus.publish(instanceId, getHueFromEvent(ev), false); window.removeEventListener('mousemove', move); window.removeEventListener('touchmove', move); window.removeEventListener('mouseup', up); window.removeEventListener('touchend', up); }; window.addEventListener('mousemove', move, { passive:false }); window.addEventListener('touchmove', move, { passive:false }); window.addEventListener('mouseup', up, { once:true }); window.addEventListener('touchend', up, { once:true }); };
91
+ if (slider){ slider.addEventListener('mousedown', onDown); slider.addEventListener('touchstart', onDown, { passive:false }); slider.addEventListener('keydown', (e)=>{ const step=e.shiftKey?10:2; if (e.key==='ArrowLeft'){ e.preventDefault(); const { hue } = bus.get(); const h=hue-step; updateUI(h, true); bus.publish(instanceId, h, true); bus.publish(instanceId, h, false); } if (e.key==='ArrowRight'){ e.preventDefault(); const { hue } = bus.get(); const h=hue+step; updateUI(h, true); bus.publish(instanceId, h, true); bus.publish(instanceId, h, false); } }); }
92
+ const ro=new MutationObserver(()=>{ if (!document.body.contains(root)){ unsubscribe && unsubscribe(); ro.disconnect(); } }); ro.observe(document.body, { childList:true, subtree:true });
93
+ };
94
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootstrap, { once:true }); else bootstrap();
95
+ })();
96
+ </script>
97
+
98
+
app/src/components/HtmlEmbed.astro CHANGED
@@ -33,11 +33,7 @@ const configAttr = typeof config === 'string' ? config : (config != null ? JSON.
33
  <div><!-- Fragment not found: {src} --></div>
34
  ) }
35
 
36
- <script type="module" is:inline>
37
- // Ensure global color palettes generator is loaded once per page
38
- import '../scripts/color-palettes.js';
39
- export {};
40
- </script>
41
 
42
  <script>
43
  // Re-execute <script> tags inside the injected fragment (innerHTML doesn't run scripts)
 
33
  <div><!-- Fragment not found: {src} --></div>
34
  ) }
35
 
36
+
 
 
 
 
37
 
38
  <script>
39
  // Re-execute <script> tags inside the injected fragment (innerHTML doesn't run scripts)
app/src/components/Palettes.astro ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ const rootId = `palettes-${Math.random().toString(36).slice(2)}`;
3
+ ---
4
+ <div class="palettes" id={rootId} style="width:100%; margin: 10px 0;">
5
+ <style is:global>
6
+ .palettes { box-sizing: border-box; }
7
+ .palettes .palettes__grid { display: grid; grid-template-columns: 1fr; gap: 12px; max-width: 100%; }
8
+ .palettes .palette-card { position: relative; display: grid; grid-template-columns: 1fr minmax(0, 220px); align-items: stretch; gap: 12px; border: 1px solid var(--border-color); border-radius: 10px; background: var(--surface-bg); padding: 12px; transition: box-shadow .18s ease, transform .18s ease, border-color .18s ease; min-height: 60px; }
9
+ .palettes .palette-card__preview { width: 48px; height: 48px; border-radius: 999px; flex: 0 0 auto; background-size: cover; background-position: center; }
10
+ .palettes .palette-card__copy { position: absolute; top: 50%; left: 100%; transform: translateY(-50%); z-index: 3; border-left: none; border-top-left-radius: 0; border-bottom-left-radius: 0; }
11
+ .palettes .palette-card__copy svg { width: 18px; height: 18px; fill: currentColor; display: block; color: inherit; }
12
+ .palettes .palette-card__swatches { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); grid-auto-rows: 1fr; gap: 2px; margin: 0; min-height: 60px; }
13
+ .palettes .palette-card__swatches .sw { width: 100%; min-width: 0; height: auto; border-radius: 0; border: 1px solid var(--border-color); }
14
+ .palettes .palette-card__swatches .sw:first-child { border-top-left-radius: 8px; border-bottom-left-radius: 8px; }
15
+ .palettes .palette-card__swatches .sw:last-child { border-top-right-radius: 8px; border-bottom-right-radius: 8px; }
16
+ .palettes .palette-card__content { display: flex; flex-direction: row; align-items: center; justify-content: flex-start; gap: 12px; min-width: 0; padding-right: 12px; }
17
+ .palettes .palette-card__preview { width: 48px; height: 48px; border-radius: 999px; position: relative; flex: 0 0 auto; overflow: hidden; }
18
+ .palettes .palette-card__preview .dot { position: absolute; width: 4px; height: 4px; background: #fff; border-radius: 999px; box-shadow: 0 0 0 1px var(--border-color); }
19
+ .palettes .palette-card__preview .donut-hole { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 24px; height: 24px; border-radius: 999px; background: var(--surface-bg); box-shadow: 0 0 0 1px var(--border-color) inset; }
20
+
21
+ .palettes .palette-card__content__info { display: flex; flex-direction: column; }
22
+ .palettes .palette-card__title { text-align: left; font-weight: 800; font-size: 15px; }
23
+ .palettes .palette-card__desc { text-align: left; color: var(--muted-color); line-height: 1.5; font-size: 12px; }
24
+
25
+ .palettes .palettes__select { width: 100%; max-width: 100%; border: 1px solid var(--border-color); background: var(--surface-bg); color: var(--text-color); padding: 8px 10px; border-radius: 8px; }
26
+ .palettes .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 1px, 1px); white-space: nowrap; border: 0; }
27
+ .palettes .palettes__controls { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; margin: 8px 0 14px; }
28
+ .palettes .palettes__field { display: flex; flex-direction: column; gap: 6px; min-width: 0; flex: 1 1 280px; max-width: 100%; }
29
+ .palettes .palettes__label { font-size: 12px; color: var(--muted-color); font-weight: 800; }
30
+ .palettes .palettes__label-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
31
+ .palettes .ghost-badge { font-size: 11px; padding: 1px 6px; border-radius: 999px; border: 1px solid var(--border-color); color: var(--muted-color); background: transparent; font-variant-numeric: tabular-nums; }
32
+ .palettes .palettes__count { display: flex; align-items: center; gap: 8px; max-width: 100%; }
33
+ .palettes .palettes__count input[type="range"] { width: 100%; }
34
+ .palettes .palettes__count output { min-width: 28px; text-align: center; font-variant-numeric: tabular-nums; font-size: 12px; color: var(--muted-color); }
35
+ .palettes input[type="range"] { -webkit-appearance: none; appearance: none; height: 24px; background: transparent; cursor: pointer; accent-color: var(--primary-color); }
36
+ .palettes input[type="range"]:focus { outline: none; }
37
+ .palettes input[type="range"]::-webkit-slider-runnable-track { height: 6px; background: var(--border-color); border-radius: 999px; }
38
+ .palettes input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; margin-top: -6px; width: 18px; height: 18px; background: var(--primary-color); border: 2px solid var(--surface-bg); border-radius: 50%; }
39
+ .palettes input[type="range"]::-moz-range-track { height: 6px; background: var(--border-color); border: none; border-radius: 999px; }
40
+ .palettes input[type="range"]::-moz-range-progress { height: 6px; background: var(--primary-color); border-radius: 999px; }
41
+ .palettes input[type="range"]::-moz-range-thumb { width: 18px; height: 18px; background: var(--primary-color); border: 2px solid var(--surface-bg); border-radius: 50%; }
42
+ html.cb-grayscale, body.cb-grayscale { filter: grayscale(1) !important; }
43
+ html.cb-protanopia, body.cb-protanopia { filter: url(#cb-protanopia) !important; }
44
+ html.cb-deuteranopia, body.cb-deuteranopia { filter: url(#cb-deuteranopia) !important; }
45
+ html.cb-tritanopia, body.cb-tritanopia { filter: url(#cb-tritanopia) !important; }
46
+ html.cb-achromatopsia, body.cb-achromatopsia { filter: url(#cb-achromatopsia) !important; }
47
+ @media (max-width: 1100px) { .palettes .palette-card { grid-template-columns: 1fr; align-items: stretch; gap: 10px; } .palettes .palette-card__swatches { grid-template-columns: repeat(6, minmax(0, 1fr)); } .palettes .palette-card__content { border-right: none; padding-right: 0; } .palettes .palette-card__copy { display: none; } }
48
+ </style>
49
+ <div class="palettes__controls">
50
+ <div class="palettes__field">
51
+ <label class="palettes__label" for="cb-select">Color vision simulation</label>
52
+ <select id="cb-select" class="palettes__select">
53
+ <option value="none">Normal color vision — typical for most people</option>
54
+ <option value="achromatopsia">Achromatopsia — no color at all</option>
55
+ <option value="protanopia">Protanopia — reduced/absent reds</option>
56
+ <option value="deuteranopia">Deuteranopia — reduced/absent greens</option>
57
+ <option value="tritanopia">Tritanopia — reduced/absent blues</option>
58
+ </select>
59
+ </div>
60
+ <div class="palettes__field">
61
+ <div class="palettes__label-row">
62
+ <label class="palettes__label" for="color-count">Number of colors</label>
63
+ <output id="color-count-out" for="color-count" class="ghost-badge">8</output>
64
+ </div>
65
+ <div class="palettes__count">
66
+ <input id="color-count" type="range" min="6" max="10" step="1" value="8" aria-label="Number of colors" />
67
+ </div>
68
+ </div>
69
+ </div>
70
+ <div class="palettes__grid"></div>
71
+ <div class="palettes__simu" role="group" aria-labelledby="cb-sim-title">
72
+ <svg aria-hidden="true" focusable="false" width="0" height="0" style="position:absolute; left:-9999px; overflow:hidden;">
73
+ <defs>
74
+ <filter id="cb-protanopia"><feColorMatrix type="matrix" values="0.567 0.433 0 0 0 0.558 0.442 0 0 0 0 0.242 0.758 0 0 0 0 0 1 0"/></filter>
75
+ <filter id="cb-deuteranopia"><feColorMatrix type="matrix" values="0.625 0.375 0 0 0 0.7 0.3 0 0 0 0 0.3 0.7 0 0 0 0 0 1 0"/></filter>
76
+ <filter id="cb-tritanopia"><feColorMatrix type="matrix" values="0.95 0.05 0 0 0 0 0.433 0.567 0 0 0 0.475 0.525 0 0 0 0 0 1 0"/></filter>
77
+ <filter id="cb-achromatopsia"><feColorMatrix type="matrix" values="0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0 0 0 1 0"/></filter>
78
+ </defs>
79
+ </svg>
80
+ </div>
81
+ </div>
82
+ <script type="module" is:inline>
83
+ import '/src/scripts/color-palettes.js';
84
+ const ROOT_ID = "{rootId}";
85
+ (() => {
86
+ const cards = [
87
+ { key: 'categorical', title: 'Categorical', desc: 'For <strong>non‑numeric categories</strong>; <strong>visually distinct</strong> colors.' },
88
+ { key: 'sequential', title: 'Sequential', desc: 'For <strong>numeric scales</strong>; gradient from <strong>dark to light</strong>. Ideal for <strong>heatmaps</strong>.' },
89
+ { key: 'diverging', title: 'Diverging', desc: 'For numeric scales with negative and positive; Opposing extremes with smooth contrast around a neutral midpoint.' }
90
+ ];
91
+ const getPaletteColors = (key, count) => {
92
+ const total=Number(count)||6;
93
+ if (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function') {
94
+ return window.ColorPalettes.getColors(key,total) || [];
95
+ }
96
+ return [];
97
+ };
98
+ const render = () => {
99
+ const root = document.getElementById(ROOT_ID) || document.querySelector('.palettes');
100
+ if (!root) return;
101
+ const grid=root.querySelector('.palettes__grid'); if (!grid) return;
102
+ const input=document.getElementById('color-count'); const total=input ? Number(input.value)||6 : 6;
103
+ const html = cards.map(c => {
104
+ const colors=getPaletteColors(c.key,total);
105
+ const swatches=colors.map(col=>`<div class=\"sw\" style=\"background:${col}\"></div>`).join('');
106
+ const baseHex = (window.ColorPalettes && typeof window.ColorPalettes.getPrimary==='function') ? window.ColorPalettes.getPrimary() : (colors[0] || '#FF0000');
107
+ const hueDeg = (()=>{ try { const s=baseHex.replace('#',''); const v=s.length===3?s.split('').map(ch=>ch+ch).join(''):s; const r=parseInt(v.slice(0,2),16)/255, g=parseInt(v.slice(2,4),16)/255, b=parseInt(v.slice(4,6),16)/255; const M=Math.max(r,g,b), m=Math.min(r,g,b), d=M-m; if (d===0) return 0; let h=0; if (M===r) h=((g-b)/d)%6; else if (M===g) h=(b-r)/d+2; else h=(r-g)/d+4; h*=60; if (h<0) h+=360; return h; } catch { return 0; } })();
108
+ const gradient = c.key==='categorical'
109
+ ? (() => {
110
+ const steps = 60; // smooth hue wheel (fixed orientation)
111
+ const wheel = Array.from({ length: steps }, (_, i) => `hsl(${Math.round((i/steps)*360)}, 100%, 50%)`).join(', ');
112
+ return `conic-gradient(${wheel})`;
113
+ })()
114
+ : (colors.length ? `linear-gradient(90deg, ${colors.join(', ')})` : `linear-gradient(90deg, var(--border-color), var(--border-color))`);
115
+ const previewInner = (()=>{
116
+ if (c.key !== 'categorical' || !colors.length) return '';
117
+ const ring = 18; const cx = 24; const cy = 24; const offset = (hueDeg/360) * 2 * Math.PI;
118
+ return colors.map((col,i)=>{
119
+ const angle = offset + (i/colors.length) * 2 * Math.PI;
120
+ const x = cx + ring * Math.cos(angle);
121
+ const y = cy + ring * Math.sin(angle);
122
+ return `<span class=\"dot\" style=\"left:${x-2}px; top:${y-2}px\"></span>`;
123
+ }).join('');
124
+ })();
125
+ const donutHole = (c.key === 'categorical') ? '<span class=\"donut-hole\"></span>' : '';
126
+ return `
127
+ <div class="palette-card" data-colors="${colors.join(',')}">
128
+ <div class="palette-card__content">
129
+ <div class=\"palette-card__preview\" aria-hidden=\"true\" style=\"background:${gradient}\">${previewInner}${donutHole}</div>
130
+ <div class="palette-card__content__info">
131
+ <div class="palette-card__title">${c.title}</div>
132
+ <div class="palette-card__desc">${c.desc}</div>
133
+ </div>
134
+ </div>
135
+ <div class="palette-card__swatches" style="grid-template-columns: repeat(${colors.length}, minmax(0, 1fr));">${swatches}</div>
136
+ <button class="palette-card__copy button--ghost" type="button" aria-label="Copy palette">
137
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
138
+ </button>
139
+ </div>`;
140
+ }).join('');
141
+ grid.innerHTML=html;
142
+ };
143
+ const MODE_TO_CLASS = { protanopia:'cb-protanopia', deuteranopia:'cb-deuteranopia', tritanopia:'cb-tritanopia', achromatopsia:'cb-achromatopsia' };
144
+ const CLEAR_CLASSES = Object.values(MODE_TO_CLASS);
145
+ const clearCbClasses = () => { const rootEl=document.documentElement; CLEAR_CLASSES.forEach(cls=>rootEl.classList.remove(cls)); };
146
+ const applyCbClass = (mode) => { clearCbClasses(); const cls=MODE_TO_CLASS[mode]; if (cls) document.documentElement.classList.add(cls); };
147
+ const currentCbMode = () => { const rootEl=document.documentElement; for (const [mode, cls] of Object.entries(MODE_TO_CLASS)) { if (rootEl.classList.contains(cls)) return mode; } return 'none'; };
148
+ const setupCbSim = () => { const select=document.getElementById('cb-select'); if (!select) return; try { select.value=currentCbMode(); } catch{} select.addEventListener('change', () => applyCbClass(select.value)); };
149
+ const setupCountControl = () => { const input=document.getElementById('color-count'); const out=document.getElementById('color-count-out'); if (!input) return; const clamp=(n,min,max)=>Math.max(min,Math.min(max,n)); const read=()=>clamp(Number(input.value)||6,6,10); const syncOut=()=>{ if (out) out.textContent=String(read()); }; const onChange=()=>{ syncOut(); render(); }; syncOut(); input.addEventListener('input', onChange); document.addEventListener('palettes:updated', () => { syncOut(); render(); }); };
150
+ let copyDelegationSetup=false; const setupCopyDelegation = () => { if (copyDelegationSetup) return; const grid=document.querySelector('.palettes .palettes__grid'); if (!grid) return; grid.addEventListener('click', async (e) => { const btn = e.target.closest ? e.target.closest('.palette-card__copy') : null; if (!btn) return; const card = btn.closest('.palette-card'); if (!card) return; const colors=(card.dataset.colors||'').split(',').filter(Boolean); const json=JSON.stringify(colors,null,2); try { await navigator.clipboard.writeText(json); const old=btn.innerHTML; btn.innerHTML='<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 16.2l-3.5-3.5-1.4 1.4L9 19 20.3 7.7l-1.4-1.4z"/></svg>'; setTimeout(()=> btn.innerHTML=old, 900); } catch { window.prompt('Copy palette', json); } }); copyDelegationSetup=true; };
151
+ const bootstrap = () => {
152
+ setupCbSim();
153
+ setupCountControl();
154
+ setupCopyDelegation();
155
+ // Render immediately
156
+ render();
157
+ // Re-render on palette updates
158
+ document.addEventListener('palettes:updated', render);
159
+ // Force an immediate notify after listeners are attached (ensures initial render)
160
+ try {
161
+ if (window.ColorPalettes && typeof window.ColorPalettes.notify === 'function') window.ColorPalettes.notify();
162
+ else if (window.ColorPalettes && typeof window.ColorPalettes.refresh === 'function') window.ColorPalettes.refresh();
163
+ } catch {}
164
+ };
165
+ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', bootstrap, { once: true }); } else { bootstrap(); }
166
+ })();
167
+ </script>
168
+
169
+
app/src/content/chapters/introduction.mdx CHANGED
@@ -17,7 +17,8 @@ import Sidenote from "../../components/Sidenote.astro";
17
  <span className="tag">Markdown-based</span>
18
  <span className="tag">KaTeX math</span>
19
  <span className="tag">Syntax highlighting</span>
20
- <span className="tag">Citations & footnotes</span>
 
21
  <span className="tag">Table of contents</span>
22
  <span className="tag">Mermaid diagrams</span>
23
  <span className="tag">Plotly ready</span>
 
17
  <span className="tag">Markdown-based</span>
18
  <span className="tag">KaTeX math</span>
19
  <span className="tag">Syntax highlighting</span>
20
+ <span className="tag">Citations in all flavors</span>
21
+ <span className="tag">Footnotes</span>
22
  <span className="tag">Table of contents</span>
23
  <span className="tag">Mermaid diagrams</span>
24
  <span className="tag">Plotly ready</span>
app/src/content/chapters/vibe-coding-charts.mdx CHANGED
@@ -27,8 +27,8 @@ I want you to code a d3 chart that visualizes the data.
27
 
28
  They can be found in the `app/src/content/embeds` folder and you can also use them as a starting point or examples to vibe code with.
29
 
30
- <HtmlEmbed
31
- title="d3-benchmark: LLM Benchmark"
32
  src="d3-benchmark.html" desc={`Figure 1: Grouped bar chart comparing model scores across benchmarks (MMLU, GSM8K, HellaSwag, TruthfulQA, ARC‑C). Each group is a benchmark; colors encode models; values are accuracy/score (higher is better).`} />
33
  ---
34
  <HtmlEmbed
 
27
 
28
  They can be found in the `app/src/content/embeds` folder and you can also use them as a starting point or examples to vibe code with.
29
 
30
+ <HtmlEmbed
31
+ title="d3-benchmark: LLM Benchmark"
32
  src="d3-benchmark.html" desc={`Figure 1: Grouped bar chart comparing model scores across benchmarks (MMLU, GSM8K, HellaSwag, TruthfulQA, ARC‑C). Each group is a benchmark; colors encode models; values are accuracy/score (higher is better).`} />
33
  ---
34
  <HtmlEmbed
app/src/content/chapters/writing-your-content.mdx CHANGED
@@ -6,6 +6,8 @@ import Wide from '../../components/Wide.astro';
6
  import Note from '../../components/Note.astro';
7
  import FullWidth from '../../components/FullWidth.astro';
8
  import HtmlEmbed from '../../components/HtmlEmbed.astro';
 
 
9
  import audioDemo from '../assets/audio/audio-example.wav';
10
  import Accordion from '../../components/Accordion.astro';
11
 
@@ -130,7 +132,7 @@ Use the **color picker** below to see how the primary color affects the theme.
130
  #### Brand color
131
 
132
  <Sidenote>
133
- <HtmlEmbed frameless src="demo/color-picker.html" />
134
  <Fragment slot="aside">
135
  You can use the color picker to select the right color.
136
 
@@ -143,7 +145,7 @@ Use the **color picker** below to see how the primary color affects the theme.
143
 
144
  Here is a suggestion of **color palettes** for your **data visualizations** that align with your **brand identity**. These palettes are generated from your `--primary-color`.
145
 
146
- <HtmlEmbed frameless src="demo/palettes.html" />
147
  <br/>
148
  **Use color with care.**
149
  Color should rarely be the only channel of meaning.
 
6
  import Note from '../../components/Note.astro';
7
  import FullWidth from '../../components/FullWidth.astro';
8
  import HtmlEmbed from '../../components/HtmlEmbed.astro';
9
+ import ColorPicker from '../../components/ColorPicker.astro';
10
+ import Palettes from '../../components/Palettes.astro';
11
  import audioDemo from '../assets/audio/audio-example.wav';
12
  import Accordion from '../../components/Accordion.astro';
13
 
 
132
  #### Brand color
133
 
134
  <Sidenote>
135
+ <ColorPicker />
136
  <Fragment slot="aside">
137
  You can use the color picker to select the right color.
138
 
 
145
 
146
  Here is a suggestion of **color palettes** for your **data visualizations** that align with your **brand identity**. These palettes are generated from your `--primary-color`.
147
 
148
+ <Palettes />
149
  <br/>
150
  **Use color with care.**
151
  Color should rarely be the only channel of meaning.
app/src/content/embeds/d3-bar.html CHANGED
@@ -1,16 +1,5 @@
1
  <div class="d3-bar" ></div>
2
  <style>
3
- /* Theme-driven axis/tick/grid variables */
4
- .d3-bar {
5
- --axis-color: rgba(0,0,0,0.25);
6
- --tick-color: rgba(0,0,0,0.55);
7
- --grid-color: rgba(0,0,0,0.05);
8
- }
9
- [data-theme="dark"] .d3-bar {
10
- --axis-color: rgba(255,255,255,0.25);
11
- --tick-color: rgba(255,255,255,0.70);
12
- --grid-color: rgba(255,255,255,0.08);
13
- }
14
  .d3-bar .controls { margin-top: 0; display: flex; gap: 16px; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
15
  .d3-bar .controls .control-group { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
16
  .d3-bar .controls label { font-size: 12px; color: var(--text-color); font-weight: 700; }
 
1
  <div class="d3-bar" ></div>
2
  <style>
 
 
 
 
 
 
 
 
 
 
 
3
  .d3-bar .controls { margin-top: 0; display: flex; gap: 16px; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
4
  .d3-bar .controls .control-group { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
5
  .d3-bar .controls label { font-size: 12px; color: var(--text-color); font-weight: 700; }
app/src/content/embeds/d3-benchmark.html CHANGED
@@ -1,16 +1,6 @@
1
  <div class="d3-benchmark"></div>
2
  <style>
3
- .d3-benchmark {
4
- position: relative;
5
- --axis-color: rgba(0,0,0,0.25);
6
- --tick-color: rgba(0,0,0,0.55);
7
- --grid-color: rgba(0,0,0,0.05);
8
- }
9
- [data-theme="dark"] .d3-benchmark {
10
- --axis-color: rgba(255,255,255,0.25);
11
- --tick-color: rgba(255,255,255,0.70);
12
- --grid-color: rgba(255,255,255,0.08);
13
- }
14
  .d3-benchmark .controls {
15
  display: flex;
16
  align-items: center;
 
1
  <div class="d3-benchmark"></div>
2
  <style>
3
+ .d3-benchmark { position: relative; }
 
 
 
 
 
 
 
 
 
 
4
  .d3-benchmark .controls {
5
  display: flex;
6
  align-items: center;
app/src/content/embeds/d3-line-quad.html CHANGED
@@ -184,6 +184,8 @@
184
  <script>
185
  (() => {
186
  const THIS_SCRIPT = document.currentScript;
 
 
187
  // Pretty label mapping for metric keys
188
  const prettyMetricLabel = (key) => {
189
  if (!key) return '';
@@ -270,8 +272,12 @@
270
  let axisLabelY = 'Value';
271
 
272
  // Colors and markers (match original embeds)
273
- const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
274
- const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])];
 
 
 
 
275
  // Shapes supprimés: on n'utilise que la couleur
276
  // Ready signal for async load completion
277
  let readyResolve = null;
@@ -371,9 +377,15 @@
371
  }
372
 
373
  axisLabelY = isRankStrict ? 'Rank' : prettyMetricLabel(metricKey);
 
374
  const { innerWidth, innerHeight } = updateScales();
375
 
376
- const series = runs.map((r, i) => ({ run:r, color: pool[i % pool.length], values:(map[r]||[]).slice().sort((a,b)=>a.step-b.step).map(pt => isRankStrict ? { step: pt.step, value: Math.round(pt.value), stderr: pt.stderr } : pt) }));
 
 
 
 
 
377
 
378
  // zones ± stderr (métriques non rank)
379
  gAreas.selectAll('*').remove();
@@ -407,10 +419,10 @@
407
  .attr('opacity',0)
408
  .attr('stroke', d=>d.color)
409
  .attr('d', d=>lineGen(d.values))
410
- .transition().duration(450).ease(d3.easeCubicOut)
411
  .attr('opacity',0.9);
412
  paths
413
- .transition().duration(450).ease(d3.easeCubicOut)
414
  .attr('stroke', d=>d.color)
415
  .attr('opacity',0.9)
416
  .attr('d', d=>lineGen(d.values));
@@ -421,14 +433,16 @@
421
  const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d=> `${d.run}-${d.step}`);
422
  ptsSel.enter().append('circle').attr('class','pt')
423
  .attr('data-run', d=>d.run)
424
- .attr('r', 2)
425
  .attr('fill', d=>d.color)
426
  .attr('fill-opacity', 0.6)
427
  .attr('stroke', 'none')
428
  .attr('cx', d=>xScale(d.step))
429
  .attr('cy', d=>yScale(d.value))
430
  .merge(ptsSel)
431
- .transition().duration(180)
 
 
432
  .attr('cx', d=>xScale(d.step))
433
  .attr('cy', d=>yScale(d.value));
434
  ptsSel.exit().remove();
@@ -579,13 +593,22 @@
579
  if (r.ok && window.d3 && window.d3.csvParse) {
580
  const txt = await r.text();
581
  const rows = window.d3.csvParse(txt);
582
- const runList = Array.from(new Set(rows.map(row => String(row.run||'').trim()).filter(Boolean)));
583
- const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
584
- const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...((window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'])];
585
- legendItemsHost.innerHTML = runList.map((name, i)=> {
586
- const color = pool[i % pool.length];
 
 
 
 
587
  return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`;
588
  }).join('');
 
 
 
 
 
589
  // Legend hover ghosting across all cells
590
  legendItemsHost.querySelectorAll('.item').forEach(el => {
591
  el.addEventListener('mouseenter', () => {
 
184
  <script>
185
  (() => {
186
  const THIS_SCRIPT = document.currentScript;
187
+ // Shared run->color mapping to keep legend and series perfectly in sync
188
+ let SHARED_RUN_COLOR = null;
189
  // Pretty label mapping for metric keys
190
  const prettyMetricLabel = (key) => {
191
  if (!key) return '';
 
272
  let axisLabelY = 'Value';
273
 
274
  // Colors and markers (match original embeds)
275
+ const getRunColors = (n) => {
276
+ try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', n); } catch(_) {}
277
+ const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
278
+ return [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])].slice(0, n);
279
+ };
280
+ const pool = getRunColors(12);
281
  // Shapes supprimés: on n'utilise que la couleur
282
  // Ready signal for async load completion
283
  let readyResolve = null;
 
377
  }
378
 
379
  axisLabelY = isRankStrict ? 'Rank' : prettyMetricLabel(metricKey);
380
+ const tChange = (window.d3 && d3.transition) ? d3.transition().duration(260).ease(d3.easeCubicOut) : null;
381
  const { innerWidth, innerHeight } = updateScales();
382
 
383
+ const colorForRun = (run, idx) => {
384
+ if (SHARED_RUN_COLOR && Object.prototype.hasOwnProperty.call(SHARED_RUN_COLOR, run)) return SHARED_RUN_COLOR[run];
385
+ const j = (typeof idx === 'number' ? idx : runs.indexOf(run));
386
+ return pool[(j >= 0 ? j : 0) % pool.length];
387
+ };
388
+ const series = runs.map((r, i) => ({ run:r, color: colorForRun(r, i), values:(map[r]||[]).slice().sort((a,b)=>a.step-b.step).map(pt => isRankStrict ? { step: pt.step, value: Math.round(pt.value), stderr: pt.stderr } : pt) }));
389
 
390
  // zones ± stderr (métriques non rank)
391
  gAreas.selectAll('*').remove();
 
419
  .attr('opacity',0)
420
  .attr('stroke', d=>d.color)
421
  .attr('d', d=>lineGen(d.values))
422
+ .transition(tChange || undefined)
423
  .attr('opacity',0.9);
424
  paths
425
+ .transition(tChange || undefined)
426
  .attr('stroke', d=>d.color)
427
  .attr('opacity',0.9)
428
  .attr('d', d=>lineGen(d.values));
 
433
  const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d=> `${d.run}-${d.step}`);
434
  ptsSel.enter().append('circle').attr('class','pt')
435
  .attr('data-run', d=>d.run)
436
+ .attr('r', 1.5)
437
  .attr('fill', d=>d.color)
438
  .attr('fill-opacity', 0.6)
439
  .attr('stroke', 'none')
440
  .attr('cx', d=>xScale(d.step))
441
  .attr('cy', d=>yScale(d.value))
442
  .merge(ptsSel)
443
+ .attr('fill', d=>d.color)
444
+ .transition(tChange || undefined)
445
+ .attr('r', 2)
446
  .attr('cx', d=>xScale(d.step))
447
  .attr('cy', d=>yScale(d.value));
448
  ptsSel.exit().remove();
 
593
  if (r.ok && window.d3 && window.d3.csvParse) {
594
  const txt = await r.text();
595
  const rows = window.d3.csvParse(txt);
596
+ const runList = Array.from(new Set(rows.map(row => String(row.run||'').trim()).filter(Boolean))).sort();
597
+ const poolLegend = (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function')
598
+ ? window.ColorPalettes.getColors('categorical', runList.length)
599
+ : (()=>{ const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB'; return [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...((window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'])]; })();
600
+ // Build shared run->color map once
601
+ SHARED_RUN_COLOR = {};
602
+ runList.forEach((name, i) => { SHARED_RUN_COLOR[name] = poolLegend[i % poolLegend.length]; });
603
+ legendItemsHost.innerHTML = runList.map((name) => {
604
+ const color = SHARED_RUN_COLOR[name];
605
  return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`;
606
  }).join('');
607
+ // Re-render all cells with the shared mapping to ensure perfect sync
608
+ try {
609
+ const currentMetric = (select && select.value) || def;
610
+ if (currentMetric) applyAll(currentMetric);
611
+ } catch {}
612
  // Legend hover ghosting across all cells
613
  legendItemsHost.querySelectorAll('.item').forEach(el => {
614
  el.addEventListener('mouseenter', () => {
app/src/content/embeds/d3-line.html CHANGED
@@ -1,17 +1,6 @@
1
  <div class="d3-line-simple"></div>
2
  <style>
3
  .d3-line-simple { position: relative; }
4
- /* Theme-driven axis/tick/grid variables */
5
- .d3-line-simple {
6
- --axis-color: rgba(0,0,0,0.25);
7
- --tick-color: rgba(0,0,0,0.55);
8
- --grid-color: rgba(0,0,0,0.05);
9
- }
10
- [data-theme="dark"] .d3-line-simple {
11
- --axis-color: rgba(255,255,255,0.25);
12
- --tick-color: rgba(255,255,255,0.70);
13
- --grid-color: rgba(255,255,255,0.08);
14
- }
15
  .d3-line-simple .controls {
16
  margin-top: 0;
17
  display: flex;
 
1
  <div class="d3-line-simple"></div>
2
  <style>
3
  .d3-line-simple { position: relative; }
 
 
 
 
 
 
 
 
 
 
 
4
  .d3-line-simple .controls {
5
  margin-top: 0;
6
  display: flex;
app/src/content/embeds/d3-neural-network.html CHANGED
@@ -105,7 +105,7 @@
105
  const gOutText = gRoot.append('g').attr('class','out-probs');
106
 
107
  // Network structure (compact: 8 -> 8 -> 10)
108
- const layerSizes = [8, 8, 8];
109
  const layers = layerSizes.map((n, li)=> Array.from({length:n}, (_, i)=>({ id:`L${li}N${i}`, layer: li, index: i, a:0 })));
110
  // Links only between hidden->hidden and hidden->output
111
  const links = [];
 
105
  const gOutText = gRoot.append('g').attr('class','out-probs');
106
 
107
  // Network structure (compact: 8 -> 8 -> 10)
108
+ const layerSizes = [8, 8, 10];
109
  const layers = layerSizes.map((n, li)=> Array.from({length:n}, (_, i)=>({ id:`L${li}N${i}`, layer: li, index: i, a:0 })));
110
  // Links only between hidden->hidden and hidden->output
111
  const links = [];
app/src/content/embeds/d3-pie.html CHANGED
@@ -201,7 +201,11 @@
201
 
202
  // Catégories (triées) + échelle de couleurs harmonisée avec banner.html
203
  const categories = Array.from(new Set(rows.map(r => r.eagle_cathegory || 'Unknown'))).sort();
204
- const color = d3.scaleOrdinal().domain(categories).range(d3.schemeTableau10);
 
 
 
 
205
  const colorOf = (cat) => color(cat || 'Unknown');
206
 
207
  // Clear plots grid
 
201
 
202
  // Catégories (triées) + échelle de couleurs harmonisée avec banner.html
203
  const categories = Array.from(new Set(rows.map(r => r.eagle_cathegory || 'Unknown'))).sort();
204
+ const getCatColors = (n) => {
205
+ try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', n); } catch(_) {}
206
+ return (d3.schemeTableau10 ? d3.schemeTableau10.slice(0, n) : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'].slice(0, n));
207
+ };
208
+ const color = d3.scaleOrdinal().domain(categories).range(getCatColors(categories.length));
209
  const colorOf = (cat) => color(cat || 'Unknown');
210
 
211
  // Clear plots grid
app/src/content/embeds/d3-scatter.html CHANGED
@@ -122,12 +122,16 @@
122
 
123
  function refreshPalette(){
124
  try {
125
- const raw = getComputedStyle(document.documentElement).getPropertyValue('--palette-categorical-json').trim();
126
- const arr = raw ? JSON.parse(raw) : [];
127
- if (arr && arr.length) color.range(arr);
128
- else color.range(['var(--primary-color)','#4EA5B7','#E38A42','#CEC0FA','#98C97C','#F6BD60']);
 
 
 
129
  } catch {
130
- color.range(['var(--primary-color)','#4EA5B7','#E38A42','#CEC0FA','#98C97C','#F6BD60']);
 
131
  }
132
  // Recolor existing marks/labels after palette changes
133
  try { if (data && data.length) draw(); } catch {}
 
122
 
123
  function refreshPalette(){
124
  try {
125
+ const cats = categories && categories.length ? categories.length : 6;
126
+ if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
127
+ const arr = window.ColorPalettes.getColors('categorical', cats) || [];
128
+ if (arr && arr.length) { color.range(arr); return; }
129
+ }
130
+ // fallback
131
+ color.range((d3.schemeTableau10 ? d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab']).slice(0, cats));
132
  } catch {
133
+ const cats = categories && categories.length ? categories.length : 6;
134
+ color.range((d3.schemeTableau10 ? d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab']).slice(0, cats));
135
  }
136
  // Recolor existing marks/labels after palette changes
137
  try { if (data && data.length) draw(); } catch {}
app/src/content/embeds/vibe-code-d3-embeds-directives.md CHANGED
@@ -3,7 +3,7 @@
3
  ### Quickstart (TL;DR)
4
  - Create a single self-contained HTML fragment: root div + scoped style + IIFE script.
5
  - Draw marks/axes in SVG; render UI (legend and controls) in HTML.
6
- - Place legend and controls ABOVE the chart. Include a legend title "Legend" and a select labeled "Metric" when relevant.
7
  - Load data from public `/data` first, then fall back to `assets/data`.
8
  - Use `window.ColorPalettes` for colors; stick to CSS variables for theming.
9
 
@@ -160,27 +160,25 @@ Minimal skeleton:
160
  - Prefer CSS-only where possible.
161
  - Keep backgrounds light and borders subtle; the outer card frame is handled by `HtmlEmbed.astro`.
162
 
163
- Standard axis/tick/grid colors (reuse across charts):
164
 
165
  ```css
166
- .your-root-class {
167
- --axis-color: rgba(0,0,0,0.25);
168
- --tick-color: rgba(0,0,0,0.55);
169
- --grid-color: rgba(0,0,0,0.05);
 
170
  }
171
- [data-theme="dark"] .your-root-class {
172
- --axis-color: rgba(255,255,255,0.25);
173
- --tick-color: rgba(255,255,255,0.70);
174
- --grid-color: rgba(255,255,255,0.08);
175
  }
176
- /* Example axis application (D3) */
177
- g.axis-x, g.axis-y { }
178
- /* In JS after calling axis: */
179
- // g.selectAll('path, line').attr('stroke', 'var(--axis-color)');
180
- // g.selectAll('text').attr('fill', 'var(--tick-color)').style('font-size','12px');
181
- /* Gridlines: */
182
- // grid.call(d3.axisLeft(y).ticks(6).tickSize(-innerWidth).tickFormat(''))
183
- // .call(g => g.selectAll('.tick line').attr('stroke','var(--grid-color)'));
184
  ```
185
 
186
  #### 3.1) Text on fixed-colored backgrounds
 
3
  ### Quickstart (TL;DR)
4
  - Create a single self-contained HTML fragment: root div + scoped style + IIFE script.
5
  - Draw marks/axes in SVG; render UI (legend and controls) in HTML.
6
+ - Place legend and controls BELOW the chart (header appended after the chart). Include a legend title "Legend" and a select labeled "Metric" when relevant.
7
  - Load data from public `/data` first, then fall back to `assets/data`.
8
  - Use `window.ColorPalettes` for colors; stick to CSS variables for theming.
9
 
 
160
  - Prefer CSS-only where possible.
161
  - Keep backgrounds light and borders subtle; the outer card frame is handled by `HtmlEmbed.astro`.
162
 
163
+ Standard axis/tick/grid colors (global variables from `_variables.css`):
164
 
165
  ```css
166
+ /* Provided globally */
167
+ :root {
168
+ --axis-color: var(--text-color);
169
+ --tick-color: var(--muted-color);
170
+ --grid-color: rgba(0,0,0,.08);
171
  }
172
+ [data-theme="dark"] {
173
+ --axis-color: var(--text-color);
174
+ --tick-color: var(--muted-color);
175
+ --grid-color: rgba(255,255,255,.10);
176
  }
177
+ /* Apply inside charts */
178
+ .your-root-class .axes path,
179
+ .your-root-class .axes line { stroke: var(--axis-color); }
180
+ .your-root-class .axes text { fill: var(--tick-color); }
181
+ .your-root-class .grid line { stroke: var(--grid-color); }
 
 
 
182
  ```
183
 
184
  #### 3.1) Text on fixed-colored backgrounds
app/src/scripts/color-palettes.js CHANGED
@@ -214,6 +214,7 @@
214
  };
215
  window.ColorPalettes = {
216
  refresh: updatePalettes,
 
217
  getPrimary: () => getPrimaryHex(),
218
  getColors: (key, count = 6) => {
219
  const primary = getPrimaryHex();
 
214
  };
215
  window.ColorPalettes = {
216
  refresh: updatePalettes,
217
+ notify: () => { try { const primary = getPrimaryHex(); document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary } })); } catch {} },
218
  getPrimary: () => getPrimaryHex(),
219
  getColors: (key, count = 6) => {
220
  const primary = getPrimaryHex();