mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-21 18:55:36 +02:00
feat(ui): add Storybook design system refinements
This commit is contained in:
parent
46ad816c4c
commit
3ccfcce87b
15 changed files with 2839 additions and 666 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -23,3 +23,10 @@
|
|||
!frontend/**
|
||||
frontend/node_modules/
|
||||
frontend/node_modules/**
|
||||
frontend/result
|
||||
frontend/result/**
|
||||
frontend/dist/
|
||||
frontend/dist/**
|
||||
.DS_Store
|
||||
frontend/storybook-static/
|
||||
frontend/storybook-static/**
|
||||
|
|
|
|||
13
frontend/.storybook/main.js
Normal file
13
frontend/.storybook/main.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||
const config = {
|
||||
stories: ['../src/**/*.stories.{js,jsx,ts,tsx}'],
|
||||
addons: [
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
5
frontend/.storybook/preview.js
Normal file
5
frontend/.storybook/preview.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import '../src/styles.css';
|
||||
|
||||
export const parameters = {
|
||||
layout: 'fullscreen',
|
||||
};
|
||||
1892
frontend/bun.lock
1892
frontend/bun.lock
File diff suppressed because it is too large
Load diff
|
|
@ -6,14 +6,24 @@
|
|||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^7.6.20",
|
||||
"@storybook/addon-interactions": "^7.6.20",
|
||||
"@storybook/blocks": "^7.6.20",
|
||||
"@storybook/preview-api": "^7.6.20",
|
||||
"@storybook/react": "^7.6.20",
|
||||
"@storybook/react-vite": "^7.6.20",
|
||||
"@storybook/test": "^7.6.20",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"storybook": "^7.6.20",
|
||||
"vite": "^5.4.11"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,23 +172,32 @@ export default function App() {
|
|||
<main className="app-shell">
|
||||
<div className="workspace">
|
||||
{view === 'archive' && (
|
||||
<div className="search-row">
|
||||
<input
|
||||
className="search-input"
|
||||
type="search"
|
||||
aria-label="Search archive"
|
||||
aria-busy={searchBusy}
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<div className="result-count">
|
||||
{resultCount}
|
||||
<div className="toolbar">
|
||||
<div className="search-field">
|
||||
<span className="ico" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
||||
<circle cx="11" cy="11" r="7"/><line x1="16.5" y1="16.5" x2="21" y2="21"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
className="search-input"
|
||||
type="search"
|
||||
aria-label="Search archive"
|
||||
aria-busy={searchBusy}
|
||||
placeholder="Search titles, URLs, types, tags…"
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<span className="kbd">⌘K</span>
|
||||
</div>
|
||||
<span className="result-count">
|
||||
{resultCount && <><b>{resultCount.split(' ')[0]}</b>{' '}{resultCount.split(' ').slice(1).join(' ')}</>}
|
||||
{tagFilter && (
|
||||
<button className="tag-filter-badge" onClick={handleClearTagFilter}>
|
||||
× {tagFilter}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{view === 'archive' && (
|
||||
|
|
|
|||
41
frontend/src/components/CaptureDialog.stories.jsx
Normal file
41
frontend/src/components/CaptureDialog.stories.jsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { useState } from 'react';
|
||||
import CaptureDialog from './CaptureDialog';
|
||||
|
||||
export default {
|
||||
component: CaptureDialog,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
function CaptureDialogWrapper(args) {
|
||||
const [open, setOpen] = useState(args.open);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setOpen(true)} style={{ padding: '8px 16px', marginBottom: '16px' }}>
|
||||
Open Capture Dialog
|
||||
</button>
|
||||
<CaptureDialog
|
||||
{...args}
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Default = {
|
||||
render: (args) => <CaptureDialogWrapper {...args} />,
|
||||
args: {
|
||||
open: false,
|
||||
archiveId: 'archive_1',
|
||||
onCaptured: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
export const Open = {
|
||||
args: {
|
||||
open: true,
|
||||
archiveId: 'archive_1',
|
||||
onCaptured: () => {},
|
||||
},
|
||||
};
|
||||
|
|
@ -192,7 +192,7 @@ export default function CollectionsView({ archiveId }) {
|
|||
{collections.map(c => (
|
||||
<button
|
||||
key={c.collection_uid}
|
||||
className={`coll-row${selectedUid === c.collection_uid ? ' is-active' : ''}`}
|
||||
className={`coll-sidebar-row${selectedUid === c.collection_uid ? ' is-active' : ''}`}
|
||||
onClick={() => setSelectedUid(c.collection_uid)}
|
||||
>
|
||||
<span className="coll-row-name">{c.name}</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import { useState, useEffect, useRef } from 'react'
|
||||
import { fetchEntryDetail, fetchEntryTags, assignTag, removeTag, listEntryCollections } from '../api'
|
||||
import { formatTimestamp, formatBytes, valueText } from '../utils'
|
||||
import { formatTimestamp, formatBytes, valueText, sourceIconSvg } from '../utils'
|
||||
|
||||
const VIS_LABEL = { 0: 'Private', 1: 'Public', 2: 'Users only', 3: 'Public' }
|
||||
|
||||
const ExternalIcon = () => (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M7 17 17 7M9 7h8v8"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet, tagNodes, onTagsRefresh }) {
|
||||
const [detail, setDetail] = useState(null)
|
||||
|
|
@ -53,54 +61,60 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet,
|
|||
const updated = await fetchEntryTags(archiveId, selectedEntry.entry_uid)
|
||||
setTags(updated)
|
||||
onTagsRefresh()
|
||||
} catch (e) {
|
||||
// silently ignore for now; could add error state
|
||||
} catch {
|
||||
// silently ignore
|
||||
}
|
||||
}
|
||||
|
||||
const metaRows = detail ? [
|
||||
['Added', formatTimestamp(detail.summary.archived_at)],
|
||||
['Source', detail.summary.source_kind],
|
||||
['Type', detail.summary.entity_kind],
|
||||
['Visibility', detail.summary.visibility],
|
||||
['Root', detail.structured_root_relpath],
|
||||
] : []
|
||||
|
||||
return (
|
||||
<aside className="context-rail">
|
||||
<div className="rail-title">Context</div>
|
||||
{!selectedEntry ? (
|
||||
<div className="rail-body">Select an entry.</div>
|
||||
) : !detail ? (
|
||||
<div className="rail-body">Loading…</div>
|
||||
) : (
|
||||
<div className="rail-body">
|
||||
<strong className="rail-entry-title">
|
||||
{valueText(detail.summary.title) || valueText(detail.summary.entry_uid)}
|
||||
</strong>
|
||||
<div className="rail-eyebrow">Context</div>
|
||||
|
||||
<div className="rail-section">
|
||||
{detail.summary.original_url && (
|
||||
<div className="rail-item">
|
||||
<span className="rail-label">Original URL</span>:{' '}
|
||||
<a
|
||||
href={detail.summary.original_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rail-url-link"
|
||||
>
|
||||
{detail.summary.original_url}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{[
|
||||
['Added', formatTimestamp(detail.summary.archived_at)],
|
||||
['Source', detail.summary.source_kind],
|
||||
['Type', detail.summary.entity_kind],
|
||||
['Visibility', detail.summary.visibility],
|
||||
['Structured root', detail.structured_root_relpath],
|
||||
].map(([label, value]) => (
|
||||
<div key={label} className="rail-item">
|
||||
<span className="rail-label">{label}</span>: {valueText(value)}
|
||||
{!selectedEntry ? (
|
||||
<p className="tags-empty">Select an entry.</p>
|
||||
) : !detail ? (
|
||||
<p className="tags-empty">Loading…</p>
|
||||
) : (
|
||||
<>
|
||||
<h2 className="rail-title">
|
||||
{valueText(detail.summary.title) || valueText(detail.summary.entry_uid)}
|
||||
</h2>
|
||||
|
||||
{detail.summary.original_url && (
|
||||
<a
|
||||
className="url-tile"
|
||||
href={detail.summary.original_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="ico" dangerouslySetInnerHTML={{ __html: sourceIconSvg(detail.summary.source_kind) }} />
|
||||
<span className="u-text">{detail.summary.original_url}</span>
|
||||
<span className="ext"><ExternalIcon /></span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
<div className="meta-list">
|
||||
{metaRows.filter(([, v]) => v != null && v !== '').map(([label, value]) => (
|
||||
<div key={label} className="meta-item">
|
||||
<span className="meta-k">{label}</span>
|
||||
<span className={`meta-v${label === 'Root' ? ' mono' : ''}`}>{valueText(value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{detail.artifacts.length > 0 ? (
|
||||
{detail.artifacts.length > 0 && (
|
||||
<div className="rail-section">
|
||||
<div className="rail-section-heading">Artifacts ({detail.artifacts.length})</div>
|
||||
<div className="rail-section-heading">
|
||||
Artifacts <span className="num">{detail.artifacts.length}</span>
|
||||
</div>
|
||||
<ul className="artifact-list">
|
||||
{detail.artifacts.map((artifact, index) => (
|
||||
<li key={index}>
|
||||
|
|
@ -110,64 +124,65 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet,
|
|||
rel="noopener noreferrer"
|
||||
className="artifact-link"
|
||||
>
|
||||
{artifact.artifact_role.replace(/_/g, ' ')}
|
||||
{artifact.byte_size != null ? ` (${formatBytes(artifact.byte_size)})` : ''}
|
||||
<span className="artifact-name">{artifact.artifact_role.replace(/_/g, ' ')}</span>
|
||||
<span className="artifact-size">
|
||||
{artifact.byte_size != null ? formatBytes(artifact.byte_size) : '—'}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rail-item muted">No artifacts.</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedEntry && (
|
||||
<>
|
||||
<div className="entry-tags">
|
||||
<div className="rail-section">
|
||||
<div className="rail-section-heading">Tags</div>
|
||||
{tags.length === 0 ? (
|
||||
<span className="muted">No tags.</span>
|
||||
<p className="tags-empty">No tags yet.</p>
|
||||
) : (
|
||||
tags.map(tag => (
|
||||
<span key={tag.tag_uid} className="tag-pill" title={tag.full_path}>
|
||||
{tag.name}
|
||||
<button
|
||||
className="remove-tag"
|
||||
title={`Remove tag ${tag.full_path}`}
|
||||
onClick={() => handleRemoveTag(tag.tag_uid)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<div className="assign-tag-form">
|
||||
<input
|
||||
className="assign-tag-input"
|
||||
type="text"
|
||||
placeholder="/science/cs"
|
||||
autoComplete="off"
|
||||
value={assignInput}
|
||||
onChange={e => setAssignInput(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleAssignTag() }}
|
||||
/>
|
||||
<button className="assign-tag-btn" onClick={handleAssignTag}>Add tag</button>
|
||||
{assignError && (
|
||||
<div className="muted" style={{ fontSize: '0.85em', color: 'var(--accent)' }}>
|
||||
{assignError}
|
||||
<div className="tags-wrap">
|
||||
{tags.map(tag => (
|
||||
<span key={tag.tag_uid} className="tag-pill" title={tag.full_path}>
|
||||
{tag.name}
|
||||
<button
|
||||
className="remove"
|
||||
title={`Remove tag ${tag.full_path}`}
|
||||
onClick={() => handleRemoveTag(tag.tag_uid)}
|
||||
>×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{assignError && (
|
||||
<p style={{ color: 'var(--accent)', fontSize: 13, margin: '0 0 8px' }}>{assignError}</p>
|
||||
)}
|
||||
<div className="tag-input-wrap">
|
||||
<span className="hash">/</span>
|
||||
<input
|
||||
className="tag-input"
|
||||
type="text"
|
||||
placeholder="science/cs"
|
||||
autoComplete="off"
|
||||
value={assignInput}
|
||||
onChange={e => setAssignInput(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleAssignTag() }}
|
||||
/>
|
||||
<button className="tag-add-btn" onClick={handleAssignTag}>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
{selectedEntry && entryCollections.length > 0 && (
|
||||
|
||||
{entryCollections.length > 0 && (
|
||||
<div className="rail-section">
|
||||
<div className="rail-section-heading">Collections</div>
|
||||
{entryCollections.map(c => (
|
||||
<div key={c.collection_uid} className="rail-item">
|
||||
<span className="rail-label">{c.collection_uid}</span>:{' '}
|
||||
<span className="muted">
|
||||
{{ 0: 'Private', 1: 'Public', 2: 'Users only', 3: 'Public' }[c.visibility_bits] ?? `bits:${c.visibility_bits}`}
|
||||
<div key={c.collection_uid} className="coll-row">
|
||||
<span className="coll-name">{c.collection_uid}</span>
|
||||
<span className="vis-badge">
|
||||
{VIS_LABEL[c.visibility_bits] ?? `bits:${c.visibility_bits}`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
66
frontend/src/components/EntryRow.stories.jsx
Normal file
66
frontend/src/components/EntryRow.stories.jsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import EntryRow from './EntryRow';
|
||||
|
||||
export default {
|
||||
component: EntryRow,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
const sampleEntry = {
|
||||
entry_uid: 'entry_123',
|
||||
title: 'A Great Article About Web Design',
|
||||
archived_at: '2026-06-27T10:30:00Z',
|
||||
entity_kind: 'page',
|
||||
source_kind: 'web',
|
||||
total_artifact_bytes: 2048576,
|
||||
original_url: 'https://example.com/article',
|
||||
has_favicon: false,
|
||||
};
|
||||
|
||||
const videoEntry = {
|
||||
entry_uid: 'entry_456',
|
||||
title: 'React Performance Tips',
|
||||
archived_at: '2026-06-26T14:15:00Z',
|
||||
entity_kind: 'video',
|
||||
source_kind: 'youtube',
|
||||
total_artifact_bytes: 104857600,
|
||||
original_url: 'https://youtube.com/watch?v=xyz',
|
||||
has_favicon: false,
|
||||
};
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
entry: sampleEntry,
|
||||
archiveId: 'archive_1',
|
||||
isSelected: false,
|
||||
onSelect: () => {},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '178px 38% 130px 110px 34%',
|
||||
gap: '10px',
|
||||
padding: '10px',
|
||||
background: 'var(--paper-3)',
|
||||
}}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const Selected = {
|
||||
args: {
|
||||
...Default.args,
|
||||
isSelected: true,
|
||||
},
|
||||
decorators: Default.decorators,
|
||||
};
|
||||
|
||||
export const Video = {
|
||||
args: {
|
||||
...Default.args,
|
||||
entry: videoEntry,
|
||||
},
|
||||
decorators: Default.decorators,
|
||||
};
|
||||
|
|
@ -16,10 +16,12 @@ export default function Topbar({ archives, archiveId, onArchiveChange, view, onV
|
|||
return (
|
||||
<header className="topbar">
|
||||
<div className="brand">Archivr</div>
|
||||
<select className="archive-switcher" aria-label="Select archive"
|
||||
value={archiveId ?? ''} onChange={e => onArchiveChange(e.target.value)}>
|
||||
{archives.map(a => <option key={a.id} value={a.id}>{a.label}</option>)}
|
||||
</select>
|
||||
<div className="switcher">
|
||||
<select aria-label="Select archive"
|
||||
value={archiveId ?? ''} onChange={e => onArchiveChange(e.target.value)}>
|
||||
{archives.map(a => <option key={a.id} value={a.id}>{a.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<nav className="nav" aria-label="Primary">
|
||||
{['archive', 'runs', 'admin', 'tags', 'collections', 'settings'].map(name => (
|
||||
<button key={name} className={`nav-link${view === name ? ' is-active' : ''}`}
|
||||
|
|
|
|||
36
frontend/src/components/Topbar.stories.jsx
Normal file
36
frontend/src/components/Topbar.stories.jsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import Topbar from './Topbar';
|
||||
|
||||
export default {
|
||||
component: Topbar,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
const defaultArchives = [
|
||||
{ id: '1', label: 'Main Archive' },
|
||||
{ id: '2', label: 'Research' },
|
||||
{ id: '3', label: 'Screenshots' },
|
||||
];
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
archives: defaultArchives,
|
||||
archiveId: '1',
|
||||
onArchiveChange: () => {},
|
||||
view: 'archive',
|
||||
onViewChange: () => {},
|
||||
onCaptureClick: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithUser = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div style={{ background: 'var(--paper)' }}>
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
102
frontend/src/components/UIPatterns.stories.jsx
Normal file
102
frontend/src/components/UIPatterns.stories.jsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
export default {
|
||||
title: 'UI Patterns',
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export const Buttons = () => (
|
||||
<div style={{ padding: '20px', display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
|
||||
<button className="capture-button">+ Capture</button>
|
||||
<button className="nav-link">Nav Link</button>
|
||||
<button className="nav-link is-active">Nav Link (Active)</button>
|
||||
<button className="assign-tag-btn">Add Tag</button>
|
||||
<button className="assign-tag-btn" style={{ pointerEvents: 'none', opacity: 0.6 }}>Disabled</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const FormInputs = () => (
|
||||
<div style={{ padding: '20px', maxWidth: '400px', display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--muted)', fontSize: '12px' }}>Search Input</label>
|
||||
<input className="search-input" type="search" placeholder="Search archive..." />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--muted)', fontSize: '12px' }}>Capture Input</label>
|
||||
<input className="capture-input" type="text" placeholder="tweet:1234567890 or https://..." />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--muted)', fontSize: '12px' }}>Tag Input</label>
|
||||
<input className="assign-tag-input" type="text" placeholder="/science/cs" />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--muted)', fontSize: '12px' }}>Archive Switcher</label>
|
||||
<select className="archive-switcher" style={{ width: '100%' }}>
|
||||
<option>Main Archive</option>
|
||||
<option>Research</option>
|
||||
<option>Screenshots</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Pills = () => (
|
||||
<div style={{ padding: '20px', display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
<span className="type-pill">page</span>
|
||||
<span className="type-pill">video</span>
|
||||
<span className="type-pill">tweet_thread</span>
|
||||
<span className="type-pill">file</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const TagPills = () => (
|
||||
<div style={{ padding: '20px', display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
<span className="tag-pill">
|
||||
science
|
||||
<button className="remove-tag">×</button>
|
||||
</span>
|
||||
<span className="tag-pill">
|
||||
computer-science
|
||||
<button className="remove-tag">×</button>
|
||||
</span>
|
||||
<span className="tag-pill">
|
||||
learning
|
||||
<button className="remove-tag">×</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ColorPalette = () => (
|
||||
<div style={{ padding: '20px', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px' }}>
|
||||
<div>
|
||||
<div style={{ height: '80px', background: 'var(--ink)', marginBottom: '8px', borderRadius: '4px' }} />
|
||||
<strong>Ink</strong> (#20251f)
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ height: '80px', background: 'var(--paper)', border: '1px solid var(--line)', marginBottom: '8px', borderRadius: '4px' }} />
|
||||
<strong>Paper</strong> (#f5f0e7)
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ height: '80px', background: 'var(--accent)', marginBottom: '8px', borderRadius: '4px' }} />
|
||||
<strong>Accent</strong> (#8d3f30)
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ height: '80px', background: 'var(--accent-2)', marginBottom: '8px', borderRadius: '4px' }} />
|
||||
<strong>Accent 2</strong> (#b78342)
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ height: '80px', background: 'var(--link)', marginBottom: '8px', borderRadius: '4px' }} />
|
||||
<strong>Link</strong> (#245f72)
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ height: '80px', background: 'var(--top)', marginBottom: '8px', borderRadius: '4px' }} />
|
||||
<strong>Top</strong> (#141d18)
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ height: '80px', background: 'var(--muted)', marginBottom: '8px', borderRadius: '4px' }} />
|
||||
<strong>Muted</strong> (#666a61)
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ height: '80px', background: 'var(--line)', marginBottom: '8px', borderRadius: '4px' }} />
|
||||
<strong>Line</strong> (#d2c6b5)
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -5,7 +5,7 @@ export default defineConfig({
|
|||
plugins: [react()],
|
||||
build: {
|
||||
outDir: "../crates/archivr-server/static",
|
||||
emptyOutDir: false,
|
||||
emptyOutDir: true,
|
||||
assetsDir: "assets",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue