rastack 0.0.24 → 0.0.26
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/CHANGELOG.md +4 -0
- package/dist/rastack-design.d.ts +17 -0
- package/dist/rastack-design.js +134 -0
- package/dist/rastack-tokens.d.ts +15 -0
- package/dist/rastack-tokens.js +43 -0
- package/dist/rastack-wasm-build.d.ts +1 -1
- package/dist/rastack-wasm-build.js +3 -3
- package/dist/rastack.d.ts +2 -0
- package/dist/rastack.js +12 -0
- package/dist/tokens/color.d.ts +22 -0
- package/dist/tokens/color.js +70 -0
- package/dist/tokens/compile.d.ts +34 -0
- package/dist/tokens/compile.js +103 -0
- package/dist/tokens/css.d.ts +30 -0
- package/dist/tokens/css.js +102 -0
- package/dist/tokens/define.d.ts +71 -0
- package/dist/tokens/define.js +98 -0
- package/dist/tokens/index.d.ts +17 -0
- package/dist/tokens/index.js +33 -0
- package/dist/tokens/resolve.d.ts +40 -0
- package/dist/tokens/resolve.js +135 -0
- package/dist/tokens/studio.d.ts +21 -0
- package/dist/tokens/studio.js +328 -0
- package/dist/tokens/theme.d.ts +55 -0
- package/dist/tokens/theme.js +139 -0
- package/dist/tokens/ts.d.ts +15 -0
- package/dist/tokens/ts.js +73 -0
- package/dist/tokens/types.d.ts +92 -0
- package/dist/tokens/types.js +35 -0
- package/hooks/query/delete.ts +3 -1
- package/jest.config.cjs +6 -0
- package/package.json +3 -2
- package/src/rastack-design.ts +117 -0
- package/src/rastack-tokens.ts +46 -0
- package/src/rastack-wasm-build.ts +3 -3
- package/src/rastack.ts +12 -0
- package/src/tokens/color.ts +74 -0
- package/src/tokens/compile.ts +85 -0
- package/src/tokens/css.ts +138 -0
- package/src/tokens/define.ts +128 -0
- package/src/tokens/index.ts +18 -0
- package/src/tokens/resolve.ts +170 -0
- package/src/tokens/studio.ts +357 -0
- package/src/tokens/theme.ts +180 -0
- package/src/tokens/ts.ts +80 -0
- package/src/tokens/types.ts +125 -0
- package/test/tokens.spec.ts +302 -0
- package/theme/index.ts +9 -0
- package/theme/provider.tsx +157 -0
- package/tokens.ts +9 -0
- package/tsconfig.json +3 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The design studio page — a self-contained, dependency-free HTML app that
|
|
4
|
+
* `rastack design` serves. It **showcases** the design system (colour swatches,
|
|
5
|
+
* type ramp, spacing, radii, shadows, and live component previews built from the
|
|
6
|
+
* tokens themselves) and lets you **edit** token values in place — with a live
|
|
7
|
+
* preview — then save them back to the source, or export the DTCG JSON / CSS.
|
|
8
|
+
*
|
|
9
|
+
* Pure: given a themed doc it returns an HTML string, so it renders identically
|
|
10
|
+
* whether served by the CLI or snapshotted in a test. The page talks to the CLI
|
|
11
|
+
* server over two tiny endpoints: `POST /save` (persist) and it embeds
|
|
12
|
+
* everything else it needs inline.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.renderStudio = renderStudio;
|
|
16
|
+
const css_1 = require("./css");
|
|
17
|
+
const resolve_1 = require("./resolve");
|
|
18
|
+
function toView(prefix, tok) {
|
|
19
|
+
return {
|
|
20
|
+
path: tok.path,
|
|
21
|
+
cssVar: (0, resolve_1.cssVarName)(tok.path, prefix),
|
|
22
|
+
type: tok.type,
|
|
23
|
+
decl: tok.aliasOf ? `var(${(0, resolve_1.cssVarName)(tok.aliasOf, prefix)})` : (0, css_1.formatCssValue)(tok),
|
|
24
|
+
literal: (0, css_1.formatCssValue)(tok),
|
|
25
|
+
aliasOf: tok.aliasOf,
|
|
26
|
+
description: tok.$description,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Render the full design-studio HTML document for a themed token set. */
|
|
30
|
+
function renderStudio(themed, options = {}) {
|
|
31
|
+
const prefix = options.prefix ?? "";
|
|
32
|
+
const editable = options.editable !== false;
|
|
33
|
+
const title = options.title ?? "Design System";
|
|
34
|
+
const resolved = (0, resolve_1.resolveTheme)(themed);
|
|
35
|
+
const layers = {
|
|
36
|
+
base: resolved.base.map((t) => toView(prefix, t)),
|
|
37
|
+
};
|
|
38
|
+
for (const [name, toks] of Object.entries(resolved.modes)) {
|
|
39
|
+
layers[name] = toks.map((t) => toView(prefix, t));
|
|
40
|
+
}
|
|
41
|
+
const modeNames = Object.keys(resolved.modes);
|
|
42
|
+
const css = (0, css_1.emitCss)(themed, { prefix });
|
|
43
|
+
const payload = {
|
|
44
|
+
title,
|
|
45
|
+
prefix,
|
|
46
|
+
editable,
|
|
47
|
+
defaultMode: resolved.defaultMode ?? null,
|
|
48
|
+
modeNames,
|
|
49
|
+
layers,
|
|
50
|
+
raw: themed,
|
|
51
|
+
};
|
|
52
|
+
return page(title, css, payload);
|
|
53
|
+
}
|
|
54
|
+
function page(title, css, payload) {
|
|
55
|
+
const data = JSON.stringify(payload).replace(/</g, "\\u003c");
|
|
56
|
+
return `<!doctype html>
|
|
57
|
+
<html lang="en" data-theme="${payload.defaultMode ?? ""}">
|
|
58
|
+
<head>
|
|
59
|
+
<meta charset="utf-8" />
|
|
60
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
61
|
+
<title>${escapeHtml(title)} · rastack design</title>
|
|
62
|
+
<style id="tokens-css">${css}</style>
|
|
63
|
+
<style>${STUDIO_CSS}</style>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<div id="app"></div>
|
|
67
|
+
<script id="studio-data" type="application/json">${data}</script>
|
|
68
|
+
<script>${STUDIO_JS}</script>
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
function escapeHtml(s) {
|
|
74
|
+
return s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
75
|
+
}
|
|
76
|
+
/* -- The studio's own chrome. Deliberately neutral so it frames, not fights,
|
|
77
|
+
the design system it is displaying. --------------------------------- */
|
|
78
|
+
const STUDIO_CSS = `
|
|
79
|
+
* { box-sizing: border-box; }
|
|
80
|
+
body { margin: 0; font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
|
|
81
|
+
background: #0d1117; color: #e6edf3; -webkit-font-smoothing: antialiased; }
|
|
82
|
+
.studio-header { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; gap: 16px;
|
|
83
|
+
padding: 14px 24px; background: #161b22ee; backdrop-filter: blur(8px); border-bottom: 1px solid #30363d; }
|
|
84
|
+
.studio-header h1 { font-size: 15px; font-weight: 650; margin: 0; letter-spacing: .2px; }
|
|
85
|
+
.studio-header .spacer { flex: 1; }
|
|
86
|
+
.pill { font: inherit; font-size: 12px; padding: 6px 12px; border-radius: 999px; border: 1px solid #30363d;
|
|
87
|
+
background: #21262d; color: #e6edf3; cursor: pointer; }
|
|
88
|
+
.pill.active { background: #388bfd33; border-color: #388bfd; color: #cae3ff; }
|
|
89
|
+
.pill:hover { border-color: #58a6ff; }
|
|
90
|
+
.btn { font: inherit; font-size: 12px; font-weight: 600; padding: 6px 14px; border-radius: 6px; border: 1px solid #30363d;
|
|
91
|
+
background: #21262d; color: #e6edf3; cursor: pointer; }
|
|
92
|
+
.btn.primary { background: #238636; border-color: #2ea043; }
|
|
93
|
+
.btn:disabled { opacity: .5; cursor: default; }
|
|
94
|
+
.layout { max-width: 1180px; margin: 0 auto; padding: 28px 24px 96px; }
|
|
95
|
+
section { margin: 40px 0 0; }
|
|
96
|
+
section > h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .12em; color: #8b949e; margin: 0 0 16px;
|
|
97
|
+
padding-bottom: 8px; border-bottom: 1px solid #21262d; }
|
|
98
|
+
.grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }
|
|
99
|
+
.card { background: #161b22; border: 1px solid #21262d; border-radius: 12px; padding: 14px; }
|
|
100
|
+
.swatch { height: 56px; border-radius: 8px; border: 1px solid #ffffff1a; margin-bottom: 10px; }
|
|
101
|
+
.tok-name { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; color: #7ee787; word-break: break-all; }
|
|
102
|
+
.tok-meta { font-size: 11px; color: #8b949e; margin-top: 2px; }
|
|
103
|
+
.tok-alias { color: #d2a8ff; }
|
|
104
|
+
.row { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
|
|
105
|
+
.row input[type=text] { flex: 1; min-width: 0; font: inherit; font-family: ui-monospace, monospace; font-size: 12px;
|
|
106
|
+
padding: 6px 8px; border-radius: 6px; border: 1px solid #30363d; background: #0d1117; color: #e6edf3; }
|
|
107
|
+
.row input[type=color] { width: 34px; height: 30px; padding: 0; border: 1px solid #30363d; border-radius: 6px; background: none; }
|
|
108
|
+
.bar { height: 12px; max-width: 100%; background: var(--_v); border-radius: 4px; }
|
|
109
|
+
.radius-box { height: 56px; background: #388bfd33; border: 1px solid #58a6ff88; }
|
|
110
|
+
.shadow-box { height: 56px; background: #ffffff; border-radius: 8px; margin: 6px 4px 12px; }
|
|
111
|
+
.type-sample { font-size: 20px; margin: 6px 0; color: #e6edf3; }
|
|
112
|
+
.preview { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-start; }
|
|
113
|
+
.saved-toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #238636;
|
|
114
|
+
padding: 10px 18px; border-radius: 8px; font-size: 13px; opacity: 0; transition: opacity .25s; pointer-events: none; }
|
|
115
|
+
.saved-toast.show { opacity: 1; }
|
|
116
|
+
.dim-note { font-size: 11px; color: #6e7681; margin-top: 16px; }
|
|
117
|
+
`;
|
|
118
|
+
/* -- The studio client. Renders sections from the embedded payload, applies
|
|
119
|
+
edits live to :root, and posts the mutated doc back to the server. ---- */
|
|
120
|
+
const STUDIO_JS = `
|
|
121
|
+
(function () {
|
|
122
|
+
var data = JSON.parse(document.getElementById("studio-data").textContent);
|
|
123
|
+
var root = document.documentElement;
|
|
124
|
+
var activeLayer = data.defaultMode || "base";
|
|
125
|
+
var app = document.getElementById("app");
|
|
126
|
+
|
|
127
|
+
// Deep clone the raw doc so edits are collected without touching the payload.
|
|
128
|
+
var draft = JSON.parse(JSON.stringify(data.raw));
|
|
129
|
+
var dirty = false;
|
|
130
|
+
|
|
131
|
+
function tokenType(views, path) {
|
|
132
|
+
for (var i = 0; i < views.length; i++) if (views[i].path === path) return views[i].type;
|
|
133
|
+
}
|
|
134
|
+
function setPath(tree, path, value, type) {
|
|
135
|
+
var segs = path.split(".");
|
|
136
|
+
var node = tree;
|
|
137
|
+
for (var i = 0; i < segs.length - 1; i++) { node[segs[i]] = node[segs[i]] || {}; node = node[segs[i]]; }
|
|
138
|
+
var leaf = node[segs[segs.length - 1]] || {};
|
|
139
|
+
leaf.$value = value; if (type && !leaf.$type) leaf.$type = type;
|
|
140
|
+
node[segs[segs.length - 1]] = leaf;
|
|
141
|
+
}
|
|
142
|
+
function layerTree(layer) {
|
|
143
|
+
if (layer === "base") return draft.tokens;
|
|
144
|
+
draft.modes = draft.modes || {};
|
|
145
|
+
draft.modes[layer] = draft.modes[layer] || {};
|
|
146
|
+
return draft.modes[layer];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Apply a layer's declarations to :root so the preview reflects it exactly.
|
|
150
|
+
function applyLayer(layer) {
|
|
151
|
+
var views = data.layers[layer] || data.layers.base;
|
|
152
|
+
views.forEach(function (v) { root.style.setProperty(v.cssVar, v.decl); });
|
|
153
|
+
root.setAttribute("data-theme", layer === "base" ? "" : layer);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function onEdit(view, raw) {
|
|
157
|
+
root.style.setProperty(view.cssVar, raw);
|
|
158
|
+
setPath(layerTree(activeLayer), view.path, raw, view.type);
|
|
159
|
+
dirty = true;
|
|
160
|
+
var btn = document.getElementById("save-btn");
|
|
161
|
+
if (btn) btn.disabled = false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function group(views, pred) { return views.filter(pred); }
|
|
165
|
+
|
|
166
|
+
function sectionEl(title, nodes) {
|
|
167
|
+
if (!nodes.length) return null;
|
|
168
|
+
var s = document.createElement("section");
|
|
169
|
+
var h = document.createElement("h2"); h.textContent = title; s.appendChild(h);
|
|
170
|
+
var g = document.createElement("div"); g.className = "grid";
|
|
171
|
+
nodes.forEach(function (n) { g.appendChild(n); }); s.appendChild(g);
|
|
172
|
+
return s;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function cardShell(view, preview) {
|
|
176
|
+
var c = document.createElement("div"); c.className = "card";
|
|
177
|
+
if (preview) c.appendChild(preview);
|
|
178
|
+
var name = document.createElement("div"); name.className = "tok-name"; name.textContent = view.path;
|
|
179
|
+
c.appendChild(name);
|
|
180
|
+
var meta = document.createElement("div"); meta.className = "tok-meta";
|
|
181
|
+
meta.innerHTML = view.aliasOf ? '→ <span class="tok-alias">' + view.aliasOf + "</span>" : escapeHtml(view.literal);
|
|
182
|
+
c.appendChild(meta);
|
|
183
|
+
if (view.description) { var d = document.createElement("div"); d.className = "tok-meta"; d.textContent = view.description; c.appendChild(d); }
|
|
184
|
+
return c;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function editorRow(view, isColor) {
|
|
188
|
+
if (!data.editable || view.aliasOf) return null;
|
|
189
|
+
var row = document.createElement("div"); row.className = "row";
|
|
190
|
+
var text = document.createElement("input"); text.type = "text"; text.value = view.literal;
|
|
191
|
+
if (isColor) {
|
|
192
|
+
var color = document.createElement("input"); color.type = "color";
|
|
193
|
+
try { color.value = toHex(view.literal); } catch (e) {}
|
|
194
|
+
color.oninput = function () { text.value = color.value; onEdit(view, color.value); };
|
|
195
|
+
row.appendChild(color);
|
|
196
|
+
}
|
|
197
|
+
text.oninput = function () { onEdit(view, text.value); };
|
|
198
|
+
row.appendChild(text);
|
|
199
|
+
return row;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function render() {
|
|
203
|
+
var views = data.layers[activeLayer] || data.layers.base;
|
|
204
|
+
app.innerHTML = "";
|
|
205
|
+
app.appendChild(header());
|
|
206
|
+
var wrap = document.createElement("div"); wrap.className = "layout";
|
|
207
|
+
|
|
208
|
+
var colors = group(views, function (v) { return v.type === "color"; }).map(function (v) {
|
|
209
|
+
var sw = document.createElement("div"); sw.className = "swatch"; sw.style.background = "var(" + v.cssVar + ")";
|
|
210
|
+
var c = cardShell(v, sw); var ed = editorRow(v, true); if (ed) c.appendChild(ed); return c;
|
|
211
|
+
});
|
|
212
|
+
add(wrap, sectionEl("Colours", colors));
|
|
213
|
+
|
|
214
|
+
var isRadius = function (v) { return /radius|radii|round/i.test(v.path); };
|
|
215
|
+
var dims = group(views, function (v) { return v.type === "dimension" && !isRadius(v); }).map(function (v) {
|
|
216
|
+
var bar = document.createElement("div"); bar.className = "bar"; bar.style.width = "var(" + v.cssVar + ")"; bar.style.setProperty("--_v", "var(" + v.cssVar + ")");
|
|
217
|
+
bar.style.background = "#58a6ff";
|
|
218
|
+
var c = cardShell(v, bar); var ed = editorRow(v, false); if (ed) c.appendChild(ed); return c;
|
|
219
|
+
});
|
|
220
|
+
add(wrap, sectionEl("Spacing & dimensions", dims));
|
|
221
|
+
|
|
222
|
+
var radii = group(views, function (v) { return isRadius(v) && v.type !== "color"; });
|
|
223
|
+
// radii tokens are dimensions too; show a rounded-box preview for the named ones
|
|
224
|
+
var radiiCards = radii.map(function (v) {
|
|
225
|
+
var box = document.createElement("div"); box.className = "radius-box"; box.style.borderRadius = "var(" + v.cssVar + ")";
|
|
226
|
+
return cardShell(v, box);
|
|
227
|
+
});
|
|
228
|
+
add(wrap, sectionEl("Radii", radiiCards));
|
|
229
|
+
|
|
230
|
+
var shadows = group(views, function (v) { return v.type === "shadow"; }).map(function (v) {
|
|
231
|
+
var box = document.createElement("div"); box.className = "shadow-box"; box.style.boxShadow = "var(" + v.cssVar + ")";
|
|
232
|
+
return cardShell(v, box);
|
|
233
|
+
});
|
|
234
|
+
add(wrap, sectionEl("Shadows", shadows));
|
|
235
|
+
|
|
236
|
+
var fonts = group(views, function (v) { return v.type === "fontFamily" || v.type === "fontWeight"; }).map(function (v) {
|
|
237
|
+
var s = document.createElement("div"); s.className = "type-sample";
|
|
238
|
+
if (v.type === "fontFamily") s.style.fontFamily = "var(" + v.cssVar + ")"; else s.style.fontWeight = "var(" + v.cssVar + ")";
|
|
239
|
+
s.textContent = "The quick brown fox";
|
|
240
|
+
var c = cardShell(v, s); var ed = editorRow(v, false); if (ed) c.appendChild(ed); return c;
|
|
241
|
+
});
|
|
242
|
+
add(wrap, sectionEl("Typography", fonts));
|
|
243
|
+
|
|
244
|
+
var known = { color: 1, dimension: 1, shadow: 1, fontFamily: 1, fontWeight: 1 };
|
|
245
|
+
var other = group(views, function (v) { return !known[v.type]; }).map(function (v) {
|
|
246
|
+
var c = cardShell(v, null); var ed = editorRow(v, false); if (ed) c.appendChild(ed); return c;
|
|
247
|
+
});
|
|
248
|
+
add(wrap, sectionEl("Other tokens", other));
|
|
249
|
+
|
|
250
|
+
add(wrap, componentPreview());
|
|
251
|
+
|
|
252
|
+
var note = document.createElement("div"); note.className = "dim-note";
|
|
253
|
+
note.textContent = data.editable
|
|
254
|
+
? "Edits apply live. Aliased tokens (→) follow their source; edit the source to change them. Save writes back to the token source."
|
|
255
|
+
: "Read-only: this source is a module, not JSON. Export to capture edits.";
|
|
256
|
+
wrap.appendChild(note);
|
|
257
|
+
app.appendChild(wrap);
|
|
258
|
+
applyLayer(activeLayer);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function componentPreview() {
|
|
262
|
+
var s = document.createElement("section");
|
|
263
|
+
var h = document.createElement("h2"); h.textContent = "Components"; s.appendChild(h);
|
|
264
|
+
var box = document.createElement("div"); box.className = "preview";
|
|
265
|
+
box.innerHTML =
|
|
266
|
+
'<button style="font:inherit;font-weight:600;padding:10px 18px;border:none;border-radius:var(--radius-md,8px);' +
|
|
267
|
+
'background:var(--color-accent,#4F6BFF);color:var(--color-on-accent,#fff);cursor:pointer">Primary action</button>' +
|
|
268
|
+
'<button style="font:inherit;font-weight:600;padding:10px 18px;border:1px solid var(--color-border,#30363d);' +
|
|
269
|
+
'border-radius:var(--radius-md,8px);background:transparent;color:var(--color-text,#e6edf3);cursor:pointer">Secondary</button>' +
|
|
270
|
+
'<div style="padding:16px;border-radius:var(--radius-lg,12px);background:var(--color-surface,#161b22);' +
|
|
271
|
+
'border:1px solid var(--color-border,#30363d);box-shadow:var(--shadow-md,none);min-width:200px">' +
|
|
272
|
+
'<div style="font-weight:650;color:var(--color-text,#e6edf3)">Card title</div>' +
|
|
273
|
+
'<div style="color:var(--color-text-muted,#8b949e);font-size:13px;margin-top:4px">Body copy in the muted text tone.</div></div>';
|
|
274
|
+
s.appendChild(box);
|
|
275
|
+
return s;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function header() {
|
|
279
|
+
var h = document.createElement("div"); h.className = "studio-header";
|
|
280
|
+
var title = document.createElement("h1"); title.textContent = data.title; h.appendChild(title);
|
|
281
|
+
var layers = ["base"].concat(data.modeNames);
|
|
282
|
+
layers.forEach(function (layer) {
|
|
283
|
+
var b = document.createElement("button"); b.className = "pill" + (layer === activeLayer ? " active" : "");
|
|
284
|
+
b.textContent = layer; b.onclick = function () { activeLayer = layer; render(); };
|
|
285
|
+
h.appendChild(b);
|
|
286
|
+
});
|
|
287
|
+
var sp = document.createElement("div"); sp.className = "spacer"; h.appendChild(sp);
|
|
288
|
+
var exp = document.createElement("button"); exp.className = "btn"; exp.textContent = "Export JSON";
|
|
289
|
+
exp.onclick = exportJson; h.appendChild(exp);
|
|
290
|
+
if (data.editable) {
|
|
291
|
+
var save = document.createElement("button"); save.className = "btn primary"; save.id = "save-btn";
|
|
292
|
+
save.textContent = "Save"; save.disabled = !dirty; save.onclick = saveDoc; h.appendChild(save);
|
|
293
|
+
}
|
|
294
|
+
return h;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function saveDoc() {
|
|
298
|
+
var btn = document.getElementById("save-btn"); btn.disabled = true; btn.textContent = "Saving…";
|
|
299
|
+
fetch("/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(draft) })
|
|
300
|
+
.then(function (r) { if (!r.ok) throw new Error(); return r.json(); })
|
|
301
|
+
.then(function () { dirty = false; btn.textContent = "Save"; toast("Saved & recompiled"); })
|
|
302
|
+
.catch(function () { btn.disabled = false; btn.textContent = "Save"; toast("Save failed"); });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function exportJson() {
|
|
306
|
+
var blob = new Blob([JSON.stringify(draft, null, 2)], { type: "application/json" });
|
|
307
|
+
var a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "tokens.json"; a.click();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function toast(msg) {
|
|
311
|
+
var el = document.createElement("div"); el.className = "saved-toast show"; el.textContent = msg; document.body.appendChild(el);
|
|
312
|
+
setTimeout(function () { el.classList.remove("show"); setTimeout(function () { el.remove(); }, 300); }, 1600);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function add(parent, el) { if (el) parent.appendChild(el); }
|
|
316
|
+
function escapeHtml(s) { return String(s).replace(/[&<>"]/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """ }[c]; }); }
|
|
317
|
+
function toHex(v) {
|
|
318
|
+
v = (v || "").trim();
|
|
319
|
+
if (/^#([0-9a-f]{6})$/i.test(v)) return v;
|
|
320
|
+
if (/^#([0-9a-f]{3})$/i.test(v)) return "#" + v.slice(1).split("").map(function (c) { return c + c; }).join("");
|
|
321
|
+
var m = v.match(/rgba?\\(([^)]+)\\)/i);
|
|
322
|
+
if (m) { var p = m[1].split(",").map(function (x) { return parseInt(x, 10); }); return "#" + p.slice(0, 3).map(function (n) { return ("0" + n.toString(16)).slice(-2); }).join(""); }
|
|
323
|
+
return "#000000";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
render();
|
|
327
|
+
})();
|
|
328
|
+
`;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `defineTheme` — the simple, non-designer entry point.
|
|
3
|
+
*
|
|
4
|
+
* State a handful of decisions in plain values — your brand colour, how round
|
|
5
|
+
* the corners are, which font — and get a **complete** design system back:
|
|
6
|
+
* colour scales, semantic tokens (background, surface, text, border, accent…),
|
|
7
|
+
* and both **light and dark** modes, with readable on-colours chosen for you.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { defineTheme } from "rastack/tokens";
|
|
11
|
+
*
|
|
12
|
+
* export default defineTheme({
|
|
13
|
+
* brand: "#4F6BFF", // one colour is enough — we build the 50–950 scale
|
|
14
|
+
* radius: "rounded", // "sharp" | "rounded" | "pill" | a px number
|
|
15
|
+
* font: "Inter",
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* It returns the same {@link ThemedTokens} that `defineTokens` produces, so it
|
|
20
|
+
* plugs straight into `rastack tokens`, `rastack design`, and `rastack/theme`.
|
|
21
|
+
* `defineTheme` is sugar over `defineTokens`; reach for the raw DSL only when you
|
|
22
|
+
* want per-token control. Everything is overridable — pass `colors`, `radius`,
|
|
23
|
+
* `space`, or `extend` to adjust or add anything.
|
|
24
|
+
*/
|
|
25
|
+
import { ThemedTokens } from "./types";
|
|
26
|
+
export interface ThemeConfig {
|
|
27
|
+
/** Your main brand colour, as a hex string. The rest is derived from it. */
|
|
28
|
+
brand: string;
|
|
29
|
+
/** A secondary accent colour. Defaults to `brand`. */
|
|
30
|
+
accent?: string;
|
|
31
|
+
/** The neutral/grey seed used for backgrounds, surfaces, text and borders. */
|
|
32
|
+
neutral?: string;
|
|
33
|
+
/** Corner roundness: a keyword or a base radius in px. Default `"rounded"`. */
|
|
34
|
+
radius?: "sharp" | "rounded" | "pill" | number;
|
|
35
|
+
/** Body font family (name or stack). Default Inter/system-ui. */
|
|
36
|
+
font?: string | string[];
|
|
37
|
+
/** Which modes to generate. Default `"both"`. */
|
|
38
|
+
appearance?: "light" | "dark" | "both";
|
|
39
|
+
/** Status colours (sensible defaults provided). */
|
|
40
|
+
success?: string;
|
|
41
|
+
danger?: string;
|
|
42
|
+
warning?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Override or add any semantic colour, e.g. `{ accent: "#e11d48" }` or
|
|
45
|
+
* `{ "text-muted": "#888" }`. Applied on top of the generated palette.
|
|
46
|
+
*/
|
|
47
|
+
colors?: Record<string, string>;
|
|
48
|
+
/**
|
|
49
|
+
* Escape hatch: raw DTCG tokens deep-merged over everything (per mode via
|
|
50
|
+
* `{ modes: { dark: { … } } }`), for anything the simple config can't express.
|
|
51
|
+
*/
|
|
52
|
+
extend?: Partial<ThemedTokens>;
|
|
53
|
+
}
|
|
54
|
+
/** Turn a plain intent config into a full themed token document. */
|
|
55
|
+
export declare function defineTheme(config: ThemeConfig): ThemedTokens;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `defineTheme` — the simple, non-designer entry point.
|
|
4
|
+
*
|
|
5
|
+
* State a handful of decisions in plain values — your brand colour, how round
|
|
6
|
+
* the corners are, which font — and get a **complete** design system back:
|
|
7
|
+
* colour scales, semantic tokens (background, surface, text, border, accent…),
|
|
8
|
+
* and both **light and dark** modes, with readable on-colours chosen for you.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { defineTheme } from "rastack/tokens";
|
|
12
|
+
*
|
|
13
|
+
* export default defineTheme({
|
|
14
|
+
* brand: "#4F6BFF", // one colour is enough — we build the 50–950 scale
|
|
15
|
+
* radius: "rounded", // "sharp" | "rounded" | "pill" | a px number
|
|
16
|
+
* font: "Inter",
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* It returns the same {@link ThemedTokens} that `defineTokens` produces, so it
|
|
21
|
+
* plugs straight into `rastack tokens`, `rastack design`, and `rastack/theme`.
|
|
22
|
+
* `defineTheme` is sugar over `defineTokens`; reach for the raw DSL only when you
|
|
23
|
+
* want per-token control. Everything is overridable — pass `colors`, `radius`,
|
|
24
|
+
* `space`, or `extend` to adjust or add anything.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.defineTheme = defineTheme;
|
|
28
|
+
const color_1 = require("./color");
|
|
29
|
+
const define_1 = require("./define");
|
|
30
|
+
const resolve_1 = require("./resolve");
|
|
31
|
+
const RADIUS_KEYWORDS = { sharp: 3, rounded: 10, pill: 18 };
|
|
32
|
+
const DEFAULT_FONT = ["Inter", "system-ui", "-apple-system", "Segoe UI", "Roboto", "sans-serif"];
|
|
33
|
+
/** Turn a plain intent config into a full themed token document. */
|
|
34
|
+
function defineTheme(config) {
|
|
35
|
+
const brand = (0, color_1.scale)(config.brand);
|
|
36
|
+
const accentSeed = config.accent ?? config.brand;
|
|
37
|
+
const accent = (0, color_1.scale)(accentSeed);
|
|
38
|
+
const neutral = (0, color_1.scale)(config.neutral ?? "#64748B");
|
|
39
|
+
const onAccent = (0, color_1.readableOn)(accentSeed);
|
|
40
|
+
const baseRadius = typeof config.radius === "number"
|
|
41
|
+
? config.radius
|
|
42
|
+
: RADIUS_KEYWORDS[config.radius ?? "rounded"];
|
|
43
|
+
const colorScale = (name, s) => Object.fromEntries(Object.entries(s).map(([step, hex]) => [step, { $value: hex }]));
|
|
44
|
+
// --- base tree (also the light appearance) ------------------------------
|
|
45
|
+
const base = {
|
|
46
|
+
color: {
|
|
47
|
+
$type: "color",
|
|
48
|
+
brand: colorScale("brand", brand),
|
|
49
|
+
...(config.accent ? { "accent-scale": colorScale("accent", accent) } : {}),
|
|
50
|
+
// Semantic tokens alias the scales, so overriding a scale flows through.
|
|
51
|
+
accent: { $value: config.accent ? "{color.accent-scale.500}" : "{color.brand.500}" },
|
|
52
|
+
"accent-strong": { $value: config.accent ? "{color.accent-scale.600}" : "{color.brand.600}" },
|
|
53
|
+
"on-accent": { $value: onAccent },
|
|
54
|
+
bg: { $value: "#FFFFFF" },
|
|
55
|
+
surface: { $value: "#FFFFFF" },
|
|
56
|
+
"surface-alt": { $value: neutral["100"] },
|
|
57
|
+
border: { $value: neutral["200"] },
|
|
58
|
+
text: { $value: neutral["900"] },
|
|
59
|
+
"text-muted": { $value: neutral["500"] },
|
|
60
|
+
success: { $value: config.success ?? "#1FA971" },
|
|
61
|
+
danger: { $value: config.danger ?? "#E5484D" },
|
|
62
|
+
warning: { $value: config.warning ?? "#F5A623" },
|
|
63
|
+
},
|
|
64
|
+
space: {
|
|
65
|
+
$type: "dimension",
|
|
66
|
+
xs: { $value: "4px" },
|
|
67
|
+
sm: { $value: "8px" },
|
|
68
|
+
md: { $value: "16px" },
|
|
69
|
+
lg: { $value: "24px" },
|
|
70
|
+
xl: { $value: "40px" },
|
|
71
|
+
},
|
|
72
|
+
radius: {
|
|
73
|
+
$type: "dimension",
|
|
74
|
+
sm: { $value: `${Math.round(baseRadius * 0.6)}px` },
|
|
75
|
+
md: { $value: `${baseRadius}px` },
|
|
76
|
+
lg: { $value: `${Math.round(baseRadius * 1.6)}px` },
|
|
77
|
+
xl: { $value: `${Math.round(baseRadius * 2.4)}px` },
|
|
78
|
+
pill: { $value: "999px" },
|
|
79
|
+
},
|
|
80
|
+
font: {
|
|
81
|
+
family: {
|
|
82
|
+
$type: "fontFamily",
|
|
83
|
+
sans: { $value: config.font ?? DEFAULT_FONT },
|
|
84
|
+
},
|
|
85
|
+
weight: {
|
|
86
|
+
$type: "fontWeight",
|
|
87
|
+
regular: { $value: 400 },
|
|
88
|
+
medium: { $value: 500 },
|
|
89
|
+
bold: { $value: 700 },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
// Apply user colour overrides onto the semantic layer.
|
|
94
|
+
if (config.colors) {
|
|
95
|
+
for (const [name, hex] of Object.entries(config.colors)) {
|
|
96
|
+
base.color[name] = { $value: hex };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// --- dark appearance: only the tokens that differ ------------------------
|
|
100
|
+
const dark = {
|
|
101
|
+
color: {
|
|
102
|
+
// In dark, the accent reads better a step lighter.
|
|
103
|
+
accent: { $value: config.accent ? "{color.accent-scale.400}" : "{color.brand.400}" },
|
|
104
|
+
"accent-strong": { $value: config.accent ? "{color.accent-scale.300}" : "{color.brand.300}" },
|
|
105
|
+
bg: { $value: neutral["950"] },
|
|
106
|
+
surface: { $value: neutral["900"] },
|
|
107
|
+
"surface-alt": { $value: neutral["800"] },
|
|
108
|
+
border: { $value: neutral["700"] },
|
|
109
|
+
text: { $value: neutral["50"] },
|
|
110
|
+
"text-muted": { $value: neutral["400"] },
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
const appearance = config.appearance ?? "both";
|
|
114
|
+
const modes = {};
|
|
115
|
+
if (appearance !== "light")
|
|
116
|
+
modes.dark = dark;
|
|
117
|
+
let themed = {
|
|
118
|
+
tokens: base,
|
|
119
|
+
modes,
|
|
120
|
+
defaultMode: appearance === "dark" ? "dark" : "light",
|
|
121
|
+
};
|
|
122
|
+
// dark-only: fold the dark overrides into the base so :root is dark.
|
|
123
|
+
if (appearance === "dark") {
|
|
124
|
+
themed = { tokens: (0, resolve_1.mergeTokens)(base, dark), modes: {}, defaultMode: "dark" };
|
|
125
|
+
}
|
|
126
|
+
// Escape hatch merges last.
|
|
127
|
+
if (config.extend) {
|
|
128
|
+
if (config.extend.tokens)
|
|
129
|
+
themed.tokens = (0, resolve_1.mergeTokens)(themed.tokens, config.extend.tokens);
|
|
130
|
+
if (config.extend.modes) {
|
|
131
|
+
for (const [name, doc] of Object.entries(config.extend.modes)) {
|
|
132
|
+
themed.modes = themed.modes ?? {};
|
|
133
|
+
themed.modes[name] = themed.modes[name] ? (0, resolve_1.mergeTokens)(themed.modes[name], doc) : doc;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Normalise through defineTokens so the result is identical in shape.
|
|
138
|
+
return (0, define_1.defineTokens)(themed);
|
|
139
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed-theme emitter — the ergonomic, autocompleted output.
|
|
3
|
+
*
|
|
4
|
+
* Emits a `.ts` module exporting a nested `theme` object that mirrors the token
|
|
5
|
+
* tree, whose leaves are `var(--…)` strings, plus a `TokenPath` union of every
|
|
6
|
+
* token name. Application code reads `theme.color.brand[500]` (a real CSS
|
|
7
|
+
* variable reference) with full type-safety and rename-refactoring, while the
|
|
8
|
+
* actual values live in the emitted CSS and stay swappable per mode.
|
|
9
|
+
*/
|
|
10
|
+
import { ResolvedToken } from "./types";
|
|
11
|
+
export interface TsOptions {
|
|
12
|
+
prefix?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Emit the typed-theme `.ts` source for a resolved base token list. */
|
|
15
|
+
export declare function emitTs(tokens: ResolvedToken[], options?: TsOptions): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Typed-theme emitter — the ergonomic, autocompleted output.
|
|
4
|
+
*
|
|
5
|
+
* Emits a `.ts` module exporting a nested `theme` object that mirrors the token
|
|
6
|
+
* tree, whose leaves are `var(--…)` strings, plus a `TokenPath` union of every
|
|
7
|
+
* token name. Application code reads `theme.color.brand[500]` (a real CSS
|
|
8
|
+
* variable reference) with full type-safety and rename-refactoring, while the
|
|
9
|
+
* actual values live in the emitted CSS and stay swappable per mode.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.emitTs = emitTs;
|
|
13
|
+
const resolve_1 = require("./resolve");
|
|
14
|
+
/** A key that is a safe JS identifier can be written bare; otherwise quote it. */
|
|
15
|
+
function keyLiteral(key) {
|
|
16
|
+
return /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
|
|
17
|
+
}
|
|
18
|
+
/** Build a nested plain object from dotted paths → `var(--…)` leaf strings. */
|
|
19
|
+
function nest(tokens, prefix) {
|
|
20
|
+
const root = {};
|
|
21
|
+
for (const tok of tokens) {
|
|
22
|
+
const segments = tok.path.split(".");
|
|
23
|
+
let node = root;
|
|
24
|
+
segments.forEach((seg, i) => {
|
|
25
|
+
if (i === segments.length - 1) {
|
|
26
|
+
node[seg] = `var(${(0, resolve_1.cssVarName)(tok.path, prefix)})`;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
node[seg] = node[seg] ?? {};
|
|
30
|
+
node = node[seg];
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return root;
|
|
35
|
+
}
|
|
36
|
+
function serialize(node, indent) {
|
|
37
|
+
const inner = indent + " ";
|
|
38
|
+
const entries = Object.entries(node).map(([key, val]) => {
|
|
39
|
+
const k = keyLiteral(key);
|
|
40
|
+
if (typeof val === "string")
|
|
41
|
+
return `${inner}${k}: ${JSON.stringify(val)},`;
|
|
42
|
+
return `${inner}${k}: ${serialize(val, inner)},`;
|
|
43
|
+
});
|
|
44
|
+
return `{\n${entries.join("\n")}\n${indent}}`;
|
|
45
|
+
}
|
|
46
|
+
/** Emit the typed-theme `.ts` source for a resolved base token list. */
|
|
47
|
+
function emitTs(tokens, options = {}) {
|
|
48
|
+
const prefix = options.prefix ?? "";
|
|
49
|
+
const tree = nest(tokens, prefix);
|
|
50
|
+
const union = tokens.map((t) => ` | ${JSON.stringify(t.path)}`).join("\n") || " never";
|
|
51
|
+
return `/* Generated by \`rastack tokens\` — do not edit by hand. */
|
|
52
|
+
|
|
53
|
+
/** Every design-token path, for type-safe lookups. */
|
|
54
|
+
export type TokenPath =
|
|
55
|
+
${union};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The design system as a typed tree of CSS-variable references.
|
|
59
|
+
* \`theme.color.brand[500]\` → \`"var(--color-brand-500)"\`.
|
|
60
|
+
*/
|
|
61
|
+
export const theme = ${serialize(tree, "")} as const;
|
|
62
|
+
|
|
63
|
+
/** The CSS custom-property name for a token path. */
|
|
64
|
+
export function cssVar(path: TokenPath): string {
|
|
65
|
+
const body = path
|
|
66
|
+
.split(".")
|
|
67
|
+
.map((s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase())
|
|
68
|
+
.join("-")
|
|
69
|
+
.replace(/[^a-z0-9-]+/g, "-");
|
|
70
|
+
return ${JSON.stringify(prefix ? `--${prefix}-` : "--")} + body;
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
}
|