Repository navigation
feat(core): map design tokens to DESIGN.md fields #490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
wudilyy999
wants to merge
12
commits into
OpenCoworkAI:main
Choose a base branch
from
wudilyy999:feat/design-system-token-mapping
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,499
−18
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
6d16dc0
fix(core): classify Tailwind v4 --font-* variables as font families
wudilyy999 d752fc2
feat(core): record the theme block of CSS variable tokens
wudilyy999 77bf5f8
feat(core): convert CSS colors to the sRGB hex DESIGN.md requires
wudilyy999 5034476
feat(shared): serialize a DESIGN.md document
wudilyy999 86ccc5b
feat(core): map design tokens to DESIGN.md fields
wudilyy999 f269488
fix(core): match CSS color functions in linear time
wudilyy999 f90c579
fix(core): bound nested var() in the token mapping
wudilyy999 189c1f1
fix(core): accept the CSS spellings of a length DESIGN.md does not take
wudilyy999 224c3b0
refactor(shared): derive the DESIGN.md allowed fields and the seriali…
wudilyy999 241fc55
fix(core): export UnresolvedReason next to UnsupportedReason
wudilyy999 ee2952d
fix(core): expand each CSS variable once
wudilyy999 0c4cc7c
chore: run the checks again
wudilyy999 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@open-codesign/core": patch | ||
| "@open-codesign/shared": patch | ||
| --- | ||
|
|
||
| Prepare the design-token import. Core exports `mapTokensToDesignFields`, which turns scanned tokens into DESIGN.md colors, typography, rounded and spacing fields, with font sizes, line heights and shadows as Markdown sections. It converts `rgb()`, `hsl()` and `oklch()` colors to sRGB hex (a clamped out-of-gamut color is reported with its original value), replaces `var()` with the scanned token it names, leaves other `[data-theme]` values out, and returns what it could not map instead of logging it. Shared exports `serializeDesignMd`. Tailwind v4 `--font-*` variables are now read as font families, and a CSS variable declared in a `[data-theme]` block records that block. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { cssColorToHex } from './color.js'; | ||
|
|
||
| const ok = (hex: string, clamped = false) => ({ ok: true, hex, clamped }); | ||
|
|
||
| describe('cssColorToHex()', () => { | ||
| describe('hex', () => { | ||
| it.each([ | ||
| ['#fff', '#ffffff'], | ||
| ['#ABC', '#aabbcc'], | ||
| ['#FFAA00', '#ffaa00'], | ||
| ['#f00f', '#ff0000'], | ||
| ['#ff0000ff', '#ff0000'], | ||
| ])('%s -> %s', (input, hex) => { | ||
| expect(cssColorToHex(input)).toEqual(ok(hex)); | ||
| }); | ||
|
|
||
| it.each(['#f008', '#ff000080'])('reports alpha for %s', (input) => { | ||
| expect(cssColorToHex(input)).toEqual({ ok: false, reason: 'alpha' }); | ||
| }); | ||
|
|
||
| it.each(['#ggg', '#12345', '#', 'fff'])('reports the format of %s', (input) => { | ||
| expect(cssColorToHex(input)).toEqual({ ok: false, reason: 'format' }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('rgb()', () => { | ||
| it.each([ | ||
| ['rgb(255, 0, 0)', '#ff0000'], | ||
| ['rgb(255 128 0)', '#ff8000'], | ||
| ['rgba(255, 0, 0, 1)', '#ff0000'], | ||
| ['rgb(255 0 0 / 100%)', '#ff0000'], | ||
| ['rgb(100% 50% 0%)', '#ff8000'], | ||
| ['RGB(0,0,0)', '#000000'], | ||
| ])('%s -> %s', (input, hex) => { | ||
| expect(cssColorToHex(input)).toEqual(ok(hex)); | ||
| }); | ||
|
|
||
| it.each([ | ||
| 'rgb(255 0 0 / 50%)', | ||
| 'rgba(0, 0, 0, .5)', | ||
| 'rgb(0 0 0 / 0)', | ||
| ])('reports alpha for %s', (input) => { | ||
| expect(cssColorToHex(input)).toEqual({ ok: false, reason: 'alpha' }); | ||
| }); | ||
|
|
||
| it('clamps a channel outside 0-255 and says so', () => { | ||
| expect(cssColorToHex('rgb(300 -4 0)')).toEqual(ok('#ff0000', true)); | ||
| }); | ||
|
|
||
| it.each([ | ||
| 'rgb(1 2)', | ||
| 'rgb(1 2 3 4 5)', | ||
| 'rgb(1 2 3 4)', | ||
| 'rgb(1, 2, 3, 4, 5)', | ||
| 'rgb(1 2 3 / 0.5 / 0.2)', | ||
| 'rgb()', | ||
| 'rgb(1deg 2 3)', | ||
| 'rgb(red)', | ||
| ])('reports the format of %s', (input) => { | ||
| expect(cssColorToHex(input)).toEqual({ ok: false, reason: 'format' }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('hsl()', () => { | ||
| it.each([ | ||
| ['hsl(0 100% 50%)', '#ff0000'], | ||
| ['hsl(120, 100%, 25%)', '#008000'], | ||
| ['hsl(210 50% 40%)', '#336699'], | ||
| ['hsl(30deg 80% 60%)', '#eb9947'], | ||
| ['hsl(-120 100% 50%)', '#0000ff'], | ||
| ['hsl(0.5turn 100% 50%)', '#00ffff'], | ||
| ['hsl(200grad 100% 50%)', '#00ffff'], | ||
| ['hsla(0, 0%, 100%, 1)', '#ffffff'], | ||
| ['hsl(359 100% 50%)', '#ff0004'], | ||
| ])('%s -> %s', (input, hex) => { | ||
| expect(cssColorToHex(input)).toEqual(ok(hex)); | ||
| }); | ||
|
|
||
| it('reports alpha', () => { | ||
| expect(cssColorToHex('hsl(0 100% 50% / 0.5)')).toEqual({ ok: false, reason: 'alpha' }); | ||
| }); | ||
|
|
||
| it.each([ | ||
| 'hsl(0 100 50)', | ||
| 'hsl(0 100%)', | ||
| 'hsl(red 100% 50%)', | ||
| ])('reports the format of %s', (input) => { | ||
| expect(cssColorToHex(input)).toEqual({ ok: false, reason: 'format' }); | ||
| }); | ||
| }); | ||
|
|
||
| // Expected values come from a separate implementation: the CSS Color 4 reference route | ||
| // (OKLab -> LMS -> XYZ D65 -> linear sRGB), not the matrices used in color.ts. The Tailwind | ||
| // v4 entries match the hex values Tailwind documents for those palette steps. | ||
| describe('oklch()', () => { | ||
| it.each([ | ||
| ['oklch(1 0 0)', '#ffffff'], | ||
| ['oklch(0 0 0)', '#000000'], | ||
| ['oklch(0.5 0 0)', '#636363'], | ||
| ['oklch(0.627955 0.257683 29.2339)', '#ff0000'], | ||
| ['oklch(0.452014 0.313214 264.052)', '#0000ff'], | ||
| ['oklch(0.86644 0.294827 142.495)', '#00ff00'], | ||
| ['oklch(0.7 0.15 250)', '#4ba3f7'], | ||
| ['oklch(0.5 0.1 0)', '#904961'], | ||
| ['oklch(0.75 0.12 120)', '#a4b95e'], | ||
| ['oklch(0.637 0.237 25.331)', '#fb2c36'], | ||
| ['oklch(0.551 0.027 264.364)', '#6a7282'], | ||
| ['oklch(0.97 0.014 254.604)', '#eff6ff'], | ||
| ])('in gamut: %s -> %s', (input, hex) => { | ||
| expect(cssColorToHex(input)).toEqual(ok(hex)); | ||
| }); | ||
|
|
||
| it.each([ | ||
| ['oklch(0.7 0.4 150)', '#00d600'], | ||
| ['oklch(0.9 0.35 200)', '#00ffff'], | ||
| ['oklch(0.3 0.3 30)', '#8f0000'], | ||
| ])('out of gamut: %s is clamped to %s and says so', (input, hex) => { | ||
| expect(cssColorToHex(input)).toEqual(ok(hex, true)); | ||
| }); | ||
|
|
||
| // Only a clamp that changes a channel by a full step counts: the hex is the same as the | ||
| // rounded unclamped color, so a channel 0.06 steps outside sRGB is not reported. | ||
| it.each([ | ||
| ['oklch(0.627955 0.2577 29.2339)', false], | ||
| ['oklch(0.627955 0.258 29.2339)', true], | ||
| ['oklch(1.001 0 0)', false], | ||
| ['oklch(1.002 0 0)', true], | ||
| ['oklch(1.01 0 0)', true], | ||
| ])('reports %s as clamped: %s', (input, clamped) => { | ||
| const result = cssColorToHex(input); | ||
| expect(result).toMatchObject({ ok: true, clamped }); | ||
| expect(result).toHaveProperty('hex', input.startsWith('oklch(0.6') ? '#ff0000' : '#ffffff'); | ||
| }); | ||
|
|
||
| it('treats an achromatic color as a gray of the same lightness', () => { | ||
| expect(cssColorToHex('oklch(0.5 0 0)')).toEqual(cssColorToHex('oklch(0.5 0 217)')); | ||
| expect(cssColorToHex('oklch(0.5 none none)')).toEqual(ok('#636363')); | ||
| }); | ||
|
|
||
| it.each([ | ||
| ['oklch(50% 0 0)', '#636363'], | ||
| ['OKLCH(0.5 0% 0deg)', '#636363'], | ||
| ['oklch(0.75 30% 120)', '#a4b95e'], | ||
| ['oklch(0.75 0.12 0.3333333333turn)', '#a4b95e'], | ||
| ['oklch(0.75 0.12 133.33333grad)', '#a4b95e'], | ||
| ['oklch(0.75 0.12 2.0943951rad)', '#a4b95e'], | ||
| ['oklch(0.5 0.1 0 / 1)', '#904961'], | ||
| ['oklch(0.5 0.1 0 / 100%)', '#904961'], | ||
| ])('accepts the notation %s', (input, hex) => { | ||
| expect(cssColorToHex(input)).toEqual(ok(hex)); | ||
| }); | ||
|
|
||
| it('reports alpha', () => { | ||
| expect(cssColorToHex('oklch(0.5 0.1 0 / 50%)')).toEqual({ ok: false, reason: 'alpha' }); | ||
| }); | ||
|
|
||
| it.each([ | ||
| 'oklch(0.5 0.1)', | ||
| 'oklch(0.5 0.1 0 0)', | ||
| 'oklch(0.5, 0.1, 0)', | ||
| 'oklch(from red l c h)', | ||
| 'oklch(0.5 0.1 0px)', | ||
| 'oklch(0.5deg 0.1 0)', | ||
| ])('reports the format of %s', (input) => { | ||
| expect(cssColorToHex(input)).toEqual({ ok: false, reason: 'format' }); | ||
| }); | ||
| }); | ||
|
|
||
| it.each([ | ||
| 'red', | ||
| 'transparent', | ||
| 'currentColor', | ||
| 'var(--brand)', | ||
| 'lab(50 20 20)', | ||
| 'color-mix(in srgb, red, blue)', | ||
| '', | ||
| ])('reports the format of %j', (input) => { | ||
| expect(cssColorToHex(input)).toEqual({ ok: false, reason: 'format' }); | ||
| }); | ||
| // The input is CSS from a folder the user picked, which can be someone else's repository. | ||
| describe('long input', () => { | ||
| const LONG = 100_000; | ||
| const fast = (value: string) => { | ||
| const start = performance.now(); | ||
| const result = cssColorToHex(value); | ||
| expect(performance.now() - start).toBeLessThan(250); | ||
| return result; | ||
| }; | ||
|
|
||
| it.each([ | ||
| ['whitespace inside the parentheses', `rgb(${'\t'.repeat(LONG)}(`], | ||
| ['whitespace before the closing parenthesis', `rgb(1${' '.repeat(LONG)}x)`], | ||
| ['digits before a bad suffix', `rgb(${'1'.repeat(LONG)}x 0 0)`], | ||
| ['an exponent without digits', `rgb(1${'e'.repeat(LONG)} 0 0)`], | ||
| ])('is rejected in linear time: %s', (_name, input) => { | ||
| expect(fast(input)).toEqual({ ok: false, reason: 'format' }); | ||
| }); | ||
|
|
||
| it('still converts a long run of whitespace between valid parts', () => { | ||
| expect( | ||
| fast( | ||
| `rgb(${' '.repeat(LONG)}255${' '.repeat(LONG)}0${' '.repeat(LONG)}0${' '.repeat(LONG)})`, | ||
| ), | ||
| ).toEqual(ok('#ff0000')); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| /** | ||
| * Converts CSS color values to the sRGB hex that DESIGN.md colors require. | ||
| * | ||
| * Handles hex, `rgb()`/`rgba()`, `hsl()`/`hsla()` and `oklch()`. A color with an alpha | ||
| * below 1 or in another format is reported, never approximated. `oklch()` goes | ||
| * OKLab → linear sRGB → gamma; a channel outside sRGB is clamped to [0, 1] and the | ||
| * result says so, so the caller can show what changed. | ||
| */ | ||
|
|
||
| export type ColorConversion = | ||
| | { | ||
| ok: true; | ||
| /** Lowercase `#rrggbb`. */ | ||
| hex: string; | ||
| /** True when a channel was outside sRGB and was clamped, so the hex is not the exact color. */ | ||
| clamped: boolean; | ||
| } | ||
| | { ok: false; reason: 'alpha' | 'format' }; | ||
|
|
||
| const FORMAT: ColorConversion = { ok: false, reason: 'format' }; | ||
| const NUMBER = '[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?'; | ||
| const COMPONENT_RE = new RegExp(`^(${NUMBER})(%|deg|grad|rad|turn)?$`, 'i'); | ||
| // The body is matched greedily and split later, so whitespace has one way to match. | ||
| const FUNCTION_RE = /^(rgba?|hsla?|oklch)\(([^()]*)\)$/i; | ||
| const HEX_RE = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i; | ||
|
|
||
| interface Component { | ||
| value: number; | ||
| unit: string; | ||
| } | ||
|
|
||
| const isNumber = (value: number | null | undefined): value is number => typeof value === 'number'; | ||
|
|
||
| // `none` is the missing-component keyword; it converts as 0. | ||
| function parseComponent(raw: string): Component | null { | ||
| if (raw.toLowerCase() === 'none') return { value: 0, unit: '' }; | ||
| const match = COMPONENT_RE.exec(raw); | ||
| if (match === null) return null; | ||
| return { value: Number(match[1]), unit: (match[2] ?? '').toLowerCase() }; | ||
| } | ||
|
|
||
| function hueDegrees({ value, unit }: Component): number | null { | ||
| if (unit === '' || unit === 'deg') return value; | ||
| if (unit === 'grad') return value * 0.9; | ||
| if (unit === 'rad') return (value * 180) / Math.PI; | ||
| if (unit === 'turn') return value * 360; | ||
| return null; | ||
| } | ||
|
|
||
| function alphaValue({ value, unit }: Component): number | null { | ||
| if (unit === '') return value; | ||
| return unit === '%' ? value / 100 : null; | ||
| } | ||
|
|
||
| interface ColorArguments { | ||
| channels: string[]; | ||
| alpha?: string; | ||
| /** The legacy comma syntax, where a fourth value is the alpha; `oklch()` has no such syntax. */ | ||
| comma: boolean; | ||
| } | ||
|
|
||
| /** Splits `a b c / d` and the legacy `a, b, c, d` into the channels and the optional alpha. */ | ||
| function splitArguments(body: string): ColorArguments | null { | ||
| if (body.includes(',')) { | ||
| const parts = body.split(',').map((part) => part.trim()); | ||
| const alpha = parts.length === 4 ? parts[3] : undefined; | ||
| const channels = alpha === undefined ? parts : parts.slice(0, 3); | ||
| return alpha === undefined ? { channels, comma: true } : { channels, alpha, comma: true }; | ||
| } | ||
| const sections = body.split('/').map((part) => part.trim()); | ||
| if (sections.length > 2) return null; | ||
| const [channelText = '', alpha] = sections; | ||
| const channels = channelText.split(/\s+/).filter((part) => part !== ''); | ||
| return alpha === undefined ? { channels, comma: false } : { channels, alpha, comma: false }; | ||
| } | ||
|
|
||
| function hex(channels: [number, number, number]): ColorConversion { | ||
| const clamped = channels.some((channel) => channel < 0 || channel > 255); | ||
| const digits = channels | ||
| .map((channel) => Math.max(0, Math.min(255, channel)).toString(16).padStart(2, '0')) | ||
| .join(''); | ||
| return { ok: true, hex: `#${digits}`, clamped }; | ||
| } | ||
|
|
||
| const eightBit = (unit: number): number => Math.round(unit * 255); | ||
|
|
||
| function fromHex(digits: string): ColorConversion { | ||
| const full = digits.length <= 4 ? [...digits].map((digit) => digit + digit).join('') : digits; | ||
| if (full.length === 8 && Number.parseInt(full.slice(6), 16) < 255) { | ||
| return { ok: false, reason: 'alpha' }; | ||
| } | ||
| return { ok: true, hex: `#${full.slice(0, 6).toLowerCase()}`, clamped: false }; | ||
| } | ||
|
|
||
| function srgbChannel({ value, unit }: Component): number | null { | ||
| if (unit === '%') return eightBit(value / 100); | ||
| return unit === '' ? Math.round(value) : null; | ||
| } | ||
|
|
||
| function fromRgb(components: Component[]): ColorConversion { | ||
| const [r, g, b] = components.map(srgbChannel); | ||
| if (components.length !== 3 || !isNumber(r) || !isNumber(g) || !isNumber(b)) return FORMAT; | ||
| return hex([r, g, b]); | ||
| } | ||
|
|
||
| function fromHsl(components: Component[]): ColorConversion { | ||
| const [h, s, l] = components; | ||
| if (components.length !== 3 || h === undefined || s?.unit !== '%' || l?.unit !== '%') { | ||
| return FORMAT; | ||
| } | ||
| const degrees = hueDegrees(h); | ||
| if (degrees === null) return FORMAT; | ||
| const saturation = Math.max(0, Math.min(1, s.value / 100)); | ||
| const lightness = Math.max(0, Math.min(1, l.value / 100)); | ||
| const spread = saturation * Math.min(lightness, 1 - lightness); | ||
| const channel = (offset: number): number => { | ||
| const k = (((offset + degrees / 30) % 12) + 12) % 12; | ||
| return lightness - spread * Math.max(-1, Math.min(k - 3, 9 - k, 1)); | ||
| }; | ||
| return hex([eightBit(channel(0)), eightBit(channel(8)), eightBit(channel(4))]); | ||
| } | ||
|
|
||
| function gamma(linear: number): number { | ||
| const magnitude = Math.abs(linear); | ||
| const encoded = | ||
| magnitude <= 0.0031308 ? 12.92 * magnitude : 1.055 * magnitude ** (1 / 2.4) - 0.055; | ||
| return Math.sign(linear) * encoded; | ||
| } | ||
|
|
||
| function fromOklch(components: Component[]): ColorConversion { | ||
| const [lightness, chroma, hue] = components; | ||
| if ( | ||
| components.length !== 3 || | ||
| lightness === undefined || | ||
| chroma === undefined || | ||
| hue === undefined | ||
| ) { | ||
| return FORMAT; | ||
| } | ||
| const degrees = hueDegrees(hue); | ||
| const percentOrNumber = (c: Component) => c.unit === '' || c.unit === '%'; | ||
| if (degrees === null || !percentOrNumber(lightness) || !percentOrNumber(chroma)) return FORMAT; | ||
| const L = lightness.unit === '%' ? lightness.value / 100 : lightness.value; | ||
| const C = chroma.unit === '%' ? (chroma.value / 100) * 0.4 : chroma.value; | ||
| const a = C * Math.cos((degrees * Math.PI) / 180); | ||
| const b = C * Math.sin((degrees * Math.PI) / 180); | ||
| const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3; | ||
| const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3; | ||
| const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3; | ||
| return hex([ | ||
| eightBit(gamma(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s)), | ||
| eightBit(gamma(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s)), | ||
| eightBit(gamma(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s)), | ||
| ]); | ||
| } | ||
|
|
||
| export function cssColorToHex(value: string): ColorConversion { | ||
| const text = value.trim(); | ||
| const hexMatch = HEX_RE.exec(text); | ||
| if (hexMatch !== null) return fromHex(hexMatch[1] ?? ''); | ||
|
|
||
| const fn = FUNCTION_RE.exec(text); | ||
| if (fn === null) return FORMAT; | ||
| const name = (fn[1] ?? '').toLowerCase(); | ||
| const args = splitArguments(fn[2] ?? ''); | ||
| if (args === null || (args.comma && name === 'oklch')) return FORMAT; | ||
| const { channels, alpha } = args; | ||
| const components = channels.map(parseComponent).filter((c): c is Component => c !== null); | ||
| if (components.length !== channels.length) return FORMAT; | ||
|
|
||
| if (alpha !== undefined) { | ||
| const parsed = parseComponent(alpha); | ||
| const opacity = parsed === null ? null : alphaValue(parsed); | ||
| if (opacity === null) return FORMAT; | ||
| if (opacity < 1) return { ok: false, reason: 'alpha' }; | ||
| } | ||
|
|
||
| if (name.startsWith('rgb')) return fromRgb(components); | ||
| if (name.startsWith('hsl')) return fromHsl(components); | ||
| return fromOklch(components); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.