catpuccing moka
This commit is contained in:
parent
381ed6270e
commit
ead8819e39
8 changed files with 154 additions and 64 deletions
|
|
@ -3,8 +3,8 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#f8fafc" />
|
||||
<title>CMS</title>
|
||||
<meta name="theme-color" content="#1e1e2e" />
|
||||
<title>Urpi's backlog</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { useAuth } from './auth'
|
|||
function Shell({ children }: { children: React.ReactNode }) {
|
||||
const { user, signOut } = useAuth()
|
||||
return <main className="shell">
|
||||
<header className="header"><Link className="brand" to="/">CMS</Link>{user && <button className="text-button" onClick={() => void signOut()}>Sign out</button>}</header>
|
||||
<header className="header"><Link className="brand" to="/"><span className="brand-prompt">~/</span> Urpi's backlog<span className="brand-cursor">_</span><small>linux · go · java · ideas</small></Link>{user && <button className="text-button" onClick={() => void signOut()}>Sign out</button>}</header>
|
||||
{children}
|
||||
</main>
|
||||
}
|
||||
|
|
@ -16,8 +16,8 @@ function Login() {
|
|||
if (user) return <Navigate to="/protected" replace />
|
||||
if (loading) return <Shell><div className="card"><p>Checking your session…</p></div></Shell>
|
||||
return <Shell><section className="card auth-card">
|
||||
<span className="eyebrow">Welcome</span><h1>Sign in to CMS</h1>
|
||||
<p className="muted">Sign in securely with your external identity provider.</p>
|
||||
<span className="eyebrow">Welcome, developer</span><h1>Sign in to Urpi's backlog</h1>
|
||||
<p className="muted">A cozy backlog for Linux, Go, Java, and the ideas between them.</p>
|
||||
{error && <p className="error" role="alert">{error}</p>}
|
||||
<button className="google-button" onClick={() => void signIn()}><span aria-hidden="true">G</span> Continue with Google</button>
|
||||
</section></Shell>
|
||||
|
|
@ -49,19 +49,21 @@ function ContentList() {
|
|||
const [categories, setCategories] = useState<Category[]>([])
|
||||
const [badgeOptions, setBadgeOptions] = useState<Badge[]>([])
|
||||
const [category, setCategory] = useState('')
|
||||
const [status, setStatus] = useState('')
|
||||
const [badges, setBadges] = useState<string[]>([])
|
||||
const [badgeInput, setBadgeInput] = useState('')
|
||||
const loadingMore = useRef(false)
|
||||
const requestVersion = useRef(0)
|
||||
const sentinel = useRef<HTMLDivElement>(null)
|
||||
const filters = { category, status, badges }
|
||||
|
||||
const load = async (next = false) => {
|
||||
const load = async (next = false, activeFilters = filters) => {
|
||||
if (loadingMore.current || (next && !cursor)) return
|
||||
const version = next ? requestVersion.current : ++requestVersion.current
|
||||
loadingMore.current = true
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await listContent(next ? cursor : undefined, category, badges)
|
||||
const response = await listContent(next ? cursor : undefined, activeFilters.category, activeFilters.status, activeFilters.badges)
|
||||
if (version !== requestVersion.current) return
|
||||
const page = Array.isArray(response) ? response : response.items ?? response.content ?? response.data ?? []
|
||||
setItems(current => next ? [...current, ...page] : page)
|
||||
|
|
@ -85,19 +87,28 @@ function ContentList() {
|
|||
loadingMore.current = false
|
||||
setItems([])
|
||||
setCursor(undefined)
|
||||
void load()
|
||||
}, [category, badges.join(',')])
|
||||
void load(false, { category, status, badges })
|
||||
}, [category, status, badges.join(',')])
|
||||
useEffect(() => {
|
||||
const node = sentinel.current
|
||||
if (!node) return
|
||||
const observer = new IntersectionObserver(entries => { if (entries[0]?.isIntersecting) void load(true) }, { rootMargin: '240px' })
|
||||
observer.observe(node)
|
||||
return () => observer.disconnect()
|
||||
}, [cursor])
|
||||
}, [cursor, category, status, badges.join(',')])
|
||||
|
||||
return <Shell><section className="content-page">
|
||||
<div className="page-heading"><div><span className="eyebrow">Journal</span><h1>Latest content</h1></div></div>
|
||||
<div className="filters" aria-label="Content filters">
|
||||
<label className="status-filter">Status
|
||||
<select value={status} onChange={event => setStatus(event.target.value)}>
|
||||
<option value="">All statuses</option>
|
||||
<option value="published">Published</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="review">Review</option>
|
||||
<option value="archived">Archived</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="category-filter">Category
|
||||
<select value={category} onChange={event => setCategory(event.target.value)}>
|
||||
<option value="">All categories</option>
|
||||
|
|
@ -135,11 +146,19 @@ function ContentDetail() {
|
|||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [badgeOptions, setBadgeOptions] = useState<Badge[]>([])
|
||||
const [badgeInput, setBadgeInput] = useState('')
|
||||
const navigate = useNavigate()
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
setLoading(true)
|
||||
void getContent(id).then(value => { if (active) { setItem(value); setDraft({ title: value.title, summary: value.summary, body: value.body, status: value.status }) } })
|
||||
void Promise.all([getContent(id), listBadges()]).then(([value, options]) => {
|
||||
if (active) {
|
||||
setItem(value)
|
||||
setBadgeOptions(options)
|
||||
setDraft({ title: value.title, summary: value.summary, body: value.body, status: value.status, badges: value.badges ?? [] })
|
||||
}
|
||||
})
|
||||
.catch(err => active && setError(err instanceof Error ? err.message : 'Unable to load content.'))
|
||||
.finally(() => active && setLoading(false))
|
||||
return () => { active = false }
|
||||
|
|
@ -148,7 +167,7 @@ function ContentDetail() {
|
|||
if (error || !item || !draft) return <Shell><div className="card"><p className="error" role="alert">{error ?? 'Content not found.'}</p><Link to="/content">Back to content</Link></div></Shell>
|
||||
const save = async () => {
|
||||
setSaving(true); setError(null)
|
||||
try { const saved = await updateContent(id, draft); setItem(saved); setDraft({ title: saved.title, summary: saved.summary, body: saved.body, status: saved.status }); setEditing(false) }
|
||||
try { const saved = await updateContent(id, draft); setItem(saved); setDraft({ title: saved.title, summary: saved.summary, body: saved.body, status: saved.status, badges: saved.badges ?? [] }); setEditing(false) }
|
||||
catch (err) { setError(err instanceof Error ? err.message : 'Unable to save changes.') }
|
||||
finally { setSaving(false) }
|
||||
}
|
||||
|
|
@ -160,8 +179,21 @@ function ContentDetail() {
|
|||
<label>Summary<textarea rows={3} value={draft.summary} onChange={e => setDraft({ ...draft, summary: e.target.value })} /></label>
|
||||
<label>Body<textarea rows={12} value={draft.body} onChange={e => setDraft({ ...draft, body: e.target.value })} /></label>
|
||||
<label>Status<select value={draft.status} onChange={e => setDraft({ ...draft, status: e.target.value })}><option value="draft">Draft</option><option value="published">Published</option><option value="archived">Archived</option></select></label>
|
||||
<label>Badges
|
||||
<div className="badge-input">
|
||||
{(draft.badges ?? []).map(badge => <span className="filter-pill" key={badge}>{badge}<button type="button" aria-label={`Remove ${badge}`} onClick={() => setDraft({ ...draft, badges: (draft.badges ?? []).filter(value => value !== badge) })}>×</button></span>)}
|
||||
<input list="detail-badge-options" value={badgeInput} placeholder="Type a badge and press Enter" onChange={e => setBadgeInput(e.target.value)} onKeyDown={e => {
|
||||
if (e.key !== 'Enter') return
|
||||
e.preventDefault()
|
||||
const badge = badgeInput.trim().toLowerCase()
|
||||
if (badge && !(draft.badges ?? []).includes(badge)) setDraft({ ...draft, badges: [...(draft.badges ?? []), badge] })
|
||||
setBadgeInput('')
|
||||
}} />
|
||||
<datalist id="detail-badge-options">{badgeOptions.filter(option => !(draft.badges ?? []).includes(option.name)).map(option => <option value={option.name} key={option.id} />)}</datalist>
|
||||
</div>
|
||||
</label>
|
||||
<div className="actions"><button className="primary-button" disabled={saving} onClick={() => void save()}>{saving ? 'Saving…' : 'Save changes'}</button><button className="secondary-button" onClick={() => setEditing(false)}>Cancel</button></div>
|
||||
</div> : <><div className="detail-heading"><span className="status">{item.status}</span><h1>{item.title}</h1><p className="muted">{item.summary}</p>{canEdit && <button className="primary-button edit-button" onClick={() => setEditing(true)}>Edit</button>}</div><div className="body-copy">{item.body}</div></>}
|
||||
</div> : <><div className="detail-heading"><span className="status">{item.status}</span><h1>{item.title}</h1><p className="muted">{item.summary}</p>{item.badges && item.badges.length > 0 && <div className="badge-list">{item.badges.map(badge => <span className="badge" key={badge}>{badge}</span>)}</div>}{canEdit && <button className="primary-button edit-button" onClick={() => setEditing(true)}>Edit</button>}</div><div className="body-copy">{item.body}</div></>}
|
||||
</article></Shell>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,10 +100,11 @@ async function contentRequest<T>(path = '', init?: RequestInit): Promise<T> {
|
|||
return (await response.json()) as T
|
||||
}
|
||||
|
||||
export async function listContent(cursor?: string | null, category = '', badges: string[] = []): Promise<ContentListResponse> {
|
||||
export async function listContent(cursor?: string | null, category = '', status = '', badges: string[] = []): Promise<ContentListResponse> {
|
||||
const params = new URLSearchParams()
|
||||
if (cursor) params.set('offset', cursor)
|
||||
if (category) params.set('category', category)
|
||||
if (status) params.set('status', status)
|
||||
badges.forEach(badge => params.append('badge', badge))
|
||||
const query = params.toString() ? `?${params.toString()}` : ''
|
||||
const response = await contentRequest<ContentListResponse | (Content & { ID?: string; Slug?: string; Title?: string; Summary?: string; Body?: string; Status?: string })[]>(query)
|
||||
|
|
@ -131,7 +132,7 @@ export function getContent(id: string): Promise<Content> {
|
|||
return contentRequest<Content>(`/${encodeURIComponent(id)}`).then(value => normalizeContent(value as Content & { ID?: string; Slug?: string; Title?: string; Summary?: string; Body?: string; Status?: string }))
|
||||
}
|
||||
|
||||
export type ContentUpdate = Pick<Content, 'title' | 'summary' | 'body' | 'status'>
|
||||
export type ContentUpdate = Pick<Content, 'title' | 'summary' | 'body' | 'status' | 'badges'>
|
||||
|
||||
export function updateContent(id: string, content: ContentUpdate): Promise<Content> {
|
||||
return contentRequest<Content>(`/${encodeURIComponent(id)}`, {
|
||||
|
|
|
|||
|
|
@ -1,64 +1,84 @@
|
|||
:root { font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: #172033; background: #f5f7fb; font-synthesis: none; }
|
||||
:root {
|
||||
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
color: #cdd6f4;
|
||||
background: #1e1e2e;
|
||||
font-synthesis: none;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-width: 320px; }
|
||||
button, a { font: inherit; }
|
||||
.shell { min-height: 100vh; max-width: 960px; margin: auto; padding: 0 24px 64px; }
|
||||
.header { height: 76px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.brand { color: #172033; font-size: 1.15rem; font-weight: 750; text-decoration: none; letter-spacing: -.02em; }
|
||||
.text-button { border: 0; background: transparent; color: #536176; cursor: pointer; padding: 8px; }
|
||||
.card { margin: 10vh auto 0; max-width: 440px; padding: 40px; background: white; border: 1px solid #e3e8f0; border-radius: 16px; box-shadow: 0 14px 40px #1720330b; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
background: radial-gradient(circle at 85% -10%, #302b50 0, transparent 38rem), #1e1e2e;
|
||||
}
|
||||
button, a, input, textarea, select { font: inherit; }
|
||||
button, select { cursor: pointer; }
|
||||
.shell { min-height: 100vh; max-width: 1080px; margin: auto; padding: 0 28px 72px; }
|
||||
.header { height: 94px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.brand { position: relative; color: #cdd6f4; font-size: 1.15rem; font-weight: 750; text-decoration: none; letter-spacing: -.02em; }
|
||||
.brand:hover { color: #cba6f7; }
|
||||
.brand-prompt { color: #a6e3a1; font-family: ui-monospace, SFMono-Regular, monospace; }
|
||||
.brand-cursor { color: #f9e2af; font-family: ui-monospace, SFMono-Regular, monospace; animation: blink 1.2s steps(2, start) infinite; }
|
||||
.brand small { display: block; margin: 5px 0 0 22px; color: #7f849c; font-size: .68rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; }
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
.text-button { border: 1px solid #45475a; border-radius: 8px; background: #313244; color: #bac2de; cursor: pointer; padding: 8px 12px; }
|
||||
.text-button:hover { border-color: #89b4fa; color: #cdd6f4; }
|
||||
.card { margin: 10vh auto 0; max-width: 500px; padding: 42px; background: #242438; border: 1px solid #45475a; border-radius: 16px; box-shadow: 0 18px 50px #11111b66; }
|
||||
.auth-card { text-align: center; }
|
||||
h1 { margin: 10px 0 12px; font-size: clamp(1.7rem, 4vw, 2.25rem); letter-spacing: -.04em; }
|
||||
p { line-height: 1.6; }
|
||||
.muted { color: #657286; }
|
||||
h1 { margin: 10px 0 12px; color: #f5e0e8; font-size: clamp(1.7rem, 4vw, 2.4rem); letter-spacing: -.04em; }
|
||||
p { line-height: 1.65; }
|
||||
.muted { color: #a6adc8; }
|
||||
.small { font-size: .875rem; }
|
||||
.eyebrow { color: #536fc4; font-size: .75rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; }
|
||||
.eyebrow { color: #cba6f7; font-size: .75rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; }
|
||||
.google-button, .secondary-button { width: 100%; min-height: 48px; border-radius: 9px; cursor: pointer; font-weight: 650; }
|
||||
.google-button { margin-top: 22px; border: 1px solid #ccd4e0; color: #172033; background: #fff; }
|
||||
.google-button:hover { background: #f8fafc; border-color: #9ba8bb; }
|
||||
.google-button span { margin-right: 10px; color: #4285f4; font-weight: 800; }
|
||||
.secondary-button { margin-top: 20px; border: 0; background: #edf1f8; color: #344563; }
|
||||
.error { padding: 10px 12px; border-radius: 8px; color: #a32d2d; background: #fff0f0; font-size: .9rem; }
|
||||
.profile { margin-top: 24px; padding: 14px; border-radius: 8px; background: #f5f7fb; color: #536176; }
|
||||
.content-page, .detail-page { padding-top: 6vh; }
|
||||
.google-button { margin-top: 22px; border: 1px solid #585b70; color: #cdd6f4; background: #313244; }
|
||||
.google-button:hover { background: #45475a; border-color: #89b4fa; }
|
||||
.google-button span { margin-right: 10px; color: #89b4fa; font-weight: 800; }
|
||||
.secondary-button { margin-top: 20px; border: 0; background: #45475a; color: #cdd6f4; }
|
||||
.error { padding: 10px 12px; border-radius: 8px; color: #f38ba8; background: #3b2634; font-size: .9rem; }
|
||||
.profile { margin-top: 24px; padding: 14px; border-radius: 8px; background: #313244; color: #a6adc8; }
|
||||
.content-page, .detail-page { padding-top: 5vh; }
|
||||
.page-heading { display: flex; justify-content: space-between; align-items: end; margin-bottom: 28px; }
|
||||
.filters { display: grid; grid-template-columns: minmax(180px, .35fr) 1fr; gap: 14px; margin-bottom: 24px; padding: 16px; background: #fff; border: 1px solid #e3e8f0; border-radius: 14px; }
|
||||
.filters label { display: grid; gap: 7px; color: #344563; font-size: .85rem; font-weight: 650; }
|
||||
.filters select, .badge-input { width: 100%; min-height: 43px; border: 1px solid #ccd4e0; border-radius: 8px; padding: 6px 9px; background: #fff; color: #172033; font: inherit; font-weight: 400; }
|
||||
.page-heading h1 { color: #cdd6f4; }
|
||||
.filters { display: grid; grid-template-columns: minmax(150px, .25fr) minmax(180px, .35fr) 1fr; gap: 14px; margin-bottom: 24px; padding: 16px; background: #242438; border: 1px solid #45475a; border-radius: 14px; }
|
||||
.filters label { display: grid; gap: 7px; color: #bac2de; font-size: .85rem; font-weight: 650; }
|
||||
.filters select, .badge-input { width: 100%; min-height: 43px; border: 1px solid #585b70; border-radius: 8px; padding: 6px 9px; background: #313244; color: #cdd6f4; font: inherit; font-weight: 400; }
|
||||
.filters select:focus, .badge-input:focus-within { outline: 2px solid #89b4fa; outline-offset: 1px; }
|
||||
.badge-input { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
|
||||
.badge-input input { min-width: 180px; flex: 1; border: 0; outline: 0; padding: 5px; color: #172033; font: inherit; }
|
||||
.filter-pill, .badge { display: inline-flex; align-items: center; gap: 4px; border-radius: 999px; padding: 4px 9px; color: #344f9b; background: #eef2ff; font-size: .78rem; font-weight: 650; }
|
||||
.filter-pill button { border: 0; padding: 0 2px; color: #536fc4; background: transparent; cursor: pointer; font-size: 1rem; line-height: 1; }
|
||||
.badge-input input { min-width: 180px; flex: 1; border: 0; outline: 0; padding: 5px; color: #cdd6f4; background: transparent; font: inherit; }
|
||||
.badge-input input::placeholder { color: #7f849c; }
|
||||
.filter-pill, .badge { display: inline-flex; align-items: center; gap: 4px; border-radius: 999px; padding: 4px 9px; color: #cba6f7; background: #3b3151; font-size: .78rem; font-weight: 650; }
|
||||
.filter-pill button { border: 0; padding: 0 2px; color: #cba6f7; background: transparent; cursor: pointer; font-size: 1rem; line-height: 1; }
|
||||
.badge-list { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 18px; }
|
||||
.content-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 18px; }
|
||||
.content-card { padding: 26px; background: #fff; border: 1px solid #e3e8f0; border-radius: 14px; box-shadow: 0 10px 30px #17203308; }
|
||||
.content-card h2 { margin: 12px 0 8px; font-size: 1.3rem; letter-spacing: -.025em; }
|
||||
.content-card h2 a { color: #172033; text-decoration: none; }
|
||||
.content-card h2 a:hover, .read-link:hover { color: #536fc4; }
|
||||
.content-card p { color: #657286; margin: 0 0 18px; }
|
||||
.status { display: inline-block; color: #536fc4; background: #eef2ff; border-radius: 999px; padding: 4px 9px; font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
|
||||
.read-link, .back-link { color: #344f9b; font-weight: 650; text-decoration: none; }
|
||||
.content-card { padding: 28px; background: #242438; border: 1px solid #45475a; border-radius: 14px; box-shadow: 0 12px 34px #11111b33; }
|
||||
.content-card:hover { border-color: #585b70; box-shadow: 0 15px 40px #11111b55; }
|
||||
.content-card h2 { margin: 12px 0 8px; font-size: 1.35rem; letter-spacing: -.025em; }
|
||||
.content-card h2 a { color: #f5e0e8; text-decoration: none; }
|
||||
.content-card h2 a:hover, .read-link:hover { color: #89b4fa; }
|
||||
.content-card p { color: #a6adc8; margin: 0 0 18px; }
|
||||
.status { display: inline-block; color: #94e2d5; background: #193d3a; border-radius: 999px; padding: 4px 9px; font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
|
||||
.read-link, .back-link { color: #89b4fa; font-weight: 650; text-decoration: none; }
|
||||
.list-status { text-align: center; min-height: 54px; padding: 14px; }
|
||||
.empty-state { max-width: none; text-align: center; margin-top: 0; }
|
||||
.detail-page { max-width: 720px; margin: auto; }
|
||||
.detail-page { max-width: 760px; margin: auto; }
|
||||
.back-link { display: inline-block; margin-bottom: 32px; }
|
||||
.detail-heading { position: relative; padding-bottom: 26px; border-bottom: 1px solid #e3e8f0; }
|
||||
.detail-heading { position: relative; padding-bottom: 26px; border-bottom: 1px solid #45475a; }
|
||||
.detail-heading h1 { margin-top: 14px; }
|
||||
.detail-heading .muted { font-size: 1.1rem; }
|
||||
.edit-button { position: absolute; top: 0; right: 0; }
|
||||
.body-copy { white-space: pre-wrap; line-height: 1.8; padding-top: 28px; }
|
||||
.body-copy { white-space: pre-wrap; line-height: 1.8; padding-top: 28px; color: #bac2de; }
|
||||
.editor { display: grid; gap: 18px; }
|
||||
.editor label { display: grid; gap: 7px; color: #344563; font-size: .9rem; font-weight: 650; }
|
||||
.editor input, .editor textarea, .editor select { width: 100%; border: 1px solid #ccd4e0; border-radius: 8px; padding: 11px 12px; background: #fff; color: #172033; font: inherit; font-weight: 400; }
|
||||
.editor label { display: grid; gap: 7px; color: #bac2de; font-size: .9rem; font-weight: 650; }
|
||||
.editor input, .editor textarea, .editor select { width: 100%; border: 1px solid #585b70; border-radius: 8px; padding: 11px 12px; background: #313244; color: #cdd6f4; font: inherit; font-weight: 400; }
|
||||
.editor input:focus, .editor textarea:focus, .editor select:focus { outline: 2px solid #89b4fa; outline-offset: 1px; }
|
||||
.editor textarea { resize: vertical; }
|
||||
.actions { display: flex; gap: 10px; margin-top: 4px; }
|
||||
.primary-button, .secondary-button { border: 0; border-radius: 8px; min-height: 42px; padding: 0 17px; cursor: pointer; font-weight: 650; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
|
||||
.primary-button { background: #536fc4; color: #fff; }
|
||||
.primary-button:hover { background: #405bac; }
|
||||
.primary-button { background: #cba6f7; color: #1e1e2e; }
|
||||
.primary-button:hover { background: #d8b9ff; }
|
||||
.primary-button:disabled { opacity: .6; cursor: wait; }
|
||||
.actions .secondary-button { margin: 0; background: #edf1f8; color: #344563; }
|
||||
.actions .secondary-button { margin: 0; background: #45475a; color: #cdd6f4; }
|
||||
.inline-button { margin-top: 22px; }
|
||||
.protected-card { text-align: center; }
|
||||
@media (max-width: 480px) { .shell { padding: 0 16px; } .card { padding: 28px 22px; } }
|
||||
@media (max-width: 640px) { .content-page, .detail-page { padding-top: 3vh; } .edit-button { position: static; margin-top: 10px; } }
|
||||
@media (max-width: 640px) { .filters { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 480px) { .shell { padding: 0 16px 56px; } .card { padding: 28px 22px; } .brand small { font-size: .58rem; } }
|
||||
@media (max-width: 640px) { .content-page, .detail-page { padding-top: 3vh; } .edit-button { position: static; margin-top: 10px; } .filters { grid-template-columns: 1fr; } }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue