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] ?? '🏆';
}