Refactor: split monolith into cmd/palette + internal/{store,api,web,lang} (#35)
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
/* Palette UI tokens (mirrors sketches/themes/tokens.css, midnight approved preset) */
|
||||
:root {
|
||||
--bg: #241B30; --surface: #2D2340; --surface-2: #3A2D52;
|
||||
--muted: #7A6A9E; --muted-fg: #C0B2DE; --fg: #F2EDF8;
|
||||
--accent: #C4A8F0; --border: #42355C;
|
||||
--radius-lg: 20px; --radius: 10px; --radius-sm: 999px;
|
||||
--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--font-mono: ui-monospace, "JetBrains Mono", "Fira Code", monospace;
|
||||
}
|
||||
/* semantic status colors (dark preset values; light presets override below) */
|
||||
:root {
|
||||
--ok: #9CD49C; --warn: #E8C77B; --err: #F2A3B3;
|
||||
--on-accent: #241B30; /* text placed on accent-colored backgrounds */
|
||||
}
|
||||
[data-preset="smooth"] {
|
||||
--bg: #F6F5FA; --surface: #FFFFFF; --surface-2: #DAD7E6;
|
||||
--muted: #B5B1C9; --muted-fg: #7A7796; --fg: #2A2A36;
|
||||
--accent: #7A7796; --border: #DAD7E6;
|
||||
--ok: #456F45; --warn: #7A5E1B; --err: #9E4054;
|
||||
--on-accent: #F6F5FA;
|
||||
}
|
||||
[data-preset="pastel-lavender"] {
|
||||
--bg: #e6e0f5; --surface: #f1edfa; --surface-2: #cbb8e7;
|
||||
--muted: #a99cc9; --muted-fg: #5f5390; --fg: #3E3059;
|
||||
--accent: #806bb8; --border: #c4b6e0;
|
||||
--ok: #3E6B3E; --warn: #7A5E1B; --err: #9E4054;
|
||||
--on-accent: #f1edfa;
|
||||
}
|
||||
[data-preset="pastel-peach"] {
|
||||
--bg: #ffe0d6; --surface: #fff0ea; --surface-2: #ffc4a8;
|
||||
--muted: #d9a08c; --muted-fg: #7a4632; --fg: #4F2318;
|
||||
--accent: #f9826c; --border: #ffc9b5;
|
||||
--ok: #3E6B3E; --warn: #7A5E1B; --err: #9E4054;
|
||||
--on-accent: #4F2318;
|
||||
}
|
||||
[data-preset="pastel-cloud"] {
|
||||
--bg: #fff0f6; --surface: #fff7fb; --surface-2: #ffc8dd;
|
||||
--muted: #d9b9c9; --muted-fg: #7d5670; --fg: #4A3355;
|
||||
--accent: #a2d2ff; --border: #ffccd9;
|
||||
--ok: #3E6B3E; --warn: #7A5E1B; --err: #9E4054;
|
||||
--on-accent: #274a6b;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
line-height: 1.45;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-height: 100vh;
|
||||
font-size: 24.2px;
|
||||
}
|
||||
.topbar {
|
||||
display: flex; align-items: center; gap: 20px;
|
||||
padding: 0 22px; height: 76px;
|
||||
background: var(--surface); border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.logo { font-weight: 700; font-size: 27.6px; letter-spacing: -0.02em; text-decoration: none; color: var(--fg); }
|
||||
.logo em { font-style: normal; color: var(--muted-fg); font-weight: 400; }
|
||||
.topbar nav { display: flex; gap: 4px; }
|
||||
.topbar nav a { color: var(--muted-fg); text-decoration: none; padding: 6px 12px; border-radius: var(--radius); font-size: 23.2px; }
|
||||
.topbar nav a:hover { background: var(--surface-2); color: var(--fg); }
|
||||
.topbar nav a.on { background: var(--accent); color: var(--bg); }
|
||||
.topbar .spacer { flex: 1; }
|
||||
.kbd { font-family: var(--font-mono); font-size: 18.9px; border: 1px solid var(--border); border-radius: var(--radius); padding: 2px 6px; color: var(--muted-fg); }
|
||||
|
||||
.float {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, .10),
|
||||
0 2px 6px rgba(0, 0, 0, .08),
|
||||
0 8px 24px rgba(0, 0, 0, .07);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* new paste page */
|
||||
.deck {
|
||||
display: grid; grid-template-columns: 1fr 300px; gap: 16px;
|
||||
padding: 16px 20px 20px; height: calc(100vh - 76px);
|
||||
max-width: 1400px; margin: 0 auto;
|
||||
}
|
||||
.pane-r { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; padding-bottom: 4px; }
|
||||
.side-section { padding: 14px 16px; flex-shrink: 0; }
|
||||
.side-section h3 { font-size: 18.9px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted-fg); margin-bottom: 10px; }
|
||||
.pane-l-col { display: flex; flex-direction: column; gap: 14px; min-height: 0; }
|
||||
.pane-l-head { flex-shrink: 0; }
|
||||
.editor-head {
|
||||
display: flex; align-items: center; gap: 12px; padding: 12px 16px;
|
||||
}
|
||||
.editor-head input {
|
||||
border: none; outline: none; background: transparent; color: var(--fg); font: inherit; font-size: 23.2px; flex: 1;
|
||||
}
|
||||
.editor-head select {
|
||||
border: 1px solid var(--border); background: var(--surface-2); color: var(--muted-fg);
|
||||
border-radius: var(--radius); padding: 4px 10px; font: inherit; font-size: 21.6px; cursor: pointer;
|
||||
}
|
||||
/* shared code line metrics (#50): gutter + code must share one line box */
|
||||
:root { --code-lh: 1.7; --code-fs: 21.6px; }
|
||||
|
||||
.editor-wrap { flex: 1; display: flex; min-height: 0; }
|
||||
.gutter {
|
||||
padding: 14px 10px; text-align: right; color: var(--muted); font-family: var(--font-mono);
|
||||
font-size: var(--code-fs); line-height: var(--code-lh); user-select: none; white-space: pre; overflow: hidden;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
.editor {
|
||||
flex: 1; padding: 14px 16px; font-family: var(--font-mono); font-size: 21.6px; line-height: 1.7;
|
||||
white-space: pre; outline: none; overflow: auto; border: none; background: transparent; color: var(--fg);
|
||||
resize: none; width: 100%;
|
||||
}
|
||||
.editor::placeholder { color: var(--muted); }
|
||||
.actionbar {
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
padding: 10px 4px;
|
||||
}
|
||||
.actionbar .btn { padding: 10px 26px; }
|
||||
.btn {
|
||||
background: var(--accent); color: var(--bg); border: none; cursor: pointer;
|
||||
padding: 8px 18px; border-radius: var(--radius); font: inherit; font-size: 22.4px; font-weight: 600;
|
||||
}
|
||||
.btn:hover { filter: brightness(1.08); }
|
||||
.hint { font-size: 20.7px; color: var(--muted-fg); }
|
||||
.hint b { color: var(--fg); font-weight: 550; }
|
||||
.seg { display: flex; flex-direction: column; gap: 2px; }
|
||||
.seg label { display: flex; align-items: center; gap: 8px; padding: 5px 8px; border-radius: var(--radius); cursor: pointer; font-size: 22.4px; }
|
||||
.seg label:hover { background: var(--surface-2); }
|
||||
.seg input { accent-color: var(--accent); }
|
||||
.toggle { display: flex; align-items: center; gap: 8px; font-size: 22.4px; cursor: pointer; padding: 5px 8px; border-radius: var(--radius); }
|
||||
.toggle:hover { background: var(--surface-2); }
|
||||
.toggle input { accent-color: var(--accent); }
|
||||
.deck .row { display: flex; justify-content: space-between; align-items: center; font-size: 22.4px; padding: 4px 0; }
|
||||
.row input[type="text"] {
|
||||
border: 1px solid var(--border); border-radius: var(--radius); padding: 5px 8px; background: var(--bg);
|
||||
color: var(--fg); font: inherit; font-size: 21.6px; width: 130px;
|
||||
}
|
||||
.created-banner {
|
||||
display: none; padding: 10px 16px; font-size: 22.4px; background: var(--surface-2);
|
||||
border-bottom: 1px solid var(--border); word-break: break-all;
|
||||
}
|
||||
.created-banner a { color: var(--accent); }
|
||||
|
||||
/* paste view */
|
||||
.meta-bar { display: flex; align-items: center; gap: 12px; padding: 12px 18px; flex-wrap: wrap; }
|
||||
.meta-bar h1 { font-size: 29.2px; font-weight: 600; }
|
||||
.slug { font-family: var(--font-mono); font-size: 21.6px; color: var(--muted-fg); background: var(--surface-2); padding: 3px 9px; border-radius: var(--radius); }
|
||||
.tag { font-size: 19.8px; color: var(--muted-fg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 2px 9px; }
|
||||
.paste-title-bar { display: flex; align-items: center; gap: 12px; padding: 12px 18px; flex-wrap: wrap; }
|
||||
.paste-title-bar h1 { font-size: 29.2px; font-weight: 600; margin: 0; }
|
||||
.stats-pill { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
||||
.stats-head { display: flex; align-items: center; gap: 16px; width: 100%; background: none; border: 0; color: var(--muted-fg); font: inherit; font-size: 21.6px; padding: 14px 18px; cursor: pointer; text-align: left; }
|
||||
.stats-head:hover { color: var(--fg); background: var(--surface-2); }
|
||||
.stats-chev { width: 18px; height: 18px; flex: none; transition: transform 0.15s ease; }
|
||||
.stats-pill.open .stats-chev { transform: rotate(180deg); }
|
||||
.stats-summary { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; letter-spacing: .01em; }
|
||||
@media (max-width: 640px) { .stats-summary { white-space: normal; word-break: break-word; } }
|
||||
.stats-body { border-top: 1px solid var(--border); }
|
||||
.stats-grid { display: grid; grid-template-columns: max-content 1fr; gap: 6px 18px; padding: 12px 16px; font-size: 20.7px; }
|
||||
.stats-k { color: var(--muted-fg); }
|
||||
.stats-v { color: var(--fg); word-break: break-all; }
|
||||
.meta-bar .spacer { flex: 1; }
|
||||
.iconbtn { border: 1px solid var(--border); background: var(--surface-2); color: var(--muted-fg); border-radius: var(--radius); padding: 5px 12px; font: inherit; font-size: 21.6px; cursor: pointer; text-decoration: none; }
|
||||
.iconbtn.gear { display: inline-flex; align-items: center; padding: 5px 9px; }
|
||||
.iconbtn.gear svg { width: 22px; height: 22px; }
|
||||
.settings-head { padding: 12px 18px; border-bottom: 1px solid var(--border); }
|
||||
.settings-head h1 { font-size: 29.2px; font-weight: 600; margin: 0; }
|
||||
.settings-body { padding: 16px 18px; color: var(--muted-fg); font-size: 21.6px; }
|
||||
.iconbtn:hover { color: var(--fg); border-color: var(--muted); }
|
||||
.iconbtn.danger:hover { color: #ff8fa3; border-color: #ff8fa3; }
|
||||
.code-head {
|
||||
display: flex; align-items: center; gap: 10px; padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--border); font-size: 21.6px; color: var(--muted-fg);
|
||||
}
|
||||
.code-head .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
|
||||
.code {
|
||||
font-family: var(--font-mono); font-size: var(--code-fs); line-height: var(--code-lh);
|
||||
padding: 14px 0; display: flex; overflow-x: auto;
|
||||
}
|
||||
.code .gutter { flex-shrink: 0; }
|
||||
/* gutter/code share line metrics; the editor gutter keeps its own padding (#50) */
|
||||
.code .gutter { padding-top: 0; padding-bottom: 0; }
|
||||
.codebody { padding: 0 18px; white-space: pre; }
|
||||
/* syntax highlight tokens (#1) */
|
||||
.tok-kw { color: #c792ea; }
|
||||
.tok-str { color: #a5e075; }
|
||||
.tok-num { color: #f78c6c; }
|
||||
.tok-com { color: #6a737d; font-style: italic; }
|
||||
.footnote { display: flex; gap: 20px; padding: 10px 18px; font-size: 20.7px; color: var(--muted-fg); border-top: 1px solid var(--border); flex-wrap: wrap; }
|
||||
|
||||
/* history */
|
||||
.page { max-width: 1200px; margin: 0 auto; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
|
||||
.head-row { display: flex; align-items: baseline; gap: 14px; }
|
||||
.head-row h1 { font-size: 34.5px; font-weight: 600; }
|
||||
.search {
|
||||
display: flex; align-items: center; gap: 8px; background: var(--surface);
|
||||
border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 14px; width: 260px;
|
||||
}
|
||||
.search input { border: none; outline: none; background: transparent; color: var(--fg); font: inherit; font-size: 22.4px; width: 100%; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 22.4px; table-layout: fixed; }
|
||||
th {
|
||||
text-align: left; font-size: 18.9px; text-transform: uppercase; letter-spacing: .08em;
|
||||
color: var(--muted-fg); padding: 10px 16px; border-bottom: 1px solid var(--border); font-weight: 600;
|
||||
}
|
||||
td { padding: 10px 16px; border-bottom: 1px solid var(--border); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr.row { cursor: pointer; }
|
||||
tr.row:hover td { background: var(--surface-2); }
|
||||
tr.row:hover td a.slug { color: var(--accent); }
|
||||
td a.slug { font-family: var(--font-mono); font-size: 21.6px; color: var(--fg); text-decoration: none; }
|
||||
td a.slug:hover { color: var(--accent); }
|
||||
.badge { font-size: 18.9px; border: 1px solid var(--border); color: var(--muted-fg); border-radius: var(--radius-sm); padding: 1px 8px; }
|
||||
.badge.lock { color: var(--accent); border-color: var(--accent); }
|
||||
.dim { color: var(--muted-fg); white-space: nowrap; }
|
||||
.pager { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; font-size: 21.6px; color: var(--muted-fg); }
|
||||
.pager .pg { display: flex; gap: 6px; }
|
||||
.pager button { border: 1px solid var(--border); background: var(--surface-2); color: var(--muted-fg); border-radius: var(--radius); padding: 4px 11px; font: inherit; font-size: 21.6px; cursor: pointer; }
|
||||
.pager button:hover:not(:disabled) { color: var(--fg); border-color: var(--muted); }
|
||||
.pager button.on { background: var(--accent); color: var(--bg); border-color: var(--accent); }
|
||||
.pager button:disabled { opacity: .4; cursor: default; }
|
||||
.empty { text-align: center; padding: 40px 16px; color: var(--muted-fg); font-size: 22.4px; }
|
||||
|
||||
/* unlock */
|
||||
.center { display: flex; align-items: center; justify-content: center; padding: 20px; height: calc(100vh - 52px); }
|
||||
.center .float { width: 400px; max-width: 100%; }
|
||||
.inner { padding: 28px; text-align: center; }
|
||||
.lockring {
|
||||
width: 56px; height: 56px; margin: 0 auto 16px; border-radius: 50%;
|
||||
background: var(--surface-2); display: flex; align-items: center; justify-content: center; font-size: 41.4px;
|
||||
}
|
||||
.inner h1 { font-size: 29.2px; font-weight: 600; margin-bottom: 6px; }
|
||||
.inner .sub { font-size: 22.4px; color: var(--muted-fg); margin-bottom: 20px; }
|
||||
.pwinput {
|
||||
width: 100%; padding: 10px 14px; border: 1px solid var(--border); border-radius: var(--radius);
|
||||
background: var(--bg); color: var(--fg); font: inherit; font-size: 23.2px; outline: none; text-align: center;
|
||||
letter-spacing: .12em;
|
||||
}
|
||||
.pwinput:focus { border-color: var(--accent); }
|
||||
.center .btn { width: 100%; margin-top: 12px; }
|
||||
.err { display: none; margin-top: 12px; font-size: 21.6px; color: #ff8fa3; }
|
||||
.center .foot { font-size: 20.7px; color: var(--muted-fg); padding: 14px; border-top: 1px solid var(--border); }
|
||||
|
||||
.btn-icon {
|
||||
padding: 4px 8px; font-size: 24.2px; line-height: 1; overflow: visible;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
min-width: 40px; height: 36px;
|
||||
}
|
||||
|
||||
/* selection controls: pill-style selected states (#14) */
|
||||
.seg label, .toggle {
|
||||
border: 1px solid transparent;
|
||||
transition: background .12s ease, border-color .12s ease, color .12s ease;
|
||||
}
|
||||
.seg label:hover, .toggle:hover {
|
||||
background: var(--surface-2);
|
||||
color: var(--fg);
|
||||
}
|
||||
.seg label:has(input:checked),
|
||||
.toggle:has(input:checked) {
|
||||
background: var(--surface-2);
|
||||
border-color: var(--accent);
|
||||
color: var(--fg);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.seg label:has(input:focus-visible),
|
||||
.toggle:has(input:focus-visible) {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.seg input, .toggle input { accent-color: var(--accent); width: 16px; height: 16px; margin: 0; }
|
||||
|
||||
/* toast (#19) */
|
||||
.toast {
|
||||
position: fixed; left: 50%; bottom: 32px; transform: translateX(-50%) translateY(8px);
|
||||
background: var(--surface-2); color: var(--fg); border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm); padding: 6px 18px; font-size: 20.7px;
|
||||
opacity: 0; pointer-events: none; transition: opacity .25s ease, transform .25s ease; z-index: 200;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,.25);
|
||||
}
|
||||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
/* status variants (#16) */
|
||||
.toast.success { border-color: var(--ok); color: var(--ok); }
|
||||
.toast.error { border-color: var(--err); color: var(--err); }
|
||||
|
||||
/* protection section rhythm (#20) */
|
||||
.protect { display: flex; flex-direction: column; gap: 2px; }
|
||||
.protect .pw-row { padding: 2px 8px 4px; }
|
||||
.pw-field {
|
||||
display: flex; align-items: center; gap: 2px; width: 100%;
|
||||
border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg);
|
||||
}
|
||||
.pw-field:focus-within { border-color: var(--accent); }
|
||||
.pw-field input {
|
||||
flex: 1; min-width: 0; border: none; outline: none; background: transparent; color: var(--fg);
|
||||
font: inherit; font-size: 21.6px; padding: 7px 12px; letter-spacing: .08em;
|
||||
}
|
||||
.pw-field input::placeholder { color: var(--muted); letter-spacing: normal; }
|
||||
.pw-field .reveal {
|
||||
background: none; border: none; color: var(--muted-fg); cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center; padding: 0 10px; height: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pw-field .reveal:hover { color: var(--fg); }
|
||||
.pw-field .reveal .eye-slash { display: none; }
|
||||
.pw-field .reveal.off .eye-slash { display: block; }
|
||||
.pw-field svg { width: 20px; height: 20px; display: block; }
|
||||
|
||||
/* custom URL input (#22) */
|
||||
.deck .row input[type="text"] { width: 100%; }
|
||||
.custom-input {
|
||||
display: block; width: 100%;
|
||||
border: 1px solid var(--border); border-radius: var(--radius); padding: 7px 12px;
|
||||
background: var(--bg); color: var(--fg); font: inherit; font-size: 21.6px; outline: none;
|
||||
}
|
||||
.custom-input:focus { border-color: var(--accent); }
|
||||
.custom-input::placeholder { color: var(--muted); }
|
||||
|
||||
/* btn-icon svg (#24) */
|
||||
.btn-icon svg { width: 20px; height: 20px; display: block; }
|
||||
|
||||
/* search spinner (#32) */
|
||||
.search-spinner {
|
||||
width: 16px; height: 16px; flex-shrink: 0;
|
||||
border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%;
|
||||
animation: spin .8s linear infinite; visibility: hidden;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* unlock redesign (#27) */
|
||||
.unlock-card .pw-field { margin: 18px 0 4px; text-align: left; }
|
||||
.unlock-card .pw-field input { text-align: left; }
|
||||
.unlock-err { margin-top: 10px; font-size: 20.7px; color: #ff8fa3; }
|
||||
|
||||
/* paste name under slug pill in Paste column (#43) */
|
||||
.paste-sub { font-size: 19.8px; color: var(--muted-fg); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.paste-sub.dim { color: var(--muted); }
|
||||
td .url-link { font-size: 19.8px; }
|
||||
td .id-link { color: var(--muted-fg); text-decoration: none; font-family: var(--font-mono); font-size: 19.8px; }
|
||||
td .id-link:hover { color: var(--accent); }
|
||||
|
||||
/* sortable column headers (#42) */
|
||||
th.sortable { cursor: pointer; user-select: none; }
|
||||
th.sortable:hover { color: var(--fg); }
|
||||
.sort-ind { display: inline-block; width: 0; height: 0; margin-left: 6px; vertical-align: middle; border-left: 5px solid transparent; border-right: 5px solid transparent; }
|
||||
th.sorted.asc .sort-ind { border-bottom: 6px solid var(--accent); }
|
||||
th.sorted.desc .sort-ind { border-top: 6px solid var(--accent); }
|
||||
|
||||
/* ============================================================
|
||||
Consistency audit (#18) — shared tokens across inputs, buttons,
|
||||
headings. Visual-only, no behavior change.
|
||||
============================================================ */
|
||||
|
||||
/* shared text-input treatment: .search input, .pw-field input, #title, #custom */
|
||||
.search input,
|
||||
.pw-field input,
|
||||
.editor-head input#title,
|
||||
.custom-input,
|
||||
.row input[type="text"] {
|
||||
font-size: 21.6px;
|
||||
color: var(--fg);
|
||||
}
|
||||
.search input::placeholder,
|
||||
.pw-field input::placeholder,
|
||||
.editor-head input#title::placeholder,
|
||||
.custom-input::placeholder {
|
||||
color: var(--muted);
|
||||
}
|
||||
.custom-input { border-radius: var(--radius); }
|
||||
|
||||
/* buttons (incl. icon-only variants) share one radius + focus ring */
|
||||
.btn-icon, .iconbtn {
|
||||
border-radius: var(--radius);
|
||||
transition: color .12s ease, border-color .12s ease, background .12s ease;
|
||||
}
|
||||
.iconbtn:focus-visible, .btn:focus-visible, .btn-icon:focus-visible, .pager button:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/* in-place copy success feedback (#53) */
|
||||
.iconbtn.ok, .btn.ok {
|
||||
color: var(--ok);
|
||||
border-color: var(--ok);
|
||||
}
|
||||
|
||||
/* headings: unified treatment (mirrors .side-section h3) */
|
||||
.settings-head h1, .paste-title-bar h1, .head-row h1, .inner h1 {
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* consistent card padding scale: 12px 18px for wide card heads/bodies */
|
||||
.settings-head { padding: 12px 18px; }
|
||||
.settings-body { padding: 16px 18px; }
|
||||
|
||||
/* topbar: Git external-link arrow (#56) */
|
||||
.topbar nav a .ext { width: 14px; height: 14px; margin-left: 4px; opacity: .55; vertical-align: -1px; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
body { font-size: 16px; }
|
||||
|
||||
/* topbar: tighten so logo + nav + gear fit */
|
||||
.topbar { gap: 10px; padding: 0 12px; height: 56px; }
|
||||
.logo { font-size: 17px; white-space: nowrap; }
|
||||
.logo em { display: none; }
|
||||
.topbar nav { gap: 2px; flex-shrink: 0; }
|
||||
.topbar nav a { padding: 5px 8px; font-size: 15px; }
|
||||
.iconbtn.gear { padding: 4px 7px; flex-shrink: 0; }
|
||||
.iconbtn.gear svg { width: 18px; height: 18px; }
|
||||
|
||||
/* new paste: stack editor above sidebar, natural page height */
|
||||
.deck {
|
||||
display: flex; flex-direction: column;
|
||||
height: auto; min-height: calc(100vh - 56px);
|
||||
padding: 12px; gap: 12px;
|
||||
}
|
||||
.pane-l-col { order: 0; }
|
||||
.pane-r { order: 1; overflow-y: visible; }
|
||||
.editor-wrap { min-height: 45vh; }
|
||||
.editor { font-size: 15px; }
|
||||
.gutter { font-size: 15px; }
|
||||
.editor-head input { min-width: 0; font-size: 16px; }
|
||||
.editor-head select { max-width: 120px; font-size: 14px; }
|
||||
.actionbar { flex-wrap: wrap; }
|
||||
.actionbar .btn { padding: 12px 22px; }
|
||||
.hint { font-size: 13px; }
|
||||
|
||||
/* history: horizontal-scroll table inside its card */
|
||||
.page { padding: 12px; }
|
||||
.head-row { flex-wrap: wrap; }
|
||||
.head-row h1 { font-size: 22px; }
|
||||
.search { width: 100%; }
|
||||
.search input { font-size: 16px; }
|
||||
.float { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||
table { min-width: 720px; }
|
||||
th { padding: 8px 10px; font-size: 12px; white-space: nowrap; }
|
||||
td { padding: 8px 10px; font-size: 14px; }
|
||||
.pager { flex-wrap: wrap; gap: 8px; font-size: 13px; }
|
||||
|
||||
/* paste view */
|
||||
.paste-title-bar { padding: 10px 12px; gap: 8px; }
|
||||
.paste-title-bar h1 { font-size: 18px; }
|
||||
.slug { font-size: 13px; word-break: break-all; }
|
||||
.stats-head { font-size: 13px; }
|
||||
.stats-grid { font-size: 13px; padding: 10px 12px; }
|
||||
.code { font-size: 13px; }
|
||||
.codebody { padding: 0 12px; }
|
||||
.footnote { font-size: 12px; padding: 8px 12px; gap: 10px; }
|
||||
.created-banner { font-size: 13px; }
|
||||
.iconbtn { font-size: 13px; padding: 6px 10px; }
|
||||
|
||||
/* unlock card */
|
||||
.center { height: auto; min-height: calc(100vh - 56px); padding: 16px; }
|
||||
.center .float { width: 100%; }
|
||||
.inner { padding: 20px 16px; }
|
||||
.inner h1 { font-size: 20px; }
|
||||
.inner .sub { font-size: 14px; }
|
||||
.unlock-err { font-size: 13px; }
|
||||
.center .foot { font-size: 13px; }
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// Shared table logic for history (/api/public) and saved (/api/mine) pages (#57).
|
||||
// Provides: live search, client-side sort with indicators, row rendering via
|
||||
// a page-supplied rowHtml(), pagination state, and row click-through.
|
||||
const PaletteTable = (() => {
|
||||
const $ = id => document.getElementById(id);
|
||||
const esc = s => { const d = document.createElement('div'); d.textContent = s == null ? '' : s; return d.innerHTML; };
|
||||
const fmtSize = n => { if (n == null) return 'none'; if (n < 1024) return n + ' B'; if (n < 1048576) return (n/1024).toFixed(1) + ' KB'; return (n/1048576).toFixed(1) + ' MB'; };
|
||||
const ago = ts => {
|
||||
const s = Math.floor(Date.now()/1000) - ts;
|
||||
if (s < 60) return s + 's ago';
|
||||
if (s < 3600) return Math.floor(s/60) + 'm ago';
|
||||
if (s < 86400) return Math.floor(s/3600) + 'h ago';
|
||||
return Math.floor(s/86400) + 'd ago';
|
||||
};
|
||||
|
||||
const sortVal = (it, k) => {
|
||||
let v = it[k];
|
||||
if (k === 'title' || k === 'custom_slug') v = (v == null || v === '') ? null : String(v).toLowerCase();
|
||||
if (k === 'language') v = (v == null || v === '') ? 'text' : String(v).toLowerCase();
|
||||
if (k === 'size' || k === 'view_count' || k === 'created_at') return v == null ? -1 : v;
|
||||
return v == null ? null : v;
|
||||
};
|
||||
|
||||
function init(opts) {
|
||||
// opts: {endpoint, perPage, hasPager, rowHtml(it), emptyFiltered, emptyAll}
|
||||
const state = { filter: '', sortKey: null, sortDir: 1, page: 1, total: 0 };
|
||||
let timer = null;
|
||||
|
||||
function sortItems(items) {
|
||||
if (!state.sortKey) return items;
|
||||
const k = state.sortKey, dir = state.sortDir;
|
||||
return items.slice().sort((a, b) => {
|
||||
const va = sortVal(a, k), vb = sortVal(b, k);
|
||||
const na = va == null, nb = vb == null;
|
||||
if (na && nb) return 0;
|
||||
if (na) return 1;
|
||||
if (nb) return -1;
|
||||
if (va < vb) return -1 * dir;
|
||||
if (va > vb) return 1 * dir;
|
||||
return (a.created_at || 0) < (b.created_at || 0) ? 1 : -1;
|
||||
});
|
||||
}
|
||||
|
||||
function matches(it) {
|
||||
if (!state.filter) return true;
|
||||
const f = state.filter.toLowerCase();
|
||||
return (it.title || '').toLowerCase().includes(f) || (it.id || '').toLowerCase().includes(f) ||
|
||||
(it.custom_slug || '').toLowerCase().includes(f);
|
||||
}
|
||||
|
||||
function renderSortIndicators() {
|
||||
document.querySelectorAll('th.sortable').forEach(th => {
|
||||
th.classList.toggle('sorted', th.dataset.sort === state.sortKey);
|
||||
th.classList.toggle('asc', th.dataset.sort === state.sortKey && state.sortDir === 1);
|
||||
th.classList.toggle('desc', th.dataset.sort === state.sortKey && state.sortDir === -1);
|
||||
});
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const spinner = $('search-spinner');
|
||||
if (spinner) spinner.style.visibility = 'visible';
|
||||
try {
|
||||
const filtered = state.filter.length > 0;
|
||||
const off = (state.page - 1) * opts.perPage;
|
||||
const url = (filtered || state.sortKey)
|
||||
? opts.endpoint + '?limit=500&offset=0'
|
||||
: opts.endpoint + '?limit=' + opts.perPage + '&offset=' + off;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
state.total = data.total;
|
||||
let items = filtered ? data.items.filter(matches) : data.items;
|
||||
items = sortItems(items);
|
||||
|
||||
const count = $('count');
|
||||
if (count) count.textContent = filtered
|
||||
? items.length.toLocaleString() + ' matches (of ' + state.total.toLocaleString() + ' total)'
|
||||
: state.total.toLocaleString() + ' total';
|
||||
|
||||
const rows = $('rows'), empty = $('empty');
|
||||
if (!items.length) {
|
||||
rows.innerHTML = '';
|
||||
empty.style.display = 'block';
|
||||
empty.textContent = filtered ? opts.emptyFiltered : opts.emptyAll;
|
||||
} else {
|
||||
empty.style.display = 'none';
|
||||
rows.innerHTML = items.map(opts.rowHtml).join('');
|
||||
}
|
||||
|
||||
const pager = $('pg'), showing = $('showing');
|
||||
if (opts.hasPager && pager && showing) {
|
||||
const pages = Math.max(1, Math.ceil(state.total / opts.perPage));
|
||||
if (filtered || state.sortKey) {
|
||||
showing.textContent = state.sortKey
|
||||
? 'Sorted by ' + state.sortKey + ' (' + (state.sortDir === 1 ? 'ascending' : 'descending') + ') · ' + items.length.toLocaleString() + ' of ' + state.total.toLocaleString()
|
||||
: 'Showing ' + items.length.toLocaleString() + ' matches for "' + state.filter + '"';
|
||||
pager.innerHTML = '';
|
||||
} else {
|
||||
showing.textContent = state.total === 0 ? 'Nothing here yet' :
|
||||
`Showing ${off+1}–${Math.min(off+opts.perPage, state.total)} of ${state.total.toLocaleString()} · page ${state.page} of ${pages}`;
|
||||
const btns = [];
|
||||
const add = (label, target, o={}) => btns.push(`<button ${o.on?'class="on"':''} ${o.dis?'disabled':''} data-p="${target}">${label}</button>`);
|
||||
add('‹', state.page-1, {dis: state.page===1});
|
||||
const win = new Set([1, 2, state.page-1, state.page, state.page+1, pages]);
|
||||
let last = 0;
|
||||
for (let i = 1; i <= pages; i++) {
|
||||
if (win.has(i)) {
|
||||
if (last && i - last > 1) btns.push('<span class="dim">…</span>');
|
||||
add(String(i), i, {on: i===state.page});
|
||||
last = i;
|
||||
}
|
||||
}
|
||||
add('›', state.page+1, {dis: state.page===pages});
|
||||
pager.innerHTML = btns.join('');
|
||||
}
|
||||
} else if (showing) {
|
||||
showing.textContent = '';
|
||||
}
|
||||
renderSortIndicators();
|
||||
} finally {
|
||||
if (spinner) spinner.style.visibility = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelector('thead').addEventListener('click', e => {
|
||||
const th = e.target.closest('th.sortable');
|
||||
if (!th) return;
|
||||
const k = th.dataset.sort;
|
||||
if (state.sortKey === k) { state.sortDir = -state.sortDir; } else { state.sortKey = k; state.sortDir = 1; }
|
||||
renderSortIndicators();
|
||||
load();
|
||||
});
|
||||
|
||||
const rows = $('rows');
|
||||
if (rows) rows.addEventListener('click', e => {
|
||||
const tr = e.target.closest('tr.row[data-href]');
|
||||
if (!tr || e.target.closest('a') || e.target.closest('button')) return;
|
||||
window.location.href = tr.dataset.href;
|
||||
});
|
||||
|
||||
const pg = $('pg');
|
||||
if (pg) pg.addEventListener('click', e => {
|
||||
const b = e.target.closest('button[data-p]');
|
||||
if (!b || b.disabled) return;
|
||||
state.page = parseInt(b.dataset.p);
|
||||
load();
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
|
||||
const filter = $('filter');
|
||||
if (filter) filter.addEventListener('input', e => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
state.filter = e.target.value.trim();
|
||||
state.page = 1;
|
||||
load();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
return { load, state, esc, fmtSize, ago };
|
||||
}
|
||||
|
||||
return { init, esc, fmtSize, ago };
|
||||
})();
|
||||
Reference in New Issue
Block a user