UI: text wraps inside its box everywhere (global wrapping, shrinkable flex items, wrapping buttons, unknown icon fallback)

This commit is contained in:
Claude committed 2026-10-01 01:37:42 +00:00
1 parent 6c58042a32
commit 6fd78f32ea
8 files changed
+54 -9

No files matched your search

+14 -1
View File
@@ -40,10 +40,17 @@ html, body {
-webkit-font-smoothing: antialiased;
user-select: none;
-webkit-user-select: none;
/* Long names and unbroken strings wrap inside their box instead of spilling out of it. */
overflow-wrap: anywhere;
}
#app { height: 100%; }
/* Flex and grid items may shrink below their content, so long names wrap or truncate inside their box instead of pushing it wider. */
*, *::before, *::after { min-width: 0; }
svg, img, canvas, video, input[type='checkbox'], input[type='radio'] { flex-shrink: 0; }
h1, h2, h3, h4 { margin: 0; letter-spacing: -0.015em; }
p { margin: 0; }
a { color: var(--accent); text-decoration: none; }
@@ -85,8 +92,11 @@ button {
font-weight: 520;
font-size: 0.9rem;
transition: background 0.15s, border-color 0.15s, opacity 0.15s;
white-space: nowrap;
/* Labels may wrap when space runs out; compact buttons below stay on one line. */
white-space: normal;
max-width: 100%;
}
button.primary, button.ghost, button.danger, button.icon, button.tool { white-space: nowrap; }
button:hover { background: color-mix(in srgb, var(--text) 11%, transparent); }
button:active { background: color-mix(in srgb, var(--text) 14%, transparent); }
button:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }
@@ -154,3 +164,6 @@ label.field { display: flex; flex-direction: column; gap: 0.45rem; font-size: 0.
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes fade-up { from { opacity: 0; transform: translateY(0.6rem); } to { opacity: 1; transform: none; } }
@keyframes fade { from { opacity: 0; } }
code, pre, kbd, samp { overflow-wrap: anywhere; word-break: break-word; }
pre { white-space: pre-wrap; }
+1 -1
View File
@@ -15,7 +15,7 @@
{/if}
<style>
.plink { all: unset; cursor: pointer; font: inherit; color: inherit; border-radius: 4px; transition: color 0.12s ease; }
.plink { all: unset; cursor: pointer; font: inherit; color: inherit; border-radius: 4px; transition: color 0.12s ease; display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: inherit; vertical-align: bottom; }
.plink:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
.plink:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
</style>
+1 -1
View File
@@ -86,7 +86,7 @@
.inst.active { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--text); border-color: color-mix(in srgb, var(--accent) 55%, transparent); box-shadow: 0 0 16px color-mix(in srgb, var(--accent) 30%, transparent); }
.inst.active::before { content: ''; position: absolute; left: -0.75rem; top: 0.65rem; bottom: 0.65rem; width: 3.5px; border-radius: 0 3px 3px 0; background: var(--accent); box-shadow: 0 0 8px var(--accent); }
.inst.running::after { content: ''; position: absolute; right: -0.2rem; bottom: -0.2rem; width: 0.8rem; height: 0.8rem; border-radius: 50%; background: var(--success); border: 2px solid var(--bg); }
.tip { position: fixed; left: 5.2rem; transform: translateY(0); font-style: normal; font-size: 0.82rem; font-weight: 600; white-space: nowrap; padding: 0.4rem 0.75rem; border-radius: 0.5rem; background: var(--surface); border: 1px solid var(--line-strong); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); opacity: 0; pointer-events: none; transition: opacity 0.15s; z-index: 50; color: var(--text); }
.tip { position: fixed; left: 5.2rem; transform: translateY(0); font-style: normal; font-size: 0.82rem; font-weight: 600; max-width: 16rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0.4rem 0.75rem; border-radius: 0.5rem; background: var(--surface); border: 1px solid var(--line-strong); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); opacity: 0; pointer-events: none; transition: opacity 0.15s; z-index: 50; color: var(--text); }
.inst:hover .tip { opacity: 1; }
.bottom { display: flex; flex-direction: column; align-items: center; gap: 0.4rem; }
.big { width: 2.6rem; height: 2.6rem; position: relative; border-radius: var(--radius-sm); transition: transform 0.12s; }
+1 -1
View File
@@ -67,7 +67,7 @@
.ph.on { background: color-mix(in srgb, var(--accent) 30%, transparent); }
.info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.05rem; }
.name { gap: 0.5rem; }
.name strong { font-size: 0.85rem; font-weight: 560; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.name strong { min-width: 0; flex: 0 1 auto; font-size: 0.85rem; font-weight: 560; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.state { font-size: 0.72rem; font-weight: 520; color: var(--muted); display: inline-flex; align-items: center; gap: 0.3rem; }
.state::before { content: ''; width: 0.45rem; height: 0.45rem; border-radius: 50%; background: currentColor; }
.state.online { color: var(--success); }
+15
View File
@@ -133,7 +133,22 @@ const wallet = {
};
const mockWallet = () => JSON.parse(JSON.stringify(wallet));
// `?stress` in the dev URL makes every name, title and description much longer, to check that no screen lets text spill out of its box.
const STRESS = typeof location !== 'undefined' && new URLSearchParams(location.search).has('stress');
const TEXT_KEYS = /(^|_)(name|title|username|description|motd|message|desc|label|bio|tagline|text|rank|guild)(_|$)/i;
function stress(value: unknown, key = ''): unknown {
if (Array.isArray(value)) return value.map((v) => stress(v, key));
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, stress(v, k)]));
if (typeof value === 'string' && TEXT_KEYS.test(key) && /^[A-Za-z][A-Za-z '&:,.\-]{2,60}$/.test(value)) return `${value} With An Extraordinarily Long Unbroken Continuation`;
return value;
}
export async function mockInvoke(cmd: string, args: Record<string, any>): Promise<unknown> {
const result = await mockInvokeInner(cmd, args);
return STRESS ? stress(result) : result;
}
async function mockInvokeInner(cmd: string, args: Record<string, any>): Promise<unknown> {
await sleep(40);
switch (cmd) {
case 'get_public_servers':
+19 -2
View File
@@ -38,7 +38,12 @@ body {
-webkit-font-smoothing: antialiased;
}
body { background: var(--bg); }
body { background: var(--bg); overflow-wrap: anywhere; }
/* Flex and grid items may shrink below their content, so long names wrap or truncate inside their box instead of pushing it wider. */
*, *::before, *::after { min-width: 0; }
svg, img, canvas, video, input[type='checkbox'], input[type='radio'] { flex-shrink: 0; }
a {
color: var(--accent-2);
@@ -143,8 +148,11 @@ button, .btn {
gap: 8px;
font-weight: 520;
transition: background 0.15s, border-color 0.15s, opacity 0.15s;
white-space: nowrap;
/* Labels may wrap when space runs out; compact buttons below stay on one line. */
white-space: normal;
max-width: 100%;
}
button.primary, button.ghost, button.danger, button.icon, .btn { white-space: nowrap; }
button:hover { background: var(--surface-3); }
button:disabled { opacity: 0.5; cursor: not-allowed; }
button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {
@@ -299,3 +307,12 @@ table.table {
@media (max-width: 900px) {
.page { padding: 20px 16px 48px; }
}
/* Code and preformatted text wrap inside their box rather than running past it. */
code, pre, kbd, samp { overflow-wrap: anywhere; word-break: break-word; }
pre { white-space: pre-wrap; }
@media (max-width: 700px) {
button, .btn, button.primary, button.ghost, button.danger { white-space: normal; }
.page > header, .page header { flex-wrap: wrap; gap: 10px; }
.table-wrap { overflow-x: auto; }
}
+1 -1
View File
@@ -13,6 +13,6 @@
{/if}
<style>
.plink { color: inherit; text-decoration: none; border-radius: 4px; }
.plink { color: inherit; text-decoration: none; border-radius: 4px; display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: inherit; vertical-align: bottom; }
.plink:hover { color: var(--accent, #8b6cff); text-decoration: underline; text-underline-offset: 3px; }
</style>
+2 -2
View File
@@ -98,12 +98,12 @@ const isId = (v: string) => /^[a-z0-9_]+$/.test(v);
export interface ResolvedIcon { emoji: string; background: string; borderColor: string; glow: string }
export function resolveIcon(a: { icon_item?: string | null; icon_bg?: string | null; icon_border?: string | null }): ResolvedIcon {
const item = (a.icon_item ?? '').trim();
const item = (a.icon_item ?? '').trim().replace(/^minecraft:/, '');
let emoji = '🏆';
if (item) {
const known = ITEM_ICONS.find((i) => i.id === item);
if (known) emoji = known.emoji;
else if (!isId(item)) emoji = item; // already an emoji or symbol
else if (!isId(item) && [...item].length <= 4) emoji = item; // already an emoji or symbol
else emoji = KEYWORDS.find(([re]) => re.test(item))?.[1] ?? '🏆';
}