From cdd046531269d3f1d1d50a04eaf837d85695ff25 Mon Sep 17 00:00:00 2001 From: codemenschendev Date: Sat, 26 Sep 2026 16:29:26 +0700 Subject: [PATCH 1/2] docs: Teams reports are posted by each session again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Routing Teams posts through the "Bộ chỉ huy" session was dropped the same day: a request arrives with nobody there whenever that session is closed. Each session posts with teams-commit.sh again, and runs teams-chrome.sh ensure when the headless Chrome has died. Project: Appwerk --- CLAUDE.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1eab7fa..91ee244 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,10 +55,8 @@ (`--force` overrides). Never hand-run `docker compose build/up` on the server: that is a side door (a hand-run compose caused a 502 on manager, 2026-09-24). Server checkout `/var/www/ai-factory`, stack `infra/docker-compose.prod.yml`. Verify by grepping the new code inside the container, not by uptime. -- After every commit and push/deploy, the report goes to Teams through the session "Bộ chỉ huy" - (2026-09-26): send it the `~/.openclaw/workspace/ops/teams-commit.sh deployed` lines with - SendMessage and let it post. Never run `teams-commit.sh` or `teams-send.sh` from this session: all - sends share one headless Chrome (CDP 9333), and two sessions at once crash it. +- After every commit and push/deploy, post it to Teams yourself with `~/.openclaw/workspace/ops/teams-commit.sh`. + If it fails with a CDP or login-looking error, run `~/.openclaw/workspace/ops/teams-chrome.sh ensure` and retry. - Mail goes out through the company's world4you SMTP (`smtp.world4you.com:587`, STARTTLS) as `developerweb@codemenschen.at`, DKIM-signed for codemenschen.at. Not Resend (its Tokyo IPs landed the sign-in mail in spam, 2026-09-07) and not the host's sendmail (generic rDNS, no DKIM). From 6d1e1881c7468ece814fdf8dedf7a4e37970e49b Mon Sep 17 00:00:00 2001 From: codemenschendev Date: Sat, 26 Sep 2026 16:29:26 +0700 Subject: [PATCH 2/2] design(nav): tighter header menu that keeps the start button The bar listed the prototype page twice, as a link and as the start button, and hid that button below 1320px, so most laptops had no call to action in the header. The duplicate link is gone, the button stays with a short label between 640 and 1180px and moves into the phone menu below. The current page is marked in the bar and the menu, the phone menu closes on a click outside, uses drawn icons and hovers with a colour that exists, and on a small phone the account link shrinks to an icon so a signed-in header no longer overflows at 360px. Project: Appwerk --- apps/web/src/app/(site)/[locale]/layout.tsx | 19 +++++++------- apps/web/src/app/globals.css | 29 +++++++++++++++------ apps/web/src/components/AccountLink.tsx | 8 ++++-- apps/web/src/components/MobileNav.tsx | 29 +++++++++++++++------ apps/web/src/components/NavLinks.tsx | 19 ++++++++++++++ apps/web/src/dictionaries/de.ts | 1 + apps/web/src/dictionaries/en.ts | 1 + 7 files changed, 78 insertions(+), 28 deletions(-) create mode 100644 apps/web/src/components/NavLinks.tsx diff --git a/apps/web/src/app/(site)/[locale]/layout.tsx b/apps/web/src/app/(site)/[locale]/layout.tsx index e653566..da17dc2 100644 --- a/apps/web/src/app/(site)/[locale]/layout.tsx +++ b/apps/web/src/app/(site)/[locale]/layout.tsx @@ -6,6 +6,7 @@ import { AccountLink } from "@/components/AccountLink"; import { LangSwitch } from "@/components/LangSwitch"; import { Logo } from "@/components/Logo"; import { MobileNav } from "@/components/MobileNav"; +import { NavLinks } from "@/components/NavLinks"; import { PageViews } from "@/components/PageViews"; import { AdConsentBanner, AdConsentLink } from "@/components/AdConsent"; import "../../globals.css"; @@ -29,14 +30,15 @@ export default async function LocaleLayout({ const { locale } = await params; if (!isLocale(locale)) notFound(); const dict = getDict(locale as Locale); - // The same links in the bar and, on a phone, behind the menu button. + // The same links in the bar and, on a phone, behind the menu button. The prototype page is the + // start button's, not a fifth link: two ways to the same page only made the bar longer. const navLinks = [ { href: `/${locale}#services`, label: dict.nav.services }, { href: `/${locale}#how`, label: dict.nav.how }, { href: `/${locale}#prices`, label: dict.nav.pricing }, { href: `/${locale}/app`, label: dict.nav.appDev }, - { href: `/${locale}/prototype`, label: dict.proto.navLink }, ]; + const cta = { href: `/${locale}/prototype`, label: dict.nav.cta }; return ( @@ -48,18 +50,15 @@ export default async function LocaleLayout({ - +
- - {dict.nav.cta} + + {dict.nav.cta} + {dict.nav.ctaShort} - +
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 6c85c99..4b491b1 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -76,22 +76,32 @@ section { padding: 72px 0; } .logo-name { font-family: var(--font-body); font-size: 1.45rem; font-weight: 800; letter-spacing: -0.03em; } .logo-by { font-family: var(--font-body); font-size: 0.68rem; font-weight: 500; color: var(--ink-soft); letter-spacing: 0; } .nav-links { display: flex; gap: 22px; margin: 0 auto; } -.nav-links a { color: var(--ink-soft); text-decoration: none; font-size: 0.92rem; font-weight: 500; white-space: nowrap; } -.nav-links a:hover { color: var(--ink); } +.nav-links a { position: relative; color: var(--ink-soft); text-decoration: none; font-size: 0.92rem; font-weight: 500; white-space: nowrap; padding: 6px 0; } +.nav-links a:hover, .nav-links a[aria-current="page"] { color: var(--ink); } +.nav-links a::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; border-radius: 2px; background: var(--accent); transform: scaleX(0); transition: transform 0.18s ease; } +.nav-links a:hover::after, .nav-links a[aria-current="page"]::after { transform: scaleX(1); } .nav-right { display: flex; align-items: center; gap: 10px; margin-left: auto; } .nav-right > * { white-space: nowrap; } .lang-toggle { font-size: 13px; font-weight: 600; border: 1px solid var(--border); border-radius: 99px; padding: 4px 12px; color: var(--ink-soft); text-decoration: none; background: var(--surface); } .lang-toggle:hover { border-color: var(--ink-soft); color: var(--ink); } -/* German labels are longer; drop the header CTA before the bar can wrap */ -@media (max-width: 1320px) { .nav-right .nav-cta { display: none; } } +.nav-account { display: inline-flex; align-items: center; } +.nav-account-ico { display: none; width: 16px; height: 16px; } +/* The start button always stays: its long label where there is room, a short one where there is not, + and on a phone it moves into the menu panel. */ +.nav-cta-short { display: none; } +@media (max-width: 1180px) { .nav-cta-long { display: none; } .nav-cta-short { display: inline; } .nav-links { gap: 18px; } } @media (max-width: 1000px) { .nav-links { display: none; } } +@media (max-width: 640px) { .nav-right .nav-cta { display: none; } } /* The menu button and its panel: without them a phone had no menu at all. */ .nav-burger { display: none; position: relative; } -.nav-burger-btn { display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; font-size: 18px; line-height: 1; border: 1px solid var(--border); border-radius: 99px; background: var(--surface); color: var(--ink); cursor: pointer; } -.nav-panel { position: absolute; right: 0; top: calc(100% + 10px); min-width: 210px; display: grid; gap: 2px; padding: 8px; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; box-shadow: 0 12px 30px rgba(0,0,0,.12); } -.nav-panel a { padding: 10px 12px; border-radius: 10px; color: var(--ink); text-decoration: none; font-size: 15px; font-weight: 500; } -.nav-panel a:hover { background: var(--bg); } +.nav-burger-btn { display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; padding: 0; border: 1px solid var(--border); border-radius: 99px; background: var(--surface); color: var(--ink); cursor: pointer; } +.nav-burger-btn[aria-expanded="true"] { border-color: var(--ink-soft); } +.nav-panel { position: absolute; right: 0; top: calc(100% + 10px); width: min(280px, calc(100vw - 32px)); display: grid; gap: 2px; padding: 8px; background: var(--surface); border: 1px solid var(--border); border-radius: 16px; box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14); } +.nav-panel a:not(.btn) { padding: 11px 12px; border-radius: 10px; color: var(--ink); text-decoration: none; font-size: 15px; font-weight: 500; } +.nav-panel a:not(.btn):hover { background: var(--tint); } +.nav-panel a[aria-current="page"] { background: var(--accent-soft); color: var(--accent-hover); } +.nav-panel-cta { margin-top: 6px; text-align: center; } @media (max-width: 1000px) { .nav-burger { display: block; } } @media (max-width: 560px) { .logo-by { display: none; } } /* German "Anmelden" made the bar 11px wider than a 360px phone; tighten it there */ @@ -100,6 +110,9 @@ section { padding: 72px 0; } .nav-right { gap: 6px; } .nav-right .lang-toggle { padding: 4px 9px; } .logo-name { font-size: 1.25rem; } + .nav-account-label { display: none; } + .nav-account-ico { display: block; } + .nav-right .nav-account { padding: 6px 9px; } } /* ---------- Buttons ---------- */ diff --git a/apps/web/src/components/AccountLink.tsx b/apps/web/src/components/AccountLink.tsx index 8c7f6cd..2835185 100644 --- a/apps/web/src/components/AccountLink.tsx +++ b/apps/web/src/components/AccountLink.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import type { Locale } from "@/lib/i18n"; import { useToken } from "@/lib/token"; +import { Icon } from "./LineIcon"; /** * Nav entry to the customer portal: "My projects" once a portal token exists on this device, "Sign in" before. @@ -11,9 +12,12 @@ import { useToken } from "@/lib/token"; */ export function AccountLink({ locale, labels }: { locale: Locale; labels: { account: string; login: string } }) { const signedIn = !!useToken(); + const label = signedIn ? labels.account : labels.login; + // On a small phone the words do not fit next to the logo: the person icon stands in for them. return ( - - {signedIn ? labels.account : labels.login} + + + {label} ); } diff --git a/apps/web/src/components/MobileNav.tsx b/apps/web/src/components/MobileNav.tsx index 1d428f3..8f59343 100644 --- a/apps/web/src/components/MobileNav.tsx +++ b/apps/web/src/components/MobileNav.tsx @@ -1,28 +1,36 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; /** - * The menu on a narrow screen. The links in the bar are hidden under 1000px, and until now - * nothing took their place: on a phone the site had no menu at all. + * The menu on a narrow screen: the links of the bar, and the start button, which the bar has no + * room for on a phone. Closes on Escape, on a click outside and when a link is followed. */ -export function MobileNav({ links }: { links: { href: string; label: string }[] }) { +export function MobileNav({ links, cta }: { links: { href: string; label: string }[]; cta: { href: string; label: string } }) { const path = usePathname(); // Open is remembered per page, so going somewhere else closes the menu without an effect. const [openOn, setOpenOn] = useState(null); const open = openOn === path; const setOpen = (next: boolean) => setOpenOn(next ? path : null); + const box = useRef(null); useEffect(() => { const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpenOn(null); + const onClick = (e: MouseEvent) => { + if (box.current && !box.current.contains(e.target as Node)) setOpenOn(null); + }; window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); + document.addEventListener("click", onClick); + return () => { + window.removeEventListener("keydown", onKey); + document.removeEventListener("click", onClick); + }; }, []); return ( -
+
{open && ( )}
diff --git a/apps/web/src/components/NavLinks.tsx b/apps/web/src/components/NavLinks.tsx new file mode 100644 index 0000000..4567d52 --- /dev/null +++ b/apps/web/src/components/NavLinks.tsx @@ -0,0 +1,19 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +/** The links of the bar; the page one is on is marked, so the menu says where you are. */ +export function NavLinks({ links }: { links: { href: string; label: string }[] }) { + const path = usePathname(); + + return ( + + ); +} diff --git a/apps/web/src/dictionaries/de.ts b/apps/web/src/dictionaries/de.ts index 93be0e4..ec8ae82 100644 --- a/apps/web/src/dictionaries/de.ts +++ b/apps/web/src/dictionaries/de.ts @@ -9,6 +9,7 @@ export const de = { pricing: "Preise", faq: "FAQ", cta: "Kostenlose Vorschau starten", + ctaShort: "Gratis Vorschau", account: "Meine Projekte", login: "Anmelden", }, diff --git a/apps/web/src/dictionaries/en.ts b/apps/web/src/dictionaries/en.ts index 57facd2..d1e595d 100644 --- a/apps/web/src/dictionaries/en.ts +++ b/apps/web/src/dictionaries/en.ts @@ -9,6 +9,7 @@ export const en = { pricing: "Pricing", faq: "FAQ", cta: "Get a free preview", + ctaShort: "Free preview", account: "My projects", login: "Sign in", },