@godxjp/ui 18.0.2 → 18.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  declare const buttonVariants: (props?: ({
3
3
  variant?: "default" | "ghost" | "destructive" | "outline" | "dashed" | "secondary" | "link" | null | undefined;
4
- size?: "default" | "xs" | "sm" | "md" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
4
+ size?: "default" | "sm" | "md" | "lg" | "xs" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
5
5
  shape?: "default" | "pill" | "sharp" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export type { ButtonProp, ButtonProp as ButtonProps } from "../../props/components/general.prop.js";
@@ -0,0 +1,22 @@
1
+ import type { CenteredShellProp } from "../../props/components/layout.prop.js";
2
+ export type { CenteredShellProp, CenteredShellProp as CenteredShellProps, } from "../../props/components/layout.prop.js";
3
+ /**
4
+ * CenteredShell — authenticated, no-sidebar, centred-column page shell: the hosted-ID "My Page",
5
+ * account / self-service, and standalone-settings shape. It fills the gap between the two existing
6
+ * shells:
7
+ *
8
+ * - `AppShell` REQUIRES a sidebar — its padded topbar chrome (`.app-topbar`) is a grid area beside
9
+ * the nav rail, so you cannot get a padded top bar without a rail.
10
+ * - `AuthShell` is the UNAUTHENTICATED root (login/mfa/reset); it centres a narrow ~24rem card
11
+ * VERTICALLY and has no top-right actions slot.
12
+ *
13
+ * CenteredShell gives a **padded top bar with real actions** (banner) reusing the SAME chrome as
14
+ * `.app-topbar` (padding-inline · border · backdrop) WITHOUT a sidebar, a scrollable `main` whose
15
+ * content is a **centred column** of a configurable medium width (`width` = sm|md|lg, all wider than
16
+ * the 24rem auth card) **top-aligned** so sections flow and scroll (not vertically centred), and an
17
+ * optional footer (contentinfo). A hosted account page thus needs ZERO custom CSS and never
18
+ * hand-rolls a bar (the bare `Topbar` primitive ships no padding — the `.ui-topbar` zero-inset
19
+ * footgun). Layout-only: delegate motion to `Reveal` (wrap a section) so `prefers-reduced-motion`
20
+ * is honoured in one place.
21
+ */
22
+ export declare function CenteredShell({ topbar, footer, children, width, className, }: CenteredShellProp): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,35 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useTranslation } from "../../i18n/use-translation.js";
4
+ import { cn } from "../../lib/utils.js";
5
+ function CenteredShell({
6
+ topbar,
7
+ footer,
8
+ children,
9
+ width = "md",
10
+ className
11
+ }) {
12
+ const { t } = useTranslation();
13
+ return /* @__PURE__ */ jsxs("div", { "data-slot": "centered-shell", className: cn("ui-centered-shell", className), children: [
14
+ topbar !== void 0 && /* @__PURE__ */ jsx(
15
+ "header",
16
+ {
17
+ className: "ui-centered-shell-bar ui-scale-fixed",
18
+ "aria-label": t("layout.centeredShell.headerLabel"),
19
+ children: topbar
20
+ }
21
+ ),
22
+ /* @__PURE__ */ jsx("main", { className: "ui-centered-shell-main", "aria-label": t("layout.centeredShell.mainLabel"), children: /* @__PURE__ */ jsx("div", { className: "ui-centered-shell-column", "data-width": width, children }) }),
23
+ footer !== void 0 && /* @__PURE__ */ jsx(
24
+ "footer",
25
+ {
26
+ className: "ui-centered-shell-footer",
27
+ "aria-label": t("layout.centeredShell.footerLabel"),
28
+ children: footer
29
+ }
30
+ )
31
+ ] });
32
+ }
33
+ export {
34
+ CenteredShell
35
+ };
@@ -7,6 +7,9 @@ export { AppShell } from "./app-shell.js";
7
7
  export type { AppShellProps } from "./app-shell.js";
8
8
  export { AuthShell } from "./auth-shell.js";
9
9
  export type { AuthShellProp, AuthShellProps } from "./auth-shell.js";
10
+ export { CenteredShell } from "./centered-shell.js";
11
+ export type { CenteredShellProp, CenteredShellProps } from "./centered-shell.js";
12
+ export type { CenteredShellWidthProp } from "../../props/vocabulary/index.js";
10
13
  export { Breadcrumb } from "./breadcrumb.js";
11
14
  export type { BreadcrumbProps } from "./breadcrumb.js";
12
15
  export { Sidebar, SidebarHeader, SidebarItem, SidebarSection } from "./sidebar.js";
@@ -3,6 +3,7 @@ import { Flex } from "./flex.js";
3
3
  import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from "./resizable.js";
4
4
  import { AppShell } from "./app-shell.js";
5
5
  import { AuthShell } from "./auth-shell.js";
6
+ import { CenteredShell } from "./centered-shell.js";
6
7
  import { Breadcrumb } from "./breadcrumb.js";
7
8
  import { Sidebar, SidebarHeader, SidebarItem, SidebarSection } from "./sidebar.js";
8
9
  import { Topbar } from "./topbar.js";
@@ -15,6 +16,7 @@ export {
15
16
  AspectRatio,
16
17
  AuthShell,
17
18
  Breadcrumb,
19
+ CenteredShell,
18
20
  Flex,
19
21
  PageContainer,
20
22
  ResizableHandle,
@@ -0,0 +1,23 @@
1
+ import type { FormStateAdapter } from "../props/components/form.prop.js";
2
+ /**
3
+ * Context carrying a framework-agnostic {@link FormStateAdapter}. `FormRoot` provides it on the
4
+ * adapter path (server-driven form libraries — Inertia/formik/TanStack); it stays `null` on the
5
+ * built-in react-hook-form path, where `FormFieldControl` falls back to RHF's own context.
6
+ */
7
+ export declare const FormAdapterContext: import("react").Context<FormStateAdapter | null>;
8
+ /** Read the active form-state adapter, or `null` when `FormRoot` runs the react-hook-form path. */
9
+ export declare function useFormAdapter(): FormStateAdapter | null;
10
+ /**
11
+ * Whether the surrounding `FormRoot` is currently submitting — works for BOTH paths: the adapter's
12
+ * `isSubmitting` (e.g. Inertia's `processing`) or react-hook-form's `formState.isSubmitting`. Spread
13
+ * onto a submit button so `processing` drives its loading state automatically:
14
+ * `<Button type="submit" loading={useFormSubmitting()}>`.
15
+ */
16
+ export declare function useFormSubmitting(): boolean;
17
+ /**
18
+ * Normalise a control's change payload to a bare value. The `FormFieldControl` render-prop spreads
19
+ * `onChange` onto a control that may call it with a DOM `ChangeEvent` (native `<Input>`) OR a raw
20
+ * value (a `Select`'s `onValueChange`). Mirror react-hook-form's own event unwrapping so the adapter
21
+ * path binds identically for every control kind.
22
+ */
23
+ export declare function extractControlValue(arg: unknown): unknown;
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { createContext, useContext } from "react";
3
+ import { useFormContext } from "react-hook-form";
4
+ const FormAdapterContext = createContext(null);
5
+ function useFormAdapter() {
6
+ return useContext(FormAdapterContext);
7
+ }
8
+ function useFormSubmitting() {
9
+ const adapter = useContext(FormAdapterContext);
10
+ const rhf = useFormContext();
11
+ if (adapter) return adapter.isSubmitting;
12
+ return rhf?.formState?.isSubmitting ?? false;
13
+ }
14
+ function extractControlValue(arg) {
15
+ if (arg && typeof arg === "object" && "target" in arg) {
16
+ const target = arg.target;
17
+ if (target && typeof target === "object" && "type" in target) {
18
+ const el = target;
19
+ return el.type === "checkbox" ? el.checked : el.value;
20
+ }
21
+ }
22
+ return arg;
23
+ }
24
+ export {
25
+ FormAdapterContext,
26
+ extractControlValue,
27
+ useFormAdapter,
28
+ useFormSubmitting
29
+ };
@@ -2,6 +2,9 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { Controller, useFormContext } from "react-hook-form";
4
4
  import { FormField } from "../components/data-entry/form-field.js";
5
+ import { extractControlValue, useFormAdapter } from "./form-context.js";
6
+ const noopRef = () => {
7
+ };
5
8
  function FormFieldControl({
6
9
  name,
7
10
  label,
@@ -10,12 +13,35 @@ function FormFieldControl({
10
13
  className,
11
14
  children
12
15
  }) {
13
- const { control } = useFormContext();
16
+ const adapter = useFormAdapter();
17
+ const rhf = useFormContext();
18
+ if (adapter) {
19
+ const fieldName = String(name);
20
+ return /* @__PURE__ */ jsx(
21
+ FormField,
22
+ {
23
+ id: fieldName,
24
+ label,
25
+ required,
26
+ helper,
27
+ error: adapter.getError(fieldName),
28
+ className,
29
+ children: children({
30
+ id: fieldName,
31
+ name: fieldName,
32
+ value: adapter.getValue(fieldName),
33
+ onChange: (...args) => adapter.setValue(fieldName, extractControlValue(args[0])),
34
+ onBlur: () => adapter.onBlur?.(fieldName),
35
+ ref: noopRef
36
+ })
37
+ }
38
+ );
39
+ }
14
40
  return /* @__PURE__ */ jsx(
15
41
  Controller,
16
42
  {
17
43
  name,
18
- control,
44
+ control: rhf.control,
19
45
  render: ({ field, fieldState }) => /* @__PURE__ */ jsx(
20
46
  FormField,
21
47
  {
@@ -1,4 +1,10 @@
1
1
  import { type FieldValues } from "react-hook-form";
2
2
  import type { FormRootProp } from "../props/components/form.prop.js";
3
3
  export type { FormRootProp } from "../props/components/form.prop.js";
4
- export declare function FormRoot<TFieldValues extends FieldValues>({ form, onSubmit, children, className, id, }: FormRootProp<TFieldValues>): import("react/jsx-runtime").JSX.Element;
4
+ /**
5
+ * FormRoot — the form shell. Provide EITHER `form` (built-in react-hook-form + Zod, client-side
6
+ * validation) OR `adapter` (a framework-agnostic {@link FormStateAdapter} for a server-driven form
7
+ * library — Inertia's `useForm` via `@godxjp/ui/inertia`, formik, TanStack Form). Both drive the same
8
+ * `FormFieldControl` auto-binding; the core keeps ZERO dependency on any non-RHF form library.
9
+ */
10
+ export declare function FormRoot<TFieldValues extends FieldValues>({ form, adapter, onSubmit, children, className, id, }: FormRootProp<TFieldValues>): import("react/jsx-runtime").JSX.Element;
@@ -1,13 +1,34 @@
1
+ "use client";
1
2
  import { jsx } from "react/jsx-runtime";
2
3
  import { FormProvider } from "react-hook-form";
3
4
  import { cn } from "../lib/utils.js";
5
+ import { FormAdapterContext } from "./form-context.js";
4
6
  function FormRoot({
5
7
  form,
8
+ adapter,
6
9
  onSubmit,
7
10
  children,
8
11
  className,
9
12
  id
10
13
  }) {
14
+ if (adapter) {
15
+ return /* @__PURE__ */ jsx(FormAdapterContext.Provider, { value: adapter, children: /* @__PURE__ */ jsx(
16
+ "form",
17
+ {
18
+ id,
19
+ className: cn("ui-stack-md", className),
20
+ onSubmit: (event) => {
21
+ event.preventDefault();
22
+ void onSubmit(adapter.getValues?.() ?? {});
23
+ },
24
+ noValidate: true,
25
+ children
26
+ }
27
+ ) });
28
+ }
29
+ if (!form) {
30
+ throw new Error("FormRoot requires either a `form` (react-hook-form) or an `adapter` prop.");
31
+ }
11
32
  return /* @__PURE__ */ jsx(FormProvider, { ...form, children: /* @__PURE__ */ jsx(
12
33
  "form",
13
34
  {
@@ -4,4 +4,5 @@ export { FormRoot } from "./form-root.js";
4
4
  export type { FormRootProp } from "./form-root.js";
5
5
  export { FormFieldControl } from "./form-field-control.js";
6
6
  export type { FormFieldControlProp } from "./form-field-control.js";
7
- export type { ZodSchemaProp, FieldErrorMessageProp } from "../props/components/form.prop.js";
7
+ export { useFormSubmitting, useFormAdapter } from "./form-context.js";
8
+ export type { ZodSchemaProp, FieldErrorMessageProp, FormStateAdapter, } from "../props/components/form.prop.js";
@@ -1,8 +1,11 @@
1
1
  import { useZodForm } from "./use-zod-form.js";
2
2
  import { FormRoot } from "./form-root.js";
3
3
  import { FormFieldControl } from "./form-field-control.js";
4
+ import { useFormSubmitting, useFormAdapter } from "./form-context.js";
4
5
  export {
5
6
  FormFieldControl,
6
7
  FormRoot,
8
+ useFormAdapter,
9
+ useFormSubmitting,
7
10
  useZodForm
8
11
  };
@@ -138,6 +138,11 @@
138
138
  "mainLabel": "Main content",
139
139
  "footerLabel": "Footer"
140
140
  },
141
+ "centeredShell": {
142
+ "headerLabel": "Header",
143
+ "mainLabel": "Main content",
144
+ "footerLabel": "Footer"
145
+ },
141
146
  "topbar": {
142
147
  "toggleSidebar": "Toggle sidebar",
143
148
  "search": "Search",
@@ -138,6 +138,11 @@
138
138
  "mainLabel": "メインコンテンツ",
139
139
  "footerLabel": "フッター"
140
140
  },
141
+ "centeredShell": {
142
+ "headerLabel": "ヘッダー",
143
+ "mainLabel": "メインコンテンツ",
144
+ "footerLabel": "フッター"
145
+ },
141
146
  "topbar": {
142
147
  "toggleSidebar": "サイドバーの切り替え",
143
148
  "search": "検索",
@@ -138,6 +138,11 @@
138
138
  "mainLabel": "Nội dung chính",
139
139
  "footerLabel": "Chân trang"
140
140
  },
141
+ "centeredShell": {
142
+ "headerLabel": "Đầu trang",
143
+ "mainLabel": "Nội dung chính",
144
+ "footerLabel": "Chân trang"
145
+ },
141
146
  "topbar": {
142
147
  "toggleSidebar": "Bật/tắt thanh bên",
143
148
  "search": "Tìm kiếm",
@@ -0,0 +1,65 @@
1
+ import type { FormStateAdapter } from "../props/components/form.prop.js";
2
+ /**
3
+ * Structural shape of Inertia's `useForm` return that the adapter needs. Duck-typed on purpose —
4
+ * `@godxjp/ui` keeps ZERO dependency on `@inertiajs/react`; the real `useForm()` object satisfies
5
+ * this shape, so a consumer passes it directly with no import from this package.
6
+ */
7
+ export interface InertiaFormLike<TData extends Record<string, unknown> = Record<string, unknown>> {
8
+ /** Current form values (server-driven state). */
9
+ data: TData;
10
+ /** Write a single field back (Inertia signature: `setData(key, value)`). */
11
+ setData(key: string, value: unknown): void;
12
+ /** Server (Laravel FormRequest) validation errors, keyed by field name. */
13
+ errors: Partial<Record<string, string>>;
14
+ /** True while a submit request is in flight. */
15
+ processing: boolean;
16
+ }
17
+ /**
18
+ * Wrap an Inertia `useForm()` object as a framework-agnostic {@link FormStateAdapter} so it drives
19
+ * `FormRoot`/`FormFieldControl` with the SAME auto-binding as the react-hook-form path — no manual
20
+ * `value`/`onChange`/`error`/`aria-invalid` per field, and `processing` flows to submit-button
21
+ * loading via `useFormSubmitting()`. Server errors in `form.errors` surface on the matching
22
+ * `FormFieldControl` and clear on correction (Inertia clears the error on `setData`).
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * import { FormRoot, FormFieldControl, useFormSubmitting } from "@godxjp/ui/form";
27
+ * import { inertiaAdapter } from "@godxjp/ui/inertia";
28
+ * import { useForm } from "@inertiajs/react";
29
+ *
30
+ * const form = useForm({ email: "", password: "" });
31
+ * return (
32
+ * <FormRoot adapter={inertiaAdapter(form)} onSubmit={() => form.post("/login")}>
33
+ * <FormFieldControl name="email" label="Email" required>
34
+ * {(field) => <Input {...field} type="email" value={String(field.value ?? "")} />}
35
+ * </FormFieldControl>
36
+ * <Button type="submit" loading={useFormSubmitting()}>Sign in</Button>
37
+ * </FormRoot>
38
+ * );
39
+ * ```
40
+ */
41
+ export declare function inertiaAdapter<TData extends Record<string, unknown>>(form: InertiaFormLike<TData>): FormStateAdapter;
42
+ /**
43
+ * Lighter helper for consumers who compose `FormField` by hand rather than `FormRoot`. Returns the
44
+ * exact props to spread onto a `FormField` + its control — `value`, `onChange`, `error`, `name` —
45
+ * so no field is hand-wired. Use `inertiaAdapter` + `FormRoot` for the full auto-binding instead.
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * const form = useForm({ email: "" });
50
+ * const email = useInertiaField(form, "email");
51
+ * <FormField id="email" label="Email" error={email.error}>
52
+ * <Input {...email} value={String(email.value ?? "")} type="email" />
53
+ * </FormField>
54
+ * ```
55
+ */
56
+ export declare function useInertiaField<TData extends Record<string, unknown>>(form: InertiaFormLike<TData>, name: string): {
57
+ name: string;
58
+ value: unknown;
59
+ error: string | undefined;
60
+ onChange: (event: {
61
+ target: {
62
+ value: unknown;
63
+ };
64
+ }) => void;
65
+ };
@@ -0,0 +1,22 @@
1
+ "use client";
2
+ function inertiaAdapter(form) {
3
+ return {
4
+ getValue: (name) => form.data[name],
5
+ setValue: (name, value) => form.setData(name, value),
6
+ getError: (name) => form.errors[name],
7
+ isSubmitting: form.processing,
8
+ getValues: () => form.data
9
+ };
10
+ }
11
+ function useInertiaField(form, name) {
12
+ return {
13
+ name,
14
+ value: form.data[name],
15
+ error: form.errors[name],
16
+ onChange: (event) => form.setData(name, event?.target?.value)
17
+ };
18
+ }
19
+ export {
20
+ inertiaAdapter,
21
+ useInertiaField
22
+ };
@@ -9,9 +9,41 @@ export type ZodSchemaProp<T extends z.ZodType = z.ZodType> = T;
9
9
  export type UseZodFormOptionsProp<TFieldValues extends FieldValues> = Omit<UseFormProps<TFieldValues>, "resolver">;
10
10
  /** Return type of useZodForm. */
11
11
  export type UseZodFormReturnProp<TFieldValues extends FieldValues> = UseFormReturn<TFieldValues>;
12
+ /**
13
+ * Framework-agnostic form-state adapter — decouples `FormRoot`/`FormFieldControl` from any single
14
+ * form library. The DS ships the built-in react-hook-form path (`form`), and an adapter lets a
15
+ * SERVER-driven form library plug into the same auto-binding: Inertia's `useForm`
16
+ * (`@godxjp/ui/inertia`), formik, or TanStack Form. The core has ZERO dependency on any of them —
17
+ * a consumer wraps its own form object in an adapter and passes it to `FormRoot`.
18
+ */
19
+ export interface FormStateAdapter {
20
+ /** Current value of the field `name` (dotted paths allowed if the underlying store supports them). */
21
+ getValue(name: string): unknown;
22
+ /** Write `value` back to the field `name` (e.g. Inertia's `setData(name, value)`). */
23
+ setValue(name: string, value: unknown): void;
24
+ /** Server/validation error message for `name`, or undefined when the field is valid. */
25
+ getError(name: string): string | undefined;
26
+ /** True while a submit is in flight (e.g. Inertia's `processing`) — drives submit-button loading. */
27
+ isSubmitting: boolean;
28
+ /** Optional blur handler (e.g. touch-tracking); called with the field name. */
29
+ onBlur?(name: string): void;
30
+ /** Optional snapshot of all values, passed to `onSubmit`; defaults to `{}` when absent. */
31
+ getValues?(): unknown;
32
+ }
12
33
  /** @see FormRoot */
13
34
  export type FormRootProp<TFieldValues extends FieldValues> = {
14
- form: UseZodFormReturnProp<TFieldValues>;
35
+ /**
36
+ * react-hook-form return (client-side Zod validation). Provide EITHER `form` OR `adapter` — `form`
37
+ * is the built-in client path; `adapter` plugs in a server-driven form library (Inertia/formik).
38
+ */
39
+ form?: UseZodFormReturnProp<TFieldValues>;
40
+ /**
41
+ * Framework-agnostic form-state adapter (e.g. `inertiaAdapter(form)` from `@godxjp/ui/inertia`).
42
+ * Provide EITHER `form` OR `adapter`. With an adapter, `FormFieldControl` auto-binds each field by
43
+ * `name` (value/onChange/error/aria) and `useFormSubmitting()` reads `isSubmitting`.
44
+ */
45
+ adapter?: FormStateAdapter;
46
+ /** Submit handler. RHF path receives validated `values`; the adapter path calls it with a snapshot (`getValues()`), typically ignored in favour of the form library's own submit (`form.post(url)`). */
15
47
  onSubmit: (values: TFieldValues) => void | Promise<void>;
16
48
  children: React.ReactNode;
17
49
  className?: string;
@@ -1,7 +1,7 @@
1
1
  /** Layout component prop types — @see docs/COMPONENTS.md#layout */
2
2
  import type * as React from "react";
3
3
  import type { ComponentType, ReactNode, SVGProps } from "react";
4
- import type { BreadcrumbProp, TitleProp, SubtitleProp, ExtraProp, FooterProp, PageDensityProp, PageContainerVariantProp, GapProp, ClassNameProp, ChildrenProp } from "../vocabulary/index.js";
4
+ import type { BreadcrumbProp, TitleProp, SubtitleProp, ExtraProp, FooterProp, PageDensityProp, PageContainerVariantProp, CenteredShellWidthProp, GapProp, ClassNameProp, ChildrenProp } from "../vocabulary/index.js";
5
5
  /** @see PageContainer */
6
6
  export type PageContainerProp = {
7
7
  title: TitleProp;
@@ -108,6 +108,34 @@ export type AuthShellProp = {
108
108
  footer?: ReactNode;
109
109
  className?: ClassNameProp;
110
110
  };
111
+ /**
112
+ * @see CenteredShell — authenticated, no-sidebar, centred-column page shell (hosted-ID "My Page",
113
+ * account / self-service, standalone settings). A padded top bar with real actions (banner) reusing
114
+ * AppShell's `.app-topbar` chrome WITHOUT a sidebar, a scrollable `main` holding a centred column of
115
+ * configurable medium width (`width` = sm|md|lg, all wider than AuthShell's 24rem card) top-aligned
116
+ * so sections flow + scroll, and an optional footer (contentinfo). Fills the gap between AppShell
117
+ * (needs a sidebar) and AuthShell (unauthenticated, narrow vertically-centred card) — so a hosted
118
+ * account page needs ZERO custom CSS and never hand-rolls a bar (the `.ui-topbar` zero-inset
119
+ * footgun). Layout-only; delegate motion to `Reveal`.
120
+ */
121
+ export type CenteredShellProp = {
122
+ /** Centred column content — page sections (identity hero, org picker, service grid, team list). */
123
+ children: ReactNode;
124
+ /**
125
+ * Top bar slot (banner) — a `<Topbar>` with brand + real actions (an `AppSettingPicker`, a user
126
+ * menu, sign-out). CenteredShell wraps it in the SAME padded chrome as AppShell's topbar
127
+ * (padding-inline · border · backdrop), so you never hand-roll a bar. Omit → no banner.
128
+ */
129
+ topbar?: ReactNode;
130
+ /** Footer slot (contentinfo) pinned to the bottom (legal links, locale switch, support). Omit → none. */
131
+ footer?: ReactNode;
132
+ /**
133
+ * Max-width of the centred content column: `sm` ~32rem, `md` (default) ~46rem, `lg` ~64rem — all
134
+ * wider than AuthShell's 24rem auth card. A service retunes each tier via `--centered-shell-width-*`.
135
+ */
136
+ width?: CenteredShellWidthProp;
137
+ className?: ClassNameProp;
138
+ };
111
139
  /** @see Sidebar */
112
140
  export type SidebarProductProp = {
113
141
  name: string;
@@ -184,6 +184,11 @@ export declare const VOCABULARY_REGISTRY: {
184
184
  readonly category: "layout";
185
185
  readonly description: "Page shell layout — default, narrow, flush, ghost";
186
186
  };
187
+ readonly CenteredShellWidthProp: {
188
+ readonly file: "vocabulary/layout.prop.ts";
189
+ readonly category: "layout";
190
+ readonly description: "CenteredShell column max-width — sm, md, lg";
191
+ };
187
192
  readonly GapProp: {
188
193
  readonly file: "vocabulary/layout.prop.ts";
189
194
  readonly category: "layout";
@@ -453,6 +458,11 @@ export declare const COMPONENT_PROP_REGISTRY: {
453
458
  readonly file: "components/layout.prop.ts";
454
459
  readonly vocabulary: readonly ["ChildrenProp", "ClassNameProp"];
455
460
  };
461
+ readonly CenteredShellProp: {
462
+ readonly group: "layout";
463
+ readonly file: "components/layout.prop.ts";
464
+ readonly vocabulary: readonly ["ChildrenProp", "ClassNameProp", "CenteredShellWidthProp"];
465
+ };
456
466
  readonly SidebarProductProp: {
457
467
  readonly group: "layout";
458
468
  readonly file: "components/layout.prop.ts";
@@ -182,6 +182,11 @@ const VOCABULARY_REGISTRY = {
182
182
  category: "layout",
183
183
  description: "Page shell layout \u2014 default, narrow, flush, ghost"
184
184
  },
185
+ CenteredShellWidthProp: {
186
+ file: "vocabulary/layout.prop.ts",
187
+ category: "layout",
188
+ description: "CenteredShell column max-width \u2014 sm, md, lg"
189
+ },
185
190
  GapProp: {
186
191
  file: "vocabulary/layout.prop.ts",
187
192
  category: "layout",
@@ -438,6 +443,11 @@ const COMPONENT_PROP_REGISTRY = {
438
443
  file: "components/layout.prop.ts",
439
444
  vocabulary: ["ChildrenProp", "ClassNameProp"]
440
445
  },
446
+ CenteredShellProp: {
447
+ group: "layout",
448
+ file: "components/layout.prop.ts",
449
+ vocabulary: ["ChildrenProp", "ClassNameProp", "CenteredShellWidthProp"]
450
+ },
441
451
  SidebarProductProp: {
442
452
  group: "layout",
443
453
  file: "components/layout.prop.ts",
@@ -1,7 +1,7 @@
1
1
  /** Barrel — all vocabulary prop types. */
2
2
  export type { ClassNameProp, ChildrenProp, IdProp, OpenProp, DefaultOpenProp, OnOpenChangeProp, HandlerProp, PendingProp, RequiredProp, DisabledProp, LabelProp, HelperProp, ErrorProp, PlaceholderProp, NameProp, ValueProp, DefaultValueProp, OnValueChangeProp, OnChangeProp, OnClickProp, AsChildProp, WidthProp, } from "./shared.prop.js";
3
3
  export type { TitleProp, SubtitleProp, DescriptionProp, ExtraProp, FooterProp, ActionProp, IconProp, ConfirmLabelProp, CancelLabelProp, ActionsProp, EmptyMessageProp, } from "./content.prop.js";
4
- export type { PageDensityProp, PageContainerVariantProp, TableDensityProp, DensityProp, GapProp, } from "./layout.prop.js";
4
+ export type { PageDensityProp, PageContainerVariantProp, CenteredShellWidthProp, TableDensityProp, DensityProp, GapProp, } from "./layout.prop.js";
5
5
  export type { ButtonVariantProp, ButtonSizeProp, BadgeVariantProp, AppSettingPickerAppearanceProp, ShapeProp, TextSizeProp, TextToneProp, FontWeightProp, HeadingLevelProp, TextAlignProp, SizeProp, FormLayoutProp, BreakpointProp, ConfirmVariantProp, ToneProp, AlertVariantProp, SortDirectionProp, ColumnAlignProp, SortStateProp, RevealDelayProp, } from "./interaction.prop.js";
6
6
  export type { BreadcrumbItemProp, BreadcrumbProp } from "./navigation.prop.js";
7
7
  export type { GetRowIdProp, OnRowClickProp, ColumnDefProp, SelectedIdsProp, OnSelectChangeProp, OnTableDensityChangeProp, OnSortChangeProp, OnSearchChangeProp, OnClearFiltersProp, HasActiveFiltersProp, } from "./data.prop.js";
@@ -8,6 +8,8 @@ export type PageDensityProp = "compact" | "default" | "comfortable";
8
8
  export type DensityProp = "compact" | "default" | "comfortable";
9
9
  /** Page shell layout — orthogonal to PageDensityProp. */
10
10
  export type PageContainerVariantProp = "default" | "narrow" | "flush" | "ghost";
11
+ /** CenteredShell content-column max-width tier — sm ~32rem, md ~46rem, lg ~64rem. */
12
+ export type CenteredShellWidthProp = "sm" | "md" | "lg";
11
13
  /** Shared gap between layout children; components may document subsets. */
12
14
  export type GapProp = "xs" | "sm" | "md" | "lg" | "xl";
13
15
  /** DataTable row density subset. */
@@ -123,6 +123,72 @@
123
123
  color: hsl(var(--muted-foreground));
124
124
  }
125
125
 
126
+ /*
127
+ * CENTERED SHELL — authenticated, no-sidebar, centred-column page shell (hosted-ID "My Page",
128
+ * account / self-service, standalone settings). A flex column over the same brand-wash surface as
129
+ * the app/auth shells: a padded top bar that MIRRORS `.app-topbar` chrome (fixed height + inline
130
+ * padding + border + backdrop) but WITHOUT a sidebar grid, a scrollable `main` whose content is a
131
+ * width-tiered centred column that is TOP-aligned (sections flow + scroll, never vertically
132
+ * centred like the auth card), and an optional footer. Every knob reads a token.
133
+ */
134
+ .ui-centered-shell {
135
+ display: flex;
136
+ min-height: 100dvh;
137
+ flex-direction: column;
138
+ background:
139
+ linear-gradient(180deg, hsl(var(--primary) / 0.035), transparent 14rem),
140
+ hsl(var(--background));
141
+ }
142
+
143
+ .ui-centered-shell-bar {
144
+ display: flex;
145
+ min-width: 0;
146
+ min-height: var(--centered-shell-bar-height);
147
+ flex: 0 0 auto;
148
+ align-items: center;
149
+ gap: var(--space-3);
150
+ padding-inline: var(--centered-shell-bar-padding-x);
151
+ border-bottom: 1px solid hsl(var(--border));
152
+ background: hsl(var(--background) / 0.92);
153
+ /* Opt-in brand-chrome wash — shares the topbar hook, invisible by default (none). */
154
+ background-image: var(--topbar-gradient);
155
+ backdrop-filter: blur(12px);
156
+ }
157
+
158
+ .ui-centered-shell-main {
159
+ display: flex;
160
+ min-width: 0;
161
+ flex: 1 1 auto;
162
+ flex-direction: column;
163
+ overflow: auto;
164
+ /* Ambient brand glow hook — shares the app-main hook, invisible by default (none). */
165
+ background-image: var(--gradient-glow);
166
+ /* Sole scroll/clip boundary for the page area (same containment rationale as `.app-main`). */
167
+ contain: paint;
168
+ padding: var(--centered-shell-main-padding);
169
+ }
170
+
171
+ .ui-centered-shell-column {
172
+ width: 100%;
173
+ margin-inline: auto;
174
+ max-width: var(--centered-shell-width-md);
175
+ }
176
+
177
+ .ui-centered-shell-column[data-width="sm"] {
178
+ max-width: var(--centered-shell-width-sm);
179
+ }
180
+
181
+ .ui-centered-shell-column[data-width="lg"] {
182
+ max-width: var(--centered-shell-width-lg);
183
+ }
184
+
185
+ .ui-centered-shell-footer {
186
+ padding: var(--centered-shell-footer-padding);
187
+ border-top: 1px solid hsl(var(--border));
188
+ text-align: center;
189
+ color: hsl(var(--muted-foreground));
190
+ }
191
+
126
192
  .app-topbar-rail {
127
193
  display: flex;
128
194
  min-width: 0;
@@ -42,4 +42,16 @@
42
42
  --auth-shell-bar-padding: var(--space-5) var(--space-6);
43
43
  --auth-shell-main-padding: var(--space-6);
44
44
  --auth-shell-footer-padding: var(--space-3) var(--space-6) var(--space-4);
45
+
46
+ /* CenteredShell — authenticated, no-sidebar, centred-column page shell (hosted-ID "My Page",
47
+ * account, standalone settings). The bar mirrors AppShell's `.app-topbar` chrome (fixed height +
48
+ * inline padding); the column max-width has three tiers, all wider than the 24rem auth card. A
49
+ * service retunes the bar inset, block padding and each width tier without forking CSS. */
50
+ --centered-shell-bar-height: 3.25rem;
51
+ --centered-shell-bar-padding-x: var(--space-4);
52
+ --centered-shell-main-padding: var(--space-6);
53
+ --centered-shell-footer-padding: var(--space-3) var(--space-6) var(--space-4);
54
+ --centered-shell-width-sm: 32rem;
55
+ --centered-shell-width-md: 46rem;
56
+ --centered-shell-width-lg: 64rem;
45
57
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "18.0.2",
4
- "godxUiMcp": "18.0.1",
3
+ "version": "18.1.0",
4
+ "godxUiMcp": "18.0.3",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.29.1",
7
7
  "sideEffects": false,
@@ -88,6 +88,10 @@
88
88
  "types": "./dist/form/index.d.ts",
89
89
  "import": "./dist/form/index.js"
90
90
  },
91
+ "./inertia": {
92
+ "types": "./dist/inertia/index.d.ts",
93
+ "import": "./dist/inertia/index.js"
94
+ },
91
95
  "./app": {
92
96
  "types": "./dist/app/index.d.ts",
93
97
  "import": "./dist/app/index.js"
@@ -278,6 +282,7 @@
278
282
  "check:example-imports": "node scripts/check-example-imports.mjs",
279
283
  "check:frame-coverage": "node scripts/frame-coverage.mjs",
280
284
  "check:screen-reader-evidence": "node scripts/check-screen-reader-evidence.mjs",
285
+ "check:voiceover-capture": "node scripts/check-voiceover-capture.mjs",
281
286
  "check:data-entry-frame-runtime": "node scripts/check-data-entry-frame-runtime.mjs",
282
287
  "check:data-entry-owner-source": "node scripts/check-data-entry-owner-source.mjs",
283
288
  "check:data-entry-touch-aria": "node scripts/check-data-entry-touch-aria.mjs",
@@ -287,7 +292,7 @@
287
292
  "check:display-runtime-evidence": "node scripts/check-display-runtime-evidence.mjs",
288
293
  "check:data-table-pagination-wrap": "node scripts/check-data-table-pagination-wrap.mjs",
289
294
  "check:button-icon-xs": "node scripts/check-button-icon-xs.mjs",
290
- "check:frame-contracts": "pnpm check:frame-coverage && pnpm check:screen-reader-evidence && pnpm check:data-entry-owner-source && pnpm check:display-runtime-evidence",
295
+ "check:frame-contracts": "pnpm check:component-api-manifest && pnpm audit:component-cases && pnpm check:frame-coverage && pnpm check:screen-reader-evidence && pnpm check:data-entry-owner-source && pnpm check:display-runtime-evidence",
291
296
  "check:final-touch-rtl": "node scripts/check-final-touch-rtl.mjs",
292
297
  "check:frame-runtime": "pnpm check:data-entry-frame-runtime && pnpm check:data-entry-touch-aria && pnpm check:layout-nav-frames && pnpm check:provider-feedback-query-runtime && pnpm check:layout-nav-closure && pnpm check:final-touch-rtl && pnpm check:data-table-pagination-wrap && pnpm check:button-icon-xs",
293
298
  "pretest": "pnpm check:frame-coverage",
@@ -299,6 +304,9 @@
299
304
  "check:mcp-sync": "node scripts/check-mcp-sync.mjs",
300
305
  "check:mcp-orphans": "node scripts/check-mcp-orphans.mjs",
301
306
  "check:mcp-prop-sync": "node scripts/check-mcp-prop-sync.mjs",
307
+ "gen:component-api-manifest": "node scripts/gen-component-api-manifest.mjs",
308
+ "check:component-api-manifest": "node scripts/gen-component-api-manifest.mjs --check",
309
+ "audit:component-cases": "node scripts/check-component-case-evidence.mjs",
302
310
  "check:prop-vocabulary": "node scripts/check-prop-vocabulary.mjs",
303
311
  "check:token-tiers": "node scripts/check-token-tiers.mjs",
304
312
  "check:control-sizing": "node scripts/check-control-sizing.mjs",