Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d1d5c1090 |
No files matched your search
+3
-22
@@ -206,30 +206,11 @@ registry is orphaned and nothing reads it.
|
|||||||
variable, and set `UNLEASH_URL` to `http://<UNLEASH_IP>:4242/api`.
|
variable, and set `UNLEASH_URL` to `http://<UNLEASH_IP>:4242/api`.
|
||||||
5. Redeploy the application stacks.
|
5. Redeploy the application stacks.
|
||||||
|
|
||||||
### Adding a flag to Unleash
|
|
||||||
|
|
||||||
**Unleash does not create flags by itself.** The SDK reads definitions from the
|
|
||||||
server and never registers anything, and metrics for a flag the server has
|
|
||||||
never heard of are discarded. So a flag declared in `backend/flags.py` will be
|
|
||||||
evaluated on every request, stay `False` forever, and never appear in the UI
|
|
||||||
until someone creates it there by hand.
|
|
||||||
|
|
||||||
For each flag in the registry, create one in Unleash with:
|
|
||||||
|
|
||||||
- **Name** — character for character what `backend/flags.py` declares.
|
|
||||||
snake_case, no hyphens or spaces. A typo produces a flag that looks correct
|
|
||||||
in the UI and is read by nothing.
|
|
||||||
- **Type** — Release. No strategies, constraints or variants: these are plain
|
|
||||||
on/off switches, by design.
|
|
||||||
|
|
||||||
### Turning a feature on
|
### Turning a feature on
|
||||||
|
|
||||||
Toggle the flag in the environment matching the stack you mean: **development**
|
Toggle the flag in the environment you want. Flags appear in the Unleash UI
|
||||||
for staging, **production** for prod. The token in each stack is scoped to one
|
after the backend has evaluated them once, so a newly declared flag shows up
|
||||||
environment, so toggling the other one has no visible effect.
|
shortly after the deploy that introduced it.
|
||||||
|
|
||||||
The SDK refreshes every 15 seconds, so the API reflects the change almost at
|
|
||||||
once; the pages follow on their own schedule, below.
|
|
||||||
|
|
||||||
A flip reaches school pages within about five minutes and place pages within
|
A flip reaches school pages within about five minutes and place pages within
|
||||||
the hour. Next's ISR does the propagating — it revalidates a route at the
|
the hour. Next's ISR does the propagating — it revalidates a route at the
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Guards against light-theme-only CSS.
|
|
||||||
*
|
|
||||||
* The site themes entirely through tokens redefined under
|
|
||||||
* `@media (prefers-color-scheme: dark)`. A hardcoded colour therefore does not
|
|
||||||
* fail loudly — it renders perfectly in the theme it was written for and
|
|
||||||
* quietly wrongly in the other, which nobody sees unless they happen to be in
|
|
||||||
* dark mode when they look.
|
|
||||||
*
|
|
||||||
* Both rules below are drawn from real defects in SchoolHeroMap.module.css,
|
|
||||||
* found by eye rather than by any test:
|
|
||||||
*
|
|
||||||
* - the map's fade to the header ramped through hardcoded white and landed on
|
|
||||||
* `var(--bg-card)`. Invisible in light; a bright band across the full width
|
|
||||||
* of a near-black card in dark.
|
|
||||||
* - the controls floating over the map paired a hardcoded white background
|
|
||||||
* with `color: var(--text-primary)`, which resolves to #E9EEF0 in dark —
|
|
||||||
* near-white text on a near-white button.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const COMPONENTS = path.join(__dirname, '..', '..', 'components');
|
|
||||||
|
|
||||||
function stylesheets(dir: string): string[] {
|
|
||||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
||||||
const full = path.join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) return stylesheets(full);
|
|
||||||
return entry.name.endsWith('.module.css') ? [full] : [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Innermost `selector { body }` pairs. Nested at-rules never match as rules,
|
|
||||||
* because their body contains braces. */
|
|
||||||
function rules(css: string): Array<{ selector: string; body: string }> {
|
|
||||||
return Array.from(css.matchAll(/([^{}]+)\{([^{}]*)\}/g), (m) => ({
|
|
||||||
selector: m[1].trim().split('\n').pop()!.trim(),
|
|
||||||
body: m[2],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const HARDCODED_WHITE_BG = /background[^;]*(?:255,\s*255,\s*255|#fff\b|#ffffff\b)/i;
|
|
||||||
const THEMED_COLOR = /(?:^|[^-])color:\s*var\(--/;
|
|
||||||
|
|
||||||
const files = stylesheets(COMPONENTS);
|
|
||||||
|
|
||||||
describe('dark-theme safety', () => {
|
|
||||||
it('finds stylesheets to check', () => {
|
|
||||||
expect(files.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('never pairs a hardcoded white background with a themed text colour', () => {
|
|
||||||
const offenders = files.flatMap((file) =>
|
|
||||||
rules(fs.readFileSync(file, 'utf8'))
|
|
||||||
.filter((r) => HARDCODED_WHITE_BG.test(r.body) && THEMED_COLOR.test(r.body))
|
|
||||||
.map((r) => `${path.relative(COMPONENTS, file)} ${r.selector}`));
|
|
||||||
|
|
||||||
// Either the surface follows the theme and so should the text, or it does
|
|
||||||
// not and the text must be literal too. Mixing them is how near-white text
|
|
||||||
// ends up on a near-white button.
|
|
||||||
expect(offenders).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('never fades to a themed colour through a hardcoded one', () => {
|
|
||||||
const offenders = files.flatMap((file) =>
|
|
||||||
rules(fs.readFileSync(file, 'utf8'))
|
|
||||||
.filter((r) => /linear-gradient/.test(r.body)
|
|
||||||
&& /var\(--bg-(card|primary|secondary)\)/.test(r.body)
|
|
||||||
&& /255,\s*255,\s*255|#fff\b/i.test(r.body))
|
|
||||||
.map((r) => `${path.relative(COMPONENTS, file)} ${r.selector}`));
|
|
||||||
|
|
||||||
// A gradient that lands on a token has to be made of that token, or the
|
|
||||||
// ramp and its destination disagree in one theme. Use the matching
|
|
||||||
// `--*-rgb` token for the transparent stops.
|
|
||||||
expect(offenders).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -28,9 +28,6 @@
|
|||||||
--bg-primary: #FAFAF8; /* Warm White */
|
--bg-primary: #FAFAF8; /* Warm White */
|
||||||
--bg-secondary: #F5EFE6; /* Sand — hero panels, sunken rows */
|
--bg-secondary: #F5EFE6; /* Sand — hero panels, sunken rows */
|
||||||
--bg-card: #FFFFFF;
|
--bg-card: #FFFFFF;
|
||||||
/* For gradients that have to fade to the card colour. A hardcoded white
|
|
||||||
ramp reads as a bright band against a dark card. */
|
|
||||||
--bg-card-rgb: 255, 255, 255;
|
|
||||||
--surface-inverse: #0F766E;
|
--surface-inverse: #0F766E;
|
||||||
|
|
||||||
/* ── Ink ────────────────────────────────────────────────────────── */
|
/* ── Ink ────────────────────────────────────────────────────────── */
|
||||||
@@ -237,7 +234,6 @@
|
|||||||
--bg-primary: #111A20;
|
--bg-primary: #111A20;
|
||||||
--bg-secondary: #16222A;
|
--bg-secondary: #16222A;
|
||||||
--bg-card: #18242C;
|
--bg-card: #18242C;
|
||||||
--bg-card-rgb: 24, 36, 44;
|
|
||||||
--surface-inverse: #E9EEF0;
|
--surface-inverse: #E9EEF0;
|
||||||
|
|
||||||
--text-primary: #E9EEF0;
|
--text-primary: #E9EEF0;
|
||||||
|
|||||||
@@ -47,13 +47,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background:
|
background:
|
||||||
/* Sweeps toward the card colour, which is a shade lighter than this
|
linear-gradient(100deg, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, .5) 50%, rgba(255, 255, 255, 0) 60%) var(--bg-secondary);
|
||||||
ground in both themes. Hardcoded white was a bright flash across a
|
|
||||||
dark page every 1.4s while the tiles loaded. */
|
|
||||||
linear-gradient(100deg,
|
|
||||||
rgba(var(--bg-card-rgb), 0) 40%,
|
|
||||||
rgba(var(--bg-card-rgb), .5) 50%,
|
|
||||||
rgba(var(--bg-card-rgb), 0) 60%) var(--bg-secondary);
|
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
animation: shimmer 1.4s infinite;
|
animation: shimmer 1.4s infinite;
|
||||||
}
|
}
|
||||||
@@ -82,15 +76,6 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Controls that float ON the map.
|
|
||||||
*
|
|
||||||
* The map tiles are light in both themes, so these deliberately do NOT follow
|
|
||||||
* the theme — they follow the map. The literal ink below is the point: paired
|
|
||||||
* with a hardcoded white background, `color: var(--text-primary)` resolved to
|
|
||||||
* #E9EEF0 in the dark theme and put near-white text on a near-white button.
|
|
||||||
* A themed token is the wrong tool for a surface that never changes.
|
|
||||||
*/
|
|
||||||
.openHint {
|
.openHint {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -100,8 +85,7 @@
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
/* See "Controls that float ON the map" above. */
|
color: var(--text-primary);
|
||||||
color: #1C2731;
|
|
||||||
background: rgba(255, 255, 255, .85);
|
background: rgba(255, 255, 255, .85);
|
||||||
-webkit-backdrop-filter: blur(6px);
|
-webkit-backdrop-filter: blur(6px);
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px);
|
||||||
@@ -129,18 +113,11 @@
|
|||||||
on top of the blend. */
|
on top of the blend. */
|
||||||
z-index: 450;
|
z-index: 450;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
/* The card colour, not white.
|
|
||||||
This ramp was hardcoded white and ended at var(--bg-card). In the light
|
|
||||||
theme that is white into white and invisible, as intended. In the dark
|
|
||||||
theme it climbed to 95% WHITE and then met a near-black card — a bright
|
|
||||||
band across the full width, right where the map is supposed to dissolve
|
|
||||||
into the header. Fading to the same colour the gradient lands on is the
|
|
||||||
whole trick, and it only works if that colour is a token. */
|
|
||||||
background: linear-gradient(to bottom,
|
background: linear-gradient(to bottom,
|
||||||
rgba(var(--bg-card-rgb), 0) 0%,
|
rgba(255, 255, 255, 0) 0%,
|
||||||
rgba(var(--bg-card-rgb), .35) 35%,
|
rgba(255, 255, 255, .35) 35%,
|
||||||
rgba(var(--bg-card-rgb), .75) 62%,
|
rgba(255, 255, 255, .75) 62%,
|
||||||
rgba(var(--bg-card-rgb), .95) 82%,
|
rgba(255, 255, 255, .95) 82%,
|
||||||
var(--bg-card) 100%);
|
var(--bg-card) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,8 +134,7 @@
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: rgba(255, 255, 255, .92);
|
background: rgba(255, 255, 255, .92);
|
||||||
/* See "Controls that float ON the map" above. */
|
color: var(--text-primary);
|
||||||
color: #1C2731;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 2px 10px rgba(var(--shadow-rgb), .2);
|
box-shadow: 0 2px 10px rgba(var(--shadow-rgb), .2);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user