@momoi-labs/kiso-react 0.3.0 → 0.4.1

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,10 +23,20 @@ export function ApplicationForm() {
23
23
  }
24
24
  ```
25
25
 
26
- `@momoi-labs/kiso` is a regular dependency. The stylesheet imports its tokens
27
- and component layer; consumers do not need to import those again. React and
28
- React DOM are peer dependencies supplied by the application. No Tailwind setup
29
- is required. The Kiso base stylesheet includes global styles and Google Fonts.
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
+
35
+ `@momoi-labs/kiso` is a regular dependency. The stylesheet imports Google Fonts
36
+ first, then Kiso tokens and the component layer; consumers do not need to import
37
+ those again. React and React DOM are peer dependencies supplied by the
38
+ application. No Tailwind setup is required. The stylesheet includes global
39
+ styles and Google Fonts.
30
40
 
31
41
  ## Available components
32
42
 
@@ -45,7 +55,9 @@ Data and structure
45
55
  - EmptyState, EmptyStateIcon, EmptyStateTitle, EmptyStateDescription, EmptyStateActions.
46
56
  - Header, PageHeader, PageHeaderTitle, PageHeaderDescription.
47
57
  - Sidebar, SidebarHeader, SidebarBody, SidebarFooter.
48
- - AppShell and AppShellMain, the sidebar and main columns of a console.
58
+ - AppShell and AppShellMain, the low-level columns of a console.
59
+ - ApplicationShell, the standard brand, navigation, header, footer, and page
60
+ arrangement.
49
61
  - Split, Pane, and Splitter, a list-detail layout with a resizable divider.
50
62
  - LogView, LogViewLine, LogViewTime, and LogViewLevel.
51
63
  - Stat, StatHeader, StatLabel, StatValue, StatFoot, and StatDelta.
@@ -69,8 +81,10 @@ Feedback
69
81
 
70
82
  - Alert, AlertContent, AlertTitle, AlertDescription, and `alertVariants`.
71
83
  - Spinner, Skeleton, and their variants.
84
+ - Toasts and `useToast`, for notifications raised from descendant event
85
+ handlers.
72
86
  - ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription,
73
- ToastContent, ToastAction, ToastClose.
87
+ ToastContent, ToastAction, ToastClose, for controlled composition.
74
88
 
75
89
  Overlays
76
90
 
@@ -88,7 +102,21 @@ Button variants are `default`, `primary`, `destructive`, and `ghost`; sizes are
88
102
  `warning`, and `danger`. Alert and Toast variants follow their contracts:
89
103
  `info` / `success` / `warning` / `error`, and `neutral` / `success` / `warning`
90
104
  / `error`. Native props and refs pass through to the underlying control.
91
- FormField adds `label` and optional `hint` to Input's props.
105
+ FormField adds `label`, optional `hint`, and optional `error` to Input's props.
106
+ With one child, it wires those parts to that control instead of rendering an
107
+ Input. Use `fieldClassName` to style the wrapper.
108
+
109
+ Wrap an application in Toasts and call `useToast()` below it. The returned
110
+ function takes a variant, title, and optional body:
111
+
112
+ ```tsx
113
+ const notify = useToast();
114
+ notify('error', `Could not stop ${app.name}`, details);
115
+ ```
116
+
117
+ ApplicationShell takes navigation groups as data. Destinations keep routing in
118
+ the product through `{ href, active, onClick? }`; the package owns the repeated
119
+ Sidebar, Navigation, Header, and main arrangement.
92
120
 
93
121
  Navigation, Sidebar, and Link know nothing about routing. Mark the current
94
122
  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/styles.css CHANGED
@@ -1,3 +1,4 @@
1
+ @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap");
1
2
  @import "@momoi-labs/kiso/tokens.css";
2
3
  @import "@momoi-labs/kiso/ui.css";
3
4
 
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.1",
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.1",
36
36
  "class-variance-authority": "0.7.1",
37
37
  "clsx": "2.1.1",
38
38
  "radix-ui": "1.6.7"