@cosxai/ui 0.13.5 → 0.15.0
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/package.json
CHANGED
package/src/command/rank.ts
CHANGED
|
@@ -16,6 +16,11 @@ interface Ranked {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function scoreOne(item: CommandItem, q: string): number | null {
|
|
19
|
+
// Secret items exist only for their exact phrase — never in empty
|
|
20
|
+
// listings, never via partial/label matches.
|
|
21
|
+
if (item.secret) {
|
|
22
|
+
return q.trim().toLowerCase() === item.secret.toLowerCase() ? 200 : null;
|
|
23
|
+
}
|
|
19
24
|
if (!q) return 1; // empty query — everyone passes, sorted by registration order
|
|
20
25
|
const ql = q.toLowerCase();
|
|
21
26
|
const lab = item.label.toLowerCase();
|
package/src/command/types.ts
CHANGED
|
@@ -23,4 +23,10 @@ export interface CommandItem {
|
|
|
23
23
|
// palette — useful when the command opens a sub-flow that wants
|
|
24
24
|
// to keep the palette open.
|
|
25
25
|
run: (api: { close: () => void }) => void;
|
|
26
|
+
// Cheat-code gating: when set, the item is INVISIBLE to every
|
|
27
|
+
// query except an exact (case-insensitive, trimmed) match of this
|
|
28
|
+
// phrase — it never appears in empty-query listings or partial
|
|
29
|
+
// matches. Use for hidden debug/test menus; pair with a "Debug"
|
|
30
|
+
// group so the reveal is self-describing.
|
|
31
|
+
secret?: string;
|
|
26
32
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { cn } from "../lib/cn";
|
|
2
|
+
|
|
3
|
+
// SegmentedControl — a horizontal set of mutually-exclusive options
|
|
4
|
+
// rendered as one pill-shaped track with a raised "active" segment.
|
|
5
|
+
// Use it where the choice swaps the content below it (mode switch,
|
|
6
|
+
// filter scope), i.e. wherever a pair of radio buttons would be
|
|
7
|
+
// semantically right but visually too quiet. For true page-level
|
|
8
|
+
// navigation reach for router links instead; for on/off state use
|
|
9
|
+
// ToggleSwitch.
|
|
10
|
+
//
|
|
11
|
+
// The caller owns the selection: pass `value` + `onChange`, one
|
|
12
|
+
// option per segment. Per-option `disabled` renders the segment
|
|
13
|
+
// non-interactive with a hint via `title` (a disabled option should
|
|
14
|
+
// tell the user why — pass `disabledHint`).
|
|
15
|
+
//
|
|
16
|
+
// Semantics are a radiogroup (not tabs): arrow keys move selection
|
|
17
|
+
// directly, matching native radio behaviour, and screen readers
|
|
18
|
+
// announce "x of y" positioning for free.
|
|
19
|
+
|
|
20
|
+
export interface SegmentedControlOption<V extends string = string> {
|
|
21
|
+
value: V;
|
|
22
|
+
label: React.ReactNode;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
/** Shown as the native tooltip on a disabled segment — say WHY. */
|
|
25
|
+
disabledHint?: string;
|
|
26
|
+
"data-testid"?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SegmentedControlProps<V extends string = string> {
|
|
30
|
+
value: V;
|
|
31
|
+
onChange: (next: V) => void;
|
|
32
|
+
options: SegmentedControlOption<V>[];
|
|
33
|
+
/** Disables the whole control (per-option disabled still applies). */
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
/** "full" stretches the track and gives segments equal width. */
|
|
36
|
+
fit?: "auto" | "full";
|
|
37
|
+
"aria-label"?: string;
|
|
38
|
+
className?: string;
|
|
39
|
+
"data-testid"?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function SegmentedControl<V extends string = string>({
|
|
43
|
+
value,
|
|
44
|
+
onChange,
|
|
45
|
+
options,
|
|
46
|
+
disabled,
|
|
47
|
+
fit = "auto",
|
|
48
|
+
"aria-label": ariaLabel,
|
|
49
|
+
className,
|
|
50
|
+
"data-testid": testId,
|
|
51
|
+
}: SegmentedControlProps<V>) {
|
|
52
|
+
// Arrow keys move selection to the nearest enabled neighbour,
|
|
53
|
+
// wrapping — the native radio-group keyboard model.
|
|
54
|
+
const move = (from: number, step: 1 | -1) => {
|
|
55
|
+
for (let i = 1; i <= options.length; i++) {
|
|
56
|
+
const next = options[(from + step * i + options.length * i) % options.length];
|
|
57
|
+
if (next && !next.disabled) {
|
|
58
|
+
onChange(next.value);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
role="radiogroup"
|
|
67
|
+
aria-label={ariaLabel}
|
|
68
|
+
className={cn("ck-segmented", className)}
|
|
69
|
+
data-testid={testId}
|
|
70
|
+
style={{
|
|
71
|
+
display: fit === "full" ? "flex" : "inline-flex",
|
|
72
|
+
alignItems: "stretch",
|
|
73
|
+
gap: 2,
|
|
74
|
+
padding: 2,
|
|
75
|
+
borderRadius: 8,
|
|
76
|
+
border: "1px solid var(--ck-border-subtle)",
|
|
77
|
+
background: "var(--ck-bg-muted)",
|
|
78
|
+
opacity: disabled ? 0.5 : 1,
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{options.map((opt, i) => {
|
|
82
|
+
const selected = opt.value === value;
|
|
83
|
+
const optDisabled = disabled || opt.disabled;
|
|
84
|
+
return (
|
|
85
|
+
<button
|
|
86
|
+
key={opt.value}
|
|
87
|
+
type="button"
|
|
88
|
+
role="radio"
|
|
89
|
+
aria-checked={selected}
|
|
90
|
+
disabled={optDisabled}
|
|
91
|
+
title={opt.disabled ? opt.disabledHint : undefined}
|
|
92
|
+
data-testid={opt["data-testid"]}
|
|
93
|
+
// Roving tabindex: the group is one tab stop; arrows walk it.
|
|
94
|
+
tabIndex={selected ? 0 : -1}
|
|
95
|
+
onClick={() => !optDisabled && !selected && onChange(opt.value)}
|
|
96
|
+
onKeyDown={(e) => {
|
|
97
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
move(i, 1);
|
|
100
|
+
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
move(i, -1);
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
105
|
+
style={{
|
|
106
|
+
flex: fit === "full" ? 1 : undefined,
|
|
107
|
+
padding: "6px 14px",
|
|
108
|
+
borderRadius: 6,
|
|
109
|
+
border: "1px solid transparent",
|
|
110
|
+
font: `500 13px/1.2 var(--ck-font-sans)`,
|
|
111
|
+
color: selected
|
|
112
|
+
? "var(--ck-text-primary)"
|
|
113
|
+
: "var(--ck-text-secondary)",
|
|
114
|
+
background: selected ? "var(--ck-bg-surface)" : "transparent",
|
|
115
|
+
boxShadow: selected ? "0 1px 2px rgba(0,0,0,0.08)" : "none",
|
|
116
|
+
borderColor: selected ? "var(--ck-border-subtle)" : "transparent",
|
|
117
|
+
cursor: optDisabled ? "not-allowed" : "pointer",
|
|
118
|
+
opacity: opt.disabled && !disabled ? 0.45 : 1,
|
|
119
|
+
whiteSpace: "nowrap",
|
|
120
|
+
transition: "background 120ms ease, color 120ms ease",
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{opt.label}
|
|
124
|
+
</button>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
package/src/primitives/index.ts
CHANGED
|
@@ -32,3 +32,5 @@ export { SignaturePad } from "./SignaturePad";
|
|
|
32
32
|
export type { SignaturePadProps, SignaturePadHandle } from "./SignaturePad";
|
|
33
33
|
export { CopyField } from "./CopyField";
|
|
34
34
|
export type { CopyFieldProps } from "./CopyField";
|
|
35
|
+
export { SegmentedControl } from "./SegmentedControl";
|
|
36
|
+
export type { SegmentedControlProps, SegmentedControlOption } from "./SegmentedControl";
|