feat: production-harden app and finalize merge-ready UX/theme baseline
- complete tokenized theme preset system and admin style guide visibility - refine dashboard layout/scroll behavior and shared async/request UI states - add user nav-visibility settings across API, migration, and frontend stores - harden security headers/CSP handling and related test coverage - enforce CI repository hygiene + frontend contract/theme/build quality gates - update docs/env references and make migration smoke test head-aware
This commit is contained in:
parent
ab1d29b203
commit
ae2ca8f149
66 changed files with 5655 additions and 853 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import AppPage from "@/components/layout/AppPage.vue";
|
||||
import QueueHealthBadge from "@/components/patterns/QueueHealthBadge.vue";
|
||||
|
|
@ -12,17 +12,107 @@ import AppCheckbox from "@/components/ui/AppCheckbox.vue";
|
|||
import AppInput from "@/components/ui/AppInput.vue";
|
||||
import AppSelect from "@/components/ui/AppSelect.vue";
|
||||
import AppTable from "@/components/ui/AppTable.vue";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
|
||||
const sampleName = ref("Ada Lovelace");
|
||||
const sampleEmail = ref("ada@example.com");
|
||||
const sampleRole = ref("operator");
|
||||
const sampleEnabled = ref(true);
|
||||
const theme = useThemeStore();
|
||||
|
||||
const isDarkTheme = computed(() => theme.active === "dark");
|
||||
const toggleThemeLabel = computed(() =>
|
||||
isDarkTheme.value ? "Switch to light mode" : "Switch to dark mode",
|
||||
);
|
||||
const selectedPreset = computed({
|
||||
get: () => theme.preset,
|
||||
set: (value: string) => theme.setPreset(value),
|
||||
});
|
||||
|
||||
const surfaceSamples = [
|
||||
{ label: "App surface", value: "--theme-surface-app" },
|
||||
{ label: "Nav surface", value: "--theme-surface-nav" },
|
||||
{ label: "Active nav", value: "--theme-surface-nav-active" },
|
||||
{ label: "Card surface", value: "--theme-surface-card" },
|
||||
{ label: "Table header", value: "--theme-surface-table-header" },
|
||||
{ label: "Input surface", value: "--theme-surface-input" },
|
||||
];
|
||||
|
||||
const textSamples = [
|
||||
{ label: "Primary text", value: "--theme-text-primary" },
|
||||
{ label: "Secondary text", value: "--theme-text-secondary" },
|
||||
{ label: "Muted text", value: "--theme-text-muted" },
|
||||
{ label: "Inverse text", value: "--theme-text-inverse" },
|
||||
{ label: "Link text", value: "--theme-text-link" },
|
||||
];
|
||||
|
||||
const actionSamples = [
|
||||
{ label: "Primary action", value: "--theme-action-primary-bg" },
|
||||
{ label: "Secondary action", value: "--theme-action-secondary-bg" },
|
||||
{ label: "Ghost action", value: "--theme-action-ghost-bg" },
|
||||
{ label: "Danger action", value: "--theme-action-danger-bg" },
|
||||
];
|
||||
|
||||
const borderSamples = [
|
||||
{ label: "Default border", value: "--theme-border-default" },
|
||||
{ label: "Strong border", value: "--theme-border-strong" },
|
||||
{ label: "Subtle border", value: "--theme-border-subtle" },
|
||||
{ label: "Interactive border", value: "--theme-border-interactive" },
|
||||
];
|
||||
|
||||
const focusSamples = [
|
||||
{ label: "Focus ring", value: "--theme-focus-ring" },
|
||||
{ label: "Focus offset", value: "--theme-focus-ring-offset" },
|
||||
];
|
||||
|
||||
const stateSamples = [
|
||||
{ label: "Info state", value: "--theme-state-info-bg", text: "--theme-state-info-text", border: "--theme-state-info-border" },
|
||||
{
|
||||
label: "Success state",
|
||||
value: "--theme-state-success-bg",
|
||||
text: "--theme-state-success-text",
|
||||
border: "--theme-state-success-border",
|
||||
},
|
||||
{
|
||||
label: "Warning state",
|
||||
value: "--theme-state-warning-bg",
|
||||
text: "--theme-state-warning-text",
|
||||
border: "--theme-state-warning-border",
|
||||
},
|
||||
{ label: "Danger state", value: "--theme-state-danger-bg", text: "--theme-state-danger-text", border: "--theme-state-danger-border" },
|
||||
];
|
||||
|
||||
const scaleFamilies = ["brand", "info", "success", "warning", "danger"] as const;
|
||||
const scaleSteps = ["50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950"] as const;
|
||||
|
||||
const sampleRows = [
|
||||
{ id: 101, title: "Entity Resolution Improvements", status: "running", owner: "Scheduler" },
|
||||
{ id: 102, title: "Retry Queue Drain", status: "partial_failure", owner: "Ops" },
|
||||
{ id: 103, title: "Weekly Publication Sync", status: "success", owner: "Ingestion" },
|
||||
];
|
||||
|
||||
function cssVarStyle(tokenName: string): Record<string, string> {
|
||||
return { backgroundColor: `rgb(var(${tokenName}) / 1)` };
|
||||
}
|
||||
|
||||
function cssVarBorderStyle(tokenName: string): Record<string, string> {
|
||||
return { borderColor: `rgb(var(${tokenName}) / 1)` };
|
||||
}
|
||||
|
||||
function textSampleStyle(tokenName: string): Record<string, string> {
|
||||
if (tokenName === "--theme-text-inverse") {
|
||||
return { backgroundColor: "rgb(var(--theme-surface-overlay) / 1)" };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function colorScaleStyle(family: (typeof scaleFamilies)[number], step: (typeof scaleSteps)[number]): Record<string, string> {
|
||||
return { backgroundColor: `rgb(var(--color-${family}-${step}) / 1)` };
|
||||
}
|
||||
|
||||
function onToggleTheme(): void {
|
||||
theme.setPreference(isDarkTheme.value ? "light" : "dark");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -30,9 +120,193 @@ const sampleRows = [
|
|||
title="Style Guide"
|
||||
subtitle="Reference page for Tailwind component patterns used across the application."
|
||||
>
|
||||
<AppCard class="space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-lg font-semibold text-ink-primary">Theme Lab</h2>
|
||||
<p class="text-sm text-secondary">Preview preset + mode behavior and inspect semantic token groups.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<AppSelect v-model="selectedPreset" class="w-40">
|
||||
<option v-for="preset in theme.availablePresets" :key="preset.id" :value="preset.id">
|
||||
{{ preset.label }}
|
||||
</option>
|
||||
</AppSelect>
|
||||
<AppButton variant="secondary" @click="onToggleTheme">
|
||||
{{ toggleThemeLabel }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-muted">Surfaces</p>
|
||||
<ul class="grid gap-2">
|
||||
<li
|
||||
v-for="sample in surfaceSamples"
|
||||
:key="sample.value"
|
||||
class="flex items-center justify-between rounded-lg border border-stroke-default px-3 py-2 text-xs text-ink-secondary"
|
||||
:style="cssVarStyle(sample.value)"
|
||||
>
|
||||
<span class="font-medium text-ink-primary">{{ sample.label }}</span>
|
||||
<code>{{ sample.value }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-muted">Text</p>
|
||||
<ul class="grid gap-2">
|
||||
<li
|
||||
v-for="sample in textSamples"
|
||||
:key="sample.value"
|
||||
class="flex items-center justify-between rounded-lg border border-stroke-default bg-surface-card px-3 py-2 text-xs"
|
||||
:style="textSampleStyle(sample.value)"
|
||||
>
|
||||
<span class="font-medium" :style="{ color: `rgb(var(${sample.value}) / 1)` }">{{ sample.label }}</span>
|
||||
<code class="text-ink-muted">{{ sample.value }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-muted">Actions</p>
|
||||
<ul class="grid gap-2">
|
||||
<li
|
||||
v-for="sample in actionSamples"
|
||||
:key="sample.value"
|
||||
class="flex items-center justify-between rounded-lg border border-stroke-default px-3 py-2 text-xs text-ink-secondary"
|
||||
:style="cssVarStyle(sample.value)"
|
||||
>
|
||||
<span class="font-medium text-ink-primary">{{ sample.label }}</span>
|
||||
<code>{{ sample.value }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-muted">Borders</p>
|
||||
<ul class="grid gap-2">
|
||||
<li
|
||||
v-for="sample in borderSamples"
|
||||
:key="sample.value"
|
||||
class="flex items-center justify-between rounded-lg border-2 bg-surface-card px-3 py-2 text-xs text-ink-secondary"
|
||||
:style="cssVarBorderStyle(sample.value)"
|
||||
>
|
||||
<span class="font-medium text-ink-primary">{{ sample.label }}</span>
|
||||
<code>{{ sample.value }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-muted">Focus</p>
|
||||
<ul class="grid gap-2">
|
||||
<li
|
||||
v-for="sample in focusSamples"
|
||||
:key="sample.value"
|
||||
class="flex items-center justify-between rounded-lg border border-stroke-default bg-surface-card px-3 py-2 text-xs text-ink-secondary"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md px-2 py-1 font-medium text-ink-primary focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
:style="{ '--tw-ring-color': `rgb(var(${sample.value}) / 1)`, '--tw-ring-offset-color': 'rgb(var(--theme-focus-ring-offset) / 1)' }"
|
||||
>
|
||||
Focus me
|
||||
</button>
|
||||
<code>{{ sample.value }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-muted">States</p>
|
||||
<ul class="grid gap-2">
|
||||
<li
|
||||
v-for="sample in stateSamples"
|
||||
:key="sample.value"
|
||||
class="flex items-center justify-between rounded-lg border px-3 py-2 text-xs"
|
||||
:style="{
|
||||
backgroundColor: `rgb(var(${sample.value}) / 1)`,
|
||||
borderColor: `rgb(var(${sample.border}) / 1)`,
|
||||
color: `rgb(var(${sample.text}) / 1)`,
|
||||
}"
|
||||
>
|
||||
<span class="font-medium">{{ sample.label }}</span>
|
||||
<code>{{ sample.value }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<AppCard class="space-y-4">
|
||||
<h2 class="text-lg font-semibold text-ink-primary">Scale Ramps</h2>
|
||||
<div class="grid gap-3">
|
||||
<div v-for="family in scaleFamilies" :key="family" class="grid gap-1">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-muted">{{ family }}</p>
|
||||
<div class="grid grid-cols-11 gap-1">
|
||||
<span
|
||||
v-for="step in scaleSteps"
|
||||
:key="`${family}-${step}`"
|
||||
class="h-8 rounded-md border border-stroke-default"
|
||||
:style="colorScaleStyle(family, step)"
|
||||
:title="`--color-${family}-${step}`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<AppCard class="space-y-4">
|
||||
<h2 class="text-lg font-semibold text-ink-primary">Shell Preview</h2>
|
||||
<p class="text-sm text-secondary">Quick check for app background, nav, card, table, action, and status token coverage.</p>
|
||||
<div class="grid gap-3 rounded-2xl border border-stroke-default bg-surface-app p-3 lg:grid-cols-[14rem_minmax(0,1fr)]">
|
||||
<aside class="space-y-2 rounded-xl border border-stroke-default bg-surface-nav p-3">
|
||||
<div class="rounded-lg bg-surface-nav-active px-2 py-1 text-sm font-semibold text-ink-inverse">Dashboard</div>
|
||||
<div class="rounded-lg px-2 py-1 text-sm text-ink-secondary">Publications</div>
|
||||
<div class="rounded-lg px-2 py-1 text-sm text-ink-secondary">Scholars</div>
|
||||
</aside>
|
||||
<section class="space-y-3 rounded-xl border border-stroke-default bg-surface-card p-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<p class="text-sm font-semibold text-ink-primary">Latest Update Check</p>
|
||||
<RunStatusBadge status="running" />
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<AppButton>Primary action</AppButton>
|
||||
<AppButton variant="secondary">Secondary</AppButton>
|
||||
<AppBadge tone="warning">Needs review</AppBadge>
|
||||
</div>
|
||||
<AppTable label="Shell preview table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Scholar</th>
|
||||
<th scope="col">State</th>
|
||||
<th scope="col">Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Ada Lovelace</td>
|
||||
<td>Checked</td>
|
||||
<td>4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Geoffrey Hinton</td>
|
||||
<td>Queued</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</AppTable>
|
||||
</section>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<section class="grid gap-4 xl:grid-cols-2">
|
||||
<AppCard class="space-y-4">
|
||||
<h2 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">Buttons</h2>
|
||||
<h2 class="text-lg font-semibold text-ink-primary">Buttons</h2>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<AppButton>Primary</AppButton>
|
||||
<AppButton variant="secondary">Secondary</AppButton>
|
||||
|
|
@ -44,7 +318,7 @@ const sampleRows = [
|
|||
</AppCard>
|
||||
|
||||
<AppCard class="space-y-4">
|
||||
<h2 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">Status System</h2>
|
||||
<h2 class="text-lg font-semibold text-ink-primary">Status System</h2>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<AppBadge tone="neutral">Neutral</AppBadge>
|
||||
<AppBadge tone="info">Info</AppBadge>
|
||||
|
|
@ -64,19 +338,19 @@ const sampleRows = [
|
|||
|
||||
<section class="grid gap-4 xl:grid-cols-2">
|
||||
<AppCard class="space-y-4">
|
||||
<h2 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">Forms</h2>
|
||||
<h2 class="text-lg font-semibold text-ink-primary">Forms</h2>
|
||||
<form class="grid gap-3" @submit.prevent>
|
||||
<label class="grid gap-2 text-sm font-medium text-zinc-700 dark:text-zinc-300">
|
||||
<label class="grid gap-2 text-sm font-medium text-ink-secondary">
|
||||
<span>Name</span>
|
||||
<AppInput v-model="sampleName" autocomplete="name" />
|
||||
</label>
|
||||
|
||||
<label class="grid gap-2 text-sm font-medium text-zinc-700 dark:text-zinc-300">
|
||||
<label class="grid gap-2 text-sm font-medium text-ink-secondary">
|
||||
<span>Email</span>
|
||||
<AppInput v-model="sampleEmail" type="email" autocomplete="email" />
|
||||
</label>
|
||||
|
||||
<label class="grid gap-2 text-sm font-medium text-zinc-700 dark:text-zinc-300">
|
||||
<label class="grid gap-2 text-sm font-medium text-ink-secondary">
|
||||
<span>Role</span>
|
||||
<AppSelect v-model="sampleRole">
|
||||
<option value="viewer">Viewer</option>
|
||||
|
|
@ -95,7 +369,7 @@ const sampleRows = [
|
|||
</AppCard>
|
||||
|
||||
<AppCard class="space-y-4">
|
||||
<h2 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">Alerts</h2>
|
||||
<h2 class="text-lg font-semibold text-ink-primary">Alerts</h2>
|
||||
<AppAlert tone="info">
|
||||
<template #title>Informational</template>
|
||||
<p>Use for contextual guidance and non-critical notices.</p>
|
||||
|
|
@ -112,7 +386,7 @@ const sampleRows = [
|
|||
</section>
|
||||
|
||||
<AppCard class="space-y-4">
|
||||
<h2 class="text-lg font-semibold text-zinc-900 dark:text-zinc-100">Table Pattern</h2>
|
||||
<h2 class="text-lg font-semibold text-ink-primary">Table Pattern</h2>
|
||||
<AppTable label="Style guide sample run table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue