feat: refactor backend services and add direct PDF links, import/export, and dashboard sync

This commit is contained in:
Justin Visser 2026-02-19 23:45:52 +01:00
parent ba7976d935
commit 7f7a8ce2b0
26 changed files with 4170 additions and 2440 deletions

View file

@ -11,6 +11,7 @@ export interface PublicationItem {
citation_count: number;
venue_text: string | null;
pub_url: string | null;
pdf_url: string | null;
is_read: boolean;
first_seen_at: string;
is_new_in_latest_run: boolean;

View file

@ -37,6 +37,50 @@ export interface ScholarSearchResult {
warnings: string[];
}
export interface ScholarExportItem {
scholar_id: string;
display_name: string | null;
is_enabled: boolean;
profile_image_override_url: string | null;
}
export interface PublicationExportItem {
scholar_id: string;
cluster_id: string | null;
fingerprint_sha256: string | null;
title: string;
year: number | null;
citation_count: number;
author_text: string | null;
venue_text: string | null;
pub_url: string | null;
pdf_url: string | null;
is_read: boolean;
}
export interface DataExportPayload {
schema_version: number;
exported_at: string;
scholars: ScholarExportItem[];
publications: PublicationExportItem[];
}
export interface DataImportPayload {
schema_version?: number;
scholars: ScholarExportItem[];
publications: PublicationExportItem[];
}
export interface DataImportResult {
scholars_created: number;
scholars_updated: number;
publications_created: number;
publications_updated: number;
links_created: number;
links_updated: number;
skipped_records: number;
}
interface ScholarsListData {
scholars: ScholarProfile[];
}
@ -115,3 +159,20 @@ export async function clearScholarImage(
});
return response.data;
}
export async function exportScholarData(): Promise<DataExportPayload> {
const response = await apiRequest<DataExportPayload>("/scholars/export", {
method: "GET",
});
return response.data;
}
export async function importScholarData(
payload: DataImportPayload,
): Promise<DataImportResult> {
const response = await apiRequest<DataImportResult>("/scholars/import", {
method: "POST",
body: payload,
});
return response.data;
}

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { fetchDashboardSnapshot, type DashboardSnapshot } from "@/features/dashboard";
import { ApiRequestError } from "@/lib/api/errors";
@ -14,7 +14,7 @@ import AppCard from "@/components/ui/AppCard.vue";
import AppEmptyState from "@/components/ui/AppEmptyState.vue";
import AppHelpHint from "@/components/ui/AppHelpHint.vue";
import { useAuthStore } from "@/stores/auth";
import { useRunStatusStore } from "@/stores/run_status";
import { RUN_STATUS_POLL_INTERVAL_MS, useRunStatusStore } from "@/stores/run_status";
import { useUserSettingsStore } from "@/stores/user_settings";
const loading = ref(true);
@ -26,6 +26,7 @@ const refreshingAfterCompletion = ref(false);
const auth = useAuthStore();
const runStatus = useRunStatusStore();
const userSettings = useUserSettingsStore();
let latestRunSyncTimer: ReturnType<typeof setInterval> | null = null;
const isStartBlocked = computed(
() =>
@ -118,6 +119,39 @@ function formatDate(value: string | null): string {
return asDate.toLocaleString();
}
function shouldRefreshAfterRunChange(
nextRun: typeof runStatus.latestRun,
previousRun: typeof runStatus.latestRun,
): boolean {
if (!nextRun || !previousRun) {
return false;
}
if (nextRun.status === "running") {
return false;
}
if (nextRun.id !== previousRun.id) {
return true;
}
return previousRun.status === "running";
}
function startLatestRunSyncLoop(): void {
if (latestRunSyncTimer !== null) {
return;
}
latestRunSyncTimer = setInterval(() => {
void runStatus.syncLatest();
}, RUN_STATUS_POLL_INTERVAL_MS);
}
function stopLatestRunSyncLoop(): void {
if (latestRunSyncTimer === null) {
return;
}
clearInterval(latestRunSyncTimer);
latestRunSyncTimer = null;
}
async function loadSnapshot(): Promise<void> {
loading.value = true;
errorMessage.value = null;
@ -172,6 +206,12 @@ async function onTriggerRun(): Promise<void> {
onMounted(() => {
void loadSnapshot();
void runStatus.syncLatest();
startLatestRunSyncLoop();
});
onUnmounted(() => {
stopLatestRunSyncLoop();
});
watch(
@ -180,13 +220,7 @@ watch(
if (refreshingAfterCompletion.value) {
return;
}
if (!nextRun || !previousRun) {
return;
}
if (nextRun.id !== previousRun.id) {
return;
}
if (previousRun.status !== "running" || nextRun.status === "running") {
if (!shouldRefreshAfterRunChange(nextRun, previousRun)) {
return;
}

View file

@ -99,6 +99,10 @@ function scholarLabel(item: ScholarProfile): string {
return item.display_name || item.scholar_id;
}
function publicationPrimaryUrl(item: PublicationItem): string | null {
return item.pub_url || item.pdf_url;
}
const selectedScholarName = computed(() => {
const selectedId = Number(selectedScholarFilter.value);
if (!Number.isInteger(selectedId) || selectedId <= 0) {
@ -590,16 +594,27 @@ watch(
/>
</td>
<td>
<a
v-if="item.pub_url"
:href="item.pub_url"
target="_blank"
rel="noreferrer"
class="link-inline"
>
{{ item.title }}
</a>
<span v-else>{{ item.title }}</span>
<div class="grid gap-1">
<a
v-if="publicationPrimaryUrl(item)"
:href="publicationPrimaryUrl(item) || ''"
target="_blank"
rel="noreferrer"
class="link-inline"
>
{{ item.title }}
</a>
<span v-else>{{ item.title }}</span>
<a
v-if="item.pdf_url"
:href="item.pdf_url"
target="_blank"
rel="noreferrer"
class="link-inline text-xs"
>
Direct PDF
</a>
</div>
</td>
<td>{{ item.scholar_label }}</td>
<td>{{ item.year ?? "n/a" }}</td>
@ -607,7 +622,7 @@ watch(
<td>
<div class="flex flex-wrap items-center gap-2">
<AppBadge :tone="item.is_new_in_latest_run ? 'info' : 'neutral'">
{{ item.is_new_in_latest_run ? "New this check" : "Seen before" }}
{{ item.is_new_in_latest_run ? "New" : "Seen before" }}
</AppBadge>
<AppBadge :tone="item.is_read ? 'success' : 'warning'">
{{ item.is_read ? "Read" : "Unread" }}

View file

@ -17,11 +17,15 @@ import {
clearScholarImage,
createScholar,
deleteScholar,
exportScholarData,
importScholarData,
listScholars,
searchScholarsByName,
setScholarImageUrl,
toggleScholar,
uploadScholarImage,
type DataImportPayload,
type DataImportResult,
type ScholarProfile,
type ScholarSearchCandidate,
type ScholarSearchResult,
@ -37,6 +41,9 @@ const imageSavingScholarId = ref<number | null>(null);
const imageUploadingScholarId = ref<number | null>(null);
const addingCandidateScholarId = ref<string | null>(null);
const activeScholarSettingsId = ref<number | null>(null);
const importingData = ref(false);
const exportingData = ref(false);
const importFileInput = ref<HTMLInputElement | null>(null);
const scholars = ref<ScholarProfile[]>([]);
const imageUrlDraftByScholarId = ref<Record<number, string>>({});
@ -197,6 +204,30 @@ function scholarLabel(profile: ScholarProfile): string {
return profile.display_name || profile.scholar_id;
}
function suggestExportFilename(exportedAt: string): string {
const date = exportedAt.slice(0, 10) || "unknown-date";
return `scholarr-export-${date}.json`;
}
function downloadJsonFile(filename: string, payload: unknown): void {
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.click();
URL.revokeObjectURL(url);
}
function importSummary(result: DataImportResult): string {
return (
`Import complete. Scholars +${result.scholars_created}` +
` / updated ${result.scholars_updated}; publications +${result.publications_created}` +
` / updated ${result.publications_updated}; links +${result.links_created}` +
` / updated ${result.links_updated}; skipped ${result.skipped_records}.`
);
}
function isImageBusy(scholarProfileId: number): boolean {
return (
imageSavingScholarId.value === scholarProfileId ||
@ -246,6 +277,74 @@ async function loadScholars(): Promise<void> {
}
}
async function onExportData(): Promise<void> {
exportingData.value = true;
errorMessage.value = null;
errorRequestId.value = null;
successMessage.value = null;
try {
const payload = await exportScholarData();
downloadJsonFile(suggestExportFilename(payload.exported_at), payload);
successMessage.value = "Export complete.";
} catch (error) {
if (error instanceof ApiRequestError) {
errorMessage.value = error.message;
errorRequestId.value = error.requestId;
} else {
errorMessage.value = "Unable to export scholars and publications.";
}
} finally {
exportingData.value = false;
}
}
function onOpenImportPicker(): void {
importFileInput.value?.click();
}
function parseImportedJson(raw: string): DataImportPayload {
const parsed = JSON.parse(raw) as DataImportPayload;
if (!parsed || !Array.isArray(parsed.scholars) || !Array.isArray(parsed.publications)) {
throw new Error("Invalid import file: expected scholars[] and publications[] arrays.");
}
return parsed;
}
async function onImportFileSelected(event: Event): Promise<void> {
const input = event.target as HTMLInputElement | null;
const file = input?.files?.[0] ?? null;
if (!file) {
return;
}
importingData.value = true;
errorMessage.value = null;
errorRequestId.value = null;
successMessage.value = null;
try {
const payload = parseImportedJson(await file.text());
const result = await importScholarData(payload);
successMessage.value = importSummary(result);
await loadScholars();
} catch (error) {
if (error instanceof ApiRequestError) {
errorMessage.value = error.message;
errorRequestId.value = error.requestId;
} else if (error instanceof Error) {
errorMessage.value = error.message;
} else {
errorMessage.value = "Unable to import scholars and publications.";
}
} finally {
importingData.value = false;
if (input) {
input.value = "";
}
}
}
async function onAddScholars(): Promise<void> {
saving.value = true;
errorMessage.value = null;
@ -706,11 +805,24 @@ onMounted(() => {
>
{{ trackedCountLabel }}
</span>
<AppButton variant="secondary" :disabled="loading || exportingData" @click="onExportData">
{{ exportingData ? "Exporting..." : "Export" }}
</AppButton>
<AppButton variant="secondary" :disabled="loading || importingData" @click="onOpenImportPicker">
{{ importingData ? "Importing..." : "Import" }}
</AppButton>
<AppButton variant="secondary" :disabled="loading || saving" @click="loadScholars">
{{ loading ? "Refreshing..." : "Refresh" }}
</AppButton>
</div>
</div>
<input
ref="importFileInput"
type="file"
class="sr-only"
accept=".json,application/json"
@change="onImportFileSelected"
/>
<div class="grid gap-2 sm:grid-cols-[minmax(0,1fr)_12rem]">
<label class="grid gap-1 text-xs font-medium uppercase tracking-wide text-secondary" for="tracked-scholar-filter">

View file

@ -1,313 +0,0 @@
{
"id": "graphite",
"label": "Graphite",
"description": "Neutral slate base with crisp indigo accents.",
"modes": {
"light": {
"scale": {
"brand": {
"50": "#f8fafc",
"100": "#f1f5f9",
"200": "#e2e8f0",
"300": "#cbd5e1",
"400": "#94a3b8",
"500": "#64748b",
"600": "#475569",
"700": "#334155",
"800": "#1e293b",
"900": "#0f172a",
"950": "#020617"
},
"info": {
"50": "#eef2ff",
"100": "#e0e7ff",
"200": "#c7d2fe",
"300": "#a5b4fc",
"400": "#818cf8",
"500": "#6366f1",
"600": "#4f46e5",
"700": "#4338ca",
"800": "#3730a3",
"900": "#312e81",
"950": "#1e1b4b"
},
"success": {
"50": "#ecfdf5",
"100": "#d1fae5",
"200": "#a7f3d0",
"300": "#6ee7b7",
"400": "#34d399",
"500": "#10b981",
"600": "#059669",
"700": "#047857",
"800": "#065f46",
"900": "#064e3b",
"950": "#022c22"
},
"warning": {
"50": "#fffbeb",
"100": "#fef3c7",
"200": "#fde68a",
"300": "#fcd34d",
"400": "#fbbf24",
"500": "#f59e0b",
"600": "#d97706",
"700": "#b45309",
"800": "#92400e",
"900": "#78350f",
"950": "#451a03"
},
"danger": {
"50": "#fff1f2",
"100": "#ffe4e6",
"200": "#fecdd3",
"300": "#fda4af",
"400": "#fb7185",
"500": "#f43f5e",
"600": "#e11d48",
"700": "#be123c",
"800": "#9f1239",
"900": "#881337",
"950": "#4c0519"
}
},
"surface": {
"app": "#f5f7fb",
"nav": "#ffffff",
"nav_active": "#334155",
"card": "#ffffff",
"card_muted": "#f1f5f9",
"table": "#ffffff",
"table_header": "#f8fafc",
"input": "#ffffff",
"overlay": "#0f172a"
},
"text": {
"primary": "#111827",
"secondary": "#374151",
"muted": "#6b7280",
"inverse": "#f9fafb",
"link": "#334155"
},
"border": {
"default": "#dbe2ea",
"strong": "#cbd5e1",
"subtle": "#eef2f7",
"interactive": "#94a3b8"
},
"focus": {
"ring": "#6366f1",
"ring_offset": "#f5f7fb"
},
"action": {
"primary": {
"bg": "#334155",
"border": "#334155",
"text": "#f9fafb",
"hover_bg": "#1e293b",
"hover_border": "#1e293b",
"hover_text": "#ffffff"
},
"secondary": {
"bg": "#f3f4f6",
"border": "#d1d5db",
"text": "#1f2937",
"hover_bg": "#e5e7eb",
"hover_border": "#cbd5e1",
"hover_text": "#111827"
},
"ghost": {
"bg": "#f8fafc",
"border": "#cbd5e1",
"text": "#334155",
"hover_bg": "#f1f5f9",
"hover_border": "#94a3b8",
"hover_text": "#1f2937"
},
"danger": {
"bg": "#ffe4e6",
"border": "#fecdd3",
"text": "#be123c",
"hover_bg": "#fecdd3",
"hover_border": "#fda4af",
"hover_text": "#9f1239"
}
},
"state": {
"info": {
"bg": "#eef2ff",
"border": "#c7d2fe",
"text": "#3730a3"
},
"success": {
"bg": "#ecfdf5",
"border": "#a7f3d0",
"text": "#047857"
},
"warning": {
"bg": "#fffbeb",
"border": "#fde68a",
"text": "#92400e"
},
"danger": {
"bg": "#fff1f2",
"border": "#fecdd3",
"text": "#be123c"
}
}
},
"dark": {
"scale": {
"brand": {
"50": "#f8fafc",
"100": "#f1f5f9",
"200": "#e2e8f0",
"300": "#cbd5e1",
"400": "#94a3b8",
"500": "#64748b",
"600": "#475569",
"700": "#334155",
"800": "#1e293b",
"900": "#0f172a",
"950": "#020617"
},
"info": {
"50": "#eef2ff",
"100": "#e0e7ff",
"200": "#c7d2fe",
"300": "#a5b4fc",
"400": "#818cf8",
"500": "#6366f1",
"600": "#4f46e5",
"700": "#4338ca",
"800": "#3730a3",
"900": "#312e81",
"950": "#1e1b4b"
},
"success": {
"50": "#ecfdf5",
"100": "#d1fae5",
"200": "#a7f3d0",
"300": "#6ee7b7",
"400": "#34d399",
"500": "#10b981",
"600": "#059669",
"700": "#047857",
"800": "#065f46",
"900": "#064e3b",
"950": "#022c22"
},
"warning": {
"50": "#fffbeb",
"100": "#fef3c7",
"200": "#fde68a",
"300": "#fcd34d",
"400": "#fbbf24",
"500": "#f59e0b",
"600": "#d97706",
"700": "#b45309",
"800": "#92400e",
"900": "#78350f",
"950": "#451a03"
},
"danger": {
"50": "#fff1f2",
"100": "#ffe4e6",
"200": "#fecdd3",
"300": "#fda4af",
"400": "#fb7185",
"500": "#f43f5e",
"600": "#e11d48",
"700": "#be123c",
"800": "#9f1239",
"900": "#881337",
"950": "#4c0519"
}
},
"surface": {
"app": "#0b1120",
"nav": "#111827",
"nav_active": "#64748b",
"card": "#111827",
"card_muted": "#1f2937",
"table": "#111827",
"table_header": "#1f2937",
"input": "#0f172a",
"overlay": "#020617"
},
"text": {
"primary": "#e5e7eb",
"secondary": "#cbd5e1",
"muted": "#94a3b8",
"inverse": "#0b1120",
"link": "#a5b4fc"
},
"border": {
"default": "#334155",
"strong": "#475569",
"subtle": "#1f2937",
"interactive": "#64748b"
},
"focus": {
"ring": "#818cf8",
"ring_offset": "#0b1120"
},
"action": {
"primary": {
"bg": "#64748b",
"border": "#64748b",
"text": "#020617",
"hover_bg": "#94a3b8",
"hover_border": "#94a3b8",
"hover_text": "#020617"
},
"secondary": {
"bg": "#1f2937",
"border": "#475569",
"text": "#e5e7eb",
"hover_bg": "#334155",
"hover_border": "#64748b",
"hover_text": "#f8fafc"
},
"ghost": {
"bg": "#0f172a",
"border": "#334155",
"text": "#cbd5e1",
"hover_bg": "#1f2937",
"hover_border": "#475569",
"hover_text": "#f8fafc"
},
"danger": {
"bg": "#4c0519",
"border": "#881337",
"text": "#fecdd3",
"hover_bg": "#9f1239",
"hover_border": "#be123c",
"hover_text": "#fff1f2"
}
},
"state": {
"info": {
"bg": "#1e1b4b",
"border": "#4338ca",
"text": "#c7d2fe"
},
"success": {
"bg": "#022c22",
"border": "#047857",
"text": "#a7f3d0"
},
"warning": {
"bg": "#451a03",
"border": "#b45309",
"text": "#fde68a"
},
"danger": {
"bg": "#4c0519",
"border": "#be123c",
"text": "#fecdd3"
}
}
}
}
}

View file

@ -1,313 +0,0 @@
{
"id": "scholarly",
"label": "Scholarly",
"description": "Calm, academic blues with restrained contrast.",
"modes": {
"light": {
"scale": {
"brand": {
"50": "#f3f6fb",
"100": "#e7edf7",
"200": "#cedcee",
"300": "#afc6df",
"400": "#8eaac8",
"500": "#6f8fac",
"600": "#5a7490",
"700": "#495f77",
"800": "#3f5063",
"900": "#374554",
"950": "#242d38"
},
"info": {
"50": "#f0f9ff",
"100": "#e0f2fe",
"200": "#bae6fd",
"300": "#7dd3fc",
"400": "#38bdf8",
"500": "#0ea5e9",
"600": "#0284c7",
"700": "#0369a1",
"800": "#075985",
"900": "#0c4a6e",
"950": "#082f49"
},
"success": {
"50": "#ecfdf5",
"100": "#d1fae5",
"200": "#a7f3d0",
"300": "#6ee7b7",
"400": "#34d399",
"500": "#10b981",
"600": "#059669",
"700": "#047857",
"800": "#065f46",
"900": "#064e3b",
"950": "#022c22"
},
"warning": {
"50": "#fffbeb",
"100": "#fef3c7",
"200": "#fde68a",
"300": "#fcd34d",
"400": "#fbbf24",
"500": "#f59e0b",
"600": "#d97706",
"700": "#b45309",
"800": "#92400e",
"900": "#78350f",
"950": "#451a03"
},
"danger": {
"50": "#fff1f2",
"100": "#ffe4e6",
"200": "#fecdd3",
"300": "#fda4af",
"400": "#fb7185",
"500": "#f43f5e",
"600": "#e11d48",
"700": "#be123c",
"800": "#9f1239",
"900": "#881337",
"950": "#4c0519"
}
},
"surface": {
"app": "#f3f6fb",
"nav": "#ffffff",
"nav_active": "#495f77",
"card": "#ffffff",
"card_muted": "#e7edf7",
"table": "#ffffff",
"table_header": "#f3f6fb",
"input": "#ffffff",
"overlay": "#0f172a"
},
"text": {
"primary": "#1f2937",
"secondary": "#475569",
"muted": "#64748b",
"inverse": "#f8fafc",
"link": "#495f77"
},
"border": {
"default": "#cedcee",
"strong": "#afc6df",
"subtle": "#e7edf7",
"interactive": "#8eaac8"
},
"focus": {
"ring": "#6f8fac",
"ring_offset": "#f3f6fb"
},
"action": {
"primary": {
"bg": "#5a7490",
"border": "#5a7490",
"text": "#f8fafc",
"hover_bg": "#495f77",
"hover_border": "#495f77",
"hover_text": "#ffffff"
},
"secondary": {
"bg": "#eef2f7",
"border": "#cedcee",
"text": "#334155",
"hover_bg": "#e2e8f0",
"hover_border": "#afc6df",
"hover_text": "#1f2937"
},
"ghost": {
"bg": "#f3f6fb",
"border": "#cedcee",
"text": "#495f77",
"hover_bg": "#e7edf7",
"hover_border": "#afc6df",
"hover_text": "#374554"
},
"danger": {
"bg": "#ffe4e6",
"border": "#fecdd3",
"text": "#be123c",
"hover_bg": "#fecdd3",
"hover_border": "#fda4af",
"hover_text": "#9f1239"
}
},
"state": {
"info": {
"bg": "#f0f9ff",
"border": "#bae6fd",
"text": "#075985"
},
"success": {
"bg": "#ecfdf5",
"border": "#a7f3d0",
"text": "#047857"
},
"warning": {
"bg": "#fffbeb",
"border": "#fde68a",
"text": "#92400e"
},
"danger": {
"bg": "#fff1f2",
"border": "#fecdd3",
"text": "#be123c"
}
}
},
"dark": {
"scale": {
"brand": {
"50": "#f3f6fb",
"100": "#e7edf7",
"200": "#cedcee",
"300": "#afc6df",
"400": "#8eaac8",
"500": "#6f8fac",
"600": "#5a7490",
"700": "#495f77",
"800": "#3f5063",
"900": "#374554",
"950": "#242d38"
},
"info": {
"50": "#f0f9ff",
"100": "#e0f2fe",
"200": "#bae6fd",
"300": "#7dd3fc",
"400": "#38bdf8",
"500": "#0ea5e9",
"600": "#0284c7",
"700": "#0369a1",
"800": "#075985",
"900": "#0c4a6e",
"950": "#082f49"
},
"success": {
"50": "#ecfdf5",
"100": "#d1fae5",
"200": "#a7f3d0",
"300": "#6ee7b7",
"400": "#34d399",
"500": "#10b981",
"600": "#059669",
"700": "#047857",
"800": "#065f46",
"900": "#064e3b",
"950": "#022c22"
},
"warning": {
"50": "#fffbeb",
"100": "#fef3c7",
"200": "#fde68a",
"300": "#fcd34d",
"400": "#fbbf24",
"500": "#f59e0b",
"600": "#d97706",
"700": "#b45309",
"800": "#92400e",
"900": "#78350f",
"950": "#451a03"
},
"danger": {
"50": "#fff1f2",
"100": "#ffe4e6",
"200": "#fecdd3",
"300": "#fda4af",
"400": "#fb7185",
"500": "#f43f5e",
"600": "#e11d48",
"700": "#be123c",
"800": "#9f1239",
"900": "#881337",
"950": "#4c0519"
}
},
"surface": {
"app": "#0b1220",
"nav": "#111827",
"nav_active": "#8eaac8",
"card": "#111827",
"card_muted": "#1f2937",
"table": "#111827",
"table_header": "#1f2937",
"input": "#0f172a",
"overlay": "#020617"
},
"text": {
"primary": "#e5e7eb",
"secondary": "#cbd5e1",
"muted": "#94a3b8",
"inverse": "#0b1220",
"link": "#afc6df"
},
"border": {
"default": "#334155",
"strong": "#475569",
"subtle": "#1f2937",
"interactive": "#64748b"
},
"focus": {
"ring": "#8eaac8",
"ring_offset": "#0b1220"
},
"action": {
"primary": {
"bg": "#8eaac8",
"border": "#8eaac8",
"text": "#0b1220",
"hover_bg": "#afc6df",
"hover_border": "#afc6df",
"hover_text": "#020617"
},
"secondary": {
"bg": "#1f2937",
"border": "#475569",
"text": "#e5e7eb",
"hover_bg": "#334155",
"hover_border": "#64748b",
"hover_text": "#f8fafc"
},
"ghost": {
"bg": "#0f172a",
"border": "#334155",
"text": "#cbd5e1",
"hover_bg": "#1f2937",
"hover_border": "#475569",
"hover_text": "#f8fafc"
},
"danger": {
"bg": "#4c0519",
"border": "#881337",
"text": "#fecdd3",
"hover_bg": "#9f1239",
"hover_border": "#be123c",
"hover_text": "#fff1f2"
}
},
"state": {
"info": {
"bg": "#082f49",
"border": "#0369a1",
"text": "#bae6fd"
},
"success": {
"bg": "#022c22",
"border": "#047857",
"text": "#a7f3d0"
},
"warning": {
"bg": "#451a03",
"border": "#b45309",
"text": "#fde68a"
},
"danger": {
"bg": "#4c0519",
"border": "#be123c",
"text": "#fecdd3"
}
}
}
}
}

View file

@ -1,313 +0,0 @@
{
"id": "tide",
"label": "Tide",
"description": "Ocean-inspired palette with teal emphasis.",
"modes": {
"light": {
"scale": {
"brand": {
"50": "#f0fdfa",
"100": "#ccfbf1",
"200": "#99f6e4",
"300": "#5eead4",
"400": "#2dd4bf",
"500": "#14b8a6",
"600": "#0d9488",
"700": "#0f766e",
"800": "#115e59",
"900": "#134e4a",
"950": "#042f2e"
},
"info": {
"50": "#f0f9ff",
"100": "#e0f2fe",
"200": "#bae6fd",
"300": "#7dd3fc",
"400": "#38bdf8",
"500": "#0ea5e9",
"600": "#0284c7",
"700": "#0369a1",
"800": "#075985",
"900": "#0c4a6e",
"950": "#082f49"
},
"success": {
"50": "#ecfdf5",
"100": "#d1fae5",
"200": "#a7f3d0",
"300": "#6ee7b7",
"400": "#34d399",
"500": "#10b981",
"600": "#059669",
"700": "#047857",
"800": "#065f46",
"900": "#064e3b",
"950": "#022c22"
},
"warning": {
"50": "#fffbeb",
"100": "#fef3c7",
"200": "#fde68a",
"300": "#fcd34d",
"400": "#fbbf24",
"500": "#f59e0b",
"600": "#d97706",
"700": "#b45309",
"800": "#92400e",
"900": "#78350f",
"950": "#451a03"
},
"danger": {
"50": "#fff1f2",
"100": "#ffe4e6",
"200": "#fecdd3",
"300": "#fda4af",
"400": "#fb7185",
"500": "#f43f5e",
"600": "#e11d48",
"700": "#be123c",
"800": "#9f1239",
"900": "#881337",
"950": "#4c0519"
}
},
"surface": {
"app": "#f0fdfa",
"nav": "#ffffff",
"nav_active": "#0f766e",
"card": "#ffffff",
"card_muted": "#ccfbf1",
"table": "#ffffff",
"table_header": "#f0fdfa",
"input": "#ffffff",
"overlay": "#0f172a"
},
"text": {
"primary": "#134e4a",
"secondary": "#0f766e",
"muted": "#115e59",
"inverse": "#f8fafc",
"link": "#0d9488"
},
"border": {
"default": "#99f6e4",
"strong": "#5eead4",
"subtle": "#ccfbf1",
"interactive": "#2dd4bf"
},
"focus": {
"ring": "#14b8a6",
"ring_offset": "#f0fdfa"
},
"action": {
"primary": {
"bg": "#0d9488",
"border": "#0d9488",
"text": "#f8fafc",
"hover_bg": "#0f766e",
"hover_border": "#0f766e",
"hover_text": "#ffffff"
},
"secondary": {
"bg": "#ecfeff",
"border": "#99f6e4",
"text": "#115e59",
"hover_bg": "#ccfbf1",
"hover_border": "#5eead4",
"hover_text": "#134e4a"
},
"ghost": {
"bg": "#f0fdfa",
"border": "#99f6e4",
"text": "#0f766e",
"hover_bg": "#ccfbf1",
"hover_border": "#5eead4",
"hover_text": "#115e59"
},
"danger": {
"bg": "#ffe4e6",
"border": "#fecdd3",
"text": "#be123c",
"hover_bg": "#fecdd3",
"hover_border": "#fda4af",
"hover_text": "#9f1239"
}
},
"state": {
"info": {
"bg": "#f0f9ff",
"border": "#bae6fd",
"text": "#075985"
},
"success": {
"bg": "#ecfdf5",
"border": "#a7f3d0",
"text": "#047857"
},
"warning": {
"bg": "#fffbeb",
"border": "#fde68a",
"text": "#92400e"
},
"danger": {
"bg": "#fff1f2",
"border": "#fecdd3",
"text": "#be123c"
}
}
},
"dark": {
"scale": {
"brand": {
"50": "#f0fdfa",
"100": "#ccfbf1",
"200": "#99f6e4",
"300": "#5eead4",
"400": "#2dd4bf",
"500": "#14b8a6",
"600": "#0d9488",
"700": "#0f766e",
"800": "#115e59",
"900": "#134e4a",
"950": "#042f2e"
},
"info": {
"50": "#f0f9ff",
"100": "#e0f2fe",
"200": "#bae6fd",
"300": "#7dd3fc",
"400": "#38bdf8",
"500": "#0ea5e9",
"600": "#0284c7",
"700": "#0369a1",
"800": "#075985",
"900": "#0c4a6e",
"950": "#082f49"
},
"success": {
"50": "#ecfdf5",
"100": "#d1fae5",
"200": "#a7f3d0",
"300": "#6ee7b7",
"400": "#34d399",
"500": "#10b981",
"600": "#059669",
"700": "#047857",
"800": "#065f46",
"900": "#064e3b",
"950": "#022c22"
},
"warning": {
"50": "#fffbeb",
"100": "#fef3c7",
"200": "#fde68a",
"300": "#fcd34d",
"400": "#fbbf24",
"500": "#f59e0b",
"600": "#d97706",
"700": "#b45309",
"800": "#92400e",
"900": "#78350f",
"950": "#451a03"
},
"danger": {
"50": "#fff1f2",
"100": "#ffe4e6",
"200": "#fecdd3",
"300": "#fda4af",
"400": "#fb7185",
"500": "#f43f5e",
"600": "#e11d48",
"700": "#be123c",
"800": "#9f1239",
"900": "#881337",
"950": "#4c0519"
}
},
"surface": {
"app": "#041f1f",
"nav": "#042f2e",
"nav_active": "#5eead4",
"card": "#042f2e",
"card_muted": "#134e4a",
"table": "#042f2e",
"table_header": "#134e4a",
"input": "#063a38",
"overlay": "#020617"
},
"text": {
"primary": "#e6fffb",
"secondary": "#c9fff3",
"muted": "#99f6e4",
"inverse": "#041f1f",
"link": "#5eead4"
},
"border": {
"default": "#115e59",
"strong": "#0f766e",
"subtle": "#134e4a",
"interactive": "#2dd4bf"
},
"focus": {
"ring": "#2dd4bf",
"ring_offset": "#041f1f"
},
"action": {
"primary": {
"bg": "#2dd4bf",
"border": "#2dd4bf",
"text": "#042f2e",
"hover_bg": "#5eead4",
"hover_border": "#5eead4",
"hover_text": "#042f2e"
},
"secondary": {
"bg": "#0f766e",
"border": "#14b8a6",
"text": "#e6fffb",
"hover_bg": "#0d9488",
"hover_border": "#2dd4bf",
"hover_text": "#ffffff"
},
"ghost": {
"bg": "#042f2e",
"border": "#0f766e",
"text": "#c9fff3",
"hover_bg": "#115e59",
"hover_border": "#14b8a6",
"hover_text": "#ffffff"
},
"danger": {
"bg": "#4c0519",
"border": "#881337",
"text": "#fecdd3",
"hover_bg": "#9f1239",
"hover_border": "#be123c",
"hover_text": "#fff1f2"
}
},
"state": {
"info": {
"bg": "#082f49",
"border": "#0369a1",
"text": "#bae6fd"
},
"success": {
"bg": "#022c22",
"border": "#047857",
"text": "#a7f3d0"
},
"warning": {
"bg": "#451a03",
"border": "#b45309",
"text": "#fde68a"
},
"danger": {
"bg": "#4c0519",
"border": "#be123c",
"text": "#fecdd3"
}
}
}
}
}