@godxjp/ui 18.0.3 → 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.
- package/dist/components/general/button.d.ts +1 -1
- package/dist/components/layout/centered-shell.d.ts +22 -0
- package/dist/components/layout/centered-shell.js +35 -0
- package/dist/components/layout/index.d.ts +3 -0
- package/dist/components/layout/index.js +2 -0
- package/dist/form/form-context.d.ts +23 -0
- package/dist/form/form-context.js +29 -0
- package/dist/form/form-field-control.js +28 -2
- package/dist/form/form-root.d.ts +7 -1
- package/dist/form/form-root.js +21 -0
- package/dist/form/index.d.ts +2 -1
- package/dist/form/index.js +3 -0
- package/dist/i18n/messages/en.json +5 -0
- package/dist/i18n/messages/ja.json +5 -0
- package/dist/i18n/messages/vi.json +5 -0
- package/dist/inertia/index.d.ts +65 -0
- package/dist/inertia/index.js +22 -0
- package/dist/props/components/form.prop.d.ts +33 -1
- package/dist/props/components/layout.prop.d.ts +29 -1
- package/dist/props/registry.d.ts +10 -0
- package/dist/props/registry.js +10 -0
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/props/vocabulary/layout.prop.d.ts +2 -0
- package/dist/styles/shell-layout.css +66 -0
- package/dist/tokens/components/shell.css +12 -0
- package/package.json +6 -2
|
@@ -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" | "
|
|
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
|
|
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
|
{
|
package/dist/form/form-root.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/form/form-root.js
CHANGED
|
@@ -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
|
{
|
package/dist/form/index.d.ts
CHANGED
|
@@ -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
|
|
7
|
+
export { useFormSubmitting, useFormAdapter } from "./form-context.js";
|
|
8
|
+
export type { ZodSchemaProp, FieldErrorMessageProp, FormStateAdapter, } from "../props/components/form.prop.js";
|
package/dist/form/index.js
CHANGED
|
@@ -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": "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
|
-
|
|
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;
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -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";
|
package/dist/props/registry.js
CHANGED
|
@@ -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
|
|
4
|
-
"godxUiMcp": "18.0.
|
|
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"
|