@jini-ai/ui-kit 0.6.15-rc.2 → 0.6.15

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/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 confirmLabel = `${required.confirmLabel} — ${required.title}${consequence ? `; ${consequence}` : ''}`;
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 || (input.actor === 'agent' && policy.agentMayConfirm === false))
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;
@@ -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", "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"] },
5
- 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"] },
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", "attrs-on-focus-target", "secret-reveal"], "props": ["attrs", "className", "label", "value", "disabled", "required", "placeholder", "type", "ref", "onValueChange", "secretToggleLabels"], "events": ["onValueChange"], "variants": [], "states": ["disabled"] },
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", "attrs-on-focus-target"], "props": ["attrs", "className", "label", "value", "disabled", "required", "placeholder", "ref", "onValueChange", "rows"], "events": ["onValueChange"], "variants": [], "states": ["disabled"] },
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", "attrs-on-focus-target"], "props": ["attrs", "className", "label", "value", "disabled", "required", "ref", "onValueChange", "options"], "events": ["onValueChange"], "variants": [], "states": ["disabled"] },
9
- 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"] },
10
- 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"] },
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
- const compatible = match ? Number(match[1]) === major && (Number(match[2] ?? 0) < minor ||
17
- (Number(match[2] ?? 0) === minor && Number(match[3] ?? 0) <= patch)) : contract === KIT_CONTRACT.version;
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
  }
@@ -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
- return defaultValue ??= { kit: createKit({}), agent: undefined, overlayContainer: null, toast: undefined, cancelLabel: 'Cancel', guard: 'fallback' };
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' || style.visibility === 'hidden' || style.visibility === 'collapse' || style.opacity === '0')
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
- !describedIds.includes(description.id) || !description.textContent?.includes(consequence))
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
  }
@@ -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
@@ -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 => ({ component: name, parts: kitSpec[name].parts,
39
- source: kit.failed.has(name) ? 'native-guard' : kit.overrides.includes(name) ? 'override' : 'native' }));
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' : required.disabled ? 'disabled' : 'ready',
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
- 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 });
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
  }
@@ -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?: 'info' | 'success' | 'warning' | 'danger';
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?: 'info' | 'success' | 'warning' | 'danger';
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?: 'info' | 'success' | 'warning' | 'danger';
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: '[data-jini-part="confirm.cancel"]' }] },
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: '[data-jini-part="confirm.cancel"]' }, { type: 'click', selector: '[data-jini-part="confirm.confirm"]' }],
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: '[data-jini-part="confirm.cancel"]' }, { type: 'disabled', selector: '[data-jini-part="confirm.confirm"]' }] },
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: '[data-jini-part="confirm.cancel"]', name: 'data-agent-element', value: 'erase-cancel' }] },
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: '[data-jini-part="confirm.cancel"]', key: 'Escape' }], expectations: [{ type: 'calls', name: 'cancel', count: 1 }] },
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: '[data-jini-part="confirm.cancel"]' }] },
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
- const id = opts.id ?? `toast-${++sequence}`;
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.15-rc.2",
3
+ "version": "0.6.15",
4
4
  "description": "Framework-free UI contract, injected React facades and native defaults.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {