/** App shell prop types — @see docs/COMPONENTS.md#app */ import type { Locale } from "date-fns"; import type { DayPickerProps } from "react-day-picker"; import type { AppLocale, AppRequestHeaders, AppTimeFormat, AppTimezone, AppTimezoneDefault, AppDateFormat } from "../../app/types.js"; import type { AppPreset } from "../../app/preset.js"; import type { AppBrand, AppDensity, AppFontSize, AppTheme } from "../../app/theme-axes.js"; import type { AppPreferenceAxis } from "../../app/storage.js"; import type { AppSettingPickerAppearanceProp, AppSettingCycleAppearanceProp, ChildrenProp, ClassNameProp, DisabledProp, IdProp, NameProp, OnValueChangeProp, ValueProp } from "../vocabulary/index.js"; /** * @see ThemeScope * * A marker, not a configuration carrier: the theme is read from the DOM at this element, so there * is no `primary`/`tenant`/`theme` prop to get wrong. Everything a `
` accepts is forwarded, * which is how `data-tenant`, `className="dark"` and `style={tenantTheme(hex).vars}` are applied. */ export type ThemeScopeProp = { children: ChildrenProp; className?: ClassNameProp; id?: IdProp; }; /** @see AppProvider */ export type AppProviderProp = { children: ChildrenProp; /** * Initial locale when nothing is in storage. Default: `` when it names a registered * locale, then the preset's `defaultLocale`, then `en` (v32; was `vi`). */ defaultLocale?: AppLocale; /** * Product defaults the host opts into (`godxPreset` from `@godxjp/ui/themes/godx`). Fills only * what the props leave unset; its `name` is written to ``. Omit for neutral. */ preset?: AppPreset; /** Fallback when a translation key is missing. Default: `en`. */ fallbackLocale?: AppLocale; /** Initial timezone: IANA id, `browser`, or `system`. Default: `browser`. */ defaultTimezone?: AppTimezoneDefault; /** Backend/system timezone when `defaultTimezone` is `system`. */ systemTimezone?: AppTimezone; /** Initial clock format. `"locale"` derives from `defaultLocale`. */ defaultTimeFormat?: AppTimeFormat | "locale"; /** Initial date display format. `"locale"` derives from `defaultLocale`. */ defaultDateFormat?: AppDateFormat | "locale"; /** * IANA ids offered by the timezone-picker recipe (`useAppContext().timezoneOptions`). Omit for * the full IANA list; set to restrict (e.g. */ timezoneOptions?: readonly AppTimezone[]; /** * The languages the app stores USER CONTENT in (a product name in `ja`/`en`/`vi`), in order — * source language first. Not the UI locale and not the registered UI catalogs: a Japanese UI * can edit content in ja · en · vi, and content may be stored in a language the UI has no * catalog for. Every `TranslatableInput` / `TranslatableTextarea` without its own `locales` * takes its tabs from here and selects the first one. Omit to keep the per-field default * (`getRegisteredLocales()`, first tab = the UI locale). App configuration, never persisted. */ contentLocales?: readonly string[]; /** localStorage key. Default: `godxjp.app`. */ storageKey?: string; /** * Which viewer preferences survive a reload. `true` (default) every axis, `false` none, or a * LIST of axes — `["theme", "density", "fontSize"]`. * * The list exists because the axes do not share an owner. `theme` / `brand` / `density` / * `fontSize` / `scaling` are the VIEWER's and belong in this browser. `locale` / `timezone` / * `timeFormat` / `dateFormat` are frequently the SERVER's, resolved per request from a cookie, * an account row or a header — and a stored copy then WINS over the value the server just sent, * because storage is read after the props. Faced with one all-or-nothing flag, that consumer * sets `persist={false}` and loses the viewer's theme along with it; naming the axes keeps both. */ persist?: boolean | readonly AppPreferenceAxis[]; /** * Initial theme choice. `"light"` / `"dark"` are written straight to ``; * `"system"` defers to `prefers-color-scheme` and is re-resolved whenever the OS changes. * Default: `"light"`. */ theme?: AppTheme; /** * Initial brand palette preset — written to ``. OPT-IN: omit * (or `null`) to keep the brand `--primary` your own `theme.css` defines. */ brand?: AppBrand | null; /** Initial density — written to ``. Default: `"default"`. */ density?: AppDensity; /** Initial base type size — written to ``. Default: `"default"`. */ fontSize?: AppFontSize; /** * Continuous global size multiplier — sets inline `--scaling` on ``. Every size token * (spacing, control/table/checkbox/switch heights, radius) rescales in proportion (Radix-style). */ scaling?: number | null; /** * Emit a native `name` on every control a `FormField` wraps, taken from the field's key (`field` * → `name` → `id`). `data-field` is inert metadata and is always emitted; `name` is NOT — it * changes what a native `
` submit sends. */ emitFieldNames?: boolean; onLocaleChange?: (locale: AppLocale) => void; onTimezoneChange?: (timezone: AppTimezone) => void; onTimeFormatChange?: (timeFormat: AppTimeFormat) => void; onDateFormatChange?: (dateFormat: AppDateFormat) => void; onThemeChange?: (theme: AppTheme) => void; onBrandChange?: (brand: AppBrand | null) => void; onDensityChange?: (density: AppDensity) => void; onFontSizeChange?: (fontSize: AppFontSize) => void; onScalingChange?: (scaling: number | null) => void; }; /** Which AppProvider setting the {@link AppSettingPicker} reads/writes. */ export type AppSettingKind = "locale" | "timezone" | "dateFormat" | "timeFormat" | "theme" | "brand" | "density" | "fontSize"; /** * @see AppSettingPicker — one provider-bound Select for any single AppProvider setting. * Replaces the former Locale/Timezone/Date-format/Time-format pickers; pick the target * via `kind`. Bound to `` by default; pass value + onValueChange to control. */ export type AppSettingPickerMenuProp = { /** `true` (default): open-then-choose from a menu. `false`: see {@link AppSettingPickerCycleProp}. */ menu?: true; kind: AppSettingKind; /** * Trigger presentation. `"labeled"` shows the leading icon + the selected value in a control * sized to the setting. */ appearance?: AppSettingPickerAppearanceProp; /** * Use it for an auth/legal footer locale switch (`kind="locale" appearance="labeled" compact`), * where the square icon-only default reads as a stray button and the full labelled trigger is * too tall. All geometry is tokenized (`--app-setting-picker-compact-*`). */ compact?: boolean; className?: ClassNameProp; disabled?: DisabledProp; id?: IdProp; /** Form field name — submits the selected value with the form. */ name?: NameProp; /** Controlled value; default reads/writes the matching AppProvider context. */ value?: ValueProp; onValueChange?: OnValueChangeProp; }; /** * The `menu={false}` subset of {@link AppSettingKind} — the settings whose value set is * CLOSED and short enough to cycle by tapping. `locale`, `timezone`, `dateFormat` and `brand` are * deliberately absent: a 400-entry IANA list (or a palette a service extends) is a menu, not a * cycle, and tapping through it is not a control anyone can use. */ export type AppSettingCycleKind = Extract; /** * @see AppSettingPicker `menu={false}` — one BUTTON that steps a single AppProvider setting to its * next value and shows that value as its glyph (the former `AppSettingToggle`, v32 #1223). Same * binding contract as the menu (context-bound by default, controlled via value + onValueChange), * same option order, one tap instead of open-then-choose. */ export type AppSettingPickerCycleProp = { /** `false`: no menu — each tap steps to the next value. */ menu: false; kind: AppSettingCycleKind; /** Box the button takes. Default: `"bar"` — a cycler exists for a top bar. */ appearance?: AppSettingCycleAppearanceProp; className?: ClassNameProp; disabled?: DisabledProp; id?: IdProp; /** Controlled value; default reads/writes the matching AppProvider context. */ value?: ValueProp; onValueChange?: OnValueChangeProp; }; /** @see AppSettingPicker — the menu (default) or, with `menu={false}`, the one-tap cycler. */ export type AppSettingPickerProp = AppSettingPickerMenuProp | AppSettingPickerCycleProp; /** Value exposed by `useAppContext`. */ export type AppContextValue = { locale: AppLocale; fallbackLocale: AppLocale; timezone: AppTimezone; timeFormat: AppTimeFormat; dateFormat: AppDateFormat; dateFnsLocale: Locale; dayPickerLocale: NonNullable; requestHeaders: AppRequestHeaders; /** Configured timezone list; `undefined` → full IANA in the timezone-picker recipe. */ timezoneOptions?: readonly AppTimezone[]; /** The app's content languages, source first; `undefined` when `contentLocales` is not set. */ contentLocales?: readonly string[]; /** * Current theme axes (mirror `` / inline `--scaling`). `theme` is the user's * CHOICE — it can be `"system"`, which `` never is. */ theme: AppTheme; brand: AppBrand | null; density: AppDensity; fontSize: AppFontSize; scaling: number | null; /** Default `false`. */ emitFieldNames: boolean; setLocale: (locale: AppLocale) => void; setTimezone: (timezone: AppTimezone) => void; setTimeFormat: (timeFormat: AppTimeFormat) => void; setDateFormat: (dateFormat: AppDateFormat) => void; setTheme: (theme: AppTheme) => void; setBrand: (brand: AppBrand | null) => void; setDensity: (density: AppDensity) => void; setFontSize: (fontSize: AppFontSize) => void; setScaling: (scaling: number | null) => void; };