feat(chrome)!: v0.4.0 — AdminShell + headers as /chrome sub-export

New `./chrome` entrypoint exporting `<AdminShell>` and the header
components (Search, SearchHistory, SearchOptions, Messages, BrowseApps,
HeaderCustomers, HeaderContacts, LogoutButton). Refactored from the
Chronos-style AdminShell that gscCRM was vendoring byte-for-byte —
header/footer/sidebar are now a single shared surface across apps.

Explicit props contract (no site-informations.json, no internal data
sources): `menus`, `apps`, `user`, `brand` are required; `features.*`
flags gate every section (search/browseApps/messages/notifications/
subbar*/pageHeader*/activityPanel/chat/footer); `slots.*` lets apps
inject content; `labels` overrides the next-intl "chrome" namespace.

Locale-aware navigation: chrome calls useLocale() and prepends
/{locale} to internal menu URLs, leaving externals (http(s)://…) and
the "#" sentinel alone. Breadcrumbs and the path-derived page title
strip the leading locale segment so they read "Contacts" not
"En › Contacts". Necessary for `localePrefix: 'always'` consumers like
gscCRM.

Phosphor 2.x icons: `normalizeIconClass` prepends the base `ph` class
(compound selectors `.ph.ph-house:before` require both). All hardcoded
`<i className="ph-…">` sites switched to `ph ph-…`.

`next-intl` and `next-auth` moved to peerDependencies (with devDep
copies for the kit's own typecheck/build). Consumers must symlink their
installed copies into the kit's node_modules at build time — otherwise
useTranslations()/useSession() bind to a separate React context and
next-intl throws Error(void 0) on render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude
2026-05-12 11:24:16 +02:00
parent 387e10b2fb
commit 440f815df7
18 changed files with 2146 additions and 14 deletions

72
src/chrome/labels.ts Normal file
View File

@@ -0,0 +1,72 @@
"use client";
import { useTranslations } from "next-intl";
import type { ChromeLabels } from "./types";
/**
* Default English chrome labels. Used as fallback when next-intl namespace
* "chrome" doesn't resolve a key and no per-key override is passed in props.
*/
export const DEFAULT_CHROME_LABELS: ChromeLabels = {
navigation: "Navigation",
main: "MAIN",
dashboard: "Dashboard",
support: "Support",
settings: "Settings",
allSettings: "All Settings",
logout: "Logout",
docs: "Docs",
browseApps: "Browse apps",
viewAll: "View all",
activityTitle: "Activity",
newNotifications: "New notifications",
olderNotifications: "Older notifications",
noOlderNotifications: "No older notifications",
noNewNotifications: "No new notifications",
expandSidebar: "Expand sidebar",
collapseSidebar: "Collapse sidebar",
closeSidebar: "Close sidebar",
};
/**
* Resolve chrome labels with three-level precedence:
* 1. `overrides` prop (per-key escape hatch)
* 2. next-intl namespace "chrome" message
* 3. hardcoded English default
*
* Apps add `"chrome": { ... }` to their next-intl messages to translate.
*/
export function useChromeLabels(overrides?: Partial<ChromeLabels>): ChromeLabels {
let t: ((key: string) => string) | null = null;
try {
t = useTranslations("chrome");
} catch {
t = null;
}
const resolved = { ...DEFAULT_CHROME_LABELS };
if (t) {
for (const key of Object.keys(resolved) as (keyof ChromeLabels)[]) {
try {
const v = t(key);
// next-intl returns the key path when the message is missing — treat
// that as "no translation" so we fall back to the English default.
if (v && v !== key && !v.startsWith("chrome.")) {
resolved[key] = v;
}
} catch {
// Missing key — keep the default.
}
}
}
if (overrides) {
for (const k of Object.keys(overrides) as (keyof ChromeLabels)[]) {
const v = overrides[k];
if (v != null) resolved[k] = v;
}
}
return resolved;
}