@momoi-labs/kiso-react 0.3.0 → 0.4.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/README.md CHANGED
@@ -11,7 +11,7 @@ Import the stylesheet once, before application overrides:
11
11
 
12
12
  ```tsx
13
13
  import '@momoi-labs/kiso-react/styles.css';
14
- import { Button, FormField } from '@momoi-labs/kiso-react';
14
+ import { Button, FormField, Textarea } from '@momoi-labs/kiso-react';
15
15
 
16
16
  export function ApplicationForm() {
17
17
  return (
@@ -23,6 +23,15 @@ export function ApplicationForm() {
23
23
  }
24
24
  ```
25
25
 
26
+ FormField renders an Input by default. Pass one control as its child when the
27
+ field uses another input, and add `error` for wired validation feedback:
28
+
29
+ ```tsx
30
+ <FormField label="Compose file" hint="Docker Compose YAML." error={error}>
31
+ <Textarea rows={12} />
32
+ </FormField>
33
+ ```
34
+
26
35
  `@momoi-labs/kiso` is a regular dependency. The stylesheet imports its tokens
27
36
  and component layer; consumers do not need to import those again. React and
28
37
  React DOM are peer dependencies supplied by the application. No Tailwind setup
@@ -45,7 +54,9 @@ Data and structure
45
54
  - EmptyState, EmptyStateIcon, EmptyStateTitle, EmptyStateDescription, EmptyStateActions.
46
55
  - Header, PageHeader, PageHeaderTitle, PageHeaderDescription.
47
56
  - Sidebar, SidebarHeader, SidebarBody, SidebarFooter.
48
- - AppShell and AppShellMain, the sidebar and main columns of a console.
57
+ - AppShell and AppShellMain, the low-level columns of a console.
58
+ - ApplicationShell, the standard brand, navigation, header, footer, and page
59
+ arrangement.
49
60
  - Split, Pane, and Splitter, a list-detail layout with a resizable divider.
50
61
  - LogView, LogViewLine, LogViewTime, and LogViewLevel.
51
62
  - Stat, StatHeader, StatLabel, StatValue, StatFoot, and StatDelta.
@@ -69,8 +80,10 @@ Feedback
69
80
 
70
81
  - Alert, AlertContent, AlertTitle, AlertDescription, and `alertVariants`.
71
82
  - Spinner, Skeleton, and their variants.
83
+ - Toasts and `useToast`, for notifications raised from descendant event
84
+ handlers.
72
85
  - ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription,
73
- ToastContent, ToastAction, ToastClose.
86
+ ToastContent, ToastAction, ToastClose, for controlled composition.
74
87
 
75
88
  Overlays
76
89
 
@@ -88,7 +101,21 @@ Button variants are `default`, `primary`, `destructive`, and `ghost`; sizes are
88
101
  `warning`, and `danger`. Alert and Toast variants follow their contracts:
89
102
  `info` / `success` / `warning` / `error`, and `neutral` / `success` / `warning`
90
103
  / `error`. Native props and refs pass through to the underlying control.
91
- FormField adds `label` and optional `hint` to Input's props.
104
+ FormField adds `label`, optional `hint`, and optional `error` to Input's props.
105
+ With one child, it wires those parts to that control instead of rendering an
106
+ Input. Use `fieldClassName` to style the wrapper.
107
+
108
+ Wrap an application in Toasts and call `useToast()` below it. The returned
109
+ function takes a variant, title, and optional body:
110
+
111
+ ```tsx
112
+ const notify = useToast();
113
+ notify('error', `Could not stop ${app.name}`, details);
114
+ ```
115
+
116
+ ApplicationShell takes navigation groups as data. Destinations keep routing in
117
+ the product through `{ href, active, onClick? }`; the package owns the repeated
118
+ Sidebar, Navigation, Header, and main arrangement.
92
119
 
93
120
  Navigation, Sidebar, and Link know nothing about routing. Mark the current
94
121
  destination with `active`, and pass `asChild` to render a router's own link
@@ -1,4 +1,28 @@
1
1
  import * as React from "react";
2
2
  declare function AppShell({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
3
3
  declare function AppShellMain({ className, ...props }: React.ComponentProps<"main">): React.JSX.Element;
4
- export { AppShell, AppShellMain };
4
+ type ApplicationShellDestination = {
5
+ href: string;
6
+ active?: boolean;
7
+ onClick?: () => void;
8
+ label: React.ReactNode;
9
+ leading?: React.ReactNode;
10
+ trailing?: React.ReactNode;
11
+ };
12
+ type ApplicationShellGroup = {
13
+ label?: string;
14
+ destinations: readonly ApplicationShellDestination[];
15
+ empty?: React.ReactNode;
16
+ };
17
+ type ApplicationShellProps = Omit<React.ComponentProps<typeof AppShell>, "children"> & {
18
+ brand: React.ReactNode;
19
+ primaryAction?: React.ReactNode;
20
+ navigation: readonly ApplicationShellGroup[];
21
+ navigationLabel?: string;
22
+ footer?: React.ReactNode;
23
+ header?: React.ReactNode;
24
+ children: React.ReactNode;
25
+ };
26
+ declare function ApplicationShell({ brand, primaryAction, navigation, navigationLabel, footer, header, children, ...props }: ApplicationShellProps): React.JSX.Element;
27
+ export { AppShell, AppShellMain, ApplicationShell };
28
+ export type { ApplicationShellDestination, ApplicationShellGroup, ApplicationShellProps, };
package/dist/app-shell.js CHANGED
@@ -1,5 +1,8 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { clsx as cn } from "clsx";
3
+ import { Header } from "./header.js";
4
+ import { Navigation, NavigationGroup, NavigationItem, NavigationLink, NavigationList, } from "./navigation.js";
5
+ import { Sidebar, SidebarBody, SidebarFooter, SidebarHeader } from "./sidebar.js";
3
6
  function AppShell({ className, ...props }) {
4
7
  return (_jsx("div", { "data-slot": "app-shell", className: cn("app-shell", className), ...props }));
5
8
  }
@@ -8,4 +11,12 @@ function AppShell({ className, ...props }) {
8
11
  function AppShellMain({ className, ...props }) {
9
12
  return (_jsx("main", { "data-slot": "app-shell-main", className: cn("grow", className), ...props }));
10
13
  }
11
- export { AppShell, AppShellMain };
14
+ function ApplicationShell({ brand, primaryAction, navigation, navigationLabel = "Primary", footer, header, children, ...props }) {
15
+ return (_jsxs(AppShell, { ...props, children: [_jsxs(Sidebar, { children: [_jsxs(SidebarHeader, { children: [brand, primaryAction] }), _jsx(SidebarBody, { children: _jsx(Navigation, { "aria-label": navigationLabel, children: navigation.map((group, groupIndex) => (_jsx(NavigationGroup, { label: group.label, children: group.destinations.length > 0 ? (_jsx(NavigationList, { children: group.destinations.map((destination, destinationIndex) => (_jsx(NavigationItem, { children: _jsxs(NavigationLink, { href: destination.href, active: destination.active, onClick: (event) => {
16
+ if (!destination.onClick)
17
+ return;
18
+ event.preventDefault();
19
+ destination.onClick();
20
+ }, children: [destination.leading, _jsx("span", { className: "grow truncate", children: destination.label }), destination.trailing] }) }, `${destination.href}-${destinationIndex}`))) })) : group.empty }, group.label ?? groupIndex))) }) }), footer && _jsx(SidebarFooter, { children: footer })] }), _jsxs(AppShellMain, { children: [header && _jsx(Header, { children: header }), children] })] }));
21
+ }
22
+ export { AppShell, AppShellMain, ApplicationShell };
@@ -1,6 +1,16 @@
1
- import { type ComponentProps } from "react";
1
+ import { type ComponentProps, type ReactElement, type ReactNode } from "react";
2
2
  import { Input } from "./input.js";
3
- export declare function FormField({ label, hint, id: providedId, "aria-describedby": describedBy, ...props }: ComponentProps<typeof Input> & {
4
- label: string;
5
- hint?: string;
6
- }): import("react").JSX.Element;
3
+ type FormControlProps = {
4
+ id?: string;
5
+ "aria-describedby"?: string;
6
+ "aria-invalid"?: ComponentProps<"input">["aria-invalid"];
7
+ };
8
+ type FormFieldProps = Omit<ComponentProps<typeof Input>, "children"> & {
9
+ label: ReactNode;
10
+ hint?: ReactNode;
11
+ error?: ReactNode;
12
+ children?: ReactElement<FormControlProps>;
13
+ fieldClassName?: string;
14
+ };
15
+ export declare function FormField({ label, hint, error, children, fieldClassName, id: providedId, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }: FormFieldProps): import("react").JSX.Element;
16
+ export type { FormFieldProps };
@@ -1,12 +1,25 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useId } from "react";
3
+ import { cloneElement, useId, } from "react";
4
4
  import { Input } from "./input.js";
5
5
  import { Label } from "./label.js";
6
- export function FormField({ label, hint, id: providedId, "aria-describedby": describedBy, ...props }) {
6
+ import { ValidationMessage } from "./validation-message.js";
7
+ function mergeDescriptions(...descriptions) {
8
+ const ids = descriptions.flatMap((description) => description?.split(/\s+/).filter(Boolean) ?? []);
9
+ return ids.length > 0 ? [...new Set(ids)].join(" ") : undefined;
10
+ }
11
+ export function FormField({ label, hint, error, children, fieldClassName, id: providedId, "aria-describedby": describedBy, "aria-invalid": invalid, ...props }) {
7
12
  const generatedId = useId();
8
- const id = providedId ?? generatedId;
9
- const description = [describedBy, hint ? `${id}-help` : undefined]
10
- .filter(Boolean).join(" ") || undefined;
11
- return (_jsxs("div", { className: "field", children: [_jsx(Label, { htmlFor: id, children: label }), _jsx(Input, { ...props, id: id, "aria-describedby": description }), hint && (_jsx("small", { className: "field-hint", id: `${id}-help`, children: hint }))] }));
13
+ const id = providedId ?? children?.props.id ?? generatedId;
14
+ const hintId = hint ? `${id}-help` : undefined;
15
+ const errorId = error ? `${id}-error` : undefined;
16
+ const description = mergeDescriptions(describedBy, children?.props["aria-describedby"], hintId, errorId);
17
+ const controlProps = {
18
+ id,
19
+ "aria-describedby": description,
20
+ "aria-invalid": error ? true : children?.props["aria-invalid"] ?? invalid,
21
+ };
22
+ return (_jsxs("div", { className: fieldClassName ? `field ${fieldClassName}` : "field", children: [_jsx(Label, { htmlFor: id, children: label }), children
23
+ ? cloneElement(children, controlProps)
24
+ : _jsx(Input, { ...props, ...controlProps }), hint && (_jsx("small", { className: "field-hint", id: hintId, children: hint })), error && _jsx(ValidationMessage, { id: errorId, children: error })] }));
12
25
  }
package/dist/toast.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  import { Toast as ToastPrimitive } from "radix-ui";
4
+ type ToastVariant = "neutral" | "success" | "warning" | "error";
5
+ type ToastNotify = (variant: ToastVariant, title: React.ReactNode, body?: React.ReactNode) => void;
4
6
  declare const toastVariants: (props?: ({
5
7
  variant?: "error" | "neutral" | "success" | "warning" | null | undefined;
6
8
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -12,4 +14,10 @@ declare function ToastDescription({ className, ...props }: React.ComponentProps<
12
14
  declare function ToastContent({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
13
15
  declare function ToastAction({ ...props }: React.ComponentProps<typeof ToastPrimitive.Action>): React.JSX.Element;
14
16
  declare function ToastClose({ ...props }: React.ComponentProps<typeof ToastPrimitive.Close>): React.JSX.Element;
15
- export { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastContent, ToastAction, ToastClose, toastVariants, };
17
+ declare function useToast(): ToastNotify;
18
+ declare function Toasts({ children, duration, }: {
19
+ children: React.ReactNode;
20
+ duration?: number;
21
+ }): React.JSX.Element;
22
+ export { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastContent, ToastAction, ToastClose, Toasts, useToast, toastVariants, };
23
+ export type { ToastNotify, ToastVariant };
package/dist/toast.js CHANGED
@@ -1,9 +1,12 @@
1
1
  // Adapted from shadcn/ui new-york-v4. See ../SHADCN-LICENSE.
2
2
  "use client";
3
- import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
+ import * as React from "react";
4
5
  import { cva } from "class-variance-authority";
5
6
  import { clsx as cn } from "clsx";
6
7
  import { Toast as ToastPrimitive } from "radix-ui";
8
+ import { Button } from "./button.js";
9
+ const ToastContext = React.createContext(null);
7
10
  const toastVariants = cva("toast", {
8
11
  variants: {
9
12
  variant: {
@@ -40,4 +43,27 @@ function ToastAction({ ...props }) {
40
43
  function ToastClose({ ...props }) {
41
44
  return _jsx(ToastPrimitive.Close, { "data-slot": "toast-close", ...props });
42
45
  }
43
- export { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastContent, ToastAction, ToastClose, toastVariants, };
46
+ function useToast() {
47
+ const notify = React.useContext(ToastContext);
48
+ if (!notify)
49
+ throw new Error("useToast must be used within Toasts");
50
+ return notify;
51
+ }
52
+ function Toasts({ children, duration = 6000, }) {
53
+ const [notices, setNotices] = React.useState([]);
54
+ const nextId = React.useRef(0);
55
+ const notify = React.useCallback((variant, title, body) => {
56
+ const id = nextId.current++;
57
+ setNotices((current) => [...current, { id, variant, title, body }]);
58
+ }, []);
59
+ const dismiss = React.useCallback((id) => {
60
+ setNotices((current) => current.filter((notice) => notice.id !== id));
61
+ }, []);
62
+ return (_jsx(ToastContext.Provider, { value: notify, children: _jsxs(ToastProvider, { duration: duration, children: [children, notices.map((notice) => (_jsxs(Toast, { variant: notice.variant, onOpenChange: (open) => {
63
+ if (!open)
64
+ dismiss(notice.id);
65
+ }, children: [_jsxs(ToastContent, { children: [_jsx(ToastTitle, { children: notice.title }), typeof notice.body === "string" || typeof notice.body === "number"
66
+ ? _jsx(ToastDescription, { children: notice.body })
67
+ : notice.body] }), _jsx(ToastClose, { asChild: true, children: _jsx(Button, { size: "xs", variant: "ghost", children: "Dismiss" }) })] }, notice.id))), _jsx(ToastViewport, {})] }) }));
68
+ }
69
+ export { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastContent, ToastAction, ToastClose, Toasts, useToast, toastVariants, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momoi-labs/kiso-react",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Kiso React components built with shadcn/ui and Radix",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,7 +32,7 @@
32
32
  "prepack": "npm run build"
33
33
  },
34
34
  "dependencies": {
35
- "@momoi-labs/kiso": "^0.6.0",
35
+ "@momoi-labs/kiso": "^0.7.0",
36
36
  "class-variance-authority": "0.7.1",
37
37
  "clsx": "2.1.1",
38
38
  "radix-ui": "1.6.7"