🌾 docs: give the page the identity its name implies
The library is called cereale and the page was generic dark-developer-docs
with an emoji in the brand mark. Three directions were drafted and scored by
separate judges on identity, legibility and craft; this is the disciplined
one with the two grafts that fixed its own weaknesses.
Palette. Indigo on off-white becomes rye-brown on oat paper, warm in both
themes. The accent is deliberately NOT wheat-gold: gold is both the cliche
and a collision with --warn, which already owns amber, and two amber families
would make "our brand" and "needs your attention" the same colour.
One ornament, used three times. A 2px mark repeated every 9px: as a rule
above each section head, stood on end as the stalk beside each note, and in
place of the three fake macOS traffic lights on every code panel — which were
the most generic pixels on the page. The brand emoji and the emoji favicon
both go, replaced by the same figure drawn as a five-rect SVG: an ear of grain
reduced to its skeleton, which is the section rule stood upright. An emoji is
a different picture on every OS, so it is the one brand element you do not
control.
Grafted from the two directions that lost:
- Ruled links. On a palette this warm, --accent-text and --text-muted sit
1.14:1 apart, so colour alone loses a link in prose. Links now keep body
colour and carry a 2px accent rule, which satisfies 1.4.1 outright.
- --accent-on-code. #editor:focus drew its ring in --accent against the
always-dark slab: 2.86:1, a live 1.4.11 failure on the shipped page. The
new single-valued token measures 7.03:1.
- --border-ui, for icon-only and outlined controls, which were on
--border-strong at 1.60:1. Now 3.54:1.
- --err-line/--err-text, replacing three loose literals.
Two bugs found by rendering rather than by reading. The header overflowed the
viewport by 63px across the whole 641-767px band, hidden rather than fixed by
body{overflow-x:hidden}; the Size link added in the previous commit widened
that to 109px. The nav-link breakpoint moves 640px to 860px, which is where
they actually fit.
Verified: 72 foreground/background pairs computed in both themes, none below
4.5:1 for text or 3:1 for non-text; no horizontal overflow at any of 20
widths from 320 to 1920; no page errors and no external requests in either
theme; the playground still compiles and runs; markup parses with nothing
unclosed; check:docs passes and the generated files stay in sync.
Not taken: recolouring --t-decorator to the brand. It is the best identity
idea in the set — the decorators are what the library is — but the six --t-*
tokens are a shared vocabulary, page.js emits their class names, and the
playground highlights code the reader pastes in. One token, easy to revisit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
This commit is contained in:
+149
-104
@@ -7,7 +7,7 @@
|
||||
<meta name="description" content="Zero-dependency JSON mapping and validation for TypeScript classes. Maps JSON onto your own classes and checks the validation rules against the fields they are attached to, at compile time.">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<link rel="canonical" href="https://avalon-vanguard.github.io/cereale/">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌾</text></svg>">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 18'><rect x='4' width='2' height='18' fill='%23a0784a' opacity='.35'/><rect y='2' width='10' height='2' fill='%23a0784a'/><rect y='6' width='10' height='2' fill='%23a0784a'/><rect y='10' width='10' height='2' fill='%23a0784a'/><rect y='14' width='10' height='2' fill='%23a0784a'/></svg>">
|
||||
|
||||
<!-- Link previews. No og:image: a preview card with a broken image is worse than one
|
||||
without, and there is no artwork to point at yet. -->
|
||||
@@ -22,35 +22,54 @@
|
||||
|
||||
<style>
|
||||
/* ---------------------------------------------------------------- tokens */
|
||||
/* Chaff: one paper, one ink, one repeated 2px mark. The accent is rye-brown and
|
||||
deliberately NOT wheat-gold — gold is both the cliche and a collision with --warn,
|
||||
which already owns amber. Two amber families would make "our brand" and "needs
|
||||
your attention" the same colour.
|
||||
|
||||
Every token below that differs between themes must be written in ALL FOUR places:
|
||||
this block, the prefers-color-scheme block, [data-theme="light"] and
|
||||
[data-theme="dark"]. Miss one and the toggle silently serves the light value. */
|
||||
:root {
|
||||
--bg: #fbfbfd;
|
||||
--bg-raised: #ffffff;
|
||||
--bg-sunken: #f3f3f7;
|
||||
--text: #16161d;
|
||||
--text-muted: #55555f;
|
||||
--text-faint: #6b6b78;
|
||||
--border: #e3e3ea;
|
||||
--border-strong: #cfcfd9;
|
||||
--accent: #4f46e5;
|
||||
--accent-text: #4338ca;
|
||||
--accent-soft: #eef2ff;
|
||||
--bg: #faf7f0;
|
||||
--bg-raised: #fffdf7;
|
||||
--bg-sunken: #f2ede1;
|
||||
--text: #1a1712;
|
||||
--text-muted: #5c5346;
|
||||
--text-faint: #6e6353;
|
||||
--border: #e6dfd1;
|
||||
--border-strong: #cfc5b2;
|
||||
/* Icon-only controls need 3:1 against the page (WCAG 1.4.11); --border-strong is
|
||||
decorative and sits well below it. .icon-btn and .btn-secondary use this instead. */
|
||||
--border-ui: #8e8269;
|
||||
--accent: #8a6238;
|
||||
--accent-text: #7a5530;
|
||||
--accent-soft: #f1e9dc;
|
||||
/* Filled-accent surfaces carry their own foreground: the dark theme lightens --accent
|
||||
for legibility against the page, which then leaves white text on it below AA. */
|
||||
--accent-solid: #4f46e5;
|
||||
--on-accent: #ffffff;
|
||||
--bad: #d4183d;
|
||||
--bad-soft: #fff1f3;
|
||||
--ok: #08795a;
|
||||
--ok-soft: #eefaf5;
|
||||
--warn: #9a5b00;
|
||||
--warn-soft: #fff7ea;
|
||||
--accent-solid: #6b4a28;
|
||||
--on-accent: #fffdf7;
|
||||
--bad: #a82820;
|
||||
--bad-soft: #faebe7;
|
||||
--ok: #256b3d;
|
||||
--ok-soft: #e8f2e9;
|
||||
--warn: #8a5a05;
|
||||
--warn-soft: #fbf1dc;
|
||||
|
||||
/* Code surfaces stay dark in both themes: one syntax palette, always legible. */
|
||||
--code-bg: #16161f;
|
||||
--code-bg-raised: #1e1e29;
|
||||
--code-border: #2b2b3a;
|
||||
--code-text: #d6deeb;
|
||||
--code-faint: #8a93b8;
|
||||
/* Code surfaces stay dark in both themes: one syntax palette, always legible.
|
||||
Warmed off the blue-grey axis so the slab does not read cold against oat paper. */
|
||||
--code-bg: #14120c;
|
||||
--code-bg-raised: #1c190f;
|
||||
--code-border: #2e2818;
|
||||
--code-text: #e2dccb;
|
||||
--code-faint: #9a9280;
|
||||
/* The accent is unreadable on the dark slab in light mode, so focus rings drawn on a
|
||||
code surface get their own token. Without it #editor:focus measures 2.86:1 — a live
|
||||
1.4.11 failure on the shipped page. This measures 6.96:1. */
|
||||
--accent-on-code: #c9962f;
|
||||
/* The error line is the page's one visual device; these were three loose literals. */
|
||||
--err-line: #ff6a7e;
|
||||
--err-text: #ffb3c0;
|
||||
--t-comment: #8a93b8;
|
||||
--t-string: #b8e08a;
|
||||
--t-keyword: #c792ea;
|
||||
@@ -58,64 +77,70 @@
|
||||
--t-type: #ffcb6b;
|
||||
--t-number: #f78c6c;
|
||||
|
||||
/* The whole ornament: a 2px mark every 9px. var() resolves against the winning
|
||||
cascaded value, so --accent changing with the theme retints these automatically —
|
||||
they belong on this block only, never in the three below. */
|
||||
--grain-mark: 2px;
|
||||
--grain-pitch: 9px;
|
||||
--rule-x: repeating-linear-gradient(90deg, var(--accent) 0 var(--grain-mark), transparent var(--grain-mark) var(--grain-pitch));
|
||||
--rule-y: repeating-linear-gradient(180deg, var(--accent) 0 var(--grain-mark), transparent var(--grain-mark) var(--grain-pitch));
|
||||
--rule-code: repeating-linear-gradient(90deg, var(--code-faint) 0 var(--grain-mark), transparent var(--grain-mark) var(--grain-pitch));
|
||||
|
||||
--radius: 10px;
|
||||
--radius-lg: 16px;
|
||||
--shadow: 0 1px 2px rgba(16, 16, 30, .05), 0 8px 24px -12px rgba(16, 16, 30, .18);
|
||||
--shadow: 0 1px 2px rgba(40, 30, 14, .05), 0 8px 24px -12px rgba(40, 30, 14, .20);
|
||||
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
||||
--sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--measure: 68ch;
|
||||
--measure: 64ch;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0e0e14;
|
||||
--bg-raised: #16161f;
|
||||
--bg-sunken: #12121a;
|
||||
--text: #e8e8f0;
|
||||
--text-muted: #a3a3b4;
|
||||
--text-faint: #9494ab;
|
||||
--border: #262633;
|
||||
--border-strong: #363648;
|
||||
--accent: #8b85ff;
|
||||
--accent-text: #a5a0ff;
|
||||
--accent-soft: #1c1b35;
|
||||
--accent-solid: #8b85ff;
|
||||
--on-accent: #10101a;
|
||||
--bad: #ff8095;
|
||||
--bad-soft: #2a1620;
|
||||
--ok: #5cd0a8;
|
||||
--ok-soft: #10241f;
|
||||
--warn: #e5a54b;
|
||||
--warn-soft: #251d10;
|
||||
--code-bg: #12121a;
|
||||
--code-bg-raised: #191922;
|
||||
--shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 24px -12px rgba(0, 0, 0, .7);
|
||||
--bg: #12100b;
|
||||
--bg-raised: #1a1710;
|
||||
--bg-sunken: #16130d;
|
||||
--text: #ede7da;
|
||||
--text-muted: #aba292;
|
||||
--text-faint: #9b9280;
|
||||
--border: #29241a;
|
||||
--border-strong: #3b3427;
|
||||
--border-ui: #736a59;
|
||||
--accent: #c4a97c;
|
||||
--accent-text: #d8be94;
|
||||
--accent-soft: #2a2115;
|
||||
--accent-solid: #d8be94;
|
||||
--on-accent: #17120a;
|
||||
--bad: #f2867e;
|
||||
--bad-soft: #2b1512;
|
||||
--ok: #6fc98c;
|
||||
--ok-soft: #12241a;
|
||||
--warn: #e9b45a;
|
||||
--warn-soft: #261d0c;
|
||||
--shadow: 0 1px 2px rgba(0, 0, 0, .45), 0 8px 24px -12px rgba(0, 0, 0, .7);
|
||||
}
|
||||
}
|
||||
|
||||
/* The toggle wins over the media query in both directions. */
|
||||
:root[data-theme="light"] {
|
||||
--bg: #fbfbfd; --bg-raised: #ffffff; --bg-sunken: #f3f3f7;
|
||||
--text: #16161d; --text-muted: #55555f; --text-faint: #6b6b78;
|
||||
--border: #e3e3ea; --border-strong: #cfcfd9;
|
||||
--accent: #4f46e5; --accent-text: #4338ca; --accent-soft: #eef2ff;
|
||||
--accent-solid: #4f46e5; --on-accent: #ffffff;
|
||||
--bad: #d4183d; --bad-soft: #fff1f3; --ok: #08795a; --ok-soft: #eefaf5;
|
||||
--warn: #9a5b00; --warn-soft: #fff7ea;
|
||||
--code-bg: #16161f; --code-bg-raised: #1e1e29;
|
||||
--shadow: 0 1px 2px rgba(16, 16, 30, .05), 0 8px 24px -12px rgba(16, 16, 30, .18);
|
||||
--bg: #faf7f0; --bg-raised: #fffdf7; --bg-sunken: #f2ede1;
|
||||
--text: #1a1712; --text-muted: #5c5346; --text-faint: #6e6353;
|
||||
--border: #e6dfd1; --border-strong: #cfc5b2; --border-ui: #8e8269;
|
||||
--accent: #8a6238; --accent-text: #7a5530; --accent-soft: #f1e9dc;
|
||||
--accent-solid: #6b4a28; --on-accent: #fffdf7;
|
||||
--bad: #a82820; --bad-soft: #faebe7; --ok: #256b3d; --ok-soft: #e8f2e9;
|
||||
--warn: #8a5a05; --warn-soft: #fbf1dc;
|
||||
--shadow: 0 1px 2px rgba(40, 30, 14, .05), 0 8px 24px -12px rgba(40, 30, 14, .20);
|
||||
color-scheme: light;
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
--bg: #0e0e14; --bg-raised: #16161f; --bg-sunken: #12121a;
|
||||
--text: #e8e8f0; --text-muted: #a3a3b4; --text-faint: #9494ab;
|
||||
--border: #262633; --border-strong: #363648;
|
||||
--accent: #8b85ff; --accent-text: #a5a0ff; --accent-soft: #1c1b35;
|
||||
--accent-solid: #8b85ff; --on-accent: #10101a;
|
||||
--bad: #ff8095; --bad-soft: #2a1620; --ok: #5cd0a8; --ok-soft: #10241f;
|
||||
--warn: #e5a54b; --warn-soft: #251d10;
|
||||
--code-bg: #12121a; --code-bg-raised: #191922;
|
||||
--shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 24px -12px rgba(0, 0, 0, .7);
|
||||
--bg: #12100b; --bg-raised: #1a1710; --bg-sunken: #16130d;
|
||||
--text: #ede7da; --text-muted: #aba292; --text-faint: #9b9280;
|
||||
--border: #29241a; --border-strong: #3b3427; --border-ui: #736a59;
|
||||
--accent: #c4a97c; --accent-text: #d8be94; --accent-soft: #2a2115;
|
||||
--accent-solid: #d8be94; --on-accent: #17120a;
|
||||
--bad: #f2867e; --bad-soft: #2b1512; --ok: #6fc98c; --ok-soft: #12241a;
|
||||
--warn: #e9b45a; --warn-soft: #261d0c;
|
||||
--shadow: 0 1px 2px rgba(0, 0, 0, .45), 0 8px 24px -12px rgba(0, 0, 0, .7);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
@@ -132,17 +157,23 @@ body {
|
||||
color: var(--text);
|
||||
font-family: var(--sans);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
line-height: 1.65;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
h1, h2, h3, h4 { line-height: 1.2; margin: 0; font-weight: 680; letter-spacing: -.02em; }
|
||||
h1 { font-size: clamp(2.1rem, 1.3rem + 3.4vw, 3.5rem); letter-spacing: -.035em; }
|
||||
h2 { font-size: clamp(1.5rem, 1.1rem + 1.6vw, 2.1rem); letter-spacing: -.028em; }
|
||||
h3 { font-size: 1.125rem; }
|
||||
/* 680 and 640 are fiction on a non-variable system stack — they round to 700. And -.035em
|
||||
is generic-landing-page tracking; it is most of what made this look like every other
|
||||
dev-tool site. */
|
||||
h1, h2, h3, h4 { line-height: 1.2; margin: 0; font-weight: 700; letter-spacing: -.012em; }
|
||||
h1 { font-size: clamp(2rem, 1.35rem + 2.8vw, 3rem); line-height: 1.14; letter-spacing: -.018em; }
|
||||
h2 { font-size: clamp(1.4rem, 1.1rem + 1.3vw, 1.85rem); letter-spacing: -.012em; }
|
||||
h3 { font-size: 1.0625rem; letter-spacing: -.008em; }
|
||||
p { margin: 0 0 1rem; }
|
||||
a { color: var(--accent-text); text-decoration-color: color-mix(in srgb, var(--accent) 35%, transparent); text-underline-offset: .18em; }
|
||||
a:hover { text-decoration-color: currentColor; }
|
||||
/* Links keep body colour and are marked by a rule instead. On a palette this warm,
|
||||
--accent-text and --text-muted sit 1.14:1 apart — colour alone would lose them. */
|
||||
a { color: var(--text); text-decoration-color: var(--accent); text-decoration-thickness: 2px; text-underline-offset: .16em; }
|
||||
a:hover { color: var(--accent-text); text-decoration-color: currentColor; }
|
||||
.nav-links a, .foot-grid a, .brand { text-decoration: none; }
|
||||
code, kbd, pre { font-family: var(--mono); }
|
||||
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
|
||||
|
||||
@@ -153,10 +184,17 @@ code, kbd, pre { font-family: var(--mono); }
|
||||
section { padding-block: clamp(3rem, 6vw, 5.5rem); }
|
||||
.lede { color: var(--text-muted); font-size: 1.0625rem; max-width: var(--measure); }
|
||||
.eyebrow {
|
||||
font-size: .75rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
|
||||
color: var(--accent-text); margin: 0 0 .6rem;
|
||||
font-family: var(--mono);
|
||||
font-size: .6875rem; font-weight: 500; letter-spacing: .14em; text-transform: uppercase;
|
||||
color: var(--accent-text); margin: 0 0 .7rem;
|
||||
}
|
||||
.section-head { margin-bottom: 2.25rem; }
|
||||
/* The ornament, use 1 of 3: a section begins. The hero is the page beginning, not a
|
||||
section, so it deliberately has no .section-head and no mark. */
|
||||
.section-head::before {
|
||||
content: ""; display: block; width: 4.5rem; height: 3px;
|
||||
margin-bottom: .95rem; background-image: var(--rule-x);
|
||||
}
|
||||
.skip {
|
||||
position: absolute; left: -9999px; top: 0; z-index: 100;
|
||||
background: var(--accent-solid); color: var(--on-accent); padding: .6rem 1rem; border-radius: 0 0 var(--radius) 0;
|
||||
@@ -175,8 +213,8 @@ header.nav {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.nav-inner { display: flex; align-items: center; gap: 1rem; height: 3.75rem; }
|
||||
.brand { display: flex; align-items: baseline; gap: .5rem; font-weight: 700; letter-spacing: -.02em; font-size: 1.125rem; color: var(--text); text-decoration: none; }
|
||||
.brand .grain { font-size: 1rem; }
|
||||
.brand { display: flex; align-items: center; gap: .5rem; font-weight: 700; letter-spacing: -.02em; font-size: 1.125rem; color: var(--text); text-decoration: none; }
|
||||
.brand .mark { color: var(--accent); flex: none; }
|
||||
.badge {
|
||||
font: 600 .6875rem/1 var(--mono); padding: .3rem .45rem; border-radius: 999px;
|
||||
background: var(--accent-soft); color: var(--accent-text); border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
|
||||
@@ -187,10 +225,13 @@ header.nav {
|
||||
}
|
||||
.nav-links a:hover { color: var(--text); background: var(--bg-sunken); }
|
||||
.nav-links a.ghost { border: 1px solid var(--border-strong); }
|
||||
@media (max-width: 640px) { .nav-hide { display: none; } }
|
||||
/* The section links need ~810px before they stop pushing the header past the viewport.
|
||||
At 640px they already did not fit: the page overflowed by 63px through the whole
|
||||
641-767px band, which body{overflow-x:hidden} hid rather than fixed. */
|
||||
@media (max-width: 860px) { .nav-hide { display: none; } }
|
||||
.icon-btn {
|
||||
display: inline-grid; place-items: center; width: 2.125rem; height: 2.125rem;
|
||||
border: 1px solid var(--border-strong); border-radius: var(--radius);
|
||||
border: 1px solid var(--border-ui); border-radius: var(--radius);
|
||||
background: var(--bg-raised); color: var(--text-muted); cursor: pointer; padding: 0;
|
||||
}
|
||||
.icon-btn:hover { color: var(--text); }
|
||||
@@ -209,14 +250,14 @@ header.nav {
|
||||
}
|
||||
.btn-primary { background: var(--accent-solid); color: var(--on-accent); box-shadow: var(--shadow); }
|
||||
.btn-primary:hover { filter: brightness(1.08); }
|
||||
.btn-secondary { background: var(--bg-raised); color: var(--text); border-color: var(--border-strong); }
|
||||
.btn-secondary { background: var(--bg-raised); color: var(--text); border-color: var(--border-ui); }
|
||||
.btn-secondary:hover { border-color: var(--text-faint); }
|
||||
.fact-row {
|
||||
display: flex; flex-wrap: wrap; gap: .4rem .5rem; margin-top: 1.5rem;
|
||||
font-size: .8125rem; color: var(--text-muted);
|
||||
}
|
||||
.fact { border: 1px solid var(--border); background: var(--bg-raised); border-radius: 999px; padding: .3rem .7rem; }
|
||||
.fact b { color: var(--text); font-weight: 640; }
|
||||
.fact b { color: var(--text); font-weight: 600; }
|
||||
|
||||
/* ---------------------------------------------------------------- code */
|
||||
.code {
|
||||
@@ -228,8 +269,11 @@ header.nav {
|
||||
border-bottom: 1px solid var(--code-border); background: var(--code-bg-raised);
|
||||
font: 500 .75rem/1 var(--mono); color: var(--code-faint);
|
||||
}
|
||||
.code-head .dot { width: .5rem; height: .5rem; border-radius: 50%; background: #33334a; }
|
||||
.code-head .name { margin-left: .35rem; }
|
||||
/* Use 3 of 3, and a deletion: the fake traffic lights are gone. */
|
||||
.code-head::before {
|
||||
content: ""; flex: none; width: 1.05rem; height: 2px;
|
||||
background-image: var(--rule-code);
|
||||
}
|
||||
.code-head .right { margin-left: auto; }
|
||||
.code pre {
|
||||
margin: 0; padding: 1.1rem 1.15rem; overflow-x: auto;
|
||||
@@ -246,18 +290,18 @@ header.nav {
|
||||
|
||||
/* The page's one visual device: the line the compiler refuses. */
|
||||
.ln--error {
|
||||
background: rgba(255, 92, 122, .09);
|
||||
text-decoration: underline wavy #ff5c7a;
|
||||
background: rgba(255, 106, 126, .09);
|
||||
text-decoration: underline wavy var(--err-line);
|
||||
text-decoration-skip-ink: none;
|
||||
text-underline-offset: .32em;
|
||||
}
|
||||
.tsc-error {
|
||||
display: flex; gap: .6rem; align-items: flex-start;
|
||||
margin: 0; padding: .7rem .9rem; border-top: 1px solid var(--code-border);
|
||||
background: rgba(255, 92, 122, .08); color: #ffb3c0;
|
||||
background: rgba(255, 106, 126, .08); color: var(--err-text);
|
||||
font: 500 .78125rem/1.5 var(--mono);
|
||||
}
|
||||
.tsc-error .mark { color: #ff5c7a; flex-shrink: 0; }
|
||||
.tsc-error .mark { color: var(--err-line); flex-shrink: 0; }
|
||||
|
||||
/* --------------------------------------------------------------- panels */
|
||||
.panel {
|
||||
@@ -303,7 +347,7 @@ header.nav {
|
||||
padding: 1.1rem 1.15rem; font: .84375rem/1.75 var(--mono); tab-size: 2; white-space: pre;
|
||||
overflow: auto;
|
||||
}
|
||||
#editor:focus { outline: 2px solid var(--accent); outline-offset: -2px; }
|
||||
#editor:focus { outline: 2px solid var(--accent-on-code); outline-offset: -2px; }
|
||||
#output {
|
||||
flex: 1; min-height: 400px; margin: 0; overflow: auto;
|
||||
background: var(--code-bg); color: var(--code-text); border: 1px solid var(--code-border);
|
||||
@@ -344,7 +388,8 @@ td.note { white-space: normal; color: var(--text-muted); font-size: .875rem; }
|
||||
|
||||
/* --------------------------------------------------------------- prose */
|
||||
.notes { display: grid; gap: .9rem; }
|
||||
.note-item { border-left: 2px solid var(--border-strong); padding-left: 1rem; }
|
||||
/* Use 2 of 3: the same rhythm stood on end. */
|
||||
.note-item { padding-left: 1rem; background: var(--rule-y) left top / 2px 100% no-repeat; }
|
||||
.note-item h3 { font-size: .9375rem; margin-bottom: .25rem; }
|
||||
.note-item p { color: var(--text-muted); font-size: .9375rem; margin: 0; }
|
||||
.callout {
|
||||
@@ -365,7 +410,7 @@ footer { border-top: 1px solid var(--border); padding-block: 2.5rem; color: var(
|
||||
|
||||
<header class="nav">
|
||||
<div class="wrap nav-inner">
|
||||
<a class="brand" href="#top"><span class="grain" aria-hidden="true">🌾</span>cereale <span class="badge" id="version-badge">v0.4.0</span></a>
|
||||
<a class="brand" href="#top"><svg class="mark" width="10" height="18" viewBox="0 0 10 18" aria-hidden="true" focusable="false"><rect x="4" y="0" width="2" height="18" fill="currentColor" opacity=".3"/><rect x="0" y="2" width="10" height="2" fill="currentColor"/><rect x="0" y="6" width="10" height="2" fill="currentColor"/><rect x="0" y="10" width="10" height="2" fill="currentColor"/><rect x="0" y="14" width="10" height="2" fill="currentColor"/></svg>cereale <span class="badge" id="version-badge">v0.4.0</span></a>
|
||||
<nav class="nav-links" aria-label="Primary">
|
||||
<a class="nav-hide" href="#guarantee">Guarantee</a>
|
||||
<a class="nav-hide" href="#playground">Playground</a>
|
||||
@@ -415,7 +460,7 @@ footer { border-top: 1px solid var(--border); padding-block: 2.5rem; color: var(
|
||||
<div>
|
||||
<div class="code">
|
||||
<div class="code-head">
|
||||
<span class="dot" aria-hidden="true"></span><span class="name">order.ts</span>
|
||||
<span class="name">order.ts</span>
|
||||
</div>
|
||||
<pre><code data-lang="ts" data-error-line="9">class Order {
|
||||
@JsonProperty('order_ref')
|
||||
@@ -466,7 +511,7 @@ order.total(); // methods intact</code></pre>
|
||||
<div>
|
||||
<p class="compare-label"><span class="pill pill-bad">compiles, then fails at runtime</span> class-validator</p>
|
||||
<div class="code">
|
||||
<div class="code-head"><span class="dot" aria-hidden="true"></span><span class="name">with legacy decorators</span></div>
|
||||
<div class="code-head"><span class="name">with legacy decorators</span></div>
|
||||
<pre><code data-lang="ts">class User {
|
||||
@IsString()
|
||||
age: number; // accepted by the compiler
|
||||
@@ -479,7 +524,7 @@ order.total(); // methods intact</code></pre>
|
||||
<div>
|
||||
<p class="compare-label"><span class="pill pill-ok">rejected before it runs</span> cereale</p>
|
||||
<div class="code">
|
||||
<div class="code-head"><span class="dot" aria-hidden="true"></span><span class="name">with standard decorators</span></div>
|
||||
<div class="code-head"><span class="name">with standard decorators</span></div>
|
||||
<pre><code data-lang="ts" data-error-line="2">class User {
|
||||
@IsString()
|
||||
age!: number; // Type 'number' is not
|
||||
@@ -523,7 +568,7 @@ order.total(); // methods intact</code></pre>
|
||||
</div>
|
||||
<div class="compare">
|
||||
<div class="code">
|
||||
<div class="code-head"><span class="dot" aria-hidden="true"></span><span class="name">catalogue.ts</span></div>
|
||||
<div class="code-head"><span class="name">catalogue.ts</span></div>
|
||||
<pre><code data-lang="ts">class Media {
|
||||
@IsString() title!: string;
|
||||
}
|
||||
@@ -545,7 +590,7 @@ class Playlist {
|
||||
}</code></pre>
|
||||
</div>
|
||||
<div class="code">
|
||||
<div class="code-head"><span class="dot" aria-hidden="true"></span><span class="name">what comes out</span></div>
|
||||
<div class="code-head"><span class="name">what comes out</span></div>
|
||||
<pre><code data-lang="ts">const list = fromJsonSync(Playlist, body);
|
||||
const first = list.items[0];
|
||||
|
||||
@@ -590,14 +635,14 @@ if (first instanceof Movie) {
|
||||
<div class="pg-grid">
|
||||
<div class="pg-pane">
|
||||
<div class="code-head" style="border:1px solid var(--code-border);border-bottom:none;border-radius:var(--radius-lg) var(--radius-lg) 0 0">
|
||||
<span class="dot" aria-hidden="true"></span><span class="name">playground.ts</span>
|
||||
<span class="name">playground.ts</span>
|
||||
</div>
|
||||
<textarea id="editor" spellcheck="false" autocomplete="off" autocapitalize="off" autocorrect="off"
|
||||
aria-label="TypeScript source to run"></textarea>
|
||||
</div>
|
||||
<div class="pg-pane">
|
||||
<div class="code-head" style="border:1px solid var(--code-border);border-bottom:none;border-radius:var(--radius-lg) var(--radius-lg) 0 0">
|
||||
<span class="dot" aria-hidden="true"></span><span class="name">output</span>
|
||||
<span class="name">output</span>
|
||||
<span class="right" id="pg-status"></span>
|
||||
</div>
|
||||
<pre id="output" aria-live="polite" aria-atomic="false" role="status"></pre>
|
||||
@@ -641,7 +686,7 @@ if (first instanceof Movie) {
|
||||
</div>
|
||||
|
||||
<div style="margin-top:1.5rem" class="code">
|
||||
<div class="code-head"><span class="dot" aria-hidden="true"></span><span class="name">what you get instead</span></div>
|
||||
<div class="code-head"><span class="name">what you get instead</span></div>
|
||||
<pre><code data-lang="text">JsonMappingError: lines[1].tags[0] is a Set, which cannot be serialized to JSON.
|
||||
Give the property a @JsonSerialize() serializer that converts it, or drop it from
|
||||
the output with @JsonIgnore().
|
||||
@@ -719,7 +764,7 @@ Break the cycle with @JsonIgnore() on the back-reference, or supply a
|
||||
</p>
|
||||
|
||||
<div class="code" style="margin-top:1.25rem;max-width:640px">
|
||||
<div class="code-head"><span class="dot" aria-hidden="true"></span><span class="name">vite.config.ts</span></div>
|
||||
<div class="code-head"><span class="name">vite.config.ts</span></div>
|
||||
<pre><code data-lang="ts">import { defineConfig } from 'vite';
|
||||
import { standardDecorators } from 'cereale/vite';
|
||||
|
||||
@@ -751,7 +796,7 @@ export default defineConfig({
|
||||
<div>
|
||||
<p class="compare-label"><span class="pill pill-ok">tsconfig.json</span> what the compiler needs</p>
|
||||
<div class="code">
|
||||
<div class="code-head"><span class="dot" aria-hidden="true"></span><span class="name">tsconfig.json</span></div>
|
||||
<div class="code-head"><span class="name">tsconfig.json</span></div>
|
||||
<pre><code data-lang="text">{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
@@ -771,7 +816,7 @@ export default defineConfig({
|
||||
<div>
|
||||
<p class="compare-label"><span class="pill pill-bad">not on npm yet</span> installing it today</p>
|
||||
<div class="code">
|
||||
<div class="code-head"><span class="dot" aria-hidden="true"></span><span class="name">shell</span></div>
|
||||
<div class="code-head"><span class="name">shell</span></div>
|
||||
<pre><code data-lang="text">git clone https://github.com/avalon-vanguard/cereale
|
||||
cd cereale
|
||||
npm install && npm run build
|
||||
@@ -797,7 +842,7 @@ npm install ../cereale/cereale-0.4.0.tgz</code></pre>
|
||||
a bare <code class="inline-code"><script type="module"></code>, Deno and Workers.
|
||||
</p>
|
||||
<div class="code">
|
||||
<div class="code-head"><span class="dot" aria-hidden="true"></span><span class="name">index.html</span></div>
|
||||
<div class="code-head"><span class="name">index.html</span></div>
|
||||
<pre><code data-lang="text"><script type="importmap">
|
||||
{ "imports": { "cereale": "./node_modules/cereale/dist/cereale.min.js" } }
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user