@jini-ai/ui-kit 0.6.15-rc.2 → 0.6.16
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 +12 -0
- package/README.md +1 -1
- package/dist/confirm.js +5 -2
- package/dist/kit-contract.js +8 -7
- package/dist/needs.js +6 -2
- package/dist/react/Kit.hooks.js +3 -2
- package/dist/react/SecretInput.d.ts +15 -0
- package/dist/react/SecretInput.js +14 -0
- package/dist/react/confirm/ConfirmDialog.hooks.js +3 -3
- package/dist/react/confirm/guard.js +6 -3
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +2 -0
- package/dist/react/kit.js +8 -2
- package/dist/react/native/Controls.hooks.d.ts +6 -3
- package/dist/react/native/Controls.hooks.js +3 -2
- package/dist/react/native/Dialog.hooks.js +2 -2
- package/dist/react/native/Navigation.hooks.js +2 -2
- package/dist/react/secret-reveal.hooks.d.ts +26 -0
- package/dist/react/secret-reveal.hooks.js +25 -0
- package/dist/react/testing/driver.js +4 -3
- package/dist/react/types.d.ts +4 -4
- package/dist/testing/scenarios.js +7 -6
- package/dist/toast.js +3 -1
- package/package.json +2 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @jini-ai/ui-kit
|
|
2
2
|
|
|
3
|
+
## 0.6.16
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Remove the GitHub Copilot CLI agent: its def, `createCopilotStreamHandler`/`CopilotStreamEvent`,
|
|
8
|
+
account model discovery and the `@github/copilot-sdk` optional peer, the MCP installer's `copilot`
|
|
9
|
+
slug, and its icon. A saved `copilot` agent id no longer resolves (`getAgentDef` returns `null`).
|
|
10
|
+
CopilotKit/AG-UI is unrelated and unchanged.
|
|
11
|
+
|
|
12
|
+
Drop peers no runtime entry loads: `express` from core and http-kit (type-only imports; type
|
|
13
|
+
consumers keep `@types/express`), `vue` from ui-kit, `react-dom` from vibecoding.
|
|
14
|
+
|
|
3
15
|
## 0.6.13
|
|
4
16
|
|
|
5
17
|
## 0.6.12
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Package version: `0.1.0`. UI contract: `KIT_CONTRACT.version === '1.0.0'`. These
|
|
|
14
14
|
| `@jini-ai/ui-kit/react/native/styles.css` | Optional native styling in `@layer jini.kit` |
|
|
15
15
|
| `@jini-ai/ui-kit/react/testing` | Conformance scenario data and React driver |
|
|
16
16
|
|
|
17
|
-
The root has no dependencies and imports no React, React DOM, or Vue.
|
|
17
|
+
The root has no dependencies and imports no React, React DOM, or Vue. React and React DOM remain required peers because `@jini-ai/ui`'s default entry reaches this package's React bindings. This package's own root does not load them. React consumers install React and React DOM. Vue and Angular adapters are future subpaths; v1 does not implement them or declare runtime peers for them.
|
|
18
18
|
|
|
19
19
|
## Inject a partial kit
|
|
20
20
|
|
package/dist/confirm.js
CHANGED
|
@@ -4,7 +4,8 @@ const defaultConsequences = new Map([
|
|
|
4
4
|
export function planConfirm(required, _optional = {}) {
|
|
5
5
|
const pending = required.pending === true, tone = required.tone ?? 'default';
|
|
6
6
|
const consequence = required.consequence?.trim() || defaultConsequences.get(tone);
|
|
7
|
-
const
|
|
7
|
+
const consequenceLabel = consequence ? '; ' + consequence : '';
|
|
8
|
+
const confirmLabel = `${required.confirmLabel} — ${required.title}${consequenceLabel}`;
|
|
8
9
|
const cancelLabel = required.cancelLabel ?? 'Cancel';
|
|
9
10
|
const agent = (action, label) => required.agentHandle === undefined ? undefined :
|
|
10
11
|
{ handle: `${required.agentHandle}-${action}`, role: 'button', label };
|
|
@@ -26,7 +27,9 @@ export function createConfirmController(required, _optional = {}) {
|
|
|
26
27
|
},
|
|
27
28
|
async confirm(input = {}, _optional = {}) {
|
|
28
29
|
const policy = required.read({});
|
|
29
|
-
if (executing || !policy.open || policy.pending
|
|
30
|
+
if (executing || !policy.open || policy.pending)
|
|
31
|
+
return false;
|
|
32
|
+
if (input.actor === 'agent' && policy.agentMayConfirm === false)
|
|
30
33
|
return false;
|
|
31
34
|
// Set synchronously before calling the port: two clicks in one React frame must not execute twice.
|
|
32
35
|
executing = true;
|
package/dist/kit-contract.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/** Contract semver is independent of the npm package version. Only implemented entries may be required. */
|
|
2
2
|
export const KIT_CONTRACT = { id: '@jini-ai/ui-kit', version: '1.0.0' };
|
|
3
|
+
const ATTRS_ON_FOCUS_TARGET = 'attrs-on-focus-target';
|
|
3
4
|
export const kitSpec = {
|
|
4
|
-
Button: { "status": "implemented", "tier": "control", "since": "1.0.0", "parts": ["kit.button"], "focusTarget": "button", "portal": "none", "safety": false, "behaviors": ["press", "disabled-blocks-press",
|
|
5
|
-
IconButton: { "status": "implemented", "tier": "control", "since": "1.0.0", "parts": ["kit.icon-button"], "focusTarget": "button", "portal": "none", "safety": false, "behaviors": ["accessible-label", "press",
|
|
6
|
-
TextField: { "status": "implemented", "tier": "control", "since": "1.0.0", "parts": ["kit.text-field", "kit.field-label", "kit.secret-toggle"], "focusTarget": "input", "portal": "none", "safety": false, "behaviors": ["value-change", "labelled-control",
|
|
7
|
-
TextArea: { "status": "implemented", "tier": "control", "since": "1.0.0", "parts": ["kit.text-area", "kit.field-label"], "focusTarget": "textarea", "portal": "none", "safety": false, "behaviors": ["value-change", "labelled-control",
|
|
8
|
-
Select: { "status": "implemented", "tier": "control", "since": "1.0.0", "parts": ["kit.select", "kit.field-label"], "focusTarget": "select", "portal": "none", "safety": false, "behaviors": ["native-select", "value-change",
|
|
9
|
-
Checkbox: { "status": "implemented", "tier": "control", "since": "1.0.0", "parts": ["kit.checkbox"], "focusTarget": "input", "portal": "none", "safety": false, "behaviors": ["checked-change", "labelled-control",
|
|
10
|
-
Switch: { "status": "implemented", "tier": "control", "since": "1.0.0", "parts": ["kit.switch"], "focusTarget": "input", "portal": "none", "safety": false, "behaviors": ["checked-change", "switch-role",
|
|
5
|
+
Button: { "status": "implemented", "tier": "control", "since": "1.0.0", "parts": ["kit.button"], "focusTarget": "button", "portal": "none", "safety": false, "behaviors": ["press", "disabled-blocks-press", ATTRS_ON_FOCUS_TARGET], "props": ["attrs", "className", "children", "disabled", "pending", "variant", "type", "ref", "onPress"], "events": ["onPress"], "variants": ["primary", "secondary", "danger", "warning", "ghost"], "states": ["disabled", "pending"] },
|
|
6
|
+
IconButton: { "status": "implemented", "tier": "control", "since": "1.0.0", "parts": ["kit.icon-button"], "focusTarget": "button", "portal": "none", "safety": false, "behaviors": ["accessible-label", "press", ATTRS_ON_FOCUS_TARGET], "props": ["attrs", "className", "children", "disabled", "pending", "variant", "type", "ref", "onPress", "label"], "events": ["onPress"], "variants": ["primary", "secondary", "danger", "warning", "ghost"], "states": ["disabled", "pending"] },
|
|
7
|
+
TextField: { "status": "implemented", "tier": "control", "since": "1.0.0", "parts": ["kit.text-field", "kit.field-label", "kit.secret-toggle"], "focusTarget": "input", "portal": "none", "safety": false, "behaviors": ["value-change", "labelled-control", ATTRS_ON_FOCUS_TARGET, "secret-reveal"], "props": ["attrs", "className", "label", "value", "disabled", "required", "placeholder", "type", "ref", "onValueChange", "secretToggleLabels"], "events": ["onValueChange"], "variants": [], "states": ["disabled"] },
|
|
8
|
+
TextArea: { "status": "implemented", "tier": "control", "since": "1.0.0", "parts": ["kit.text-area", "kit.field-label"], "focusTarget": "textarea", "portal": "none", "safety": false, "behaviors": ["value-change", "labelled-control", ATTRS_ON_FOCUS_TARGET], "props": ["attrs", "className", "label", "value", "disabled", "required", "placeholder", "ref", "onValueChange", "rows"], "events": ["onValueChange"], "variants": [], "states": ["disabled"] },
|
|
9
|
+
Select: { "status": "implemented", "tier": "control", "since": "1.0.0", "parts": ["kit.select", "kit.field-label"], "focusTarget": "select", "portal": "none", "safety": false, "behaviors": ["native-select", "value-change", ATTRS_ON_FOCUS_TARGET], "props": ["attrs", "className", "label", "value", "disabled", "required", "ref", "onValueChange", "options"], "events": ["onValueChange"], "variants": [], "states": ["disabled"] },
|
|
10
|
+
Checkbox: { "status": "implemented", "tier": "control", "since": "1.0.0", "parts": ["kit.checkbox"], "focusTarget": "input", "portal": "none", "safety": false, "behaviors": ["checked-change", "labelled-control", ATTRS_ON_FOCUS_TARGET], "props": ["attrs", "className", "label", "checked", "disabled", "ref", "onCheckedChange"], "events": ["onCheckedChange"], "variants": [], "states": ["disabled"] },
|
|
11
|
+
Switch: { "status": "implemented", "tier": "control", "since": "1.0.0", "parts": ["kit.switch"], "focusTarget": "input", "portal": "none", "safety": false, "behaviors": ["checked-change", "switch-role", ATTRS_ON_FOCUS_TARGET], "props": ["attrs", "className", "label", "checked", "disabled", "ref", "onCheckedChange"], "events": ["onCheckedChange"], "variants": [], "states": ["disabled"] },
|
|
11
12
|
Dialog: { "status": "implemented", "tier": "composite", "since": "1.0.0", "parts": ["kit.dialog", "kit.dialog-title", "kit.dialog-close", "kit.overlay"], "focusTarget": "dialog", "portal": "overlay", "safety": false, "behaviors": ["escape-dismissal", "pending-blocks-dismissal", "focus-restore"], "props": ["attrs", "className", "open", "title", "children", "pending", "closeLabel", "ref", "onClose"], "events": ["onClose"], "variants": [], "states": ["disabled", "pending"] },
|
|
12
13
|
ConfirmDialog: { "status": "implemented", "tier": "composite", "since": "1.0.0", "parts": ["confirm.dialog", "confirm.title", "confirm.body", "confirm.actions", "confirm.cancel", "confirm.confirm"], "focusTarget": "cancel", "portal": "overlay", "safety": true, "behaviors": ["pending-blocks-every-dismissal", "cancel-first-focus", "agent-confirm-withheld", "consequence-label"], "props": ["attrs", "className", "open", "title", "body", "confirmLabel", "cancelLabel", "tone", "pending", "consequence", "agentHandle", "agentMayConfirm", "errorLabel", "actionAccessibleNames", "onConfirm", "onCancel"], "events": ["onConfirm", "onCancel"], "variants": ["default", "warning", "danger"], "states": ["disabled", "pending"] },
|
|
13
14
|
Tabs: { "status": "implemented", "tier": "composite", "since": "1.0.0", "parts": ["kit.tabs", "kit.tab", "kit.tab-panel"], "focusTarget": "active-tab", "portal": "none", "safety": false, "behaviors": ["roving-tabs", "labelled-panel"], "props": ["attrs", "className", "label", "value", "items", "onValueChange"], "events": ["onValueChange"], "variants": [], "states": [] },
|
package/dist/needs.js
CHANGED
|
@@ -13,8 +13,12 @@ export function assertKitContract(required, _optional = {}) {
|
|
|
13
13
|
const { contract } = required;
|
|
14
14
|
const match = /^\^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(contract);
|
|
15
15
|
const [major, minor, patch] = KIT_CONTRACT.version.split('.').map(Number);
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
let compatible = contract === KIT_CONTRACT.version;
|
|
17
|
+
if (match) {
|
|
18
|
+
const requestedMinor = Number(match[2] ?? 0);
|
|
19
|
+
const withinMinor = requestedMinor < minor || (requestedMinor === minor && Number(match[3] ?? 0) <= patch);
|
|
20
|
+
compatible = Number(match[1]) === major && withinMinor;
|
|
21
|
+
}
|
|
18
22
|
if (!compatible)
|
|
19
23
|
throw new KitConfigError({ issues: [`contract ${contract} is incompatible with ${KIT_CONTRACT.version}`] });
|
|
20
24
|
}
|
package/dist/react/Kit.hooks.js
CHANGED
|
@@ -7,7 +7,8 @@ import { kitSpec } from '../kit-contract.js';
|
|
|
7
7
|
import { createToastService } from '../toast.js';
|
|
8
8
|
let defaultValue;
|
|
9
9
|
function getDefault() {
|
|
10
|
-
|
|
10
|
+
defaultValue ??= { kit: createKit({}), agent: undefined, overlayContainer: null, toast: undefined, cancelLabel: 'Cancel', guard: 'fallback' };
|
|
11
|
+
return defaultValue;
|
|
11
12
|
}
|
|
12
13
|
export function useKit(_required = {}, _optional = {}) {
|
|
13
14
|
return useKitContext({}) ?? getDefault();
|
|
@@ -42,7 +43,7 @@ function useOverlayRoot(required) {
|
|
|
42
43
|
useEffect(() => {
|
|
43
44
|
if (required.supplied) {
|
|
44
45
|
setRoot(required.supplied);
|
|
45
|
-
return;
|
|
46
|
+
return undefined;
|
|
46
47
|
}
|
|
47
48
|
const element = document.createElement('div');
|
|
48
49
|
element.dataset.jiniPart = 'kit.overlay';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type InputHTMLAttributes } from 'react';
|
|
2
|
+
import { type SecretRevealOptional } from './secret-reveal.hooks.js';
|
|
3
|
+
export interface SecretInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'>, SecretRevealOptional {
|
|
4
|
+
readonly label: string;
|
|
5
|
+
readonly toggleLabels?: {
|
|
6
|
+
readonly show: string;
|
|
7
|
+
readonly hide: string;
|
|
8
|
+
};
|
|
9
|
+
readonly toggleAttrs?: Readonly<Record<string, string | boolean | undefined>>;
|
|
10
|
+
}
|
|
11
|
+
/** Masked native input with controlled or local reveal state. Keep its toggle outside a label:
|
|
12
|
+
* the toggle's name must never join the input's accessible name. Hosts own styling and copy.
|
|
13
|
+
* @complexity O(1). */
|
|
14
|
+
export declare const SecretInput: import("react").ForwardRefExoticComponent<SecretInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
15
|
+
//# sourceMappingURL=SecretInput.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { useSecretReveal } from './secret-reveal.hooks.js';
|
|
4
|
+
import { eyeIcon } from './native/Controls.hooks.js';
|
|
5
|
+
/** Masked native input with controlled or local reveal state. Keep its toggle outside a label:
|
|
6
|
+
* the toggle's name must never join the input's accessible name. Hosts own styling and copy.
|
|
7
|
+
* @complexity O(1). */
|
|
8
|
+
export const SecretInput = forwardRef(function SecretInput(props, ref) {
|
|
9
|
+
// The toggle props are pulled off so they never reach the DOM `<input>`.
|
|
10
|
+
const { label, toggleLabels, toggleAttrs, revealed, onRevealedChange, ...inputProps } = props;
|
|
11
|
+
const reveal = useSecretReveal({ value: inputProps.value, labels: toggleLabels ?? { show: `Show ${label}`, hide: `Hide ${label}` } }, { ...(revealed === undefined ? {} : { revealed }), ...(onRevealedChange ? { onRevealedChange } : {}) });
|
|
12
|
+
return _jsxs("span", { className: "jini-secret", children: [_jsx("input", { ...inputProps, spellCheck: false, ref: ref, type: reveal.inputType }), _jsx("button", { ...toggleAttrs, type: "button", className: "jini-secret-toggle", "aria-label": reveal.toggleLabel, title: reveal.toggleLabel, disabled: inputProps.disabled, onClick: reveal.toggle, children: eyeIcon({ revealed: reveal.revealed }) })] });
|
|
13
|
+
});
|
|
14
|
+
//# sourceMappingURL=SecretInput.js.map
|
|
@@ -75,7 +75,7 @@ export function useConfirmFacade(required, _optional = {}) {
|
|
|
75
75
|
// React restores selection between cleanup and layout setup; restore the trigger after that step.
|
|
76
76
|
if (opener.current instanceof HTMLElement && opener.current.isConnected)
|
|
77
77
|
opener.current.focus();
|
|
78
|
-
return;
|
|
78
|
+
return undefined;
|
|
79
79
|
}
|
|
80
80
|
c.focusInitial({});
|
|
81
81
|
return () => { if (opener.current instanceof HTMLElement && opener.current.isConnected)
|
|
@@ -83,9 +83,9 @@ export function useConfirmFacade(required, _optional = {}) {
|
|
|
83
83
|
}, [c.open, Component]);
|
|
84
84
|
useEffect(() => {
|
|
85
85
|
if (!c.open || failed || context.guard === 'off' || !isDevelopment({}))
|
|
86
|
-
return;
|
|
86
|
+
return undefined;
|
|
87
87
|
if (!context.kit.overrides.includes('ConfirmDialog') && !context.kit.overrides.includes('Button'))
|
|
88
|
-
return;
|
|
88
|
+
return undefined;
|
|
89
89
|
// After the committed view's effects, including library portal/focus synchronization.
|
|
90
90
|
const timer = setTimeout(() => {
|
|
91
91
|
const reasons = inspectConfirm({ controller: c, checkFocus: initial.current });
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
function visible(element) {
|
|
2
2
|
for (let current = element; current; current = current.parentElement) {
|
|
3
3
|
const style = getComputedStyle(current);
|
|
4
|
-
if (current.hidden || current.getAttribute('aria-hidden') === 'true' || style.display === 'none'
|
|
4
|
+
if (current.hidden || current.getAttribute('aria-hidden') === 'true' || style.display === 'none')
|
|
5
|
+
return false;
|
|
6
|
+
if (style.visibility === 'hidden' || style.visibility === 'collapse' || style.opacity === '0')
|
|
5
7
|
return false;
|
|
6
8
|
}
|
|
7
9
|
return true;
|
|
@@ -47,8 +49,9 @@ function inspectInteraction({ controller: c, checkFocus, cancel, confirm, active
|
|
|
47
49
|
}
|
|
48
50
|
function inspectDescription({ description, frame, confirm, consequence }, _optional = {}) {
|
|
49
51
|
const describedIds = confirm.getAttribute('aria-describedby')?.split(/\s+/) ?? [];
|
|
50
|
-
if (!description || !frame.contains(description) || !visible(description)
|
|
51
|
-
|
|
52
|
+
if (!description || !frame.contains(description) || !visible(description))
|
|
53
|
+
return ['missing consequence label'];
|
|
54
|
+
if (!describedIds.includes(description.id) || !description.textContent?.includes(consequence))
|
|
52
55
|
return ['missing consequence label'];
|
|
53
56
|
return [];
|
|
54
57
|
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -5,4 +5,6 @@ export { Button, IconButton, TextField, TextArea, Select, Checkbox, Switch, Dial
|
|
|
5
5
|
export { ConfirmDialog } from './confirm/ConfirmDialog.js';
|
|
6
6
|
export { useConfirmController } from './confirm/ConfirmDialog.hooks.js';
|
|
7
7
|
export type * from './types.js';
|
|
8
|
+
export { SecretInput, type SecretInputProps } from './SecretInput.js';
|
|
9
|
+
export { useSecretReveal, type SecretRevealRequired, type SecretRevealOptional } from './secret-reveal.hooks.js';
|
|
8
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/react/index.js
CHANGED
|
@@ -4,4 +4,6 @@ export { useKit, useKitComponent, useOverlayContainer, useToast } from './Kit.ho
|
|
|
4
4
|
export { Button, IconButton, TextField, TextArea, Select, Checkbox, Switch, Dialog, Tabs, Menu, Toast, Tooltip, Notice, Spinner, Badge } from './facade.js';
|
|
5
5
|
export { ConfirmDialog } from './confirm/ConfirmDialog.js';
|
|
6
6
|
export { useConfirmController } from './confirm/ConfirmDialog.hooks.js';
|
|
7
|
+
export { SecretInput } from './SecretInput.js';
|
|
8
|
+
export { useSecretReveal } from './secret-reveal.hooks.js';
|
|
7
9
|
//# sourceMappingURL=index.js.map
|
package/dist/react/kit.js
CHANGED
|
@@ -35,8 +35,14 @@ export function extendKit(required, optional = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
export function describeKit(required, _optional = {}) {
|
|
37
37
|
const { kit } = required;
|
|
38
|
-
const coverage = implementedComponents.map(name =>
|
|
39
|
-
source
|
|
38
|
+
const coverage = implementedComponents.map(name => {
|
|
39
|
+
let source = 'native';
|
|
40
|
+
if (kit.overrides.includes(name))
|
|
41
|
+
source = 'override';
|
|
42
|
+
if (kit.failed.has(name))
|
|
43
|
+
source = 'native-guard';
|
|
44
|
+
return { component: name, parts: kitSpec[name].parts, source };
|
|
45
|
+
});
|
|
40
46
|
return { id: kit.id, contract: kit.contract, coverage,
|
|
41
47
|
fallbacks: coverage.filter(entry => entry.source !== 'override').map(entry => entry.component),
|
|
42
48
|
violations: kit.violations.map(violation => ({ ...violation, reasons: [...violation.reasons] })) };
|
|
@@ -31,6 +31,9 @@ export declare function useNativeIconButton(required: IconButtonProps, _optional
|
|
|
31
31
|
};
|
|
32
32
|
children: import("react").ReactNode;
|
|
33
33
|
};
|
|
34
|
+
export declare function eyeIcon(required: {
|
|
35
|
+
revealed: boolean;
|
|
36
|
+
}, _optional?: Record<string, never>): import("react").ReactSVGElement;
|
|
34
37
|
export declare function useNativeTextField(required: TextFieldProps, _optional?: Record<string, never>): {
|
|
35
38
|
toggle: {
|
|
36
39
|
'data-jini-part': string;
|
|
@@ -127,7 +130,7 @@ export declare function useNativeToast(required: ToastProps, _optional?: Record<
|
|
|
127
130
|
className: string;
|
|
128
131
|
role: string;
|
|
129
132
|
tabIndex: number;
|
|
130
|
-
'data-jini-variant': "warning" | "danger" | "info" | "success"
|
|
133
|
+
'data-jini-variant': NonNullable<"warning" | "danger" | "info" | "success" | undefined>;
|
|
131
134
|
id?: string;
|
|
132
135
|
'data-jini-part': string;
|
|
133
136
|
};
|
|
@@ -146,7 +149,7 @@ export declare function useNativeNotice(required: NoticeProps, _optional?: Recor
|
|
|
146
149
|
className: string;
|
|
147
150
|
tabIndex: number;
|
|
148
151
|
role: string;
|
|
149
|
-
'data-jini-variant': "warning" | "danger" | "info" | "success"
|
|
152
|
+
'data-jini-variant': NonNullable<"warning" | "danger" | "info" | "success" | undefined>;
|
|
150
153
|
id?: string;
|
|
151
154
|
'data-jini-part': string;
|
|
152
155
|
};
|
|
@@ -166,7 +169,7 @@ export declare function useNativeBadge(required: BadgeProps, _optional?: Record<
|
|
|
166
169
|
props: {
|
|
167
170
|
className: string;
|
|
168
171
|
tabIndex: number;
|
|
169
|
-
'data-jini-variant': "warning" | "danger" | "info" | "success"
|
|
172
|
+
'data-jini-variant': NonNullable<"warning" | "danger" | "info" | "success" | undefined>;
|
|
170
173
|
id?: string;
|
|
171
174
|
'data-jini-part': string;
|
|
172
175
|
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createElement, useId, useState } from 'react';
|
|
2
2
|
export function useNativeButton(required, _optional = {}) {
|
|
3
|
+
const readyState = required.disabled ? 'disabled' : 'ready';
|
|
3
4
|
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
5
|
type: required.type ?? 'button', disabled: !!required.disabled || !!required.pending,
|
|
5
6
|
'aria-busy': required.pending, 'data-jini-variant': required.variant ?? 'primary',
|
|
6
|
-
'data-jini-state': required.pending ? 'pending' :
|
|
7
|
+
'data-jini-state': required.pending ? 'pending' : readyState,
|
|
7
8
|
onClick: () => { if (!required.disabled && !required.pending)
|
|
8
9
|
required.onPress?.({}); },
|
|
9
10
|
// Opaque metadata wins over generated metadata; behavioral props cannot be supplied through attrs.
|
|
@@ -21,7 +22,7 @@ function useField(required) {
|
|
|
21
22
|
/** Eye / eye-off glyphs, decorative: the button's aria-label carries the meaning. */
|
|
22
23
|
const EYE_PATHS = ['M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z', 'M12 9a3 3 0 1 0 0 6 3 3 0 1 0 0-6z'];
|
|
23
24
|
const EYE_OFF_PATHS = ['M9.9 4.24A9.1 9.1 0 0 1 12 4c6.5 0 10 7 10 7a17.8 17.8 0 0 1-2.16 3.19M6.6 6.6C3.9 8.4 2 12 2 12s3.5 7 10 7a9.7 9.7 0 0 0 5.4-1.6M14.12 14.12a3 3 0 1 1-4.24-4.24', 'M2 2l20 20'];
|
|
24
|
-
function eyeIcon(required, _optional = {}) {
|
|
25
|
+
export function eyeIcon(required, _optional = {}) {
|
|
25
26
|
const paths = (required.revealed ? EYE_OFF_PATHS : EYE_PATHS).map(d => createElement('path', { key: d, d }));
|
|
26
27
|
return createElement('svg', { 'aria-hidden': true, focusable: 'false', viewBox: '0 0 24 24', width: 18, height: 18, fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }, paths);
|
|
27
28
|
}
|
|
@@ -10,10 +10,10 @@ export function useNativeDialog(required, optional = {}) {
|
|
|
10
10
|
useLayoutEffect(() => {
|
|
11
11
|
const dialog = dialogRef.current;
|
|
12
12
|
if (!dialog)
|
|
13
|
-
return;
|
|
13
|
+
return undefined;
|
|
14
14
|
if (!required.open) {
|
|
15
15
|
restoreDialogFocus({ opener: opener.current });
|
|
16
|
-
return;
|
|
16
|
+
return undefined;
|
|
17
17
|
}
|
|
18
18
|
opener.current = (optional.document ?? document).activeElement;
|
|
19
19
|
if (typeof dialog.showModal === 'function') {
|
|
@@ -39,7 +39,7 @@ export function useNativeMenu(required, _optional = {}) {
|
|
|
39
39
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
40
40
|
useLayoutEffect(() => {
|
|
41
41
|
if (!open || !trigger.current || !menu.current)
|
|
42
|
-
return;
|
|
42
|
+
return undefined;
|
|
43
43
|
// Portals escape the preview card's overflow clipping. Follow the anchor on scroll/resize.
|
|
44
44
|
const update = () => {
|
|
45
45
|
const anchor = trigger.current.getBoundingClientRect();
|
|
@@ -57,7 +57,7 @@ export function useNativeMenu(required, _optional = {}) {
|
|
|
57
57
|
function focus(index) { menu.current?.querySelectorAll('[role="menuitem"]:not(:disabled)')[index]?.focus(); }
|
|
58
58
|
useEffect(() => {
|
|
59
59
|
if (!open)
|
|
60
|
-
return;
|
|
60
|
+
return undefined;
|
|
61
61
|
focus(0);
|
|
62
62
|
const dismiss = (event) => {
|
|
63
63
|
if (event.target instanceof Node && !menu.current?.contains(event.target) && !trigger.current?.contains(event.target))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface SecretRevealRequired {
|
|
2
|
+
/** The input's current value. A transition to `''` re-hides (the usual sign a save succeeded). */
|
|
3
|
+
readonly value: unknown;
|
|
4
|
+
/** The toggle's full accessible names, translated ("Show API key" / "Hide API key"). */
|
|
5
|
+
readonly labels: {
|
|
6
|
+
readonly show: string;
|
|
7
|
+
readonly hide: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface SecretRevealOptional {
|
|
11
|
+
/** Controlled mode: the host owns the shown/hidden state (e.g. to reset it per provider). */
|
|
12
|
+
readonly revealed?: boolean;
|
|
13
|
+
readonly onRevealedChange?: (revealed: boolean) => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The show/hide state behind `SecretInput`'s eye toggle. Starts hidden. Uncontrolled, it re-hides on
|
|
17
|
+
* the value's transition to empty only — an empty field may still be revealed before pasting.
|
|
18
|
+
* @complexity O(1).
|
|
19
|
+
*/
|
|
20
|
+
export declare function useSecretReveal(required: SecretRevealRequired, optional?: SecretRevealOptional): {
|
|
21
|
+
readonly revealed: boolean;
|
|
22
|
+
readonly inputType: "text" | "password";
|
|
23
|
+
readonly toggleLabel: string;
|
|
24
|
+
readonly toggle: () => void;
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=secret-reveal.hooks.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The show/hide state behind `SecretInput`'s eye toggle. Starts hidden. Uncontrolled, it re-hides on
|
|
4
|
+
* the value's transition to empty only — an empty field may still be revealed before pasting.
|
|
5
|
+
* @complexity O(1).
|
|
6
|
+
*/
|
|
7
|
+
export function useSecretReveal(required, optional = {}) {
|
|
8
|
+
const [ownRevealed, setOwnRevealed] = useState(false);
|
|
9
|
+
const [lastValue, setLastValue] = useState(required.value);
|
|
10
|
+
if (required.value !== lastValue) {
|
|
11
|
+
setLastValue(required.value);
|
|
12
|
+
if (required.value === '')
|
|
13
|
+
setOwnRevealed(false);
|
|
14
|
+
}
|
|
15
|
+
const revealed = optional.revealed ?? ownRevealed;
|
|
16
|
+
const toggleLabel = revealed ? required.labels.hide : required.labels.show;
|
|
17
|
+
const toggle = () => {
|
|
18
|
+
if (optional.onRevealedChange)
|
|
19
|
+
optional.onRevealedChange(!revealed);
|
|
20
|
+
else
|
|
21
|
+
setOwnRevealed(!revealed);
|
|
22
|
+
};
|
|
23
|
+
return { revealed, inputType: revealed ? 'text' : 'password', toggleLabel, toggle };
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=secret-reveal.hooks.js.map
|
|
@@ -97,10 +97,11 @@ export async function runKitConformance(required, optional = {}) {
|
|
|
97
97
|
continue;
|
|
98
98
|
if (scenario.browserOnly && !optional.includeBrowser) {
|
|
99
99
|
results.push({ id: scenario.id, status: 'skipped', errors: ['requires real browser driver'] });
|
|
100
|
-
continue;
|
|
101
100
|
}
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
else {
|
|
102
|
+
const errors = await driver.run({ kit: required.kit, scenario, agent: optional.agent ?? testingAgent });
|
|
103
|
+
results.push({ id: scenario.id, status: errors.length ? 'failed' : 'passed', errors });
|
|
104
|
+
}
|
|
104
105
|
}
|
|
105
106
|
return results;
|
|
106
107
|
}
|
package/dist/react/types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { KitAttrs, AgentAttrsPort } from '../attrs.js';
|
|
|
3
3
|
import type { ConfirmInput, DismissReason } from '../confirm.js';
|
|
4
4
|
import type { ImplementedComponentName } from '../kit-contract.js';
|
|
5
5
|
import type { KitNeeds } from '../needs.js';
|
|
6
|
-
import type { ToastService } from '../toast.js';
|
|
6
|
+
import type { ToastMessage, ToastService } from '../toast.js';
|
|
7
7
|
export interface BaseProps {
|
|
8
8
|
readonly attrs?: KitAttrs;
|
|
9
9
|
readonly className?: string;
|
|
@@ -139,7 +139,7 @@ export interface MenuProps extends BaseProps {
|
|
|
139
139
|
}
|
|
140
140
|
export interface ToastProps extends BaseProps {
|
|
141
141
|
readonly message: string;
|
|
142
|
-
readonly tone?: '
|
|
142
|
+
readonly tone?: NonNullable<ToastMessage['tone']>;
|
|
143
143
|
readonly onDismiss?: ActionPort;
|
|
144
144
|
readonly dismissLabel?: string;
|
|
145
145
|
}
|
|
@@ -150,14 +150,14 @@ export interface TooltipProps extends BaseProps {
|
|
|
150
150
|
}
|
|
151
151
|
export interface NoticeProps extends BaseProps {
|
|
152
152
|
readonly children: ReactNode;
|
|
153
|
-
readonly tone?: '
|
|
153
|
+
readonly tone?: NonNullable<ToastMessage['tone']>;
|
|
154
154
|
}
|
|
155
155
|
export interface SpinnerProps extends BaseProps {
|
|
156
156
|
readonly label?: string;
|
|
157
157
|
}
|
|
158
158
|
export interface BadgeProps extends BaseProps {
|
|
159
159
|
readonly children: ReactNode;
|
|
160
|
-
readonly tone?: '
|
|
160
|
+
readonly tone?: NonNullable<ToastMessage['tone']>;
|
|
161
161
|
}
|
|
162
162
|
export interface ReactKitProps {
|
|
163
163
|
Button: ButtonProps;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const CONFIRM_CANCEL = '[data-jini-part="confirm.cancel"]';
|
|
1
2
|
/** Serializable cases can drive another framework or a real-browser fixture without importing React. */
|
|
2
3
|
export const kitConformanceScenarios = Object.freeze([
|
|
3
4
|
...['Button', 'IconButton', 'TextField', 'TextArea', 'Select', 'Checkbox', 'Switch', 'Dialog', 'Tabs', 'Menu', 'Toast', 'Tooltip', 'Notice', 'Spinner', 'Badge'].map(component => ({
|
|
@@ -8,21 +9,21 @@ export const kitConformanceScenarios = Object.freeze([
|
|
|
8
9
|
})),
|
|
9
10
|
{ id: 'Select:native-semantics', component: 'Select', props: {}, actions: [], expectations: [{ type: 'native-select', selector: '[data-jini-part="kit.select"]' }] },
|
|
10
11
|
{ 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:
|
|
12
|
+
{ id: 'ConfirmDialog:cancel-first-focus', component: 'ConfirmDialog', safety: true, props: {}, actions: [], expectations: [{ type: 'focused', selector: CONFIRM_CANCEL }] },
|
|
12
13
|
{ id: 'ConfirmDialog:pending-blocks-every-dismissal', component: 'ConfirmDialog', safety: true, props: { pending: true },
|
|
13
14
|
actions: [{ type: 'cancel', selector: '[data-jini-part="confirm.dialog"]' }, { type: 'click', selector: '[data-jini-part="confirm.dialog"]' },
|
|
14
|
-
{ type: 'click', selector:
|
|
15
|
+
{ type: 'click', selector: CONFIRM_CANCEL }, { type: 'click', selector: '[data-jini-part="confirm.confirm"]' }],
|
|
15
16
|
expectations: [{ type: 'calls', name: 'cancel', count: 0 }, { type: 'calls', name: 'confirm', count: 0 },
|
|
16
|
-
{ type: 'disabled', selector:
|
|
17
|
+
{ type: 'disabled', selector: CONFIRM_CANCEL }, { type: 'disabled', selector: '[data-jini-part="confirm.confirm"]' }] },
|
|
17
18
|
{ id: 'ConfirmDialog:agent-confirm-withheld', component: 'ConfirmDialog', safety: true, props: { agentHandle: 'erase', agentMayConfirm: false }, actions: [],
|
|
18
19
|
expectations: [{ type: 'absent', selector: '[data-agent-element="erase-confirm"]' },
|
|
19
|
-
{ type: 'attribute', selector:
|
|
20
|
+
{ type: 'attribute', selector: CONFIRM_CANCEL, name: 'data-agent-element', value: 'erase-cancel' }] },
|
|
20
21
|
{ id: 'ConfirmDialog:consequence-label', component: 'ConfirmDialog', safety: true, props: { agentHandle: 'erase', tone: 'danger' }, actions: [],
|
|
21
22
|
expectations: [{ type: 'attribute', selector: '[data-jini-part="confirm.confirm"]', name: 'data-agent-label', value: 'Delete — Delete record?; cannot be undone' },
|
|
22
23
|
{ type: 'attribute', selector: '[data-jini-part="confirm.confirm"]', name: 'aria-label', value: 'Delete — Delete record?; cannot be undone' }] },
|
|
23
24
|
{ id: 'ConfirmDialog:browser-escape', component: 'ConfirmDialog', safety: true, browserOnly: true, props: {},
|
|
24
|
-
actions: [{ type: 'key', selector:
|
|
25
|
+
actions: [{ type: 'key', selector: CONFIRM_CANCEL, key: 'Escape' }], expectations: [{ type: 'calls', name: 'cancel', count: 1 }] },
|
|
25
26
|
{ id: 'ConfirmDialog:browser-modal-focus', component: 'ConfirmDialog', safety: true, browserOnly: true, props: {}, actions: [],
|
|
26
|
-
expectations: [{ type: 'focused', selector:
|
|
27
|
+
expectations: [{ type: 'focused', selector: CONFIRM_CANCEL }] },
|
|
27
28
|
]);
|
|
28
29
|
//# sourceMappingURL=scenarios.js.map
|
package/dist/toast.js
CHANGED
|
@@ -23,7 +23,9 @@ export function createToastService(_required, optional = {}) {
|
|
|
23
23
|
push(input, opts = {}) {
|
|
24
24
|
if (disposed)
|
|
25
25
|
throw new Error('Toast service is disposed');
|
|
26
|
-
|
|
26
|
+
if (opts.id == null)
|
|
27
|
+
sequence += 1;
|
|
28
|
+
const id = opts.id ?? `toast-${sequence}`;
|
|
27
29
|
dismiss({ id });
|
|
28
30
|
messages = Object.freeze([...messages, Object.freeze({ ...input, id })]);
|
|
29
31
|
emit();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/ui-kit",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.16",
|
|
4
4
|
"description": "Framework-free UI contract, injected React facades and native defaults.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -65,19 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"react": "^18.3.0 || ^19.0.0",
|
|
68
|
-
"react-dom": "^18.3.0 || ^19.0.0"
|
|
69
|
-
"vue": "^3.5.0"
|
|
70
|
-
},
|
|
71
|
-
"peerDependenciesMeta": {
|
|
72
|
-
"react": {
|
|
73
|
-
"optional": true
|
|
74
|
-
},
|
|
75
|
-
"react-dom": {
|
|
76
|
-
"optional": true
|
|
77
|
-
},
|
|
78
|
-
"vue": {
|
|
79
|
-
"optional": true
|
|
80
|
-
}
|
|
68
|
+
"react-dom": "^18.3.0 || ^19.0.0"
|
|
81
69
|
},
|
|
82
70
|
"devDependencies": {
|
|
83
71
|
"@types/node": "^24.0.0",
|