@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.
- package/CHANGELOG.md +9 -0
- package/LICENSE +201 -0
- package/README.md +150 -2
- package/dist/attrs.d.ts +18 -0
- package/dist/attrs.js +2 -0
- package/dist/confirm.d.ts +42 -0
- package/dist/confirm.js +41 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -0
- package/dist/kit.spec.d.ts +491 -0
- package/dist/kit.spec.js +40 -0
- package/dist/needs.d.ts +24 -0
- package/dist/needs.js +29 -0
- package/dist/react/Kit.hooks.d.ts +39 -0
- package/dist/react/Kit.hooks.js +87 -0
- package/dist/react/KitProvider.d.ts +13 -0
- package/dist/react/KitProvider.js +15 -0
- package/dist/react/confirm/ConfirmDialog.d.ts +3 -0
- package/dist/react/confirm/ConfirmDialog.hooks.d.ts +7 -0
- package/dist/react/confirm/ConfirmDialog.hooks.js +101 -0
- package/dist/react/confirm/ConfirmDialog.js +8 -0
- package/dist/react/confirm/guard.d.ts +7 -0
- package/dist/react/confirm/guard.js +54 -0
- package/dist/react/context.d.ts +9 -0
- package/dist/react/context.js +24 -0
- package/dist/react/facade.d.ts +37 -0
- package/dist/react/facade.js +36 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.js +7 -0
- package/dist/react/kit.d.ts +35 -0
- package/dist/react/kit.js +44 -0
- package/dist/react/native/Controls.d.ts +13 -0
- package/dist/react/native/Controls.hooks.d.ts +164 -0
- package/dist/react/native/Controls.hooks.js +69 -0
- package/dist/react/native/Controls.js +49 -0
- package/dist/react/native/Dialog.d.ts +4 -0
- package/dist/react/native/Dialog.hooks.d.ts +60 -0
- package/dist/react/native/Dialog.hooks.js +56 -0
- package/dist/react/native/Dialog.js +13 -0
- package/dist/react/native/Navigation.d.ts +5 -0
- package/dist/react/native/Navigation.hooks.d.ts +96 -0
- package/dist/react/native/Navigation.hooks.js +110 -0
- package/dist/react/native/Navigation.js +15 -0
- package/dist/react/native/index.d.ts +3 -0
- package/dist/react/native/index.js +9 -0
- package/dist/react/testing/driver.d.ts +27 -0
- package/dist/react/testing/driver.js +102 -0
- package/dist/react/testing/index.d.ts +5 -0
- package/dist/react/testing/index.js +3 -0
- package/dist/react/types.d.ts +210 -0
- package/dist/react/types.js +2 -0
- package/dist/testing/scenarios.d.ts +31 -0
- package/dist/testing/scenarios.js +28 -0
- package/dist/toast.d.ts +34 -0
- package/dist/toast.js +52 -0
- package/docs/decisions/DR-001.md +32 -0
- package/docs/spec/api.spec.md +98 -0
- package/docs/spec/behavior.spec.md +49 -0
- package/docs/spec/errors.spec.md +41 -0
- package/docs/spec/state.spec.md +45 -0
- package/docs/spec/ui.spec.md +49 -0
- package/package.json +97 -3
- package/src/react/native/styles.css +4 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NativeButton, NativeIconButton, NativeTextField, NativeTextArea, NativeSelect, NativeCheckbox, NativeSwitch, NativeToast, NativeNotice, NativeSpinner, NativeBadge } from './Controls.js';
|
|
2
|
+
import { NativeDialog, NativeConfirmDialog } from './Dialog.js';
|
|
3
|
+
import { NativeTabs, NativeMenu, NativeTooltip } from './Navigation.js';
|
|
4
|
+
export const nativeKit = Object.freeze({ Button: NativeButton, IconButton: NativeIconButton,
|
|
5
|
+
TextField: NativeTextField, TextArea: NativeTextArea, Select: NativeSelect, Checkbox: NativeCheckbox,
|
|
6
|
+
Switch: NativeSwitch, Dialog: NativeDialog, ConfirmDialog: NativeConfirmDialog, Tabs: NativeTabs,
|
|
7
|
+
Menu: NativeMenu, Toast: NativeToast, Tooltip: NativeTooltip, Notice: NativeNotice, Spinner: NativeSpinner, Badge: NativeBadge });
|
|
8
|
+
// This is a concrete kit, rather than a forwarding subpath to another package's implementation.
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AgentAttrsPort } from '../../attrs.js';
|
|
2
|
+
import type { KitScenario } from '../../testing/scenarios.js';
|
|
3
|
+
import type { ImplementedComponentName } from '../../kit.spec.js';
|
|
4
|
+
import type { ResolvedKit } from '../types.js';
|
|
5
|
+
export interface KitConformanceResult {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly status: 'passed' | 'failed' | 'skipped';
|
|
8
|
+
readonly errors: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
export interface KitConformanceDriver {
|
|
11
|
+
run(required: {
|
|
12
|
+
kit: ResolvedKit;
|
|
13
|
+
scenario: KitScenario;
|
|
14
|
+
agent: AgentAttrsPort;
|
|
15
|
+
}, optional?: Record<string, never>): Promise<readonly string[]>;
|
|
16
|
+
}
|
|
17
|
+
export declare const reactConformanceDriver: KitConformanceDriver;
|
|
18
|
+
export declare function runKitConformance(required: {
|
|
19
|
+
kit: ResolvedKit;
|
|
20
|
+
}, optional?: {
|
|
21
|
+
driver?: KitConformanceDriver;
|
|
22
|
+
agent?: AgentAttrsPort;
|
|
23
|
+
only?: readonly ImplementedComponentName[];
|
|
24
|
+
scenarios?: readonly KitScenario[];
|
|
25
|
+
includeBrowser?: boolean;
|
|
26
|
+
}): Promise<readonly KitConformanceResult[]>;
|
|
27
|
+
//# sourceMappingURL=driver.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { act, createElement } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { KitProvider } from '../KitProvider.js';
|
|
4
|
+
import * as facades from '../facade.js';
|
|
5
|
+
import { ConfirmDialog } from '../confirm/ConfirmDialog.js';
|
|
6
|
+
import { describeKit } from '../kit.js';
|
|
7
|
+
import { kitConformanceScenarios } from '../../testing/scenarios.js';
|
|
8
|
+
const testingAgent = ({ handle }, options = {}) => ({ 'data-agent-element': handle, 'data-agent-role': options.role, 'data-agent-label': options.label });
|
|
9
|
+
function fixture(scenario, calls) {
|
|
10
|
+
const count = (name) => () => { calls[name] = (calls[name] ?? 0) + 1; };
|
|
11
|
+
const common = { children: 'Example', label: 'Example', value: 'a', checked: true, message: 'Saved', content: 'Help',
|
|
12
|
+
open: true, title: 'Delete record?', confirmLabel: 'Delete', body: 'Example record',
|
|
13
|
+
options: [{ value: 'a', label: 'Alpha' }, { value: 'b', label: 'Beta' }],
|
|
14
|
+
items: scenario.component === 'Menu' ? [{ id: 'a', label: 'Alpha', onPress: count('item') }] : [{ id: 'a', label: 'Alpha', content: 'Alpha panel' }],
|
|
15
|
+
onPress: count('press'), onValueChange: count('change'), onCheckedChange: count('change'), onClose: count('cancel'),
|
|
16
|
+
onConfirm: count('confirm'), onCancel: count('cancel'), ...scenario.props };
|
|
17
|
+
const Component = scenario.component === 'ConfirmDialog' ? ConfirmDialog : facades[scenario.component];
|
|
18
|
+
// The scenario union intentionally carries framework-neutral props; the driver performs the binding.
|
|
19
|
+
return createElement(Component, common);
|
|
20
|
+
}
|
|
21
|
+
function check(expectation, root, calls) {
|
|
22
|
+
if (expectation.type === 'calls')
|
|
23
|
+
return (calls[expectation.name] ?? 0) === expectation.count ? undefined : `${expectation.name} calls: expected ${expectation.count}, received ${calls[expectation.name] ?? 0}`;
|
|
24
|
+
const element = root.querySelector(expectation.selector);
|
|
25
|
+
if (expectation.type === 'absent')
|
|
26
|
+
return element ? `unexpected ${expectation.selector}` : undefined;
|
|
27
|
+
if (!element)
|
|
28
|
+
return `missing ${expectation.selector}`;
|
|
29
|
+
if (expectation.type === 'attribute')
|
|
30
|
+
return element.getAttribute(expectation.name) === expectation.value ? undefined : `${expectation.selector} did not forward ${expectation.name}=${expectation.value}`;
|
|
31
|
+
if (expectation.type === 'focused')
|
|
32
|
+
return element === document.activeElement ? undefined : `${expectation.selector} not focused`;
|
|
33
|
+
if (expectation.type === 'disabled')
|
|
34
|
+
return element.matches(':disabled') || element.getAttribute('aria-disabled') === 'true' ? undefined : `${expectation.selector} not disabled`;
|
|
35
|
+
if (expectation.type === 'native-select')
|
|
36
|
+
return element instanceof HTMLSelectElement ? undefined : 'Select must render HTMLSelectElement';
|
|
37
|
+
if (expectation.type === 'focusable')
|
|
38
|
+
return element.matches('button,input,select,textarea,[tabindex]') ? undefined : `${expectation.selector} is not focusable`;
|
|
39
|
+
}
|
|
40
|
+
export const reactConformanceDriver = {
|
|
41
|
+
async run({ kit, scenario, agent }) {
|
|
42
|
+
if (typeof document === 'undefined')
|
|
43
|
+
throw new Error('React conformance requires a DOM environment');
|
|
44
|
+
const host = document.createElement('div'), overlay = document.createElement('div'), calls = {};
|
|
45
|
+
document.body.append(host, overlay);
|
|
46
|
+
const root = createRoot(host), errors = [];
|
|
47
|
+
const before = describeKit({ kit }).violations.length;
|
|
48
|
+
try {
|
|
49
|
+
// Never let the dev guard make a broken adapter pass conformance by silently substituting native.
|
|
50
|
+
await act(async () => { root.render(createElement(KitProvider, { kit, agent, overlayContainer: overlay, guard: 'off', children: fixture(scenario, calls) })); });
|
|
51
|
+
for (const action of scenario.actions) {
|
|
52
|
+
const element = (host.querySelector(action.selector) ?? overlay.querySelector(action.selector));
|
|
53
|
+
if (!element) {
|
|
54
|
+
errors.push(`action target missing: ${action.selector}`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
await act(async () => {
|
|
58
|
+
if (action.type === 'click')
|
|
59
|
+
element.click();
|
|
60
|
+
else if (action.type === 'focus')
|
|
61
|
+
element.focus();
|
|
62
|
+
else if (action.type === 'cancel')
|
|
63
|
+
element.dispatchEvent(new Event('cancel', { bubbles: false, cancelable: true }));
|
|
64
|
+
else
|
|
65
|
+
element.dispatchEvent(new KeyboardEvent('keydown', { key: action.key ?? '', bubbles: true, cancelable: true }));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
for (const expectation of scenario.expectations) {
|
|
69
|
+
const scope = expectation.type === 'calls' || host.querySelector(expectation.selector) ? host : overlay;
|
|
70
|
+
const error = check(expectation, scope, calls);
|
|
71
|
+
if (error)
|
|
72
|
+
errors.push(error);
|
|
73
|
+
}
|
|
74
|
+
if (describeKit({ kit }).violations.length !== before)
|
|
75
|
+
errors.push('conformance must not trigger fallback');
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
await act(async () => root.unmount());
|
|
79
|
+
host.remove();
|
|
80
|
+
overlay.remove();
|
|
81
|
+
}
|
|
82
|
+
return errors;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
export async function runKitConformance(required, optional = {}) {
|
|
86
|
+
const driver = optional.driver ?? reactConformanceDriver;
|
|
87
|
+
if (optional.includeBrowser && driver === reactConformanceDriver)
|
|
88
|
+
throw new Error('Browser cases require a real browser driver');
|
|
89
|
+
const results = [];
|
|
90
|
+
for (const scenario of optional.scenarios ?? kitConformanceScenarios) {
|
|
91
|
+
if (optional.only && !optional.only.includes(scenario.component))
|
|
92
|
+
continue;
|
|
93
|
+
if (scenario.browserOnly && !optional.includeBrowser) {
|
|
94
|
+
results.push({ id: scenario.id, status: 'skipped', errors: ['requires real browser driver'] });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const errors = await driver.run({ kit: required.kit, scenario, agent: optional.agent ?? testingAgent });
|
|
98
|
+
results.push({ id: scenario.id, status: errors.length ? 'failed' : 'passed', errors });
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=driver.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { kitConformanceScenarios } from '../../testing/scenarios.js';
|
|
2
|
+
export type { KitScenario, ConformanceAction, ConformanceExpectation } from '../../testing/scenarios.js';
|
|
3
|
+
export { runKitConformance, reactConformanceDriver } from './driver.js';
|
|
4
|
+
export type { KitConformanceResult, KitConformanceDriver } from './driver.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode, Ref, RefObject, HTMLAttributes } from 'react';
|
|
2
|
+
import type { KitAttrs, AgentAttrsPort } from '../attrs.js';
|
|
3
|
+
import type { ConfirmInput, DismissReason } from '../confirm.js';
|
|
4
|
+
import type { ImplementedComponentName } from '../kit.spec.js';
|
|
5
|
+
import type { KitNeeds } from '../needs.js';
|
|
6
|
+
import type { ToastService } from '../toast.js';
|
|
7
|
+
export interface BaseProps {
|
|
8
|
+
readonly attrs?: KitAttrs;
|
|
9
|
+
readonly className?: string;
|
|
10
|
+
}
|
|
11
|
+
export type ActionPort = (required: Record<string, never>, optional?: Record<string, never>) => void;
|
|
12
|
+
export type ValuePort<T> = (required: {
|
|
13
|
+
value: T;
|
|
14
|
+
}, optional?: Record<string, never>) => void;
|
|
15
|
+
export interface ButtonProps extends BaseProps {
|
|
16
|
+
readonly children?: ReactNode;
|
|
17
|
+
readonly onPress?: ActionPort;
|
|
18
|
+
readonly disabled?: boolean;
|
|
19
|
+
readonly pending?: boolean;
|
|
20
|
+
readonly variant?: 'primary' | 'secondary' | 'danger' | 'warning' | 'ghost';
|
|
21
|
+
readonly type?: 'button' | 'submit' | 'reset';
|
|
22
|
+
readonly ref?: Ref<HTMLButtonElement>;
|
|
23
|
+
}
|
|
24
|
+
export interface IconButtonProps extends ButtonProps {
|
|
25
|
+
readonly label: string;
|
|
26
|
+
}
|
|
27
|
+
export interface TextFieldProps extends BaseProps {
|
|
28
|
+
readonly label: string;
|
|
29
|
+
readonly value: string;
|
|
30
|
+
readonly onValueChange: ValuePort<string>;
|
|
31
|
+
readonly disabled?: boolean;
|
|
32
|
+
readonly required?: boolean;
|
|
33
|
+
readonly placeholder?: string;
|
|
34
|
+
readonly type?: 'text' | 'password' | 'email' | 'search' | 'url' | 'tel' | 'number';
|
|
35
|
+
readonly ref?: Ref<HTMLInputElement>;
|
|
36
|
+
}
|
|
37
|
+
export interface TextAreaProps extends Omit<TextFieldProps, 'type' | 'ref'> {
|
|
38
|
+
readonly rows?: number;
|
|
39
|
+
readonly ref?: Ref<HTMLTextAreaElement>;
|
|
40
|
+
}
|
|
41
|
+
export interface SelectOption {
|
|
42
|
+
readonly value: string;
|
|
43
|
+
readonly label: string;
|
|
44
|
+
readonly disabled?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface SelectProps extends Omit<TextFieldProps, 'type' | 'placeholder' | 'ref'> {
|
|
47
|
+
readonly options: readonly SelectOption[];
|
|
48
|
+
readonly ref?: Ref<HTMLSelectElement>;
|
|
49
|
+
}
|
|
50
|
+
export interface CheckboxProps extends BaseProps {
|
|
51
|
+
readonly label: string;
|
|
52
|
+
readonly checked: boolean;
|
|
53
|
+
readonly onCheckedChange: (required: {
|
|
54
|
+
checked: boolean;
|
|
55
|
+
}, optional?: Record<string, never>) => void;
|
|
56
|
+
readonly disabled?: boolean;
|
|
57
|
+
readonly ref?: Ref<HTMLInputElement>;
|
|
58
|
+
}
|
|
59
|
+
export interface SwitchProps extends CheckboxProps {
|
|
60
|
+
}
|
|
61
|
+
export interface DialogProps extends BaseProps {
|
|
62
|
+
readonly open: boolean;
|
|
63
|
+
readonly title: string;
|
|
64
|
+
readonly children?: ReactNode;
|
|
65
|
+
readonly onClose: ActionPort;
|
|
66
|
+
readonly pending?: boolean;
|
|
67
|
+
readonly closeLabel?: string;
|
|
68
|
+
readonly ref?: Ref<HTMLDialogElement>;
|
|
69
|
+
}
|
|
70
|
+
export interface ConfirmDialogProps extends ConfirmInput, BaseProps {
|
|
71
|
+
readonly body?: ReactNode;
|
|
72
|
+
readonly onConfirm: (required: Record<string, never>, optional?: Record<string, never>) => void | Promise<void>;
|
|
73
|
+
readonly onCancel: ActionPort;
|
|
74
|
+
readonly errorLabel?: string;
|
|
75
|
+
/** Concise names opt in to describing the consequence separately, preserving agent labels. */
|
|
76
|
+
readonly actionAccessibleNames?: {
|
|
77
|
+
readonly cancel: string;
|
|
78
|
+
readonly confirm: string;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export interface ConfirmController {
|
|
82
|
+
readonly className?: string | undefined;
|
|
83
|
+
readonly nativeActions?: boolean;
|
|
84
|
+
readonly open: boolean;
|
|
85
|
+
readonly pending: boolean;
|
|
86
|
+
readonly title: string;
|
|
87
|
+
readonly body: ReactNode;
|
|
88
|
+
readonly consequence: string | undefined;
|
|
89
|
+
readonly error: ReactNode;
|
|
90
|
+
readonly titleAttrs: HTMLAttributes<HTMLHeadingElement>;
|
|
91
|
+
readonly consequenceAttrs?: HTMLAttributes<HTMLParagraphElement>;
|
|
92
|
+
readonly frameAttrs: KitAttrs & {
|
|
93
|
+
readonly role: 'alertdialog';
|
|
94
|
+
readonly 'aria-labelledby': string;
|
|
95
|
+
};
|
|
96
|
+
readonly cancel: ButtonProps;
|
|
97
|
+
readonly confirm: ButtonProps;
|
|
98
|
+
readonly boundaryRef: RefObject<HTMLSpanElement | null>;
|
|
99
|
+
readonly overlayContainer: HTMLElement | null;
|
|
100
|
+
readonly agentMayConfirm: boolean;
|
|
101
|
+
requestDismiss(required: {
|
|
102
|
+
reason: DismissReason;
|
|
103
|
+
}, optional?: Record<string, never>): boolean;
|
|
104
|
+
focusInitial(required: Record<string, never>, optional?: Record<string, never>): void;
|
|
105
|
+
}
|
|
106
|
+
/** Overrides receive a view model only. Raw destructive callbacks are never provided. */
|
|
107
|
+
export interface ConfirmViewProps {
|
|
108
|
+
readonly controller: ConfirmController;
|
|
109
|
+
}
|
|
110
|
+
export interface TabItem {
|
|
111
|
+
readonly id: string;
|
|
112
|
+
readonly label: string;
|
|
113
|
+
readonly content: ReactNode;
|
|
114
|
+
readonly disabled?: boolean;
|
|
115
|
+
readonly attrs?: KitAttrs;
|
|
116
|
+
}
|
|
117
|
+
export interface TabsProps extends BaseProps {
|
|
118
|
+
readonly label: string;
|
|
119
|
+
readonly value: string;
|
|
120
|
+
readonly items: readonly TabItem[];
|
|
121
|
+
readonly onValueChange: ValuePort<string>;
|
|
122
|
+
}
|
|
123
|
+
export interface MenuItem {
|
|
124
|
+
readonly id: string;
|
|
125
|
+
readonly label: string;
|
|
126
|
+
readonly disabled?: boolean;
|
|
127
|
+
readonly onPress: ActionPort;
|
|
128
|
+
readonly attrs?: KitAttrs;
|
|
129
|
+
}
|
|
130
|
+
export interface MenuProps extends BaseProps {
|
|
131
|
+
readonly label: string;
|
|
132
|
+
readonly items: readonly MenuItem[];
|
|
133
|
+
readonly disabled?: boolean;
|
|
134
|
+
}
|
|
135
|
+
export interface ToastProps extends BaseProps {
|
|
136
|
+
readonly message: string;
|
|
137
|
+
readonly tone?: 'info' | 'success' | 'warning' | 'danger';
|
|
138
|
+
readonly onDismiss?: ActionPort;
|
|
139
|
+
readonly dismissLabel?: string;
|
|
140
|
+
}
|
|
141
|
+
export interface TooltipProps extends BaseProps {
|
|
142
|
+
readonly content: ReactNode;
|
|
143
|
+
readonly children: ReactNode;
|
|
144
|
+
readonly label?: string;
|
|
145
|
+
}
|
|
146
|
+
export interface NoticeProps extends BaseProps {
|
|
147
|
+
readonly children: ReactNode;
|
|
148
|
+
readonly tone?: 'info' | 'success' | 'warning' | 'danger';
|
|
149
|
+
}
|
|
150
|
+
export interface SpinnerProps extends BaseProps {
|
|
151
|
+
readonly label?: string;
|
|
152
|
+
}
|
|
153
|
+
export interface BadgeProps extends BaseProps {
|
|
154
|
+
readonly children: ReactNode;
|
|
155
|
+
readonly tone?: 'info' | 'success' | 'warning' | 'danger';
|
|
156
|
+
}
|
|
157
|
+
export interface ReactKitProps {
|
|
158
|
+
Button: ButtonProps;
|
|
159
|
+
IconButton: IconButtonProps;
|
|
160
|
+
TextField: TextFieldProps;
|
|
161
|
+
TextArea: TextAreaProps;
|
|
162
|
+
Select: SelectProps;
|
|
163
|
+
Checkbox: CheckboxProps;
|
|
164
|
+
Switch: SwitchProps;
|
|
165
|
+
Dialog: DialogProps;
|
|
166
|
+
ConfirmDialog: ConfirmViewProps;
|
|
167
|
+
Tabs: TabsProps;
|
|
168
|
+
Menu: MenuProps;
|
|
169
|
+
Toast: ToastProps;
|
|
170
|
+
Tooltip: TooltipProps;
|
|
171
|
+
Notice: NoticeProps;
|
|
172
|
+
Spinner: SpinnerProps;
|
|
173
|
+
Badge: BadgeProps;
|
|
174
|
+
}
|
|
175
|
+
export type ReactKit = {
|
|
176
|
+
readonly [K in ImplementedComponentName]: ComponentType<ReactKitProps[K]>;
|
|
177
|
+
};
|
|
178
|
+
export type GuardMode = 'fallback' | 'warn' | 'off';
|
|
179
|
+
export interface KitViolation {
|
|
180
|
+
readonly component: ImplementedComponentName;
|
|
181
|
+
readonly reasons: readonly string[];
|
|
182
|
+
}
|
|
183
|
+
export interface ResolvedKit {
|
|
184
|
+
readonly id: string;
|
|
185
|
+
readonly contract: string;
|
|
186
|
+
readonly components: ReactKit;
|
|
187
|
+
readonly overrides: readonly ImplementedComponentName[];
|
|
188
|
+
readonly violations: KitViolation[];
|
|
189
|
+
readonly failed: Set<ImplementedComponentName>;
|
|
190
|
+
readonly guard: GuardMode;
|
|
191
|
+
}
|
|
192
|
+
export interface KitProviderProps {
|
|
193
|
+
readonly kit?: ResolvedKit;
|
|
194
|
+
readonly children: ReactNode;
|
|
195
|
+
readonly agent?: AgentAttrsPort;
|
|
196
|
+
readonly overlayContainer?: HTMLElement;
|
|
197
|
+
readonly needs?: readonly KitNeeds[];
|
|
198
|
+
readonly toast?: ToastService;
|
|
199
|
+
readonly cancelLabel?: string;
|
|
200
|
+
readonly guard?: GuardMode;
|
|
201
|
+
}
|
|
202
|
+
export interface KitContextValue {
|
|
203
|
+
readonly kit: ResolvedKit;
|
|
204
|
+
readonly agent: AgentAttrsPort | undefined;
|
|
205
|
+
readonly overlayContainer: HTMLElement | null;
|
|
206
|
+
readonly toast: ToastService | undefined;
|
|
207
|
+
readonly cancelLabel: string;
|
|
208
|
+
readonly guard: GuardMode;
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ImplementedComponentName } from '../kit.spec.js';
|
|
2
|
+
export type ConformanceAction = {
|
|
3
|
+
readonly type: 'click' | 'cancel' | 'focus' | 'key';
|
|
4
|
+
readonly selector: string;
|
|
5
|
+
readonly key?: string;
|
|
6
|
+
};
|
|
7
|
+
export type ConformanceExpectation = {
|
|
8
|
+
readonly type: 'attribute';
|
|
9
|
+
readonly selector: string;
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly value: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly type: 'focused' | 'disabled' | 'absent' | 'native-select' | 'focusable';
|
|
14
|
+
readonly selector: string;
|
|
15
|
+
} | {
|
|
16
|
+
readonly type: 'calls';
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly count: number;
|
|
19
|
+
};
|
|
20
|
+
export interface KitScenario {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly component: ImplementedComponentName;
|
|
23
|
+
readonly safety?: boolean;
|
|
24
|
+
readonly browserOnly?: boolean;
|
|
25
|
+
readonly props: Readonly<Record<string, unknown>>;
|
|
26
|
+
readonly actions: readonly ConformanceAction[];
|
|
27
|
+
readonly expectations: readonly ConformanceExpectation[];
|
|
28
|
+
}
|
|
29
|
+
/** Serializable cases can drive another framework or a real-browser fixture without importing React. */
|
|
30
|
+
export declare const kitConformanceScenarios: readonly KitScenario[];
|
|
31
|
+
//# sourceMappingURL=scenarios.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Serializable cases can drive another framework or a real-browser fixture without importing React. */
|
|
2
|
+
export const kitConformanceScenarios = Object.freeze([
|
|
3
|
+
...['Button', 'IconButton', 'TextField', 'TextArea', 'Select', 'Checkbox', 'Switch', 'Dialog', 'Tabs', 'Menu', 'Toast', 'Tooltip', 'Notice', 'Spinner', 'Badge'].map(component => ({
|
|
4
|
+
id: `${component}:opaque-attrs`, component, props: { attrs: { 'data-agent-element': 'probe', 'data-jini-part': 'x.host.part', 'aria-description': 'opaque' } }, actions: [],
|
|
5
|
+
expectations: [{ type: 'focusable', selector: '[data-agent-element="probe"]' },
|
|
6
|
+
{ type: 'attribute', selector: '[data-agent-element="probe"]', name: 'data-jini-part', value: 'x.host.part' },
|
|
7
|
+
{ type: 'attribute', selector: '[data-agent-element="probe"]', name: 'aria-description', value: 'opaque' }],
|
|
8
|
+
})),
|
|
9
|
+
{ id: 'Select:native-semantics', component: 'Select', props: {}, actions: [], expectations: [{ type: 'native-select', selector: '[data-jini-part="kit.select"]' }] },
|
|
10
|
+
{ id: 'Button:disabled-blocks-press', component: 'Button', props: { disabled: true }, actions: [{ type: 'click', selector: 'button' }], expectations: [{ type: 'calls', name: 'press', count: 0 }] },
|
|
11
|
+
{ id: 'ConfirmDialog:cancel-first-focus', component: 'ConfirmDialog', safety: true, props: {}, actions: [], expectations: [{ type: 'focused', selector: '[data-jini-part="confirm.cancel"]' }] },
|
|
12
|
+
{ id: 'ConfirmDialog:pending-blocks-every-dismissal', component: 'ConfirmDialog', safety: true, props: { pending: true },
|
|
13
|
+
actions: [{ type: 'cancel', selector: '[data-jini-part="confirm.dialog"]' }, { type: 'click', selector: '[data-jini-part="confirm.dialog"]' },
|
|
14
|
+
{ type: 'click', selector: '[data-jini-part="confirm.cancel"]' }, { type: 'click', selector: '[data-jini-part="confirm.confirm"]' }],
|
|
15
|
+
expectations: [{ type: 'calls', name: 'cancel', count: 0 }, { type: 'calls', name: 'confirm', count: 0 },
|
|
16
|
+
{ type: 'disabled', selector: '[data-jini-part="confirm.cancel"]' }, { type: 'disabled', selector: '[data-jini-part="confirm.confirm"]' }] },
|
|
17
|
+
{ id: 'ConfirmDialog:agent-confirm-withheld', component: 'ConfirmDialog', safety: true, props: { agentHandle: 'erase', agentMayConfirm: false }, actions: [],
|
|
18
|
+
expectations: [{ type: 'absent', selector: '[data-agent-element="erase-confirm"]' },
|
|
19
|
+
{ type: 'attribute', selector: '[data-jini-part="confirm.cancel"]', name: 'data-agent-element', value: 'erase-cancel' }] },
|
|
20
|
+
{ id: 'ConfirmDialog:consequence-label', component: 'ConfirmDialog', safety: true, props: { agentHandle: 'erase', tone: 'danger' }, actions: [],
|
|
21
|
+
expectations: [{ type: 'attribute', selector: '[data-jini-part="confirm.confirm"]', name: 'data-agent-label', value: 'Delete — Delete record?; cannot be undone' },
|
|
22
|
+
{ type: 'attribute', selector: '[data-jini-part="confirm.confirm"]', name: 'aria-label', value: 'Delete — Delete record?; cannot be undone' }] },
|
|
23
|
+
{ id: 'ConfirmDialog:browser-escape', component: 'ConfirmDialog', safety: true, browserOnly: true, props: {},
|
|
24
|
+
actions: [{ type: 'key', selector: '[data-jini-part="confirm.cancel"]', key: 'Escape' }], expectations: [{ type: 'calls', name: 'cancel', count: 1 }] },
|
|
25
|
+
{ id: 'ConfirmDialog:browser-modal-focus', component: 'ConfirmDialog', safety: true, browserOnly: true, props: {}, actions: [],
|
|
26
|
+
expectations: [{ type: 'focused', selector: '[data-jini-part="confirm.cancel"]' }] },
|
|
27
|
+
]);
|
|
28
|
+
//# sourceMappingURL=scenarios.js.map
|
package/dist/toast.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface ToastMessage {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly message: string;
|
|
4
|
+
readonly tone?: 'info' | 'success' | 'warning' | 'danger';
|
|
5
|
+
readonly durationMs?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ToastScheduler {
|
|
8
|
+
schedule(required: {
|
|
9
|
+
delayMs: number;
|
|
10
|
+
run: (required: Record<string, never>, optional?: Record<string, never>) => void;
|
|
11
|
+
}, optional?: Record<string, never>): unknown;
|
|
12
|
+
cancel(required: {
|
|
13
|
+
ticket: unknown;
|
|
14
|
+
}, optional?: Record<string, never>): void;
|
|
15
|
+
}
|
|
16
|
+
export interface ToastService {
|
|
17
|
+
push(required: Omit<ToastMessage, 'id'>, optional?: {
|
|
18
|
+
id?: string;
|
|
19
|
+
}): string;
|
|
20
|
+
dismiss(required: {
|
|
21
|
+
id: string;
|
|
22
|
+
}, optional?: Record<string, never>): void;
|
|
23
|
+
snapshot(required: Record<string, never>, optional?: Record<string, never>): readonly ToastMessage[];
|
|
24
|
+
subscribe(required: {
|
|
25
|
+
listener: (required: Record<string, never>, optional?: Record<string, never>) => void;
|
|
26
|
+
}, optional?: Record<string, never>): () => void;
|
|
27
|
+
dispose(required: Record<string, never>, optional?: Record<string, never>): void;
|
|
28
|
+
}
|
|
29
|
+
/** Instance-local queues avoid mixing messages between hosts and SSR requests. */
|
|
30
|
+
export declare function createToastService(_required: Record<string, never>, optional?: {
|
|
31
|
+
scheduler?: ToastScheduler;
|
|
32
|
+
durationMs?: number;
|
|
33
|
+
}): ToastService;
|
|
34
|
+
//# sourceMappingURL=toast.d.ts.map
|
package/dist/toast.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const defaultScheduler = {
|
|
2
|
+
schedule: ({ delayMs, run }) => setTimeout(() => run({}), delayMs),
|
|
3
|
+
cancel: ({ ticket }) => clearTimeout(ticket),
|
|
4
|
+
};
|
|
5
|
+
/** Instance-local queues avoid mixing messages between hosts and SSR requests. */
|
|
6
|
+
export function createToastService(_required, optional = {}) {
|
|
7
|
+
const scheduler = optional.scheduler ?? defaultScheduler;
|
|
8
|
+
let messages = Object.freeze([]), sequence = 0, disposed = false;
|
|
9
|
+
const listeners = new Set(), timers = new Map();
|
|
10
|
+
function emit() { for (const listener of listeners)
|
|
11
|
+
listener({}); }
|
|
12
|
+
function dismiss({ id }, _optional = {}) {
|
|
13
|
+
if (timers.has(id)) {
|
|
14
|
+
scheduler.cancel({ ticket: timers.get(id) });
|
|
15
|
+
timers.delete(id);
|
|
16
|
+
}
|
|
17
|
+
if (!messages.some(message => message.id === id))
|
|
18
|
+
return;
|
|
19
|
+
messages = Object.freeze(messages.filter(message => message.id !== id));
|
|
20
|
+
emit();
|
|
21
|
+
}
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
push(input, opts = {}) {
|
|
24
|
+
if (disposed)
|
|
25
|
+
throw new Error('Toast service is disposed');
|
|
26
|
+
const id = opts.id ?? `toast-${++sequence}`;
|
|
27
|
+
dismiss({ id });
|
|
28
|
+
messages = Object.freeze([...messages, Object.freeze({ ...input, id })]);
|
|
29
|
+
emit();
|
|
30
|
+
const delayMs = input.durationMs ?? optional.durationMs ?? 5000;
|
|
31
|
+
if (delayMs > 0)
|
|
32
|
+
timers.set(id, scheduler.schedule({ delayMs, run: () => dismiss({ id }) }));
|
|
33
|
+
return id;
|
|
34
|
+
},
|
|
35
|
+
dismiss,
|
|
36
|
+
snapshot(_required, _optional = {}) { return messages; },
|
|
37
|
+
subscribe({ listener }, _optional = {}) {
|
|
38
|
+
listeners.add(listener);
|
|
39
|
+
return () => { listeners.delete(listener); };
|
|
40
|
+
},
|
|
41
|
+
dispose(_required, _optional = {}) {
|
|
42
|
+
disposed = true;
|
|
43
|
+
for (const ticket of timers.values())
|
|
44
|
+
scheduler.cancel({ ticket });
|
|
45
|
+
timers.clear();
|
|
46
|
+
messages = Object.freeze([]);
|
|
47
|
+
emit();
|
|
48
|
+
listeners.clear();
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=toast.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# DR-001 — UI storefront with a framework-free contract
|
|
2
|
+
|
|
3
|
+
Status: Accepted owner decision
|
|
4
|
+
Date: 2026-10-03
|
|
5
|
+
Contract: 1.0.0
|
|
6
|
+
Package: @jini-ai/ui-kit 0.1.0
|
|
7
|
+
|
|
8
|
+
## Inputs and problem
|
|
9
|
+
|
|
10
|
+
The owner-approved Round 5 consensus and binding v1 decisions require reusable screens to request component roles from a host-selected kit. Importing UI primitives through an admin or feature package couples unrelated packages, and framework dependencies must not enter a headless or different-framework consumer.
|
|
11
|
+
|
|
12
|
+
## Decision recorded
|
|
13
|
+
|
|
14
|
+
Use one package with framework-free root contracts/rules and framework-specific subpaths. Root has no runtime dependencies. React, React DOM, and Vue are optional peers plus development dependencies. React v1 resolves 16 facade components through a provider with native defaults; partial overrides, extension, and a strict complete-kit variant serve different host needs. Every component can be replaced, including ConfirmDialog.
|
|
15
|
+
|
|
16
|
+
Safety belongs to a framework-free controller with latest-policy reads and guarded actions. Confirmation views receive controller bags, never raw destructive callbacks. Cancel-first focus, pending dismissal blocking, human-only agent-handle withholding, and consequence labels are explicit contract obligations. Development mounted-DOM diagnostics default to native fallback and can be set to warn or off; fallback uses native actions too, because the original Button override may be the source of failure.
|
|
17
|
+
|
|
18
|
+
Agent handles and metadata arrive through a host port. No agent-runtime package is imported. Context identity uses a shared global symbol for contract major 1, with a development warning on version skew. Opaque attrs target the actual documented focus target. Select keeps native semantics for a library-independent driver.
|
|
19
|
+
|
|
20
|
+
Native styles are optional, layered, and keyed on parts. Toast state belongs to each service instance; the host mounts its ToastRegion. Browser-only conformance remains explicitly tagged and separate from jsdom evidence.
|
|
21
|
+
|
|
22
|
+
## Consequences and deferred work
|
|
23
|
+
|
|
24
|
+
Screen imports can remain stable as hosts replace component libraries. Hosts must implement the semantic contract, forward refs/attrs, and run conformance/browser validation. A development guard does not authorize destructive actions in production. Sharing a context does not remove the need for a compatible host React runtime.
|
|
25
|
+
|
|
26
|
+
Planned spec entries have no stub component behavior. Vue/Angular subpaths, library adapters, motif loading, and consumer migrations are not implemented in v1. Migrate admin's provisional kit/confirmation first with direct imports and no forwarding shims; keep panel-kit logic separate. Follow with user-management, chat, and reusable UI features in separate jobs.
|
|
27
|
+
|
|
28
|
+
## Delivery boundary
|
|
29
|
+
|
|
30
|
+
This decision records supplied owner choices; it adds no architecture or security policy. Source/README/spec work is confined to ui-kit. Initial development used package-local dependency links because pgrep was unavailable, so no install step was run and this task left the lockfile unchanged. Executed test status belongs in the delivery report, not this decision record.
|
|
31
|
+
|
|
32
|
+
Output: package boundary and customization rationale. Risk: custom adapters can satisfy types while violating mounted behavior. Suggested next assignee: consumer migration owner, then browser conformance owner.
|