diff --git a/launcher/src/app.css b/launcher/src/app.css index dd103d0..25379bc 100644 --- a/launcher/src/app.css +++ b/launcher/src/app.css @@ -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; } diff --git a/launcher/src/components/PlayerLink.svelte b/launcher/src/components/PlayerLink.svelte index c023691..40e2121 100644 --- a/launcher/src/components/PlayerLink.svelte +++ b/launcher/src/components/PlayerLink.svelte @@ -15,7 +15,7 @@ {/if} diff --git a/launcher/src/components/Rail.svelte b/launcher/src/components/Rail.svelte index ab026ba..88cf0ba 100644 --- a/launcher/src/components/Rail.svelte +++ b/launcher/src/components/Rail.svelte @@ -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; } diff --git a/launcher/src/components/ServerStatus.svelte b/launcher/src/components/ServerStatus.svelte index 390533c..b8af75f 100644 --- a/launcher/src/components/ServerStatus.svelte +++ b/launcher/src/components/ServerStatus.svelte @@ -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); } diff --git a/launcher/src/lib/mock.ts b/launcher/src/lib/mock.ts index 6807625..027afad 100644 --- a/launcher/src/lib/mock.ts +++ b/launcher/src/lib/mock.ts @@ -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): Promise { + const result = await mockInvokeInner(cmd, args); + return STRESS ? stress(result) : result; +} + +async function mockInvokeInner(cmd: string, args: Record): Promise { await sleep(40); switch (cmd) { case 'get_public_servers': diff --git a/panel/web/src/app.css b/panel/web/src/app.css index 59bc7dc..29e5059 100644 --- a/panel/web/src/app.css +++ b/panel/web/src/app.css @@ -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; } +} diff --git a/panel/web/src/components/PlayerLink.svelte b/panel/web/src/components/PlayerLink.svelte index f82ec33..bd98cce 100644 --- a/panel/web/src/components/PlayerLink.svelte +++ b/panel/web/src/components/PlayerLink.svelte @@ -13,6 +13,6 @@ {/if} diff --git a/panel/web/src/lib/achievementIcons.ts b/panel/web/src/lib/achievementIcons.ts index d75ce5b..bd5c1fc 100644 --- a/panel/web/src/lib/achievementIcons.ts +++ b/panel/web/src/lib/achievementIcons.ts @@ -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] ?? '🏆'; }