@jini-ai/ui-kit 0.0.0-stage → 0.1.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +201 -0
  3. package/README.md +150 -2
  4. package/dist/attrs.d.ts +18 -0
  5. package/dist/attrs.js +2 -0
  6. package/dist/confirm.d.ts +42 -0
  7. package/dist/confirm.js +41 -0
  8. package/dist/index.d.ts +10 -0
  9. package/dist/index.js +5 -0
  10. package/dist/kit.spec.d.ts +491 -0
  11. package/dist/kit.spec.js +40 -0
  12. package/dist/needs.d.ts +24 -0
  13. package/dist/needs.js +29 -0
  14. package/dist/react/Kit.hooks.d.ts +39 -0
  15. package/dist/react/Kit.hooks.js +87 -0
  16. package/dist/react/KitProvider.d.ts +13 -0
  17. package/dist/react/KitProvider.js +15 -0
  18. package/dist/react/confirm/ConfirmDialog.d.ts +3 -0
  19. package/dist/react/confirm/ConfirmDialog.hooks.d.ts +7 -0
  20. package/dist/react/confirm/ConfirmDialog.hooks.js +101 -0
  21. package/dist/react/confirm/ConfirmDialog.js +8 -0
  22. package/dist/react/confirm/guard.d.ts +7 -0
  23. package/dist/react/confirm/guard.js +54 -0
  24. package/dist/react/context.d.ts +9 -0
  25. package/dist/react/context.js +24 -0
  26. package/dist/react/facade.d.ts +37 -0
  27. package/dist/react/facade.js +36 -0
  28. package/dist/react/index.d.ts +8 -0
  29. package/dist/react/index.js +7 -0
  30. package/dist/react/kit.d.ts +35 -0
  31. package/dist/react/kit.js +44 -0
  32. package/dist/react/native/Controls.d.ts +13 -0
  33. package/dist/react/native/Controls.hooks.d.ts +164 -0
  34. package/dist/react/native/Controls.hooks.js +69 -0
  35. package/dist/react/native/Controls.js +49 -0
  36. package/dist/react/native/Dialog.d.ts +4 -0
  37. package/dist/react/native/Dialog.hooks.d.ts +60 -0
  38. package/dist/react/native/Dialog.hooks.js +56 -0
  39. package/dist/react/native/Dialog.js +13 -0
  40. package/dist/react/native/Navigation.d.ts +5 -0
  41. package/dist/react/native/Navigation.hooks.d.ts +96 -0
  42. package/dist/react/native/Navigation.hooks.js +110 -0
  43. package/dist/react/native/Navigation.js +15 -0
  44. package/dist/react/native/index.d.ts +3 -0
  45. package/dist/react/native/index.js +9 -0
  46. package/dist/react/testing/driver.d.ts +27 -0
  47. package/dist/react/testing/driver.js +102 -0
  48. package/dist/react/testing/index.d.ts +5 -0
  49. package/dist/react/testing/index.js +3 -0
  50. package/dist/react/types.d.ts +210 -0
  51. package/dist/react/types.js +2 -0
  52. package/dist/testing/scenarios.d.ts +31 -0
  53. package/dist/testing/scenarios.js +28 -0
  54. package/dist/toast.d.ts +34 -0
  55. package/dist/toast.js +52 -0
  56. package/docs/decisions/DR-001.md +32 -0
  57. package/docs/spec/api.spec.md +98 -0
  58. package/docs/spec/behavior.spec.md +49 -0
  59. package/docs/spec/errors.spec.md +41 -0
  60. package/docs/spec/state.spec.md +45 -0
  61. package/docs/spec/ui.spec.md +49 -0
  62. package/package.json +97 -3
  63. package/src/react/native/styles.css +4 -0
@@ -0,0 +1,164 @@
1
+ import type { ChangeEvent } from 'react';
2
+ import type { ButtonProps, IconButtonProps, TextFieldProps, TextAreaProps, SelectProps, CheckboxProps, ToastProps, NoticeProps, SpinnerProps, BadgeProps } from '../types.js';
3
+ export declare function useNativeButton(required: ButtonProps, _optional?: Record<string, never>): {
4
+ children: import("react").ReactNode;
5
+ props: {
6
+ id?: string;
7
+ 'data-jini-part': string;
8
+ ref: import("react").Ref<HTMLButtonElement> | undefined;
9
+ className: string;
10
+ type: "button" | "submit" | "reset";
11
+ disabled: boolean;
12
+ 'aria-busy': boolean | undefined;
13
+ 'data-jini-variant': "warning" | "danger" | "primary" | "secondary" | "ghost";
14
+ 'data-jini-state': string;
15
+ onClick: () => void;
16
+ };
17
+ };
18
+ export declare function useNativeIconButton(required: IconButtonProps, _optional?: Record<string, never>): {
19
+ props: {
20
+ id?: string;
21
+ 'data-jini-part': string;
22
+ 'aria-label': string;
23
+ ref: import("react").Ref<HTMLButtonElement> | undefined;
24
+ className: string;
25
+ type: "button" | "submit" | "reset";
26
+ disabled: boolean;
27
+ 'aria-busy': boolean | undefined;
28
+ 'data-jini-variant': "warning" | "danger" | "primary" | "secondary" | "ghost";
29
+ 'data-jini-state': string;
30
+ onClick: () => void;
31
+ };
32
+ children: import("react").ReactNode;
33
+ };
34
+ export declare function useNativeTextField(required: TextFieldProps, _optional?: Record<string, never>): {
35
+ props: {
36
+ id: string;
37
+ ref: import("react").Ref<HTMLInputElement> | undefined;
38
+ type: "number" | "text" | "password" | "email" | "search" | "url" | "tel";
39
+ value: string;
40
+ placeholder: string | undefined;
41
+ onChange: (event: ChangeEvent<HTMLInputElement>) => void;
42
+ className: string;
43
+ disabled: boolean | undefined;
44
+ required: boolean | undefined;
45
+ 'data-jini-part'?: string;
46
+ };
47
+ id: string;
48
+ label: string;
49
+ labelAttrs: {
50
+ 'data-jini-part': string;
51
+ className: string;
52
+ };
53
+ };
54
+ export declare function useNativeTextArea(required: TextAreaProps, _optional?: Record<string, never>): {
55
+ props: {
56
+ className: string;
57
+ id: string;
58
+ ref: import("react").Ref<HTMLTextAreaElement> | undefined;
59
+ value: string;
60
+ rows: number;
61
+ placeholder: string | undefined;
62
+ onChange: (event: ChangeEvent<HTMLTextAreaElement>) => void;
63
+ disabled: boolean | undefined;
64
+ required: boolean | undefined;
65
+ 'data-jini-part'?: string;
66
+ };
67
+ id: string;
68
+ label: string;
69
+ labelAttrs: {
70
+ 'data-jini-part': string;
71
+ className: string;
72
+ };
73
+ };
74
+ export declare function useNativeSelect(required: SelectProps, _optional?: Record<string, never>): {
75
+ options: import("react").DetailedReactHTMLElement<{
76
+ className: string;
77
+ key: string;
78
+ value: string;
79
+ disabled: boolean | undefined;
80
+ }, HTMLElement>[];
81
+ props: {
82
+ className: string;
83
+ id: string;
84
+ ref: import("react").Ref<HTMLSelectElement> | undefined;
85
+ value: string;
86
+ onChange: (event: ChangeEvent<HTMLSelectElement>) => void;
87
+ disabled: boolean | undefined;
88
+ required: boolean | undefined;
89
+ 'data-jini-part'?: string;
90
+ };
91
+ id: string;
92
+ label: string;
93
+ labelAttrs: {
94
+ 'data-jini-part': string;
95
+ className: string;
96
+ };
97
+ };
98
+ export declare function useNativeCheckbox(required: CheckboxProps, optional?: {
99
+ role?: 'switch';
100
+ }): {
101
+ label: string;
102
+ id: string;
103
+ props: {
104
+ id: string;
105
+ ref: import("react").Ref<HTMLInputElement> | undefined;
106
+ className: string;
107
+ type: string;
108
+ role: "switch" | undefined;
109
+ checked: boolean;
110
+ disabled: boolean | undefined;
111
+ onChange: (event: ChangeEvent<HTMLInputElement>) => void;
112
+ 'data-jini-part'?: string;
113
+ };
114
+ };
115
+ export declare function useNativeToast(required: ToastProps, _optional?: Record<string, never>): {
116
+ props: {
117
+ className: string;
118
+ role: string;
119
+ tabIndex: number;
120
+ 'data-jini-variant': "warning" | "danger" | "info" | "success";
121
+ id?: string;
122
+ 'data-jini-part': string;
123
+ };
124
+ message: string;
125
+ dismiss: import("react").DetailedReactHTMLElement<{
126
+ type: string;
127
+ className: string;
128
+ 'data-jini-part': string;
129
+ 'aria-label': string;
130
+ onClick: () => void | undefined;
131
+ }, HTMLElement> | null;
132
+ };
133
+ export declare function useNativeNotice(required: NoticeProps, _optional?: Record<string, never>): {
134
+ children: import("react").ReactNode;
135
+ props: {
136
+ className: string;
137
+ tabIndex: number;
138
+ role: string;
139
+ 'data-jini-variant': "warning" | "danger" | "info" | "success";
140
+ id?: string;
141
+ 'data-jini-part': string;
142
+ };
143
+ };
144
+ export declare function useNativeSpinner(required: SpinnerProps, _optional?: Record<string, never>): {
145
+ props: {
146
+ className: string;
147
+ id?: string;
148
+ 'data-jini-part': string;
149
+ role: string;
150
+ 'aria-label': string;
151
+ tabIndex: number;
152
+ };
153
+ };
154
+ export declare function useNativeBadge(required: BadgeProps, _optional?: Record<string, never>): {
155
+ children: import("react").ReactNode;
156
+ props: {
157
+ className: string;
158
+ tabIndex: number;
159
+ 'data-jini-variant': "warning" | "danger" | "info" | "success";
160
+ id?: string;
161
+ 'data-jini-part': string;
162
+ };
163
+ };
164
+ //# sourceMappingURL=Controls.hooks.d.ts.map
@@ -0,0 +1,69 @@
1
+ import { createElement, useId } from 'react';
2
+ export function useNativeButton(required, _optional = {}) {
3
+ return { children: required.children, props: { 'data-jini-part': 'kit.button', ref: required.ref, className: `jini-btn jini-btn-${required.variant ?? 'primary'} ${required.className ?? ''}`.trim(),
4
+ type: required.type ?? 'button', disabled: !!required.disabled || !!required.pending,
5
+ 'aria-busy': required.pending, 'data-jini-variant': required.variant ?? 'primary',
6
+ 'data-jini-state': required.pending ? 'pending' : required.disabled ? 'disabled' : 'ready',
7
+ onClick: () => { if (!required.disabled && !required.pending)
8
+ required.onPress?.({}); },
9
+ // Opaque metadata wins over generated metadata; behavioral props cannot be supplied through attrs.
10
+ ...required.attrs } };
11
+ }
12
+ export function useNativeIconButton(required, _optional = {}) {
13
+ const vm = useNativeButton(required);
14
+ return { ...vm, props: { ...vm.props, 'data-jini-part': 'kit.icon-button', 'aria-label': required.label, ...required.attrs } };
15
+ }
16
+ function useField(required) {
17
+ const generated = useId();
18
+ return { id: required.attrs?.id ?? generated, label: required.label, labelAttrs: { 'data-jini-part': 'kit.field-label', className: 'jini-field' },
19
+ props: { ...required.attrs, className: `jini-input ${required.className ?? ''}`.trim(), disabled: required.disabled, required: required.required } };
20
+ }
21
+ export function useNativeTextField(required, _optional = {}) {
22
+ const vm = useField(required);
23
+ return { ...vm, props: { ...vm.props, id: vm.id, ref: required.ref, type: required.type ?? 'text', value: required.value,
24
+ placeholder: required.placeholder, onChange: (event) => {
25
+ if (!required.disabled)
26
+ required.onValueChange({ value: event.currentTarget.value });
27
+ } } };
28
+ }
29
+ export function useNativeTextArea(required, _optional = {}) {
30
+ const vm = useField(required);
31
+ return { ...vm, props: { ...vm.props, className: `jini-textarea ${required.className ?? ''}`.trim(), id: vm.id, ref: required.ref, value: required.value, rows: required.rows ?? 3,
32
+ placeholder: required.placeholder, onChange: (event) => {
33
+ if (!required.disabled)
34
+ required.onValueChange({ value: event.currentTarget.value });
35
+ } } };
36
+ }
37
+ export function useNativeSelect(required, _optional = {}) {
38
+ const vm = useField(required);
39
+ // Keep a real HTMLSelectElement: the host agent driver can set an option without library-specific knowledge.
40
+ const options = required.options.map(option => createElement('option', { className: 'jini-select-option', key: option.value, value: option.value, disabled: option.disabled }, option.label));
41
+ return { ...vm, options, props: { ...vm.props, className: `jini-select ${required.className ?? ''}`.trim(), id: vm.id, ref: required.ref, value: required.value,
42
+ onChange: (event) => { if (!required.disabled)
43
+ required.onValueChange({ value: event.currentTarget.value }); } } };
44
+ }
45
+ export function useNativeCheckbox(required, optional = {}) {
46
+ const generated = useId();
47
+ return { label: required.label, id: required.attrs?.id ?? generated,
48
+ props: { ...required.attrs, id: required.attrs?.id ?? generated, ref: required.ref, className: `jini-checkbox ${required.className ?? ''}`.trim(),
49
+ type: 'checkbox', role: optional.role, checked: required.checked, disabled: required.disabled,
50
+ onChange: (event) => { if (!required.disabled)
51
+ required.onCheckedChange({ checked: event.currentTarget.checked }); } } };
52
+ }
53
+ export function useNativeToast(required, _optional = {}) {
54
+ return { props: { 'data-jini-part': 'kit.toast', ...required.attrs, className: `jini-toast ${required.className ?? ''}`.trim(),
55
+ role: required.tone === 'danger' ? 'alert' : 'status', tabIndex: -1, 'data-jini-variant': required.tone ?? 'info' },
56
+ message: required.message, dismiss: required.onDismiss ? createElement('button', { type: 'button', className: 'jini-btn jini-btn-ghost jini-toast-dismiss', 'data-jini-part': 'kit.toast-dismiss',
57
+ 'aria-label': required.dismissLabel ?? 'Dismiss notification', onClick: () => required.onDismiss?.({}) }, '×') : null };
58
+ }
59
+ export function useNativeNotice(required, _optional = {}) {
60
+ return { children: required.children, props: { 'data-jini-part': 'kit.notice', ...required.attrs, className: `jini-notice jini-notice-${required.tone === 'danger' ? 'error' : required.tone ?? 'info'} ${required.className ?? ''}`.trim(), tabIndex: -1,
61
+ role: required.tone === 'danger' ? 'alert' : 'status', 'data-jini-variant': required.tone ?? 'info' } };
62
+ }
63
+ export function useNativeSpinner(required, _optional = {}) {
64
+ return { props: { 'data-jini-part': 'kit.spinner', role: 'status', 'aria-label': required.label ?? 'Loading', tabIndex: -1, ...required.attrs, className: `jini-spinner ${required.className ?? ''}`.trim() } };
65
+ }
66
+ export function useNativeBadge(required, _optional = {}) {
67
+ return { children: required.children, props: { 'data-jini-part': 'kit.badge', ...required.attrs, className: `jini-pill jini-pill-${required.tone ?? 'info'} ${required.className ?? ''}`.trim(), tabIndex: -1, 'data-jini-variant': required.tone ?? 'info' } };
68
+ }
69
+ //# sourceMappingURL=Controls.hooks.js.map
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import { useNativeButton, useNativeIconButton, useNativeTextField, useNativeTextArea, useNativeSelect, useNativeCheckbox, useNativeToast, useNativeNotice, useNativeSpinner, useNativeBadge } from './Controls.hooks.js';
4
+ // React's forwardRef callback is the framework-mandated positional exception; exported factories use two objects.
5
+ export const NativeButton = forwardRef(function NativeButton(props, ref) {
6
+ const vm = useNativeButton({ ...props, ref });
7
+ return _jsx("button", { ...vm.props, children: vm.children });
8
+ });
9
+ export const NativeIconButton = forwardRef(function NativeIconButton(props, ref) {
10
+ const vm = useNativeIconButton({ ...props, ref });
11
+ return _jsx("button", { ...vm.props, children: vm.children });
12
+ });
13
+ export const NativeTextField = forwardRef(function NativeTextField(props, ref) {
14
+ const vm = useNativeTextField({ ...props, ref });
15
+ return _jsxs("label", { htmlFor: vm.id, ...vm.labelAttrs, children: [_jsx("span", { className: "jini-field-label", children: vm.label }), _jsx("input", { ...vm.props })] });
16
+ });
17
+ export const NativeTextArea = forwardRef(function NativeTextArea(props, ref) {
18
+ const vm = useNativeTextArea({ ...props, ref });
19
+ return _jsxs("label", { htmlFor: vm.id, ...vm.labelAttrs, children: [_jsx("span", { className: "jini-field-label", children: vm.label }), _jsx("textarea", { ...vm.props })] });
20
+ });
21
+ export const NativeSelect = forwardRef(function NativeSelect(props, ref) {
22
+ const vm = useNativeSelect({ ...props, ref });
23
+ return _jsxs("label", { htmlFor: vm.id, ...vm.labelAttrs, children: [_jsx("span", { className: "jini-field-label", children: vm.label }), _jsx("select", { ...vm.props, children: vm.options })] });
24
+ });
25
+ export const NativeCheckbox = forwardRef(function NativeCheckbox(props, ref) {
26
+ const vm = useNativeCheckbox({ ...props, ref });
27
+ return _jsxs("label", { className: "jini-checkbox-field", htmlFor: vm.id, children: [_jsx("input", { ...vm.props }), vm.label] });
28
+ });
29
+ export const NativeSwitch = forwardRef(function NativeSwitch(props, ref) {
30
+ const vm = useNativeCheckbox({ ...props, ref }, { role: 'switch' });
31
+ return _jsxs("label", { className: "jini-checkbox-field", htmlFor: vm.id, children: [_jsx("input", { ...vm.props }), vm.label] });
32
+ });
33
+ export function NativeToast(props, _optional = {}) {
34
+ const vm = useNativeToast(props);
35
+ return _jsxs("div", { ...vm.props, children: [vm.message, vm.dismiss] });
36
+ }
37
+ export function NativeNotice(props, _optional = {}) {
38
+ const vm = useNativeNotice(props);
39
+ return _jsx("div", { ...vm.props, children: vm.children });
40
+ }
41
+ export function NativeSpinner(props, _optional = {}) {
42
+ const vm = useNativeSpinner(props);
43
+ return _jsx("span", { ...vm.props, children: "\u25CC" });
44
+ }
45
+ export function NativeBadge(props, _optional = {}) {
46
+ const vm = useNativeBadge(props);
47
+ return _jsx("span", { ...vm.props, children: vm.children });
48
+ }
49
+ //# sourceMappingURL=Controls.js.map
@@ -0,0 +1,4 @@
1
+ import type { DialogProps, ConfirmViewProps } from '../types.js';
2
+ export declare const NativeDialog: import("react").ForwardRefExoticComponent<Omit<DialogProps, "ref"> & import("react").RefAttributes<HTMLDialogElement>>;
3
+ export declare function NativeConfirmDialog(props: ConfirmViewProps, _optional?: Record<string, never>): import("react").JSX.Element;
4
+ //# sourceMappingURL=Dialog.d.ts.map
@@ -0,0 +1,60 @@
1
+ import type { MouseEvent, SyntheticEvent } from 'react';
2
+ import type { DialogProps, ConfirmViewProps } from '../types.js';
3
+ export declare function useNativeDialog(required: DialogProps, optional?: {
4
+ initialFocus?: () => void;
5
+ }): {
6
+ title: string;
7
+ children: import("react").ReactNode;
8
+ titleId: string;
9
+ closeLabel: string;
10
+ closeProps: {
11
+ className: string;
12
+ variant: "secondary";
13
+ attrs: {
14
+ 'data-jini-part': string;
15
+ };
16
+ disabled: boolean;
17
+ onPress: () => void;
18
+ };
19
+ props: {
20
+ className: string;
21
+ ref: import("react").RefObject<HTMLDialogElement | null>;
22
+ tabIndex: number;
23
+ 'aria-busy': boolean | undefined;
24
+ onCancel: (event: SyntheticEvent<HTMLDialogElement>) => void;
25
+ onClick: (event: MouseEvent<HTMLDialogElement>) => void;
26
+ id?: string;
27
+ 'data-jini-part': string;
28
+ 'aria-labelledby': string;
29
+ };
30
+ };
31
+ export declare function useNativeConfirm(required: ConfirmViewProps, _optional?: Record<string, never>): {
32
+ controller: import("../types.js").ConfirmController;
33
+ Button: import("react").ForwardRefExoticComponent<Omit<import("../types.js").ButtonProps, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
34
+ props: {
35
+ className: string;
36
+ role: string;
37
+ id?: string;
38
+ 'data-jini-part': string;
39
+ 'aria-labelledby': string;
40
+ ref: import("react").RefObject<HTMLDialogElement | null>;
41
+ tabIndex: number;
42
+ 'aria-busy': boolean | undefined;
43
+ onCancel: (event: SyntheticEvent<HTMLDialogElement>) => void;
44
+ onClick: (event: MouseEvent<HTMLDialogElement>) => void;
45
+ };
46
+ title: string;
47
+ children: import("react").ReactNode;
48
+ titleId: string;
49
+ closeLabel: string;
50
+ closeProps: {
51
+ className: string;
52
+ variant: "secondary";
53
+ attrs: {
54
+ 'data-jini-part': string;
55
+ };
56
+ disabled: boolean;
57
+ onPress: () => void;
58
+ };
59
+ };
60
+ //# sourceMappingURL=Dialog.hooks.d.ts.map
@@ -0,0 +1,56 @@
1
+ import { useId, useImperativeHandle, useLayoutEffect, useRef } from 'react';
2
+ import { Button } from '../facade.js';
3
+ import { NativeButton } from './Controls.js';
4
+ export function useNativeDialog(required, optional = {}) {
5
+ const dialogRef = useRef(null), opener = useRef(null), titleId = useId();
6
+ useImperativeHandle(required.ref, () => dialogRef.current, []);
7
+ useLayoutEffect(() => {
8
+ const dialog = dialogRef.current;
9
+ if (!dialog)
10
+ return;
11
+ if (!required.open) {
12
+ if (opener.current instanceof HTMLElement && opener.current.isConnected)
13
+ opener.current.focus({ preventScroll: true });
14
+ return;
15
+ }
16
+ opener.current = document.activeElement;
17
+ if (typeof dialog.showModal === 'function') {
18
+ if (!dialog.open)
19
+ dialog.showModal();
20
+ }
21
+ else
22
+ dialog.setAttribute('open', '');
23
+ if (optional.initialFocus)
24
+ optional.initialFocus();
25
+ else
26
+ (dialog.querySelector('[data-jini-autofocus]') ?? dialog.querySelector('[data-jini-part="kit.dialog-close"]') ?? dialog).focus({ preventScroll: true });
27
+ dialog.scrollTop = 0;
28
+ return () => {
29
+ if (typeof dialog.close === 'function') {
30
+ if (dialog.open)
31
+ dialog.close();
32
+ }
33
+ else
34
+ dialog.removeAttribute('open');
35
+ if (opener.current instanceof HTMLElement && opener.current.isConnected)
36
+ opener.current.focus({ preventScroll: true });
37
+ };
38
+ // Open is the transition boundary: editing content must not recapture the original trigger.
39
+ }, [required.open]);
40
+ return { title: required.title, children: required.children, titleId, closeLabel: required.closeLabel ?? 'Close',
41
+ closeProps: { className: 'jini-dialog-close', variant: 'secondary', attrs: { 'data-jini-part': 'kit.dialog-close' }, disabled: !!required.pending, onPress: () => { if (!required.pending)
42
+ required.onClose({}); } },
43
+ props: { 'aria-labelledby': titleId, 'data-jini-part': 'kit.dialog', ...required.attrs, className: `jini-dialog ${required.className ?? ''}`.trim(), ref: dialogRef, tabIndex: -1,
44
+ 'aria-busy': required.pending,
45
+ onCancel: (event) => { event.preventDefault(); if (!required.pending)
46
+ required.onClose({}); },
47
+ onClick: (event) => { if (event.target === event.currentTarget && !required.pending)
48
+ required.onClose({}); } } };
49
+ }
50
+ export function useNativeConfirm(required, _optional = {}) {
51
+ const c = required.controller;
52
+ const vm = useNativeDialog({ open: c.open, title: c.title, pending: c.pending, onClose: () => c.requestDismiss({ reason: 'close' }) }, { initialFocus: () => c.focusInitial({}) });
53
+ return { ...vm, controller: c, Button: c.nativeActions ? NativeButton : Button,
54
+ props: { ...vm.props, ...c.frameAttrs, className: `jini-dialog jini-confirm-dialog ${c.className ?? ''}`.trim(), role: 'alertdialog' } };
55
+ }
56
+ //# sourceMappingURL=Dialog.hooks.js.map
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import { useNativeDialog, useNativeConfirm } from './Dialog.hooks.js';
4
+ import { Button } from '../facade.js';
5
+ export const NativeDialog = forwardRef(function NativeDialog(props, ref) {
6
+ const vm = useNativeDialog({ ...props, ref });
7
+ return _jsxs("dialog", { ...vm.props, children: [_jsx("h2", { className: "jini-dialog-title", id: vm.titleId, "data-jini-part": "kit.dialog-title", children: vm.title }), vm.children, _jsx(Button, { ...vm.closeProps, children: vm.closeLabel })] });
8
+ });
9
+ export function NativeConfirmDialog(props, _optional = {}) {
10
+ const vm = useNativeConfirm(props);
11
+ return _jsxs("dialog", { ...vm.props, children: [_jsx("h2", { className: "jini-confirm-dialog-title", ...vm.controller.titleAttrs, children: vm.controller.title }), _jsxs("div", { className: "jini-confirm-dialog-body", "data-jini-part": "confirm.body", children: [vm.controller.body, _jsx("p", { className: "jini-confirm-dialog-consequence", ...vm.controller.consequenceAttrs, children: vm.controller.consequence }), vm.controller.error] }), _jsxs("div", { className: "jini-confirm-dialog-actions", "data-jini-part": "confirm.actions", children: [_jsx(vm.Button, { ...vm.controller.cancel }), _jsx(vm.Button, { ...vm.controller.confirm })] })] });
12
+ }
13
+ //# sourceMappingURL=Dialog.js.map
@@ -0,0 +1,5 @@
1
+ import type { TabsProps, MenuProps, TooltipProps } from '../types.js';
2
+ export declare function NativeTabs(props: TabsProps, _optional?: Record<string, never>): import("react").JSX.Element;
3
+ export declare function NativeMenu(props: MenuProps, _optional?: Record<string, never>): import("react").JSX.Element;
4
+ export declare function NativeTooltip(props: TooltipProps, _optional?: Record<string, never>): import("react").JSX.Element;
5
+ //# sourceMappingURL=Navigation.d.ts.map
@@ -0,0 +1,96 @@
1
+ import type { KeyboardEvent } from 'react';
2
+ import type { TabsProps, MenuProps, TooltipProps } from '../types.js';
3
+ export declare function useNativeTabs(required: TabsProps, _optional?: Record<string, never>): {
4
+ root: import("react").RefObject<HTMLDivElement | null>;
5
+ className: string;
6
+ label: string;
7
+ tabs: import("react").DetailedReactHTMLElement<{
8
+ 'data-tab-id': string;
9
+ 'aria-controls': string;
10
+ 'aria-selected': boolean;
11
+ disabled: boolean | undefined;
12
+ tabIndex: number;
13
+ onClick: () => void;
14
+ onKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;
15
+ id: string;
16
+ 'data-jini-part': string;
17
+ key: string;
18
+ type: string;
19
+ role: "tab";
20
+ className: string;
21
+ }, HTMLElement>[];
22
+ panels: import("react").DetailedReactHTMLElement<{
23
+ key: string;
24
+ role: "tabpanel";
25
+ tabIndex: number;
26
+ 'data-jini-part': string;
27
+ className: string;
28
+ id: string;
29
+ 'aria-labelledby': string;
30
+ hidden: boolean;
31
+ }, HTMLElement>[];
32
+ };
33
+ export declare function useNativeMenu(required: MenuProps, _optional?: Record<string, never>): {
34
+ label: string;
35
+ items: import("react").DetailedReactHTMLElement<{
36
+ className: string;
37
+ disabled: boolean | undefined;
38
+ onClick: () => void;
39
+ onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
40
+ id?: string;
41
+ 'data-jini-part': string;
42
+ key: string;
43
+ role: "menuitem";
44
+ type: string;
45
+ tabIndex: number;
46
+ }, HTMLElement>[];
47
+ popup: import("react").ReactPortal | null;
48
+ triggerProps: {
49
+ ref: import("react").RefObject<HTMLButtonElement | null>;
50
+ type: "button";
51
+ className: string;
52
+ disabled: boolean | undefined;
53
+ 'aria-label': string;
54
+ 'aria-haspopup': "menu";
55
+ 'aria-expanded': boolean;
56
+ 'aria-controls': string;
57
+ onClick: () => void;
58
+ onKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;
59
+ id?: string;
60
+ 'data-jini-part': string;
61
+ };
62
+ menuProps: {
63
+ ref: import("react").RefObject<HTMLDivElement | null>;
64
+ id: string;
65
+ role: string;
66
+ 'aria-label': string;
67
+ hidden: boolean;
68
+ className: string;
69
+ 'data-jini-part': string;
70
+ };
71
+ };
72
+ export declare function useNativeTooltip(required: TooltipProps, _optional?: Record<string, never>): {
73
+ children: import("react").ReactNode;
74
+ content: import("react").ReactNode;
75
+ triggerProps: {
76
+ className: string;
77
+ tabIndex: number;
78
+ 'aria-label': string | undefined;
79
+ 'aria-describedby': string | undefined;
80
+ onFocus: () => void;
81
+ onBlur: () => void;
82
+ onMouseEnter: () => void;
83
+ onMouseLeave: () => void;
84
+ onKeyDown: (event: KeyboardEvent<HTMLSpanElement>) => void;
85
+ id?: string;
86
+ 'data-jini-part': string;
87
+ };
88
+ tooltipProps: {
89
+ id: string;
90
+ role: string;
91
+ hidden: boolean;
92
+ className: string;
93
+ 'data-jini-part': string;
94
+ };
95
+ };
96
+ //# sourceMappingURL=Navigation.hooks.d.ts.map
@@ -0,0 +1,110 @@
1
+ import { createElement, useId, useRef, useState, useEffect, useLayoutEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ export function useNativeTabs(required, _optional = {}) {
4
+ const prefix = useId(), root = useRef(null);
5
+ const enabled = required.items.filter(item => !item.disabled);
6
+ const active = enabled.find(item => item.id === required.value) ?? enabled[0];
7
+ function onKeyDown(event) {
8
+ const index = enabled.findIndex(item => item.id === event.currentTarget.dataset.tabId);
9
+ const next = event.key === 'Home' ? 0 : event.key === 'End' ? enabled.length - 1 :
10
+ event.key === 'ArrowRight' ? (index + 1) % enabled.length : event.key === 'ArrowLeft' ? (index - 1 + enabled.length) % enabled.length : undefined;
11
+ if (next === undefined || !enabled[next])
12
+ return;
13
+ event.preventDefault();
14
+ required.onValueChange({ value: enabled[next].id });
15
+ root.current?.querySelectorAll('[role="tab"]')[required.items.findIndex(item => item.id === enabled[next].id)]?.focus();
16
+ }
17
+ const tabs = required.items.map((item, index) => createElement('button', {
18
+ key: item.id, type: 'button', role: 'tab', id: `${prefix}-tab-${index}`, 'data-jini-part': 'kit.tab', className: 'jini-tab-bar-item',
19
+ ...item.attrs, ...(item.id === active?.id ? required.attrs : {}),
20
+ 'data-tab-id': item.id, 'aria-controls': `${prefix}-panel-${index}`, 'aria-selected': item.id === active?.id,
21
+ disabled: item.disabled, tabIndex: item.id === active?.id ? 0 : -1,
22
+ onClick: () => { if (!item.disabled)
23
+ required.onValueChange({ value: item.id }); }, onKeyDown,
24
+ }, item.label));
25
+ const panels = required.items.map((item, index) => createElement('div', { key: item.id, role: 'tabpanel', tabIndex: 0,
26
+ 'data-jini-part': 'kit.tab-panel', className: 'jini-tab-panel', id: `${prefix}-panel-${index}`, 'aria-labelledby': `${prefix}-tab-${index}`, hidden: item.id !== active?.id }, item.content));
27
+ return { root, className: `jini-tabs ${required.className ?? ''}`.trim(), label: required.label, tabs, panels };
28
+ }
29
+ export function useNativeMenu(required, _optional = {}) {
30
+ const [open, setOpen] = useState(false), trigger = useRef(null), menu = useRef(null), id = useId();
31
+ const [position, setPosition] = useState({ top: 0, left: 0 });
32
+ useLayoutEffect(() => {
33
+ if (!open || !trigger.current || !menu.current)
34
+ return;
35
+ // Portals escape the preview card's overflow clipping. Follow the anchor on scroll/resize.
36
+ const update = () => {
37
+ const anchor = trigger.current.getBoundingClientRect();
38
+ const box = menu.current.getBoundingClientRect();
39
+ setPosition({ top: anchor.bottom + box.height + 4 > window.innerHeight ? Math.max(4, anchor.top - box.height - 4) : anchor.bottom + 4,
40
+ left: Math.max(4, Math.min(anchor.right - box.width, window.innerWidth - box.width - 4)) });
41
+ };
42
+ update();
43
+ window.addEventListener('resize', update);
44
+ window.addEventListener('scroll', update, true);
45
+ return () => { window.removeEventListener('resize', update); window.removeEventListener('scroll', update, true); };
46
+ }, [open]);
47
+ const enabled = required.items.filter(item => !item.disabled);
48
+ function close() { setOpen(false); trigger.current?.focus(); }
49
+ function focus(index) { menu.current?.querySelectorAll('[role="menuitem"]:not(:disabled)')[index]?.focus(); }
50
+ useEffect(() => {
51
+ if (!open)
52
+ return;
53
+ focus(0);
54
+ const dismiss = (event) => {
55
+ if (event.target instanceof Node && !menu.current?.contains(event.target) && !trigger.current?.contains(event.target))
56
+ setOpen(false);
57
+ };
58
+ document.addEventListener('pointerdown', dismiss);
59
+ return () => document.removeEventListener('pointerdown', dismiss);
60
+ }, [open]);
61
+ // `current` is the item's position among enabled items, the same order focus() walks, so no DOM lookup is needed.
62
+ function onKeyDown(event, current) {
63
+ if (event.key === 'Escape') {
64
+ event.preventDefault();
65
+ close();
66
+ return;
67
+ }
68
+ if (event.key === 'Tab') {
69
+ setOpen(false);
70
+ return;
71
+ }
72
+ const next = event.key === 'Home' ? 0 : event.key === 'End' ? enabled.length - 1 :
73
+ event.key === 'ArrowDown' ? (current + 1) % enabled.length : event.key === 'ArrowUp' ? (current - 1 + enabled.length) % enabled.length : undefined;
74
+ if (next !== undefined) {
75
+ event.preventDefault();
76
+ focus(next);
77
+ }
78
+ }
79
+ const items = required.items.map(item => createElement('button', { key: item.id, role: 'menuitem', type: 'button', tabIndex: -1,
80
+ 'data-jini-part': 'kit.menu-item', ...item.attrs, className: `jini-row-menu-item ${item.attrs?.['data-jini-variant'] === 'danger' ? 'jini-btn-danger' : ''}`.trim(), disabled: item.disabled,
81
+ onClick: () => { if (!item.disabled) {
82
+ close();
83
+ item.onPress({});
84
+ } }, onKeyDown: (event) => onKeyDown(event, enabled.indexOf(item)) }, item.label));
85
+ const menuProps = { ref: menu, id, role: 'menu', 'aria-label': required.label, hidden: !open, className: 'jini-row-menu-popup', 'data-jini-part': 'kit.menu', style: { position: 'fixed', ...position } };
86
+ return { label: '⋮', items, popup: open ? createPortal(createElement('div', menuProps, items), document.body) : null,
87
+ triggerProps: { 'data-jini-part': 'kit.menu-trigger', ...required.attrs, ref: trigger, type: 'button',
88
+ className: `jini-row-menu-trigger ${required.className ?? ''}`.trim(), disabled: required.disabled, 'aria-label': required.label, 'aria-haspopup': 'menu', 'aria-expanded': open, 'aria-controls': id,
89
+ onClick: () => { if (!required.disabled)
90
+ setOpen(!open); }, onKeyDown: (event) => {
91
+ if (!required.disabled && ['ArrowDown', 'ArrowUp'].includes(event.key)) {
92
+ event.preventDefault();
93
+ setOpen(true);
94
+ }
95
+ } },
96
+ menuProps: { ref: menu, id, role: 'menu', 'aria-label': required.label, hidden: !open, className: 'jini-row-menu-popup', 'data-jini-part': 'kit.menu' } };
97
+ }
98
+ export function useNativeTooltip(required, _optional = {}) {
99
+ const [open, setOpen] = useState(false), id = useId();
100
+ return { children: required.children, content: required.content,
101
+ triggerProps: { 'data-jini-part': 'kit.tooltip-trigger', ...required.attrs, className: `jini-tooltip-trigger ${required.className ?? ''}`.trim(), tabIndex: 0,
102
+ 'aria-label': required.label, 'aria-describedby': open ? id : undefined,
103
+ onFocus: () => setOpen(true), onBlur: () => setOpen(false), onMouseEnter: () => setOpen(true), onMouseLeave: () => setOpen(false),
104
+ onKeyDown: (event) => { if (event.key === 'Escape') {
105
+ event.preventDefault();
106
+ setOpen(false);
107
+ } } },
108
+ tooltipProps: { id, role: 'tooltip', hidden: !open, className: 'jini-tooltip', 'data-jini-part': 'kit.tooltip' } };
109
+ }
110
+ //# sourceMappingURL=Navigation.hooks.js.map
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useNativeTabs, useNativeMenu, useNativeTooltip } from './Navigation.hooks.js';
3
+ export function NativeTabs(props, _optional = {}) {
4
+ const vm = useNativeTabs(props);
5
+ return _jsxs("div", { ref: vm.root, className: vm.className, children: [_jsx("div", { className: "jini-tab-bar", role: "tablist", "aria-label": vm.label, "data-jini-part": "kit.tabs", children: vm.tabs }), vm.panels] });
6
+ }
7
+ export function NativeMenu(props, _optional = {}) {
8
+ const vm = useNativeMenu(props);
9
+ return _jsxs("div", { className: "jini-row-menu", children: [_jsx("button", { ...vm.triggerProps, children: _jsxs("svg", { className: "jini-icon", width: "16", height: "16", viewBox: "0 0 18 18", fill: "currentColor", "aria-hidden": "true", children: [_jsx("circle", { className: "jini-icon-circle", cx: "9", cy: "4.5", r: "1.5" }), _jsx("circle", { className: "jini-icon-circle", cx: "9", cy: "9", r: "1.5" }), _jsx("circle", { className: "jini-icon-circle", cx: "9", cy: "13.5", r: "1.5" })] }) }), vm.popup] });
10
+ }
11
+ export function NativeTooltip(props, _optional = {}) {
12
+ const vm = useNativeTooltip(props);
13
+ return _jsxs("span", { className: "jini-tooltip-container", children: [_jsx("span", { ...vm.triggerProps, children: vm.children }), _jsx("span", { ...vm.tooltipProps, children: vm.content })] });
14
+ }
15
+ //# sourceMappingURL=Navigation.js.map