|
|
|
@@ -0,0 +1,372 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
/**
|
|
|
|
|
* Design explorer — the gallery (milestone #251 step 3).
|
|
|
|
|
*
|
|
|
|
|
* Renders the design system against the tokens that are actually live, read at
|
|
|
|
|
* runtime rather than parsed from source, so what you see here is what the app
|
|
|
|
|
* is using right now.
|
|
|
|
|
*
|
|
|
|
|
* HONESTY RULE, and the reason parts of this page say "not implemented":
|
|
|
|
|
* a gallery of hand-written look-alikes drifts from the app within a month and
|
|
|
|
|
* then lies — which is the same failure this whole surface exists to catch. So
|
|
|
|
|
* every specimen below is either a REAL component imported from the app, or a
|
|
|
|
|
* real token read from the browser, or it is explicitly marked as missing.
|
|
|
|
|
*
|
|
|
|
|
* Buttons are the case where that bites. Rule 65 specifies four variants, but
|
|
|
|
|
* `.btn-primary` is defined four separate times in four `<style scoped>` blocks
|
|
|
|
|
* and 30 of 54 SFCs carry their own button CSS (#2273). There is no shared
|
|
|
|
|
* button to import, so rendering one here would just make this a fifth copy.
|
|
|
|
|
* It is reported as a gap instead.
|
|
|
|
|
*/
|
|
|
|
|
import { computed, onMounted, ref } from "vue";
|
|
|
|
|
|
|
|
|
|
import PriorityBadge from "@/components/PriorityBadge.vue";
|
|
|
|
|
import StatusBadge from "@/components/StatusBadge.vue";
|
|
|
|
|
import TagPill from "@/components/TagPill.vue";
|
|
|
|
|
import { groupTokens, readTokens, type DesignToken, type TokenGroup } from "@/utils/designTokens";
|
|
|
|
|
|
|
|
|
|
const tokens = ref<DesignToken[]>([]);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Read on mount, not at module scope: the values depend on the live cascade,
|
|
|
|
|
* which needs the app's stylesheets applied and the theme attribute set.
|
|
|
|
|
*/
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
tokens.value = readTokens();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const grouped = computed(() => groupTokens(tokens.value));
|
|
|
|
|
|
|
|
|
|
const GROUP_ORDER: TokenGroup[] = ["color", "radius", "glow", "gradient", "focus", "layout", "other"];
|
|
|
|
|
const orderedGroups = computed(() =>
|
|
|
|
|
GROUP_ORDER.filter((g) => grouped.value.has(g)).map((g) => ({ group: g, tokens: grouped.value.get(g)! })),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/** A token whose value reads as a colour is worth showing as a swatch. */
|
|
|
|
|
function isColourish(value: string): boolean {
|
|
|
|
|
return /^(#|rgba?\(|hsla?\(|color-mix\()/.test(value.trim());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Rule 65's four variants — none of which exists as a shared artifact (#2273). */
|
|
|
|
|
const RULEBOOK_BUTTONS = [
|
|
|
|
|
{ name: "Primary", spec: "Moss #4A5D3F bg, Parchment text, no border" },
|
|
|
|
|
{ name: "Secondary", spec: "Bronze #8B7355 bg, Parchment text, no border" },
|
|
|
|
|
{ name: "Ghost", spec: "transparent, Parchment text, 0.5px Pewter border" },
|
|
|
|
|
{ name: "Destructive", spec: "Oxblood #6B2118 bg, Parchment text, pair with icon" },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const TYPE_SPECIMENS = [
|
|
|
|
|
{ token: "Display", spec: "40 / 500 / Fraunces" },
|
|
|
|
|
{ token: "H1", spec: "32 / 500 / Fraunces" },
|
|
|
|
|
{ token: "H2", spec: "24 / 500 / Fraunces" },
|
|
|
|
|
{ token: "H3", spec: "18 / 500 / Inter" },
|
|
|
|
|
{ token: "Body", spec: "15 / 400 / Inter" },
|
|
|
|
|
{ token: "Body small", spec: "13 / 400 / Inter" },
|
|
|
|
|
{ token: "Label", spec: "12 / 500 / Inter" },
|
|
|
|
|
{ token: "Code", spec: "13 / 400 / JetBrains Mono" },
|
|
|
|
|
{ token: "Tiny", spec: "11 / 500 / Inter, uppercase +0.08em" },
|
|
|
|
|
];
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="design-view">
|
|
|
|
|
<header class="design-header">
|
|
|
|
|
<h1>Design</h1>
|
|
|
|
|
<p class="lede">
|
|
|
|
|
The system as it actually is. Token values are read from the browser at
|
|
|
|
|
runtime, so this page reflects the live cascade rather than what the
|
|
|
|
|
stylesheet says. Components shown are the real ones — where a piece of
|
|
|
|
|
the system has no shared implementation, it is marked missing rather
|
|
|
|
|
than mocked up.
|
|
|
|
|
</p>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<!-- Real components: these are imported, not recreated. -->
|
|
|
|
|
<section class="design-section">
|
|
|
|
|
<h2>Components</h2>
|
|
|
|
|
<p class="section-note">Imported from the app. What you see is what ships.</p>
|
|
|
|
|
|
|
|
|
|
<div class="specimen">
|
|
|
|
|
<span class="specimen-label">Status badge</span>
|
|
|
|
|
<div class="specimen-row">
|
|
|
|
|
<StatusBadge status="todo" />
|
|
|
|
|
<StatusBadge status="in_progress" />
|
|
|
|
|
<StatusBadge status="done" />
|
|
|
|
|
<StatusBadge status="cancelled" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="specimen">
|
|
|
|
|
<span class="specimen-label">Priority badge</span>
|
|
|
|
|
<div class="specimen-row">
|
|
|
|
|
<PriorityBadge priority="low" />
|
|
|
|
|
<PriorityBadge priority="medium" />
|
|
|
|
|
<PriorityBadge priority="high" />
|
|
|
|
|
<span class="muted">(<code>none</code> renders nothing, by design)</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="specimen">
|
|
|
|
|
<span class="specimen-label">Tag pill</span>
|
|
|
|
|
<div class="specimen-row">
|
|
|
|
|
<TagPill tag="design-system" />
|
|
|
|
|
<TagPill tag="dismissible" dismissible />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<!-- The honest gap. -->
|
|
|
|
|
<section class="design-section">
|
|
|
|
|
<h2>Buttons</h2>
|
|
|
|
|
<div class="gap-notice">
|
|
|
|
|
<strong>Not implemented as a shared component.</strong>
|
|
|
|
|
<p>
|
|
|
|
|
Rule 65 specifies four variants. In practice <code>.btn-primary</code> is
|
|
|
|
|
defined four separate times in four <code><style scoped></code>
|
|
|
|
|
blocks, and 30 of 54 single-file components carry their own button CSS.
|
|
|
|
|
There is no shared button to render here, and drawing one would make
|
|
|
|
|
this page a fifth copy of it — the exact drift this surface exists to
|
|
|
|
|
catch. Tracked as issue #2273.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<ul class="spec-list">
|
|
|
|
|
<li v-for="b in RULEBOOK_BUTTONS" :key="b.name">
|
|
|
|
|
<span class="spec-name">{{ b.name }}</span>
|
|
|
|
|
<span class="spec-detail">{{ b.spec }}</span>
|
|
|
|
|
<span class="spec-status missing">missing</span>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<!-- Typography: the families load, the scale does not exist as tokens. -->
|
|
|
|
|
<section class="design-section">
|
|
|
|
|
<h2>Type scale</h2>
|
|
|
|
|
<div class="gap-notice">
|
|
|
|
|
<strong>Families load; the scale has no tokens.</strong>
|
|
|
|
|
<p>
|
|
|
|
|
Fraunces, Inter and JetBrains Mono are imported (rule 59), but rule 60's
|
|
|
|
|
scale is not expressed as custom properties, so sizes and weights are
|
|
|
|
|
set ad hoc per component. Listed here as specification, not as a live
|
|
|
|
|
specimen — there is nothing to read.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<ul class="spec-list">
|
|
|
|
|
<li v-for="t in TYPE_SPECIMENS" :key="t.token">
|
|
|
|
|
<span class="spec-name">{{ t.token }}</span>
|
|
|
|
|
<span class="spec-detail">{{ t.spec }}</span>
|
|
|
|
|
<span class="spec-status missing">no token</span>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<!-- Tokens: entirely real, read live. -->
|
|
|
|
|
<section v-for="{ group, tokens: groupTokenList } in orderedGroups" :key="group" class="design-section">
|
|
|
|
|
<h2 class="token-group-heading">{{ group }} <span class="count">{{ groupTokenList.length }}</span></h2>
|
|
|
|
|
<ul class="token-list">
|
|
|
|
|
<li v-for="token in groupTokenList" :key="token.name" class="token-row">
|
|
|
|
|
<span
|
|
|
|
|
v-if="isColourish(token.value)"
|
|
|
|
|
class="swatch"
|
|
|
|
|
:style="{ background: token.value }"
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
/>
|
|
|
|
|
<span v-else class="swatch swatch-none" aria-hidden="true" />
|
|
|
|
|
<code class="token-name">{{ token.name }}</code>
|
|
|
|
|
<code class="token-value">{{ token.value || "—" }}</code>
|
|
|
|
|
<span v-if="token.overriddenInDark" class="token-flag" title="Re-declared in the dark block">
|
|
|
|
|
mode-aware
|
|
|
|
|
</span>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<p v-if="!tokens.length" class="muted">Reading tokens…</p>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.design-view {
|
|
|
|
|
max-width: var(--page-max-width);
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
padding: 1.5rem var(--page-padding-x) 4rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.design-header {
|
|
|
|
|
margin-bottom: 2rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lede {
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
max-width: 60ch;
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.design-section {
|
|
|
|
|
margin-bottom: 2.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.design-section h2 {
|
|
|
|
|
margin-bottom: 0.25rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.token-group-heading {
|
|
|
|
|
text-transform: capitalize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.count {
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-note,
|
|
|
|
|
.muted {
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Specimens -------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
.specimen {
|
|
|
|
|
padding: 0.75rem 0;
|
|
|
|
|
border-bottom: 1px solid var(--color-border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.specimen-label {
|
|
|
|
|
display: block;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
margin-bottom: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.specimen-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Gaps ------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
.gap-notice {
|
|
|
|
|
background: var(--color-surface);
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
border-left: 3px solid var(--color-warning);
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
padding: 0.75rem 1rem;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.gap-notice p {
|
|
|
|
|
margin: 0.5rem 0 0;
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
max-width: 70ch;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.spec-list {
|
|
|
|
|
list-style: none;
|
|
|
|
|
padding: 0;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.spec-list li {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
padding: 0.4rem 0;
|
|
|
|
|
border-bottom: 1px solid var(--color-border);
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.spec-name {
|
|
|
|
|
min-width: 8rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.spec-detail {
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.spec-status {
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
padding: 0.1rem 0.45rem;
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.spec-status.missing {
|
|
|
|
|
background: var(--color-priority-medium-bg);
|
|
|
|
|
color: var(--color-priority-medium);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Tokens ----------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
.token-list {
|
|
|
|
|
list-style: none;
|
|
|
|
|
padding: 0;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.token-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
padding: 0.3rem 0;
|
|
|
|
|
border-bottom: 1px solid var(--color-border);
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.swatch {
|
|
|
|
|
width: 1.25rem;
|
|
|
|
|
height: 1.25rem;
|
|
|
|
|
flex: none;
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.swatch-none {
|
|
|
|
|
background: repeating-linear-gradient(
|
|
|
|
|
45deg,
|
|
|
|
|
transparent,
|
|
|
|
|
transparent 3px,
|
|
|
|
|
var(--color-border) 3px,
|
|
|
|
|
var(--color-border) 4px
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.token-name {
|
|
|
|
|
min-width: 16rem;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.token-value {
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
flex: 1;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.token-flag {
|
|
|
|
|
font-size: 0.65rem;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
padding: 0.05rem 0.35rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 640px) {
|
|
|
|
|
.token-name {
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|