feat(design-explorer): the gallery, including what isn't there
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 41s
CI & Build / Build & push image (push) Successful in 40s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 41s
CI & Build / Build & push image (push) Successful in 40s
Milestone #251 step 3 (#2260). New /design view, reachable from a Palette icon beside Trash and Settings — a meta-surface like /rules, so an icon rather than a sixth primary nav link, but not hidden either, since somewhere the design system is visible is the entire point. Renders three things, and refuses to render a fourth: - REAL components, imported not recreated: StatusBadge, PriorityBadge, TagPill. - REAL tokens, read at runtime via readTokens() so the page shows the live cascade rather than what the stylesheet claims. Grouped, swatched where the value is a colour, flagged where the token is mode-aware. - Rule 65's four button variants and rule 60's type scale, listed as SPEC and marked missing. That last part is the point of the step rather than a shortfall of it. Step 3's premise was "render the real components, not copies — a gallery of look-alikes drifts from the app within a month and then lies." Buttons have no shared implementation to import: .btn-primary is defined four separate times in four <style scoped> blocks, and 30 of 54 SFCs carry their own button CSS (#2273). Drawing a button here would have made this page the fifth copy — committing the exact drift the surface exists to catch. Same for the type scale: the three families load (rule 59) but rule 60's sizes and weights are not tokens, so there is nothing to read and a rendered specimen would be invented. So the gallery reports them as gaps. A design system nobody can point at is a design system that isn't there, and saying so is more useful than a page that looks complete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
This commit is contained in:
@@ -6,7 +6,7 @@ import { useShortcuts } from "@/composables/useShortcuts";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import AppLogo from "@/components/AppLogo.vue";
|
||||
import NotificationBell from "@/components/NotificationBell.vue";
|
||||
import { Sun, Moon, Settings, Trash2 } from "lucide-vue-next";
|
||||
import { Sun, Moon, Palette, Settings, Trash2 } from "lucide-vue-next";
|
||||
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const { toggleShortcuts } = useShortcuts();
|
||||
@@ -64,6 +64,13 @@ router.afterEach(() => {
|
||||
<Moon v-else :size="16" />
|
||||
</button>
|
||||
|
||||
<!-- Design explorer. An icon rather than a sixth primary nav link: it's a
|
||||
meta-surface like Trash and Settings, but hiding it entirely would
|
||||
defeat the point of having somewhere the design system is visible. -->
|
||||
<router-link to="/design" class="btn-icon" aria-label="Design" title="Design">
|
||||
<Palette :size="16" />
|
||||
</router-link>
|
||||
|
||||
<!-- Trash link -->
|
||||
<router-link to="/trash" class="btn-icon" aria-label="Trash" title="Trash">
|
||||
<Trash2 :size="16" />
|
||||
@@ -98,6 +105,7 @@ router.afterEach(() => {
|
||||
<router-link to="/rules" class="nav-link">Rulebooks</router-link>
|
||||
<router-link to="/shared" class="nav-link">Shared</router-link>
|
||||
<div class="mobile-divider"></div>
|
||||
<router-link to="/design" class="nav-link">Design</router-link>
|
||||
<router-link to="/trash" class="nav-link">Trash</router-link>
|
||||
<router-link to="/settings" class="nav-link">Settings</router-link>
|
||||
<div class="mobile-divider"></div>
|
||||
|
||||
@@ -109,6 +109,13 @@ const router = createRouter({
|
||||
name: "rules",
|
||||
component: () => import("@/views/RulesView.vue"),
|
||||
},
|
||||
{
|
||||
// Meta-surface, same family as /rules: it describes the app rather than
|
||||
// holding the operator's records.
|
||||
path: "/design",
|
||||
name: "design",
|
||||
component: () => import("@/views/DesignView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/tasks",
|
||||
redirect: "/",
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user