@jini-ai/ui-kit 0.6.11 → 0.6.15-rc.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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # @jini-ai/ui-kit
2
2
 
3
+ ## 0.6.13
4
+
5
+ ## 0.6.12
6
+
3
7
  ## 0.6.11
4
8
 
5
9
  ## 0.6.10
@@ -50,12 +50,12 @@ export declare const kitSpec: {
50
50
  readonly status: "implemented";
51
51
  readonly tier: "control";
52
52
  readonly since: "1.0.0";
53
- readonly parts: readonly ["kit.text-field", "kit.field-label"];
53
+ readonly parts: readonly ["kit.text-field", "kit.field-label", "kit.secret-toggle"];
54
54
  readonly focusTarget: "input";
55
55
  readonly portal: "none";
56
56
  readonly safety: false;
57
- readonly behaviors: readonly ["value-change", "labelled-control", "attrs-on-focus-target"];
58
- readonly props: readonly ["attrs", "className", "label", "value", "disabled", "required", "placeholder", "type", "ref", "onValueChange"];
57
+ readonly behaviors: readonly ["value-change", "labelled-control", "attrs-on-focus-target", "secret-reveal"];
58
+ readonly props: readonly ["attrs", "className", "label", "value", "disabled", "required", "placeholder", "type", "ref", "onValueChange", "secretToggleLabels"];
59
59
  readonly events: readonly ["onValueChange"];
60
60
  readonly variants: readonly [];
61
61
  readonly states: readonly ["disabled"];
@@ -3,7 +3,7 @@ export const KIT_CONTRACT = { id: '@jini-ai/ui-kit', version: '1.0.0' };
3
3
  export const kitSpec = {
4
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
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"], "focusTarget": "input", "portal": "none", "safety": false, "behaviors": ["value-change", "labelled-control", "attrs-on-focus-target"], "props": ["attrs", "className", "label", "value", "disabled", "required", "placeholder", "type", "ref", "onValueChange"], "events": ["onValueChange"], "variants": [], "states": ["disabled"] },
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
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
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
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"] },
@@ -22,7 +22,7 @@ export declare function describeKit(required: {
22
22
  contract: string;
23
23
  coverage: {
24
24
  component: ImplementedComponentName;
25
- parts: readonly ["kit.button"] | readonly ["kit.icon-button"] | readonly ["kit.text-field", "kit.field-label"] | readonly ["kit.text-area", "kit.field-label"] | readonly ["kit.select", "kit.field-label"] | readonly ["kit.checkbox"] | readonly ["kit.switch"] | readonly ["kit.dialog", "kit.dialog-title", "kit.dialog-close", "kit.overlay"] | readonly ["confirm.dialog", "confirm.title", "confirm.body", "confirm.actions", "confirm.cancel", "confirm.confirm"] | readonly ["kit.tabs", "kit.tab", "kit.tab-panel"] | readonly ["kit.menu-trigger", "kit.menu", "kit.menu-item"] | readonly ["kit.toast", "kit.toast-dismiss", "kit.toast-region"] | readonly ["kit.tooltip-trigger", "kit.tooltip"] | readonly ["kit.notice"] | readonly ["kit.spinner"] | readonly ["kit.badge"];
25
+ parts: readonly ["kit.button"] | readonly ["kit.icon-button"] | readonly ["kit.text-field", "kit.field-label", "kit.secret-toggle"] | readonly ["kit.text-area", "kit.field-label"] | readonly ["kit.select", "kit.field-label"] | readonly ["kit.checkbox"] | readonly ["kit.switch"] | readonly ["kit.dialog", "kit.dialog-title", "kit.dialog-close", "kit.overlay"] | readonly ["confirm.dialog", "confirm.title", "confirm.body", "confirm.actions", "confirm.cancel", "confirm.confirm"] | readonly ["kit.tabs", "kit.tab", "kit.tab-panel"] | readonly ["kit.menu-trigger", "kit.menu", "kit.menu-item"] | readonly ["kit.toast", "kit.toast-dismiss", "kit.toast-region"] | readonly ["kit.tooltip-trigger", "kit.tooltip"] | readonly ["kit.notice"] | readonly ["kit.spinner"] | readonly ["kit.badge"];
26
26
  source: "native" | "native-guard" | "override";
27
27
  }[];
28
28
  fallbacks: ImplementedComponentName[];
@@ -32,11 +32,21 @@ export declare function useNativeIconButton(required: IconButtonProps, _optional
32
32
  children: import("react").ReactNode;
33
33
  };
34
34
  export declare function useNativeTextField(required: TextFieldProps, _optional?: Record<string, never>): {
35
+ toggle: {
36
+ 'data-jini-part': string;
37
+ className: string;
38
+ type: "button";
39
+ 'aria-label': string;
40
+ disabled: boolean | undefined;
41
+ onClick: () => void;
42
+ children: import("react").ReactSVGElement;
43
+ } | null;
35
44
  props: {
36
45
  id: string;
37
46
  ref: import("react").Ref<HTMLInputElement> | undefined;
38
- type: "number" | "text" | "password" | "email" | "search" | "url" | "tel";
47
+ type: string;
39
48
  value: string;
49
+ spellCheck: boolean | undefined;
40
50
  placeholder: string | undefined;
41
51
  onChange: (event: ChangeEvent<HTMLInputElement>) => void;
42
52
  className: string;
@@ -1,4 +1,4 @@
1
- import { createElement, useId } from 'react';
1
+ import { createElement, useId, useState } from 'react';
2
2
  export function useNativeButton(required, _optional = {}) {
3
3
  return { children: required.children, props: { 'data-jini-part': 'kit.button', ref: required.ref, className: `jini-btn jini-btn-${required.variant ?? 'primary'} ${required.className ?? ''}`.trim(),
4
4
  type: required.type ?? 'button', disabled: !!required.disabled || !!required.pending,
@@ -18,9 +18,41 @@ function useField(required) {
18
18
  return { id: required.attrs?.id ?? generated, label: required.label, labelAttrs: { 'data-jini-part': 'kit.field-label', className: 'jini-field' },
19
19
  props: { ...required.attrs, className: `jini-input ${required.className ?? ''}`.trim(), disabled: required.disabled, required: required.required } };
20
20
  }
21
+ /** Eye / eye-off glyphs, decorative: the button's aria-label carries the meaning. */
22
+ 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
+ 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
+ const paths = (required.revealed ? EYE_OFF_PATHS : EYE_PATHS).map(d => createElement('path', { key: d, d }));
26
+ 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
+ /**
29
+ * The show/hide state of a password field. Starts hidden, and re-hides when the value is cleared
30
+ * — the usual sign a save succeeded — so a revealed secret never outlives its submit.
31
+ * @complexity O(1).
32
+ */
33
+ function useSecretReveal(required, _optional = {}) {
34
+ const [revealed, setRevealed] = useState(false);
35
+ const [lastValue, setLastValue] = useState(required.value);
36
+ // Re-hide on the transition to empty only: an empty field may be revealed before typing.
37
+ if (required.value !== lastValue) {
38
+ setLastValue(required.value);
39
+ if (required.value === '')
40
+ setRevealed(false);
41
+ }
42
+ if (required.type !== 'password')
43
+ return { type: required.type ?? 'text', toggle: null };
44
+ // The name flips Show/Hide; no `aria-pressed` on top, or screen readers hear the state twice.
45
+ const labels = required.secretToggleLabels ?? { show: `Show ${required.label}`, hide: `Hide ${required.label}` };
46
+ return { type: revealed ? 'text' : 'password', toggle: { 'data-jini-part': 'kit.secret-toggle', className: 'jini-secret-toggle', type: 'button',
47
+ 'aria-label': revealed ? labels.hide : labels.show, disabled: required.disabled,
48
+ onClick: () => setRevealed(shown => !shown), children: eyeIcon({ revealed }) } };
49
+ }
21
50
  export function useNativeTextField(required, _optional = {}) {
22
51
  const vm = useField(required);
23
- return { ...vm, props: { ...vm.props, id: vm.id, ref: required.ref, type: required.type ?? 'text', value: required.value,
52
+ const reveal = useSecretReveal(required);
53
+ return { ...vm, toggle: reveal.toggle, props: { ...vm.props, id: vm.id, ref: required.ref, type: reveal.type, value: required.value,
54
+ // Off on a secret: once revealed as type="text", enhanced spellcheck would send it to a cloud service.
55
+ spellCheck: reveal.toggle ? false : undefined,
24
56
  placeholder: required.placeholder, onChange: (event) => {
25
57
  if (!required.disabled)
26
58
  required.onValueChange({ value: event.currentTarget.value });
@@ -12,7 +12,10 @@ export const NativeIconButton = forwardRef(function NativeIconButton(props, ref)
12
12
  });
13
13
  export const NativeTextField = forwardRef(function NativeTextField(props, ref) {
14
14
  const vm = useNativeTextField({ ...props, ref });
15
- return _jsxs("label", { htmlFor: vm.id, ...vm.labelAttrs, children: [_jsx("span", { className: "jini-field-label", children: vm.label }), _jsx("input", { ...vm.props })] });
15
+ const field = _jsxs("label", { htmlFor: vm.id, ...vm.labelAttrs, children: [_jsx("span", { className: "jini-field-label", children: vm.label }), _jsx("input", { ...vm.props })] });
16
+ // The eye toggle sits OUTSIDE the label (its name would otherwise join the input's accessible name),
17
+ // in one wrapper with it so a host can pin it inside the input's right edge.
18
+ return vm.toggle ? _jsxs("span", { className: "jini-secret", children: [field, _jsx("button", { ...vm.toggle })] }) : field;
16
19
  });
17
20
  export const NativeTextArea = forwardRef(function NativeTextArea(props, ref) {
18
21
  const vm = useNativeTextArea({ ...props, ref });
@@ -33,8 +33,13 @@ export interface TextFieldProps extends BaseProps {
33
33
  readonly placeholder?: string;
34
34
  readonly type?: 'text' | 'password' | 'email' | 'search' | 'url' | 'tel' | 'number';
35
35
  readonly ref?: Ref<HTMLInputElement>;
36
+ /** `type: 'password'` only: the eye toggle's full accessible names. Default "Show <label>" / "Hide <label>". */
37
+ readonly secretToggleLabels?: {
38
+ readonly show: string;
39
+ readonly hide: string;
40
+ };
36
41
  }
37
- export interface TextAreaProps extends Omit<TextFieldProps, 'type' | 'ref'> {
42
+ export interface TextAreaProps extends Omit<TextFieldProps, 'type' | 'ref' | 'secretToggleLabels'> {
38
43
  readonly rows?: number;
39
44
  readonly ref?: Ref<HTMLTextAreaElement>;
40
45
  }
@@ -43,7 +48,7 @@ export interface SelectOption {
43
48
  readonly label: string;
44
49
  readonly disabled?: boolean;
45
50
  }
46
- export interface SelectProps extends Omit<TextFieldProps, 'type' | 'placeholder' | 'ref'> {
51
+ export interface SelectProps extends Omit<TextFieldProps, 'type' | 'placeholder' | 'ref' | 'secretToggleLabels'> {
47
52
  readonly options: readonly SelectOption[];
48
53
  readonly ref?: Ref<HTMLSelectElement>;
49
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/ui-kit",
3
- "version": "0.6.11",
3
+ "version": "0.6.15-rc.1",
4
4
  "description": "Framework-free UI contract, injected React facades and native defaults.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {