anvilsign in

collin/anvil · 0425e5f9

feat(ui): compact file-action buttons, crisp dropdown carets, SVG icon sprite

Collin Richards · 2026-06-10 16:08 UTC · 0425e5f9009d24bff9146db889f6f4203627f119 · parent 19094cd1 · browse files

modifiedCLAUDE.md+7 −0
⋯ 45 unchanged lines
4646 The PAT is read-only (GET/HEAD only), safe to hold. If neither the ref nor
4747 `.anvil-credentials` is available, ask the user rather than guessing.
4848
49+Attachments are often phone screenshots larger than the `Read` tool's 256KB
50+cap. Downscale before reading (macOS `sips`):
51+
52+```
53+sips -Z 1100 -s formatOptions 70 /tmp/att --out /tmp/att-small.jpg # then Read that
54+```
55+
4956 Current status, resume notes, the agreed next steps (a/b/c), and the roadmap live
5057 in the TODO. Read it first:
5158
⋯ 1 unchanged line
addedcrates/anvil-web/assets/icons.svg+1 −0
1+<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style="display:none" aria-hidden="true"><symbol id="i-clipboard" viewBox="0 0 16 16"><path d="M10 1.5H6a.5.5 0 0 0-.5.5v1H4A1.5 1.5 0 0 0 2.5 4.5v9A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 12 3h-1.5V2a.5.5 0 0 0-.5-.5zm-3.5 1h3v1h-3v-1zM4 4.5h8v9H4v-9z"/></symbol><symbol id="i-pencil" viewBox="0 0 16 16"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm.176 4.225-1.69-1.69-6.857 6.857-.563 1.965 1.965-.563 6.857-6.857-.001-.001Z"/></symbol><symbol id="i-plus" viewBox="0 0 16 16"><path d="M7.25 7.25V2h1.5v5.25H14v1.5H8.75V14h-1.5V8.75H2v-1.5h5.25Z"/></symbol><symbol id="i-folder" viewBox="0 0 16 16"><path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"/></symbol><symbol id="i-file" viewBox="0 0 16 16"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/></symbol></svg>
modifiedcrates/anvil-web/src/ui.rs+60 −19
⋯ 111 unchanged lines
112112 .box .row a.fc-msg { flex:1; margin-left:24px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:left; color:var(--muted); font-size:13px; }
113113 .box .row a.fc-msg:hover { color:var(--accent); }
114114 .box .row .fc-time { margin-left:16px; white-space:nowrap; color:var(--muted); font-size:13px; }
115-.icon { width:16px; flex:none; display:inline-flex; align-items:center; justify-content:center; color:var(--muted); }
115+.icon { width:1em; height:1em; flex:none; fill:currentColor; color:var(--muted); vertical-align:-0.125em; }
116116 .icon.dir { color:#54aeff; }
117+.file-actions .btn .icon { color:inherit; }
117118 table.code { border-collapse:collapse; width:100%; font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
118119 table.code td { padding:0 10px; vertical-align:top; white-space:pre; }
119120 table.code td.ln { text-align:right; color:var(--muted); user-select:none; width:1%; border-right:1px solid var(--border); background:var(--code-bg); }
⋯ 51 unchanged lines
171172 form.stack .check { display:flex; gap:8px; align-items:flex-start; max-width:480px; }
172173 form.stack select { padding:6px 8px; border:1px solid var(--border); border-radius:6px; font:inherit; }
173174 form.stack textarea.editor { max-width:none; font:13px/1.5 ui-monospace,monospace; tab-size:4; resize:vertical; }
174-p.file-actions { margin:8px 0; }
175+p.file-actions { margin:10px 0; display:flex; gap:6px; align-items:center; }
176+.file-actions .btn { padding:3px 11px; font-size:12px; font-weight:500; border-radius:6px; display:inline-flex; align-items:center; gap:5px; }
175177 table.usage { border-collapse:collapse; width:100%; max-width:680px; margin-top:12px; }
176178 table.usage th, table.usage td { padding:6px 10px; border-bottom:1px solid var(--border); text-align:left; }
177179 table.usage .num { text-align:right; font-variant-numeric:tabular-nums; white-space:nowrap; }
⋯ 62 unchanged lines
240242 details.nav-menu { position:relative; }
241243 details.nav-menu > summary { list-style:none; cursor:pointer; color:var(--accent); font-size:14px; }
242244 details.nav-menu > summary::-webkit-details-marker { display:none; }
243-details.nav-menu > summary::after { content:" ▾"; font-size:10px; color:var(--muted); }
245+details.nav-menu > summary::after { content:""; display:inline-block; width:0; height:0; margin-left:6px; vertical-align:middle; border:4px solid transparent; border-top:5px solid var(--muted); border-bottom:0; transition:transform .15s ease; }
246+details.nav-menu > summary:hover::after { border-top-color:var(--accent); }
247+details.nav-menu[open] > summary::after { transform:rotate(180deg); }
244248 .nav-dropdown { position:absolute; right:0; top:calc(100% + 6px); background:var(--bg); border:1px solid var(--border); border-radius:6px; min-width:130px; box-shadow:0 4px 14px rgba(0,0,0,.1); z-index:200; padding:4px 0; }
245249 .nav-dropdown a, .nav-dropdown button { display:block; width:100%; padding:6px 14px; font-size:13px; color:var(--fg); text-align:left; background:none; border:0; cursor:pointer; font:inherit; text-decoration:none; }
246250 .nav-dropdown a:hover, .nav-dropdown button:hover { background:var(--code-bg); color:var(--fg); }
⋯ 4 unchanged lines
251255 details.rev-menu > summary .pill { cursor:pointer; }
252256 "#;
253257
254-/// Clipboard icon for the clone "copy" button.
255-const CLIPBOARD_SVG: &str = r#"<svg viewBox="0 0 16 16" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M10 1.5H6a.5.5 0 0 0-.5.5v1H4A1.5 1.5 0 0 0 2.5 4.5v9A1.5 1.5 0 0 0 4 15h8a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 12 3h-1.5V2a.5.5 0 0 0-.5-.5zm-3.5 1h3v1h-3v-1zM4 4.5h8v9H4v-9z"/></svg>"#;
258+/// Icon set as an SVG sprite (a hidden `<svg>` of `<symbol id="i-…">`s),
259+/// authored in `assets/icons.svg` and embedded at compile time. The layout
260+/// emits it once per page; [`icon`] references a symbol via `<use>`, so the
261+/// path data is never duplicated in the rendered HTML.
262+const ICON_SPRITE: &str = include_str!("../assets/icons.svg");
263+
264+/// The icons defined in the sprite. Each maps to a `<symbol id="i-…">` in
265+/// `assets/icons.svg` — keep the two in sync.
266+#[derive(Clone, Copy)]
267+pub(crate) enum Icon {
268+ Clipboard,
269+ Pencil,
270+ Plus,
271+ Folder,
272+ File,
273+}
274+
275+impl Icon {
276+ /// The sprite symbol id (`<symbol id="…">`).
277+ fn id(self) -> &'static str {
278+ match self {
279+ Icon::Clipboard => "i-clipboard",
280+ Icon::Pencil => "i-pencil",
281+ Icon::Plus => "i-plus",
282+ Icon::Folder => "i-folder",
283+ Icon::File => "i-file",
284+ }
285+ }
286+}
256287
257-/// Filled folder icon for directory entries in the tree view.
258-const FOLDER_SVG: &str = r#"<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"/></svg>"#;
288+/// Reference a sprite symbol as an inline `<svg>`, sized/colored by the `.icon`
289+/// CSS (1em, `currentColor`).
290+fn icon(i: Icon) -> Markup {
291+ icon_with(i, "icon")
292+}
259293
260-/// Outline file icon for blob entries in the tree view.
261-const FILE_SVG: &str = r#"<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/></svg>"#;
294+/// Like [`icon`] but with custom classes (e.g. `"icon dir"` to tint a folder).
295+fn icon_with(i: Icon, class: &str) -> Markup {
296+ PreEscaped(format!(
297+ r##"<svg class="{class}" aria-hidden="true"><use href="#{}"></use></svg>"##,
298+ i.id()
299+ ))
300+}
262301
263302 /// Delegated handlers for the clone widget: protocol toggle + copy-to-clipboard.
264303 /// Registered once on `document`, so it survives htmx body swaps.
⋯ 90 unchanged lines
355394 style { (PreEscaped(STYLE)) }
356395 }
357396 body hx-boost="true" hx-headers=[hx_headers] {
397+ (PreEscaped(ICON_SPRITE))
358398 header.top { div.container {
359399 a.brand href="/" { "anvil" }
360400 span style="margin-left:auto" {
⋯ 660 unchanged lines
10211061 div.clone-cmd {
10221062 code { (default_cmd) }
10231063 button.copy-btn type="button" title="Copy to clipboard" aria-label="Copy" {
1024- (PreEscaped(CLIPBOARD_SVG))
1064+ (icon(Icon::Clipboard))
10251065 }
10261066 span.copied-msg { "Copied!" }
10271067 }
⋯ 225 unchanged lines
12531293 (breadcrumbs(&owner, &repo, &rev, &path, true))
12541294 @if can_edit {
12551295 p.file-actions {
1256- a.btn.btn-secondary href=(format!("/{owner}/{repo}/edit/{}/{path}", enc_ref(&rev))) { "Edit" }
1296+ a.btn.btn-secondary href=(format!("/{owner}/{repo}/edit/{}/{path}", enc_ref(&rev))) {
1297+ (icon(Icon::Pencil)) "Edit"
1298+ }
12571299 @if is_todo {
1258- " "
1259- a.btn.btn-secondary href=(format!("/{owner}/{repo}/add-task/{}/{path}", enc_ref(&rev))) { "Add task" }
1300+ a.btn.btn-secondary href=(format!("/{owner}/{repo}/add-task/{}/{path}", enc_ref(&rev))) {
1301+ (icon(Icon::Plus)) "Add task"
1302+ }
12601303 }
12611304 }
12621305 }
⋯ 544 unchanged lines
18071850
18081851 /// Folder or file icon for an entry row (tree listings, pages, artifacts).
18091852 pub(crate) fn entry_icon(is_dir: bool) -> Markup {
1810- html! {
1811- @if is_dir {
1812- span.icon.dir { (PreEscaped(FOLDER_SVG)) }
1813- } @else {
1814- span.icon { (PreEscaped(FILE_SVG)) }
1815- }
1853+ if is_dir {
1854+ icon_with(Icon::Folder, "icon dir")
1855+ } else {
1856+ icon(Icon::File)
18161857 }
18171858 }
18181859
⋯ 661 unchanged lines