polish(web): favicon, Punktfunk casing, display-save toast, idle stop button

- Favicon: the brand-mark lens SVG at /favicon.svg, linked from the root head.
- Branding: user-facing "punktfunk" → "Punktfunk" (tab title, app_name,
  pairing prose, the logo aria-labels/SVG titles) — protocol identifiers
  (punktfunk/1, _punktfunk._udp, binary/module names) stay lowercase.
- The display policy auto-saves on every preset pick / field edit with no
  signal; mount @unom/ui's sonner Toaster in the root and fire a success toast
  from the save funnel's onSuccess (new i18n key, en + de). First toast usage
  in the console — errors stay inline as before.
- The stop-session button rendered destructive-red even with nothing to stop:
  variant switches to the muted `secondary` while no session is active.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 22:18:37 +02:00
parent f470858110
commit 8743f63406
9 changed files with 32 additions and 14 deletions
+4 -3
View File
@@ -1,7 +1,8 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"app_name": "punktfunk",
"app_name": "Punktfunk",
"app_tagline": "Verwaltungskonsole",
"display_settings_saved": "Display-Konfiguration gespeichert",
"nav_dashboard": "Übersicht",
"nav_host": "Host",
"nav_displays": "Virtuelle Anzeigen",
@@ -128,8 +129,8 @@
"pairing_success": "Erfolgreich gekoppelt.",
"pairing_failed": "Kopplung fehlgeschlagen — PIN prüfen und erneut versuchen.",
"pairing_native_title": "Gerät koppeln",
"pairing_native_desc": "Zeige hier eine Einmal-PIN an und gib sie in deiner punktfunk-App ein, um dieses Gerät zu koppeln.",
"pairing_native_disabled": "Der native Host läuft nicht. Starte ihn mit `serve --native`, um punktfunk-Geräte zu koppeln.",
"pairing_native_desc": "Zeige hier eine Einmal-PIN an und gib sie in deiner Punktfunk-App ein, um dieses Gerät zu koppeln.",
"pairing_native_disabled": "Der native Host läuft nicht. Starte ihn mit `serve --native`, um Punktfunk-Geräte zu koppeln.",
"pairing_native_arm": "Gerät koppeln",
"pairing_native_enter": "Gib diese PIN auf deinem Gerät ein:",
"pairing_native_expires": "Läuft ab in",
+4 -3
View File
@@ -1,7 +1,8 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"app_name": "punktfunk",
"app_name": "Punktfunk",
"app_tagline": "management console",
"display_settings_saved": "Display configuration saved",
"nav_dashboard": "Dashboard",
"nav_host": "Host",
"nav_displays": "Virtual displays",
@@ -128,8 +129,8 @@
"pairing_success": "Paired successfully.",
"pairing_failed": "Pairing failed — check the PIN and try again.",
"pairing_native_title": "Pair a device",
"pairing_native_desc": "Show a one-time PIN here, then enter it in your punktfunk app to pair this device.",
"pairing_native_disabled": "The native host isn't running. Start it with `serve --native` to pair punktfunk devices.",
"pairing_native_desc": "Show a one-time PIN here, then enter it in your Punktfunk app to pair this device.",
"pairing_native_disabled": "The native host isn't running. Start it with `serve --native` to pair Punktfunk devices.",
"pairing_native_arm": "Pair a device",
"pairing_native_enter": "Enter this PIN on your device:",
"pairing_native_expires": "Expires in",
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
<!-- Punktfunk brand mark: two overlapping circles forming a lens (see src/components/brand-mark.tsx). -->
<path d="M403.037,791.672c107.586,0 194.41,-86.824 194.41,-194.41c0,-107.586 -86.824,-194.41 -194.41,-194.41c-107.586,0 -194.41,86.824 -194.41,194.41c0,107.586 86.824,194.41 194.41,194.41Z" fill="#a79ff8"/>
<path d="M735.276,540.321c76.075,-76.075 76.075,-198.862 0,-274.937c-76.075,-76.075 -198.862,-76.075 -274.937,0c-76.075,76.075 -76.075,198.862 0,274.937c76.075,76.075 198.862,76.075 274.937,0Z" fill="#6c5bf3"/>
<path d="M647.84,590.737c-64.853,17.403 -136.871,0.597 -187.885,-50.416c-51.013,-51.013 -67.819,-123.032 -50.416,-187.885c64.853,-17.403 136.871,-0.597 187.885,50.416c51.013,51.013 67.819,123.032 50.416,187.885Z" fill="#d2c9fb"/>
</svg>

After

Width:  |  Height:  |  Size: 835 B

+1 -1
View File
@@ -48,7 +48,7 @@ export function AppShell({ children }: { children: ReactNode }) {
<aside className="sticky top-0 hidden h-dvh w-60 shrink-0 flex-col overflow-y-auto border-r bg-card/40 p-4 sm:flex">
<Link
to="/"
aria-label="punktfunk"
aria-label="Punktfunk"
className="mb-7 flex items-center gap-2 px-2 pt-1"
>
<BrandMark className="size-7 drop-shadow-[0_2px_12px_rgba(108,91,243,0.45)]" />
+2 -2
View File
@@ -5,13 +5,13 @@
export function BrandMark({ className }: { className?: string }) {
return (
<svg
aria-label="punktfunk"
aria-label="Punktfunk"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1000 1000"
className={className}
>
<title>punktfunk</title>
<title>Punktfunk</title>
<path
d="M403.037,791.672c107.586,0 194.41,-86.824 194.41,-194.41c0,-107.586 -86.824,-194.41 -194.41,-194.41c-107.586,0 -194.41,86.824 -194.41,194.41c0,107.586 86.824,194.41 194.41,194.41Z"
fill="#a79ff8"
+2 -2
View File
@@ -8,13 +8,13 @@ export function Wordmark({ className }: { className?: string }) {
return (
<svg
role="img"
aria-label="punktfunk"
aria-label="Punktfunk"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 579 136"
fill="currentColor"
className={cn("w-auto text-highlight", className)}
>
<title>punktfunk</title>
<title>Punktfunk</title>
<path d="M16.782,16.051l0,102.687l31.253,0l0,-35.563l73.436,0l0,-23.555l-73.436,0l0,-19.398l77.285,0l0,-24.171l-108.537,0Z" />
<path d="M131.785,16.051l0,47.264c0.154,16.627 0.154,16.627 0.308,20.014c0.77,15.087 2.463,21.4 7.544,26.634c7.698,8.16 20.014,10.315 59.272,10.315c23.863,0 34.178,-0.616 43.415,-2.463c11.7,-2.463 19.552,-10.623 21.246,-22.323c0.924,-7.236 1.078,-8.929 1.54,-32.176l0,-47.264l-31.253,0l0,47.264c0,2.155 -0.154,7.082 -0.308,10.623c-0.462,9.699 -1.232,12.47 -3.695,15.087c-3.387,3.695 -9.853,4.619 -31.407,4.619c-26.634,0 -32.638,-1.693 -34.332,-9.853c-0.77,-4.157 -0.77,-4.311 -1.078,-20.476l0,-47.264l-31.253,0Z" />
<path d="M271.575,15.943l0,102.687l31.868,0l-0.77,-76.669l3.387,0l54.038,76.669l54.346,0l0,-102.687l-31.868,0l0.77,76.515l-3.233,0l-53.73,-76.515l-54.808,0Z" />
+8 -2
View File
@@ -9,6 +9,7 @@ import {
useRouterState,
} from "@tanstack/react-router";
import "@fontsource-variable/geist";
import { Toaster } from "@unom/ui/toast";
import { AppShell } from "@/components/app-shell";
import appCss from "@/styles.css?url";
@@ -22,9 +23,12 @@ export const Route = createRootRouteWithContext<RouterContext>()({
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ name: "color-scheme", content: "dark light" },
{ title: "punktfunk" },
{ title: "Punktfunk" },
],
links: [
{ rel: "stylesheet", href: appCss },
{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
],
links: [{ rel: "stylesheet", href: appCss }],
}),
component: RootComponent,
});
@@ -47,6 +51,8 @@ function RootComponent() {
<Outlet />
</AppShell>
)}
{/* Sonner toaster (lazy client-side) — success feedback for auto-saved settings. */}
<Toaster />
<Scripts />
</body>
</html>
+1 -1
View File
@@ -78,7 +78,7 @@ export const DashboardView: FC<{
{m.action_request_idr()}
</Button>
<Button
variant="destructive"
variant={s.session ? "destructive" : "secondary"}
size="sm"
disabled={!s.session || isStopping}
onClick={onStopSession}
@@ -1,5 +1,6 @@
import { useQueryClient } from "@tanstack/react-query";
import { Button } from "@unom/ui/button";
import { toast } from "@unom/ui/toast";
import { Pencil, Plus, RefreshCw, Trash2 } from "lucide-react";
import { type FC, type MouseEvent, type ReactNode, useEffect, useState } from "react";
import {
@@ -62,6 +63,9 @@ export const DisplaySection: FC = () => {
onSuccess: (res) => {
setDraft(res.settings);
qc.invalidateQueries({ queryKey: getGetDisplaySettingsQueryKey() });
// The policy auto-saves on every preset pick / field edit — without a signal
// users kept looking for a Save button. Errors stay inline (apiErrorMessage).
toast.success(m.display_settings_saved());
},
},
);