Files
UnrealPrototyping/.impeccable/design.json
T
2026-09-25 17:02:24 +03:00

267 lines
23 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"schemaVersion": 2,
"generatedAt": "2026-09-18T00:00:00Z",
"title": "Design System: terrain studio",
"extensions": {
"colorMeta": {
"hot": {
"role": "primary",
"displayName": "Brass",
"canonical": "#d6a03a",
"tonalRamp": ["oklch(0.15 0.045 78)", "oklch(0.26 0.070 78)", "oklch(0.37 0.090 78)", "oklch(0.49 0.105 78)", "oklch(0.61 0.115 78)", "oklch(0.73 0.115 78)", "oklch(0.85 0.080 78)", "oklch(0.95 0.035 78)"]
},
"hot-ink": {
"role": "primary",
"displayName": "Brass Ink",
"canonical": "#17181a"
},
"ok": {
"role": "secondary",
"displayName": "Verdigris",
"canonical": "#6fbf73",
"tonalRamp": ["oklch(0.15 0.045 145)", "oklch(0.26 0.075 145)", "oklch(0.37 0.100 145)", "oklch(0.49 0.120 145)", "oklch(0.61 0.130 145)", "oklch(0.73 0.130 145)", "oklch(0.85 0.090 145)", "oklch(0.95 0.040 145)"]
},
"ok-ink": {
"role": "secondary",
"displayName": "Verdigris Light",
"canonical": "#bfe6c1"
},
"warn": {
"role": "tertiary",
"displayName": "Rust",
"canonical": "#b4553f",
"tonalRamp": ["oklch(0.15 0.050 37)", "oklch(0.26 0.080 37)", "oklch(0.37 0.110 37)", "oklch(0.49 0.128 37)", "oklch(0.61 0.130 37)", "oklch(0.73 0.115 37)", "oklch(0.85 0.080 37)", "oklch(0.95 0.035 37)"]
},
"warn-ink": {
"role": "tertiary",
"displayName": "Rust Light",
"canonical": "#f0b4a4"
},
"void": {
"role": "neutral",
"displayName": "Void",
"canonical": "#0b0e11"
},
"well": {
"role": "neutral",
"displayName": "Well",
"canonical": "#11151a"
},
"bg": {
"role": "neutral",
"displayName": "Cold Iron",
"canonical": "#14181d"
},
"panel": {
"role": "neutral",
"displayName": "Panel Iron",
"canonical": "#1b2027",
"tonalRamp": ["#0b0e11", "#11151a", "#14181d", "#1b2027", "#222831", "#252b34", "#262d36", "#2b323b"]
},
"row-hover": {
"role": "neutral",
"displayName": "Iron Hover",
"canonical": "#222831"
},
"row-live": {
"role": "neutral",
"displayName": "Iron Live",
"canonical": "#252b34"
},
"raised": {
"role": "neutral",
"displayName": "Raised Iron",
"canonical": "#262d36"
},
"line": {
"role": "neutral",
"displayName": "Hairline",
"canonical": "#2b323b"
},
"ink": {
"role": "neutral",
"displayName": "Cold Ink",
"canonical": "#d8dee6",
"tonalRamp": ["oklch(0.15 0.010 250)", "oklch(0.26 0.012 250)", "oklch(0.37 0.013 250)", "oklch(0.49 0.014 250)", "oklch(0.61 0.014 250)", "oklch(0.73 0.014 250)", "oklch(0.85 0.013 250)", "oklch(0.95 0.008 250)"]
},
"dim": {
"role": "neutral",
"displayName": "Graphite",
"canonical": "#8b95a1",
"tonalRamp": ["oklch(0.15 0.012 245)", "oklch(0.26 0.016 245)", "oklch(0.37 0.019 245)", "oklch(0.49 0.020 245)", "oklch(0.61 0.020 245)", "oklch(0.73 0.019 245)", "oklch(0.85 0.015 245)", "oklch(0.95 0.008 245)"]
},
"scrim": {
"role": "neutral",
"displayName": "Scrim",
"canonical": "#0d1014ee"
},
"scrim-hud": {
"role": "neutral",
"displayName": "Scrim HUD",
"canonical": "#0d1014dd"
},
"chip-edge": {
"role": "neutral",
"displayName": "Chip Edge",
"canonical": "#0006"
}
},
"typographyMeta": {
"body": {
"displayName": "Body",
"purpose": "Everything the author typed or chose: class names, button labels, input values, toast messages. Every control carries font: inherit, so a number in a field is the same size as the same number in a sentence."
},
"data": {
"displayName": "Data",
"purpose": "Plan reports, bake status and map-key items, set white-space: pre in graphite with <b> in cold ink for the one word that matters."
},
"note": {
"displayName": "Note",
"purpose": "The 11px graphite annotation under a control, saying why rather than what. A first-class role in this system, not a caption."
},
"label": {
"displayName": "Label",
"purpose": "Uppercased, tracked section headings in the rail and map-key titles. Lowercase in the source, uppercased by CSS."
}
},
"shadows": [],
"motion": [
{
"name": "progress-width",
"value": "width .3s",
"purpose": "The only animation in the product. It exists because a bake is a two-hour process and the bar measures something real. No other state change in this system transitions."
}
],
"breakpoints": []
},
"components": [
{
"name": "Primary Button",
"kind": "button",
"refersTo": "button-primary",
"description": "The one action in a group: Plan, Bake. Filled brass with near-black text. Hover is deliberately a no-op here, because the shared button hover only moves the border to brass and this border is brass already.",
"html": "<button class=\"ds-btn-primary\">Plan (4 s)</button>",
"css": ".ds-btn-primary { background: var(--hot, #d6a03a); color: var(--hot-ink, #17181a); border: 1px solid var(--hot, #d6a03a); border-radius: 4px; padding: 6px 10px; font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; font-weight: 600; cursor: pointer; } .ds-btn-primary:disabled { opacity: .5; cursor: default; }"
},
{
"name": "Button",
"kind": "button",
"refersTo": "button",
"description": "Everything that saves, toggles or opens. A raised iron plate with a hairline that turns brass under the cursor; the surface itself never changes and nothing moves.",
"html": "<button class=\"ds-btn\">Save legend</button>",
"css": ".ds-btn { background: var(--raised, #262d36); color: var(--ink, #d8dee6); border: 1px solid var(--line, #2b323b); border-radius: 4px; padding: 6px 10px; font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; cursor: pointer; } .ds-btn:hover { border-color: var(--hot, #d6a03a); } .ds-btn:disabled { opacity: .5; cursor: default; }"
},
{
"name": "Danger Button",
"kind": "button",
"refersTo": "button-warn",
"description": "Rust border and rust-light text on the ordinary raised plate. Exactly one exists in the product, Cancel on a running bake, and it is only rendered while there is something to cancel.",
"html": "<button class=\"ds-btn-warn\">Cancel</button>",
"css": ".ds-btn-warn { background: var(--raised, #262d36); color: var(--warn-ink, #f0b4a4); border: 1px solid var(--warn, #b4553f); border-radius: 4px; padding: 6px 10px; font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; cursor: pointer; } .ds-btn-warn:hover { border-color: var(--hot, #d6a03a); }"
},
{
"name": "Layer Tabs",
"kind": "nav",
"refersTo": "button-toggle-on",
"description": "Two full-width tabs switching the brush between the class painting and the overlay. The active one takes the outlined-brass treatment: filled means act, outlined means live. A tab that cannot apply is disabled with a title rather than hidden.",
"html": "<div class=\"ds-tabs\"><button class=\"ds-tab ds-on\">classes</button><button class=\"ds-tab\">overlay</button></div>",
"css": ".ds-tabs { display: flex; gap: 4px; } .ds-tab { flex: 1; background: var(--raised, #262d36); color: var(--ink, #d8dee6); border: 1px solid var(--line, #2b323b); border-radius: 4px; padding: 4px 6px; font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; cursor: pointer; } .ds-tab:hover { border-color: var(--hot, #d6a03a); } .ds-tab.ds-on { border-color: var(--hot, #d6a03a); color: var(--hot, #d6a03a); font-weight: 600; } .ds-tab:disabled { opacity: .5; cursor: default; }"
},
{
"name": "Class Swatch Row",
"kind": "custom",
"refersTo": "swatch-row",
"description": "The signature control of the rail and a gauge as much as a picker: chip, class name, and a right-aligned reading of what the class does to the ground. The second row shows the selected state; the third is the eraser, whose hatched chip draws absence rather than leaving it blank.",
"html": "<div class=\"ds-sw-list\"><div class=\"ds-sw\"><span class=\"ds-chip\" style=\"background:#5f7d4c\"></span><span class=\"ds-sw-nm\">lowland</span><span class=\"ds-sw-ang\">0.6&deg; plain</span></div><div class=\"ds-sw ds-sw-live\"><span class=\"ds-chip\" style=\"background:#8a6b4a\"></span><span class=\"ds-sw-nm\">upland</span><span class=\"ds-sw-ang\">3.4&deg; rolling</span></div><div class=\"ds-sw\"><span class=\"ds-chip ds-chip-erase\"></span><span class=\"ds-sw-nm\">erase</span><span class=\"ds-sw-ang\"></span></div></div>",
"css": ".ds-sw-list { font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; color: var(--ink, #d8dee6); } .ds-sw { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 4px; border: 1px solid transparent; cursor: pointer; } .ds-sw:hover { background: var(--row-hover, #222831); } .ds-sw.ds-sw-live { border-color: var(--hot, #d6a03a); background: var(--row-live, #252b34); } .ds-chip { width: 18px; height: 18px; border-radius: 3px; border: 1px solid #0006; flex: none; } .ds-chip-erase { background: repeating-linear-gradient(45deg, #3a424c 0 4px, #232931 4px 8px); } .ds-sw-nm { flex: 1; } .ds-sw-ang { color: var(--dim, #8b95a1); font-size: 11px; }"
},
{
"name": "Number Row",
"kind": "input",
"refersTo": "input-number",
"description": "The rail's parameter grammar: a graphite label at flex 1 on the left, a fixed 78px recessed field on the right, and a note underneath saying what the number does to the ground. Nested parameters are indented with two leading spaces in the label text, which is reliable because the font is monospace.",
"html": "<div class=\"ds-num-block\"><div class=\"ds-row\"><label class=\"ds-row-label\">uplift mm/yr</label><input class=\"ds-num\" type=\"number\" step=\"0.005\" value=\"0.12\"></div><div class=\"ds-row\"><label class=\"ds-row-label\">&nbsp;&nbsp;floor mm/yr</label><input class=\"ds-num\" type=\"number\" step=\"0.002\" value=\"0.017\"></div><p class=\"ds-note\">massif <b>3.4&deg;</b> rolling &nbsp;<span style=\"opacity:.6\">(10.2&deg; at a divide)</span></p></div>",
"css": ".ds-num-block { font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; color: var(--ink, #d8dee6); } .ds-row { display: flex; align-items: center; gap: 6px; margin: 3px 0; } .ds-row-label { flex: 1; color: var(--dim, #8b95a1); white-space: pre; } .ds-num { width: 78px; background: var(--well, #11151a); color: var(--ink, #d8dee6); border: 1px solid var(--line, #2b323b); border-radius: 3px; padding: 2px 5px; font: inherit; } .ds-note { color: var(--dim, #8b95a1); font-size: 11px; margin: 4px 0 0; } .ds-note b { color: var(--ink, #d8dee6); font-weight: 600; }"
},
{
"name": "Message Toast",
"kind": "custom",
"refersTo": "toast-error",
"description": "Floats at the stage's top-left over the canvas, inset 10px, at max-width 70%. Darker than any inline surface and translucent, so a message never hides what it is describing. Errors persist until replaced; the ok variant swaps to a verdigris border and removes itself after four seconds.",
"html": "<div class=\"ds-toast ds-toast-err\">This page is served by the studio, not opened from disk.\n\n Tools/Terrain/bin/terrain.exe studio\n\nthen open http://127.0.0.1:8099 in a browser.</div>",
"css": ".ds-toast { background: #0d1014ee; border: 1px solid var(--line, #2b323b); border-radius: 4px; padding: 6px 9px; max-width: 70%; white-space: pre-wrap; font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; color: var(--ink, #d8dee6); } .ds-toast.ds-toast-err { border-color: var(--warn, #b4553f); color: var(--warn-ink, #f0b4a4); } .ds-toast.ds-toast-ok { border-color: var(--ok, #6fbf73); color: var(--ok-ink, #bfe6c1); }"
},
{
"name": "Bake Progress",
"kind": "custom",
"refersTo": "progress-track",
"description": "A 6px well with a brass fill, and the only transition in the entire system. Below it the status block in Data type, with the state word bolded to cold ink and a dimmed tail of the last log lines at 65% opacity.",
"html": "<div class=\"ds-bake\"><div class=\"ds-prog\"><div style=\"width:42%\"></div></div><div class=\"ds-bake-status\"><b>baking</b> 11/26 regions 48.3 min\n 11 -12..1904 m 0.01% clipped 214 s\n 13 0..47 m 0.00% clipped 96 s\n<span style=\"opacity:.65\">region 14: 340 of 900 steps</span></div></div>",
"css": ".ds-bake { font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; } .ds-prog { height: 6px; background: var(--well, #11151a); border-radius: 3px; overflow: hidden; margin: 6px 0; } .ds-prog > div { height: 100%; background: var(--hot, #d6a03a); transition: width .3s; } .ds-bake-status { white-space: pre-wrap; font-size: 11.5px; color: var(--dim, #8b95a1); margin-top: 6px; } .ds-bake-status b { color: var(--ink, #d8dee6); font-weight: 600; }"
},
{
"name": "Map Key",
"kind": "custom",
"refersTo": "ramp",
"description": "Published with every map view and generated by the code that drew that map, so the key cannot drift from the picture. Two shapes: a 12px gradient ramp with its end values beneath for continuous fields, and a two-column chip grid for categorical ones. Under either, a note saying what the map does not mean.",
"html": "<div class=\"ds-key\"><div class=\"ds-key-title\">uplift, mm/yr</div><div class=\"ds-ramp\"></div><div class=\"ds-key-ends\"><span>0</span><span>0.412</span></div><p class=\"ds-note\">The field everything else is a consequence of, massifs included. The cyan lines are fault traces: a rate difference across a line, which erosion carves into a scarp.</p></div>",
"css": ".ds-key { font: 13px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; } .ds-key-title { color: var(--dim, #8b95a1); font-size: 11px; text-transform: uppercase; letter-spacing: .09em; margin-bottom: 4px; } .ds-ramp { height: 12px; border-radius: 2px; border: 1px solid #0006; background: linear-gradient(to right, #141a24, #2f4a52, #5d7a4e, #a8974f, #d8c9b4); } .ds-key-ends { display: flex; justify-content: space-between; color: var(--dim, #8b95a1); font-size: 11px; } .ds-key .ds-note { color: var(--dim, #8b95a1); font-size: 11px; margin: 4px 0 0; }"
},
{
"name": "Monospace Report",
"kind": "custom",
"refersTo": "typography.data",
"description": "The four-second answer the whole product is built around. Columns are padded in the code that generates it, so the alignment is the layout and the monospace font is structural. Mixes a warning, a table and prose in one flow, meant to be read whole.",
"html": "<div class=\"ds-report\">planned in 4.1 s\n\nclass share at a divide\n lowland 61.4% 1.8&deg; plain\n upland 22.7% 10.2&deg; rolling\n 16% massif, rest 0.017 mm/yr 1.4&deg; plain\n range 9.1% 28.6&deg; mountain\n\nthe seed re-rolls, the painting does not\n rock 5 types over 180.0 km provinces\n faults 38 traces on a 60 km grain\n\n26 regions, 118 min of solve estimated</div>",
"css": ".ds-report { white-space: pre; font: 11.5px/1.45 ui-monospace, 'Cascadia Mono', Menlo, Consolas, monospace; color: var(--dim, #8b95a1); }"
}
],
"narrative": {
"northStar": "The Instrument Panel",
"overview": "terrain studio is a gauge with a world in it. The left rail is calibrated measurement — a rate typed in mm/yr comes back as the hillslope angle it buys, a brush size comes back as metres on the ground, a bake comes back as minutes — and the rest of the window is the specimen those numbers describe. Everything in the chrome is either a reading, a control, or the hairline that separates the two. There is no third category, and nothing in the frame exists to be looked at.\n\nThe room is dark because the work is coloured. Every hue that means anything in this product belongs to the painting: a class colour is an uplift rate, a ramp is a field in mm/yr, a region colour is a landmass. So the instrument around it is made of iron and ink, with exactly one warm colour — brass — reserved for the two things the machine can tell you about itself: what you can act on, and what is currently live. A second hue in the chrome would be a colour that looks like data and isn't.\n\nIt is patient, and it explains itself. Under a control that takes a number nobody has an intuition for sits an 11px note in graphite saying what that number does to the ground, and the notes say why rather than what — \"a mark you cannot see is one you will paint over\". That voice is part of the system, not commentary bolted to it. The tool assumes a reader who will read, and it is written for someone who opens it every few weeks rather than every day.",
"keyCharacteristics": [
"One monospace family, three sizes (13px / 11.5px / 11px), no display type anywhere.",
"A fixed 300px rail against a fluid canvas; the canvas absorbs every pixel of a resize.",
"Flat by doctrine: not one box-shadow in the system. Depth is tonal value plus a 1px hairline.",
"Brass (#d6a03a) is the only warm colour and never describes data.",
"State changes are instant. The single animation in the product measures something that actually takes time.",
"Monospace alignment is load-bearing — reports are white-space: pre and the columns are the layout."
],
"rules": [
{ "name": "The One Warm Thing Rule", "body": "Brass is the only warm colour on screen. It marks what you can act on (filled) or what is currently live (outlined), and it never describes data. If a new element needs attention and is neither of those two things, it does not get brass — it gets a note.", "section": "colors" },
{ "name": "The Data Owns Colour Rule", "body": "Hue inside the canvas belongs to the painting, the legend and the map ramps. The chrome is iron, ink, and one accent. Nothing in the frame may be a colour that could be mistaken for something the solve will read.", "section": "colors" },
{ "name": "The Ladder Rule", "body": "The neutrals are ordered by depth, not by taste. A new surface picks its value by answering whether it sits below the panel (a well), on it (a row), or above it (a control). There is no free choice of grey.", "section": "colors" },
{ "name": "The Alignment Is Layout Rule", "body": "Reports are white-space: pre, and their columns come from padEnd/padStart in the code that generates them. Never rebuild one as a table, a grid, or flex rows. The monospace font is structural here, and switching families would silently break every report in the product.", "section": "typography" },
{ "name": "The Lowercase Rule", "body": "Headings are lowercase in the source and uppercased by CSS; labels and buttons are sentence case at most. Nothing in this interface is Title Case. The one exception is a proper noun the domain already capitalises.", "section": "typography" },
{ "name": "The Rail Is Fixed Rule", "body": "300px, never a percentage, and number inputs are 78px. A label and its value must not reflow when the window changes size; the canvas absorbs the entire delta. An author resizing the window is looking at the world, not at the panel.", "section": "layout" },
{ "name": "The Canvas Is Never Chrome Rule", "body": "Nothing in the stage has a background, a border or a panel of its own except the two floating readouts, and both are translucent. The world goes edge to edge.", "section": "layout" },
{ "name": "The No-Shadow Rule", "body": "No box-shadow, no filter: drop-shadow, no glow. If something needs to separate, it changes value or gains a hairline.", "section": "elevation" },
{ "name": "The Two-Direction Rule", "body": "A surface declares its role by which way it moves off the panel: down means you type into it, up means you press it, level means you select it. A new surface that is the same value as the panel is undecided, and undecided is a bug.", "section": "elevation" },
{ "name": "The Scrim Rule", "body": "Anything floating over the canvas is darker than the panel and translucent (#0d1014dd–ee). It never becomes opaque and it never gets lighter, because the point of a floating readout is that you can still see what it is describing.", "section": "elevation" },
{ "name": "The Reserved Border Rule", "body": "Anything that can become selected carries border: 1px solid transparent at rest, so selection changes colour and never geometry. Nothing in this rail is allowed to shift, grow or reflow when you click it.", "section": "shapes" },
{ "name": "The Square Chip Rule", "body": "A colour sample is a 3px-radius square with its own dark edge, at 18px in the rail and 12px in a key. Never a circle, never borderless, never a swatch of the class colour used as a background behind text.", "section": "shapes" }
],
"dos": [
"Do use filled brass for the one action in a group and outlined brass for the one item that is live. Never both in the same role.",
"Do reserve border: 1px solid transparent on anything selectable, so selection never changes geometry.",
"Do put an 11px graphite note under any control whose raw value has no obvious consequence, and write it to say why. The note is part of the control.",
"Do report a number back in the unit the author thinks in — degrees, metres, kilometres, minutes — beside the field that takes it.",
"Do publish a key with every map, generated by the code that drew the map, so the two cannot drift apart.",
"Do pick a new surface's value off the ladder by asking whether it sinks, sits or rises relative to the panel (#11151a / #1b2027 / #262d36), and give it the 1px hairline.",
"Do keep reports in white-space: pre with their columns padded in code.",
"Do disable a control that cannot apply and give it a title saying what would enable it, rather than hiding it."
],
"donts": [
"Don't add a box-shadow, a glow or a drop-shadow anywhere, at any elevation.",
"Don't add a transition to a state change. Hover and selection are instant. The only animation in this system is the bake progress width (.3s), and it earns that because it measures a two-hour process.",
"Don't introduce a second font family or a fourth type size. 13px / 11.5px / 11px in one monospace stack is the entire scale, and a family change would break every column-aligned report.",
"Don't put a hue in the chrome. Colour in this product means an uplift rate, a depth, a rock type or a region; a decorative hue would be indistinguishable from one of those.",
"Don't use brass on anything that is neither actionable nor live — not on a heading, not on a value, not to emphasise a word in a note.",
"Don't let a control change size, padding or position between states.",
"Don't round anything past 4px, and don't make a colour sample a circle.",
"Don't leave a control with only the browser's default focus ring. Number inputs currently do, and it is the one gap in the hairline system: a keyboard user gets a focus treatment this design never specified.",
"Don't ship a second letter-spacing for the label role. The map-key title is at 0.08em against the section heading's 0.09em; that is drift, and 0.09em is the value."
]
}
}