| index 8ae45ac..6fabe39 100644 |
| --- a/web/static/apps/appstudio.js |
| +++ b/web/static/apps/appstudio.js |
| @@ -114,13 +114,21 @@ export const app = defineApp({ |
| .studio h2 { margin: 0; font-size: 18px; } |
| .studio h3 { margin: 0 0 8px; font-size: 13px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); } |
| .studio .sub { margin: 4px 0 0; color: var(--muted); font-size: 13px; line-height: 1.5; } |
| -.studio .row { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid var(--border); } |
| -.studio .row .g { font-size: 20px; width: 28px; text-align: center; } |
| -.studio .row .meta { flex: 1 1 auto; min-width: 0; } |
| -.studio .row .meta .t { font-weight: 600; overflow-wrap: anywhere; } |
| +/* The row wraps: glyph + title hold the first line; the action cluster drops |
| + to its own line and wraps among itself when the pane is too narrow to keep |
| + it inline โ so nothing overflows off-screen and the title is never crushed |
| + letter-by-letter. On a wide pane the meta flex-grows and pushes the actions |
| + to the right, keeping the familiar single-line look. */ |
| +.studio .row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; padding: 12px 0; border-bottom: 1px solid var(--border); } |
| +.studio .row .g { font-size: 22px; width: 28px; text-align: center; flex: 0 0 auto; align-self: flex-start; } |
| +/* A solid basis keeps the title readable: on a narrow pane the actions wrap |
| + below rather than squeezing meta until "Streaks" stacks into "Strea/ks". */ |
| +.studio .row .meta { flex: 1 1 200px; min-width: 0; } |
| +.studio .row .meta .t { font-weight: 600; overflow-wrap: break-word; } |
| .studio .row .meta .by { color: var(--muted); font-size: 12px; display: flex; align-items: center; gap: 2px; flex-wrap: wrap; } |
| +.studio .row .actions { display: flex; flex-wrap: wrap; gap: 8px; } |
| .studio .bot-badge { display: inline-flex; align-items: center; color: var(--muted); } |
| -.studio .row button { cursor: var(--ui-cursor, pointer); background: none; border: 1px solid var(--border); border-radius: var(--radius); padding: 5px 10px; color: var(--ink); font-size: 13px; } |
| +.studio .row button { cursor: var(--ui-cursor, pointer); background: none; border: 1px solid var(--border); border-radius: var(--radius); padding: 6px 12px; color: var(--ink); font-size: 13px; } |
| .studio .row button.danger { color: var(--danger); border-color: var(--danger); } |
| .studio .row button.icon-btn { display: inline-flex; align-items: center; padding: 5px 9px; } |
| .studio .link-btn { align-self: flex-start; background: none; border: none; color: var(--link); cursor: var(--ui-cursor, pointer); font-size: 13px; padding: 0; } |
| @@ -194,12 +202,13 @@ export const app = defineApp({ |
| const sub = new Date(v.at * 1000).toLocaleString() + (v.byMe ? " ยท by you" : "") + (v.based_on ? ` ยท based on version ${v.based_on}` : ""); |
| meta.appendChild(el("div", "by", sub)); |
| row.appendChild(meta); |
| + const actions = el("div", "actions"); |
| if (v.source) { |
| const dl = el("button", "icon-btn"); |
| dl.innerHTML = ICON_DOWNLOAD; |
| dl.title = "Download this version"; |
| dl.onclick = () => downloadSource(v.source, `${historyTitle}-v${v.version}`); |
| - row.appendChild(dl); |
| + actions.appendChild(dl); |
| } |
| if (ENABLE_MEMBER_APPS && !v.current && v.source) { |
| const revert = el("button", "primary", "Revert to this"); |
| @@ -217,8 +226,9 @@ export const app = defineApp({ |
| historyData = null; |
| rerender(); |
| }; |
| - row.appendChild(revert); |
| + actions.appendChild(revert); |
| } |
| + row.appendChild(actions); |
| panel.appendChild(row); |
| } |
| } |
| @@ -281,10 +291,13 @@ export const app = defineApp({ |
| } |
| meta.appendChild(by); |
| row.appendChild(meta); |
| + // Buttons live in their own cluster so they wrap as a unit below the |
| + // title on a narrow pane instead of spilling off the right edge. |
| + const actions = el("div", "actions"); |
| if (m.name) { |
| const open = el("button", null, "Open"); |
| open.onclick = () => openMemberApp(m.name); |
| - row.appendChild(open); |
| + actions.appendChild(open); |
| } |
| if (a.mine && a.source) { |
| // A legacy-format app (manifest predates the places vocabulary) |
| @@ -306,7 +319,7 @@ export const app = defineApp({ |
| // apps_changed reloads to apply, same as a revert deploy. |
| up.textContent = "Upgraded โ"; |
| }; |
| - row.appendChild(up); |
| + actions.appendChild(up); |
| } |
| // Edit code leads to the (gated) editor โ with member apps off it |
| // hides with it; History, Download, and Remove stay open. |
| @@ -318,7 +331,7 @@ export const app = defineApp({ |
| editingName = m.name || ""; |
| rerender(); |
| }; |
| - row.appendChild(edit); |
| + actions.appendChild(edit); |
| } |
| const hist = el("button", null, "History"); |
| hist.onclick = () => { |
| @@ -327,12 +340,12 @@ export const app = defineApp({ |
| historyData = null; |
| rerender(); |
| }; |
| - row.appendChild(hist); |
| + actions.appendChild(hist); |
| const dl = el("button", "icon-btn"); |
| dl.innerHTML = ICON_DOWNLOAD; |
| dl.title = "Download source"; |
| dl.onclick = () => downloadSource(a.source, m.name || a.id); |
| - row.appendChild(dl); |
| + actions.appendChild(dl); |
| const del = el("button", "danger", "Remove"); |
| del.onclick = async () => { |
| if (!confirm(`Remove โ${m.title || m.name}โ? This deletes the app for everyone.`)) return; |
| @@ -346,8 +359,9 @@ export const app = defineApp({ |
| } |
| refreshList(); |
| }; |
| - row.appendChild(del); |
| + actions.appendChild(del); |
| } |
| + row.appendChild(actions); |
| listBody.appendChild(row); |
| } |
| }; |