@birdycom/omniaui-react 0.0.0-stage → 0.11.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/LICENSE +21 -0
- package/README.md +20 -2
- package/dist/index.d.ts +2640 -0
- package/dist/index.js +4813 -0
- package/manifest.json +838 -0
- package/package.json +69 -4
- package/src/blocks/BlockAboutTimeline.tsx +46 -0
- package/src/blocks/BlockAppHeader.tsx +52 -0
- package/src/blocks/BlockAppShell.tsx +56 -0
- package/src/blocks/BlockAuth.tsx +53 -0
- package/src/blocks/BlockAuthSplit.tsx +84 -0
- package/src/blocks/BlockContactGrid.tsx +71 -0
- package/src/blocks/BlockCta.tsx +23 -0
- package/src/blocks/BlockDataTable.tsx +57 -0
- package/src/blocks/BlockFaq.tsx +55 -0
- package/src/blocks/BlockFeatures.tsx +38 -0
- package/src/blocks/BlockFooter.tsx +50 -0
- package/src/blocks/BlockFormPage.tsx +26 -0
- package/src/blocks/BlockHero.tsx +27 -0
- package/src/blocks/BlockHeroCode.tsx +63 -0
- package/src/blocks/BlockHeroOrbit.tsx +73 -0
- package/src/blocks/BlockHeroSplit.tsx +28 -0
- package/src/blocks/BlockModuleHeader.tsx +34 -0
- package/src/blocks/BlockPageHero.tsx +25 -0
- package/src/blocks/BlockPortfolioGrid.tsx +50 -0
- package/src/blocks/BlockPricing.tsx +62 -0
- package/src/blocks/BlockProcess.tsx +50 -0
- package/src/blocks/BlockProductGrid.tsx +54 -0
- package/src/blocks/BlockProfileHeader.tsx +57 -0
- package/src/blocks/BlockShowcase.tsx +58 -0
- package/src/blocks/BlockSiteFooter.tsx +60 -0
- package/src/blocks/BlockSiteHeader.tsx +42 -0
- package/src/blocks/BlockSpotlight.tsx +58 -0
- package/src/blocks/BlockStats.tsx +26 -0
- package/src/blocks/BlockStatsCounter.tsx +31 -0
- package/src/blocks/BlockTeamList.tsx +48 -0
- package/src/blocks/BlockTwoFactor.tsx +57 -0
- package/src/blocks/BlockWizardDialog.tsx +101 -0
- package/src/components/Accordion.tsx +31 -0
- package/src/components/AccordionAccent.tsx +39 -0
- package/src/components/AffixInput.tsx +47 -0
- package/src/components/Alert.tsx +24 -0
- package/src/components/AlertDialog.tsx +60 -0
- package/src/components/AlphaTabs.tsx +63 -0
- package/src/components/Attachment.tsx +140 -0
- package/src/components/Avatar.tsx +32 -0
- package/src/components/BackLink.tsx +11 -0
- package/src/components/Badge.tsx +14 -0
- package/src/components/BarcodeInput.tsx +64 -0
- package/src/components/Breadcrumb.tsx +32 -0
- package/src/components/BrowserFrame.tsx +48 -0
- package/src/components/BusyOverlay.tsx +52 -0
- package/src/components/Button.tsx +49 -0
- package/src/components/Card.tsx +27 -0
- package/src/components/ChartLine.tsx +119 -0
- package/src/components/ChatComposer.tsx +73 -0
- package/src/components/Checkbox.tsx +14 -0
- package/src/components/Checklist.tsx +45 -0
- package/src/components/ChipFilter.tsx +60 -0
- package/src/components/Collapsible.tsx +43 -0
- package/src/components/ColorField.tsx +48 -0
- package/src/components/ColorSelect.tsx +49 -0
- package/src/components/ColumnMenu.tsx +66 -0
- package/src/components/Command.tsx +113 -0
- package/src/components/ConfirmDialog.tsx +95 -0
- package/src/components/CookieConsent.tsx +137 -0
- package/src/components/CopyField.tsx +44 -0
- package/src/components/Counter.tsx +21 -0
- package/src/components/DataTable.tsx +201 -0
- package/src/components/DecimalInput.tsx +87 -0
- package/src/components/DefinitionList.tsx +38 -0
- package/src/components/DocViewer.tsx +110 -0
- package/src/components/Dropdown.tsx +52 -0
- package/src/components/Dropzone.tsx +46 -0
- package/src/components/EmojiPicker.tsx +72 -0
- package/src/components/EmptyState.tsx +27 -0
- package/src/components/EntityCard.tsx +56 -0
- package/src/components/FactList.tsx +43 -0
- package/src/components/FilterCounts.tsx +50 -0
- package/src/components/FlipCard.tsx +55 -0
- package/src/components/FormField.tsx +54 -0
- package/src/components/Greeting.tsx +63 -0
- package/src/components/Heading.tsx +27 -0
- package/src/components/Icon.tsx +25 -0
- package/src/components/ImageZoom.tsx +51 -0
- package/src/components/Input.tsx +21 -0
- package/src/components/InputGroup.tsx +24 -0
- package/src/components/LanguageSelect.tsx +64 -0
- package/src/components/LauncherCard.tsx +50 -0
- package/src/components/Lightbox.tsx +83 -0
- package/src/components/LinkArrow.tsx +27 -0
- package/src/components/ListToolbar.tsx +67 -0
- package/src/components/Marquee.tsx +38 -0
- package/src/components/MediaCard.tsx +47 -0
- package/src/components/MediaRow.tsx +53 -0
- package/src/components/MediaThumb.tsx +49 -0
- package/src/components/Message.tsx +171 -0
- package/src/components/MessageThread.tsx +177 -0
- package/src/components/Modal.tsx +81 -0
- package/src/components/Navmenu.tsx +62 -0
- package/src/components/NextTeaser.tsx +29 -0
- package/src/components/NumberInput.tsx +64 -0
- package/src/components/OtpInput.tsx +48 -0
- package/src/components/PageHeader.tsx +43 -0
- package/src/components/Pagination.tsx +42 -0
- package/src/components/PasswordInput.tsx +36 -0
- package/src/components/PasswordRequirements.tsx +46 -0
- package/src/components/PhoneInput.tsx +93 -0
- package/src/components/Popover.tsx +34 -0
- package/src/components/PriceCard.tsx +83 -0
- package/src/components/ProductMark.tsx +35 -0
- package/src/components/Progress.tsx +32 -0
- package/src/components/ProjectCard.tsx +46 -0
- package/src/components/Prose.tsx +79 -0
- package/src/components/RecoveryCodes.tsx +65 -0
- package/src/components/Segmented.tsx +47 -0
- package/src/components/Select.tsx +27 -0
- package/src/components/SeverityCard.tsx +53 -0
- package/src/components/Sheet.tsx +62 -0
- package/src/components/Sidebar.tsx +145 -0
- package/src/components/Skeleton.tsx +12 -0
- package/src/components/SortableRow.tsx +46 -0
- package/src/components/StackCard.tsx +40 -0
- package/src/components/StackList.tsx +42 -0
- package/src/components/StatCard.tsx +49 -0
- package/src/components/Stepper.tsx +54 -0
- package/src/components/Switch.tsx +18 -0
- package/src/components/Table.tsx +75 -0
- package/src/components/TableActions.tsx +54 -0
- package/src/components/TableCompact.tsx +61 -0
- package/src/components/Tabs.tsx +45 -0
- package/src/components/TagInput.tsx +73 -0
- package/src/components/TerminalLog.tsx +63 -0
- package/src/components/Textarea.tsx +19 -0
- package/src/components/ThemeSwitch.tsx +45 -0
- package/src/components/TimeInput.tsx +76 -0
- package/src/components/Timeline.tsx +45 -0
- package/src/components/Toast.tsx +138 -0
- package/src/components/ToggleField.tsx +39 -0
- package/src/components/Tooltip.tsx +14 -0
- package/src/components/TopProgress.tsx +54 -0
- package/src/components/Tree.tsx +97 -0
- package/src/components/UpgradeBanner.tsx +62 -0
- package/src/components/User.tsx +31 -0
- package/src/components/VersionBanner.tsx +41 -0
- package/src/hooks.tsx +141 -0
- package/src/index.ts +142 -0
- package/src/utils.ts +25 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// File: packages/react/src/components/ChartLine.tsx
|
|
2
|
+
// Map: types + demo data -> geometry (smooth path, area, percentages) -> ChartLine (head -> y axis -> plot: svg series + hover columns -> x axis -> legend)
|
|
3
|
+
// Same markup as packages/ui/snippets/chart-line.html. The paths are computed here; tooltips open on :hover / :focus-visible (CSS only).
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
|
|
6
|
+
import { cx } from "../utils";
|
|
7
|
+
|
|
8
|
+
export interface ChartLineSeries {
|
|
9
|
+
name: string;
|
|
10
|
+
values: number[];
|
|
11
|
+
dashed?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChartLineProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
15
|
+
title?: ReactNode;
|
|
16
|
+
sub?: ReactNode;
|
|
17
|
+
/** Big number in the head (`oc-chart-line__value`). */
|
|
18
|
+
value?: ReactNode;
|
|
19
|
+
series?: ChartLineSeries[];
|
|
20
|
+
/** One label per point, used in the tooltips (e.g. "January"). */
|
|
21
|
+
labels?: string[];
|
|
22
|
+
/** Short x axis labels (default: labels; "Jan"… for the demo months). */
|
|
23
|
+
xLabels?: string[];
|
|
24
|
+
/** Top of the y axis (default: a round number above the highest value). */
|
|
25
|
+
max?: number;
|
|
26
|
+
/** Gridline rows (default 4). */
|
|
27
|
+
rows?: number;
|
|
28
|
+
/** "area" fills the series, "spark" hides axes and grid. */
|
|
29
|
+
variant?: "line" | "area" | "spark";
|
|
30
|
+
/** Dots always visible. */
|
|
31
|
+
dots?: boolean;
|
|
32
|
+
/** aria-label of the point columns. */
|
|
33
|
+
label?: string;
|
|
34
|
+
format?: (value: number) => string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DEMO: ChartLineSeries[] = [
|
|
38
|
+
{ name: "Desktop", values: [186, 305, 237, 73, 209, 214] },
|
|
39
|
+
{ name: "Mobile", values: [80, 200, 120, 190, 130, 140] },
|
|
40
|
+
];
|
|
41
|
+
const MONTHS = ["January", "February", "March", "April", "May", "June"];
|
|
42
|
+
const SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];
|
|
43
|
+
|
|
44
|
+
/** Number with at most 2 decimals and no trailing zeros ("75", "37.5", "4.69"). */
|
|
45
|
+
const n2 = (n: number) => String(parseFloat(n.toFixed(2)));
|
|
46
|
+
/** Round axis maximum: step = max / rows rounded up to 1, 1.5, 2, 2.5, 3, 4, 5, 6, 8 or 10 x 10^k. */
|
|
47
|
+
export function niceMax(value: number, rows = 4) {
|
|
48
|
+
if (value <= 0) return rows;
|
|
49
|
+
const raw = value / rows, pow = 10 ** Math.floor(Math.log10(raw));
|
|
50
|
+
return ([1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10].find((s) => s * pow >= raw) ?? 10) * pow * rows;
|
|
51
|
+
}
|
|
52
|
+
/** Smooth path through the points (x 0-100, y = 100 - value / max * 100); control points stay between their two ends. */
|
|
53
|
+
export function chartLinePath(values: number[], max: number) {
|
|
54
|
+
const pts = values.map((v, i) => [values.length > 1 ? (i / (values.length - 1)) * 100 : 50, 100 - (v / max) * 100]);
|
|
55
|
+
if (!pts.length) return "";
|
|
56
|
+
const clamp = (v: number, a: number, b: number) => Math.min(Math.max(v, Math.min(a, b)), Math.max(a, b));
|
|
57
|
+
let d = `M${n2(pts[0][0])} ${n2(pts[0][1])}`;
|
|
58
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
59
|
+
const p0 = pts[i - 1] ?? pts[i], p1 = pts[i], p2 = pts[i + 1], p3 = pts[i + 2] ?? p2;
|
|
60
|
+
const c1 = [p1[0] + (p2[0] - p0[0]) / 6, clamp(p1[1] + (p2[1] - p0[1]) / 6, p1[1], p2[1])];
|
|
61
|
+
const c2 = [p2[0] - (p3[0] - p1[0]) / 6, clamp(p2[1] - (p3[1] - p1[1]) / 6, p1[1], p2[1])];
|
|
62
|
+
d += ` C${n2(c1[0])} ${n2(c1[1])} ${n2(c2[0])} ${n2(c2[1])} ${n2(p2[0])} ${n2(p2[1])}`;
|
|
63
|
+
}
|
|
64
|
+
return d;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `figure.oc-chart-line`: line / area chart in inline SVG with axes, legend and tooltips. */
|
|
68
|
+
export const ChartLine = forwardRef<HTMLElement, ChartLineProps>(function ChartLine(
|
|
69
|
+
{ title = "Visitors", sub = "January – June 2026", value, series = DEMO, labels: labelsProp, xLabels, max, rows = 4, variant = "line", dots = true,
|
|
70
|
+
label = "Visitors per month", format = String, className, ...rest },
|
|
71
|
+
ref,
|
|
72
|
+
) {
|
|
73
|
+
const top = max ?? niceMax(Math.max(0, ...series.flatMap((s) => s.values)), rows);
|
|
74
|
+
const count = Math.max(0, ...series.map((s) => s.values.length));
|
|
75
|
+
const labels = labelsProp ?? MONTHS;
|
|
76
|
+
const axis = xLabels ?? labelsProp ?? SHORT;
|
|
77
|
+
return (
|
|
78
|
+
<figure ref={ref} className={cx("oc-chart-line", variant !== "line" && `oc-chart-line--${variant}`, dots && "oc-chart-line--dots", className)} {...rest}>
|
|
79
|
+
{(title != null || sub != null || value != null) && (
|
|
80
|
+
<figcaption className="oc-chart-line__head">
|
|
81
|
+
{title != null && <span className="oc-chart-line__title">{title}</span>}
|
|
82
|
+
{sub != null && <span className="oc-chart-line__sub">{sub}</span>}
|
|
83
|
+
{value != null && <span className="oc-chart-line__value">{value}</span>}
|
|
84
|
+
</figcaption>
|
|
85
|
+
)}
|
|
86
|
+
<div className="oc-chart-line__body">
|
|
87
|
+
<ul className="oc-chart-line__y" aria-hidden="true">
|
|
88
|
+
{Array.from({ length: rows + 1 }, (_, i) => <li key={i}>{format(top - (top / rows) * i)}</li>)}
|
|
89
|
+
</ul>
|
|
90
|
+
<div className="oc-chart-line__plot" style={rows !== 4 ? ({ "--oc-chart-line-rows": rows } as CSSProperties) : undefined}>
|
|
91
|
+
<svg className="oc-chart-line__svg" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
|
|
92
|
+
{series.map((s, i) => {
|
|
93
|
+
const d = chartLinePath(s.values, top);
|
|
94
|
+
return (
|
|
95
|
+
<g key={i} className="oc-chart-line__series">
|
|
96
|
+
<path className="oc-chart-line__area" d={`${d} L100 100 L0 100 Z`} />
|
|
97
|
+
<path className={cx("oc-chart-line__path", s.dashed && "oc-chart-line__path--dashed")} d={d} />
|
|
98
|
+
</g>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</svg>
|
|
102
|
+
<ul className="oc-chart-line__cols" style={{ "--n": count } as CSSProperties} aria-label={label}>
|
|
103
|
+
{Array.from({ length: count }, (_, i) => (
|
|
104
|
+
<li key={i} className="oc-chart-line__col" style={{ "--x": i } as CSSProperties} tabIndex={0} aria-label={`${labels[i] ?? i + 1}: ${series.map((s) => `${s.name} ${format(s.values[i])}`).join(", ")}`}>
|
|
105
|
+
{series.map((s, j) => <i key={j} className="oc-chart-line__dot" style={{ "--y": `${n2((s.values[i] / top) * 100)}%` } as CSSProperties} />)}
|
|
106
|
+
<span className="oc-chart-line__tip" aria-hidden="true">
|
|
107
|
+
<strong>{labels[i] ?? i + 1}</strong>
|
|
108
|
+
{series.map((s, j) => <span key={j} className="oc-chart-line__row">{s.name}<b>{format(s.values[i])}</b></span>)}
|
|
109
|
+
</span>
|
|
110
|
+
</li>
|
|
111
|
+
))}
|
|
112
|
+
</ul>
|
|
113
|
+
</div>
|
|
114
|
+
<ol className="oc-chart-line__x" aria-hidden="true">{axis.map((l, i) => <li key={i}>{l}</li>)}</ol>
|
|
115
|
+
</div>
|
|
116
|
+
<ul className="oc-chart-legend">{series.map((s, i) => <li key={i}><span className="oc-chart-swatch" />{s.name}</li>)}</ul>
|
|
117
|
+
</figure>
|
|
118
|
+
);
|
|
119
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// File: packages/react/src/components/ChatComposer.tsx
|
|
2
|
+
// Map: props -> ChatComposer (form -> label + textarea -> bar: tools, hint, send) -> submit / Enter handling
|
|
3
|
+
// Same markup as packages/ui/snippets/chat-composer.html. React adds onSend and (optional) Enter to send.
|
|
4
|
+
"use client";
|
|
5
|
+
|
|
6
|
+
import { forwardRef, useId } from "react";
|
|
7
|
+
import type { FormHTMLAttributes, KeyboardEvent, ReactNode } from "react";
|
|
8
|
+
import { cx } from "../utils";
|
|
9
|
+
import { useControllable } from "../hooks";
|
|
10
|
+
|
|
11
|
+
/** The event type React gives to a form's onSubmit (differs between React 18 and 19 typings). */
|
|
12
|
+
type SubmitEvent = Parameters<NonNullable<FormHTMLAttributes<HTMLFormElement>["onSubmit"]>>[0];
|
|
13
|
+
|
|
14
|
+
export interface ChatComposerProps extends Omit<FormHTMLAttributes<HTMLFormElement>, "onChange"> {
|
|
15
|
+
value?: string;
|
|
16
|
+
defaultValue?: string;
|
|
17
|
+
onValueChange?: (value: string) => void;
|
|
18
|
+
/** Called with the trimmed text on submit; the field is cleared afterwards. */
|
|
19
|
+
onSend?: (text: string) => void;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
/** Accessible label of the field (visually hidden). */
|
|
22
|
+
label?: string;
|
|
23
|
+
/** Tool buttons (`.oc-chat-composer__tool`), e.g. attach or an EmojiPicker. */
|
|
24
|
+
tools?: ReactNode;
|
|
25
|
+
hint?: ReactNode;
|
|
26
|
+
inline?: boolean;
|
|
27
|
+
/** Enter sends, Shift+Enter adds a new line. Default true. */
|
|
28
|
+
sendOnEnter?: boolean;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `form.oc-chat-composer`: growing textarea + toolbar + send button. */
|
|
33
|
+
export const ChatComposer = forwardRef<HTMLFormElement, ChatComposerProps>(function ChatComposer(
|
|
34
|
+
{ value, defaultValue = "", onValueChange, onSend, placeholder = "Write a message…", label = "Message", tools, hint, inline, sendOnEnter = true, disabled, className, onSubmit, ...rest },
|
|
35
|
+
ref,
|
|
36
|
+
) {
|
|
37
|
+
const id = `oc-chat-${useId().replace(/:/g, "")}`;
|
|
38
|
+
const [text, setText] = useControllable(value, defaultValue, onValueChange);
|
|
39
|
+
const submit = (e: SubmitEvent) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
onSubmit?.(e);
|
|
42
|
+
if (!text.trim() || disabled) return;
|
|
43
|
+
onSend?.(text.trim());
|
|
44
|
+
setText("");
|
|
45
|
+
};
|
|
46
|
+
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
47
|
+
if (sendOnEnter && e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
e.currentTarget.form?.requestSubmit();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return (
|
|
53
|
+
<form ref={ref} className={cx("oc-chat-composer", inline && "oc-chat-composer--inline", className)} onSubmit={submit} {...rest}>
|
|
54
|
+
<label className="oc-visually-hidden" htmlFor={id}>{label}</label>
|
|
55
|
+
<textarea
|
|
56
|
+
className="oc-chat-composer__input"
|
|
57
|
+
id={id}
|
|
58
|
+
rows={1}
|
|
59
|
+
placeholder={placeholder}
|
|
60
|
+
required
|
|
61
|
+
disabled={disabled}
|
|
62
|
+
value={text}
|
|
63
|
+
onChange={(e) => setText(e.target.value)}
|
|
64
|
+
onKeyDown={onKeyDown}
|
|
65
|
+
/>
|
|
66
|
+
<div className="oc-chat-composer__bar">
|
|
67
|
+
{tools != null && <div className="oc-chat-composer__tools">{tools}</div>}
|
|
68
|
+
{hint != null && <span className="oc-chat-composer__hint">{hint}</span>}
|
|
69
|
+
<button type="submit" className="oc-chat-composer__send" aria-label="Send" disabled={disabled}>➤</button>
|
|
70
|
+
</div>
|
|
71
|
+
</form>
|
|
72
|
+
);
|
|
73
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { InputHTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
|
|
6
|
+
/** When set the input is wrapped in a <label> with this text. */
|
|
7
|
+
label?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** `<input class="oc-checkbox" type="checkbox">`, wrapped in a label when `label` is given. */
|
|
11
|
+
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox({ label, className, ...rest }, ref) {
|
|
12
|
+
const input = <input ref={ref} type="checkbox" className={cx("oc-checkbox", className)} {...rest} />;
|
|
13
|
+
return label != null ? <label>{input} {label}</label> : input;
|
|
14
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import type { HTMLAttributes, LiHTMLAttributes, ReactNode } from "react";
|
|
3
|
+
import { cx } from "../utils";
|
|
4
|
+
|
|
5
|
+
export interface ChecklistProps extends HTMLAttributes<HTMLUListElement> {
|
|
6
|
+
/** Bordered rows with an action slot. */
|
|
7
|
+
card?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** `<ul class="oc-checklist">` */
|
|
11
|
+
export const Checklist = forwardRef<HTMLUListElement, ChecklistProps>(function Checklist({ card, className, ...rest }, ref) {
|
|
12
|
+
return <ul ref={ref} className={cx("oc-checklist", card && "oc-checklist--card", className)} {...rest} />;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export interface ChecklistItemProps extends Omit<LiHTMLAttributes<HTMLLIElement>, "title"> {
|
|
16
|
+
/** Rule is met / step is done (`is-done`). */
|
|
17
|
+
done?: boolean;
|
|
18
|
+
/** Card variant: title + hint text column. Without it `children` are the row content. */
|
|
19
|
+
title?: ReactNode;
|
|
20
|
+
hint?: ReactNode;
|
|
21
|
+
/** Card variant: trailing action (usually a button or link). */
|
|
22
|
+
action?: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `<li class="oc-checklist-item is-done">` */
|
|
26
|
+
export const ChecklistItem = forwardRef<HTMLLIElement, ChecklistItemProps>(function ChecklistItem(
|
|
27
|
+
{ done, title, hint, action, className, children, ...rest },
|
|
28
|
+
ref,
|
|
29
|
+
) {
|
|
30
|
+
return (
|
|
31
|
+
<li ref={ref} className={cx("oc-checklist-item", done && "is-done", className)} {...rest}>
|
|
32
|
+
{title != null ? (
|
|
33
|
+
<>
|
|
34
|
+
<span className="oc-checklist-text">
|
|
35
|
+
<span className="oc-checklist-title">{title}</span>
|
|
36
|
+
{hint != null && <span className="oc-checklist-hint">{hint}</span>}
|
|
37
|
+
</span>
|
|
38
|
+
{action}
|
|
39
|
+
</>
|
|
40
|
+
) : (
|
|
41
|
+
children
|
|
42
|
+
)}
|
|
43
|
+
</li>
|
|
44
|
+
);
|
|
45
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// File: packages/react/src/components/ChipFilter.tsx
|
|
2
|
+
// Map: types -> component (group -> one chip label per option: checkbox or radio + text + optional count)
|
|
3
|
+
// Same markup as packages/ui/snippets/chip-filter.html. multiple = checkboxes (array value), else radios (string).
|
|
4
|
+
"use client";
|
|
5
|
+
|
|
6
|
+
import { forwardRef, useId } from "react";
|
|
7
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
8
|
+
import { cx } from "../utils";
|
|
9
|
+
import { useControllable } from "../hooks";
|
|
10
|
+
|
|
11
|
+
export interface ChipFilterOption {
|
|
12
|
+
value: string;
|
|
13
|
+
label: ReactNode;
|
|
14
|
+
count?: number;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ChipFilterProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> {
|
|
19
|
+
options: ChipFilterOption[];
|
|
20
|
+
/** Several chips at once (checkboxes). */
|
|
21
|
+
multiple?: boolean;
|
|
22
|
+
/** string for single choice, string[] with `multiple`. */
|
|
23
|
+
value?: string | string[];
|
|
24
|
+
defaultValue?: string | string[];
|
|
25
|
+
onChange?: (value: string | string[]) => void;
|
|
26
|
+
name?: string;
|
|
27
|
+
size?: "sm" | "md";
|
|
28
|
+
/** One line that scrolls sideways. */
|
|
29
|
+
scroll?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `.oc-chip-filter`: rounded filter chips. */
|
|
33
|
+
export const ChipFilter = forwardRef<HTMLDivElement, ChipFilterProps>(function ChipFilter(
|
|
34
|
+
{ options, multiple, value, defaultValue, onChange, name, size = "md", scroll, className, ...rest },
|
|
35
|
+
ref,
|
|
36
|
+
) {
|
|
37
|
+
const group = name ?? `oc-chip-${useId().replace(/:/g, "")}`;
|
|
38
|
+
const [current, setCurrent] = useControllable<string | string[]>(value, defaultValue ?? (multiple ? [] : ""), onChange);
|
|
39
|
+
const selected = Array.isArray(current) ? current : current ? [current] : [];
|
|
40
|
+
|
|
41
|
+
// Checkbox: add or remove the value. Radio: replace it.
|
|
42
|
+
const toggle = (v: string) => setCurrent(multiple ? (selected.includes(v) ? selected.filter((s) => s !== v) : [...selected, v]) : v);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cx("oc-chip-filter", size === "sm" && "oc-chip-filter--sm", scroll && "oc-chip-filter--scroll", className)}
|
|
48
|
+
role={multiple ? "group" : "radiogroup"}
|
|
49
|
+
{...rest}
|
|
50
|
+
>
|
|
51
|
+
{options.map((o) => (
|
|
52
|
+
<label key={o.value} className="oc-chip-filter-item">
|
|
53
|
+
<input type={multiple ? "checkbox" : "radio"} name={group} value={o.value} disabled={o.disabled} checked={selected.includes(o.value)} onChange={() => toggle(o.value)} />
|
|
54
|
+
{" "}{o.label}
|
|
55
|
+
{o.count != null && <span className="oc-chip-filter-count">{o.count}</span>}
|
|
56
|
+
</label>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import type { DetailsHTMLAttributes, ReactNode, SyntheticEvent } from "react";
|
|
5
|
+
import { cx } from "../utils";
|
|
6
|
+
import { useControllable } from "../hooks";
|
|
7
|
+
|
|
8
|
+
export interface CollapsibleProps extends Omit<DetailsHTMLAttributes<HTMLDetailsElement>, "title" | "open" | "onToggle"> {
|
|
9
|
+
/** Summary text (`oc-collapsible-title`). */
|
|
10
|
+
title: ReactNode;
|
|
11
|
+
/** Optional leading icon, e.g. an inline `<svg className="oc-collapsible-icon">`. */
|
|
12
|
+
icon?: ReactNode;
|
|
13
|
+
/** Compact paddings. */
|
|
14
|
+
dense?: boolean;
|
|
15
|
+
open?: boolean;
|
|
16
|
+
defaultOpen?: boolean;
|
|
17
|
+
onOpenChange?: (open: boolean) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `<details class="oc-collapsible">` card-style section. */
|
|
21
|
+
export const Collapsible = forwardRef<HTMLDetailsElement, CollapsibleProps>(function Collapsible(
|
|
22
|
+
{ title, icon, dense, open, defaultOpen, onOpenChange, className, children, ...rest },
|
|
23
|
+
ref,
|
|
24
|
+
) {
|
|
25
|
+
const [isOpen, setOpen] = useControllable<boolean>(open, defaultOpen ?? false, onOpenChange);
|
|
26
|
+
return (
|
|
27
|
+
<details
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cx("oc-collapsible", dense && "oc-collapsible--dense", className)}
|
|
30
|
+
open={isOpen}
|
|
31
|
+
onToggle={(e: SyntheticEvent<HTMLDetailsElement>) => {
|
|
32
|
+
if (e.currentTarget.open !== isOpen) setOpen(e.currentTarget.open);
|
|
33
|
+
}}
|
|
34
|
+
{...rest}
|
|
35
|
+
>
|
|
36
|
+
<summary>
|
|
37
|
+
{icon}
|
|
38
|
+
<span className="oc-collapsible-title">{title}</span>
|
|
39
|
+
</summary>
|
|
40
|
+
<div className="oc-collapsible-content">{children}</div>
|
|
41
|
+
</details>
|
|
42
|
+
);
|
|
43
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useState } from "react";
|
|
4
|
+
import type { HTMLAttributes } from "react";
|
|
5
|
+
import { cx } from "../utils";
|
|
6
|
+
import { useControllable } from "../hooks";
|
|
7
|
+
|
|
8
|
+
export interface ColorFieldProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> {
|
|
9
|
+
/** Hex value, e.g. `#3366cc` (controlled). */
|
|
10
|
+
value?: string;
|
|
11
|
+
defaultValue?: string;
|
|
12
|
+
onChange?: (value: string) => void;
|
|
13
|
+
/** Id of the text input (so a <label for> can point at it). */
|
|
14
|
+
id?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const HEX = /^#[0-9a-f]{6}$/i;
|
|
20
|
+
|
|
21
|
+
/** Native color picker + monospaced hex text input, kept in sync. */
|
|
22
|
+
export const ColorField = forwardRef<HTMLDivElement, ColorFieldProps>(function ColorField(
|
|
23
|
+
{ value, defaultValue = "#000000", onChange, id, name, disabled, className, ...rest },
|
|
24
|
+
ref,
|
|
25
|
+
) {
|
|
26
|
+
const [color, setColor] = useControllable<string>(value, defaultValue, onChange);
|
|
27
|
+
const [lastValid, setLastValid] = useState(HEX.test(color) ? color : "#000000");
|
|
28
|
+
const picker = HEX.test(color) ? color : lastValid;
|
|
29
|
+
const update = (next: string) => {
|
|
30
|
+
if (HEX.test(next)) setLastValid(next);
|
|
31
|
+
setColor(next);
|
|
32
|
+
};
|
|
33
|
+
return (
|
|
34
|
+
<div ref={ref} className={cx("oc-color-field", className)} {...rest}>
|
|
35
|
+
<input type="color" aria-label="Pick a color" value={picker} disabled={disabled} onChange={(e) => update(e.target.value)} />
|
|
36
|
+
<input
|
|
37
|
+
className="oc-input"
|
|
38
|
+
type="text"
|
|
39
|
+
id={id}
|
|
40
|
+
name={name}
|
|
41
|
+
aria-label="Color value"
|
|
42
|
+
value={color}
|
|
43
|
+
disabled={disabled}
|
|
44
|
+
onChange={(e) => update(e.target.value)}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// File: packages/react/src/components/ColorSelect.tsx
|
|
2
|
+
// Map: types -> component (fieldset radiogroup -> one label per color: radio + swatch + name)
|
|
3
|
+
// Same markup as packages/ui/snippets/color-select.html.
|
|
4
|
+
"use client";
|
|
5
|
+
|
|
6
|
+
import { forwardRef, useId } from "react";
|
|
7
|
+
import type { CSSProperties, FieldsetHTMLAttributes } from "react";
|
|
8
|
+
import { cx } from "../utils";
|
|
9
|
+
import { useControllable } from "../hooks";
|
|
10
|
+
|
|
11
|
+
export interface ColorSelectOption {
|
|
12
|
+
value: string;
|
|
13
|
+
label: string;
|
|
14
|
+
/** CSS color, use Omnia tokens: "var(--color-red-70)". */
|
|
15
|
+
color: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ColorSelectProps extends Omit<FieldsetHTMLAttributes<HTMLFieldSetElement>, "onChange" | "defaultValue"> {
|
|
20
|
+
options: ColorSelectOption[];
|
|
21
|
+
value?: string;
|
|
22
|
+
defaultValue?: string;
|
|
23
|
+
onChange?: (value: string) => void;
|
|
24
|
+
/** Radio group name (generated when omitted). */
|
|
25
|
+
name?: string;
|
|
26
|
+
/** Rows with visible names instead of a swatch grid. */
|
|
27
|
+
list?: boolean;
|
|
28
|
+
size?: "sm" | "md";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `.oc-color-select`: radio swatches; the color is passed through the --oc-swatch variable. */
|
|
32
|
+
export const ColorSelect = forwardRef<HTMLFieldSetElement, ColorSelectProps>(function ColorSelect(
|
|
33
|
+
{ options, value, defaultValue, onChange, name, list, size = "md", className, ...rest },
|
|
34
|
+
ref,
|
|
35
|
+
) {
|
|
36
|
+
const group = name ?? `oc-color-${useId().replace(/:/g, "")}`;
|
|
37
|
+
const [current, setCurrent] = useControllable<string>(value, defaultValue ?? "", onChange);
|
|
38
|
+
return (
|
|
39
|
+
<fieldset ref={ref} className={cx("oc-color-select", list && "oc-color-select--list", size === "sm" && "oc-color-select--sm", className)} role="radiogroup" {...rest}>
|
|
40
|
+
{options.map((o) => (
|
|
41
|
+
<label key={o.value} className="oc-color-select-option" style={{ "--oc-swatch": o.color } as CSSProperties}>
|
|
42
|
+
<input type="radio" name={group} value={o.value} disabled={o.disabled} checked={current === o.value} onChange={() => setCurrent(o.value)} />
|
|
43
|
+
<span className="oc-color-select-swatch" />
|
|
44
|
+
<span className="oc-color-select-name">{o.label}</span>
|
|
45
|
+
</label>
|
|
46
|
+
))}
|
|
47
|
+
</fieldset>
|
|
48
|
+
);
|
|
49
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// File: packages/react/src/components/ColumnMenu.tsx
|
|
2
|
+
// Map: ColumnMenu = scope -> toolbar trigger -> popover (title, form with one checkbox per column, reset) -> children (the table)
|
|
3
|
+
// Hiding columns is pure CSS: an unchecked [data-col="n"] hides column n inside the scope (n = 1-10).
|
|
4
|
+
// The checkboxes are uncontrolled, so "Reset" (a form reset) restores the defaults.
|
|
5
|
+
import { forwardRef, useId } from "react";
|
|
6
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
7
|
+
import { attrs, cx } from "../utils";
|
|
8
|
+
|
|
9
|
+
export interface ColumnMenuColumn {
|
|
10
|
+
label: ReactNode;
|
|
11
|
+
/** Visible at the start (default true). */
|
|
12
|
+
visible?: boolean;
|
|
13
|
+
/** Always visible; the checkbox is disabled. */
|
|
14
|
+
locked?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ColumnMenuProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
18
|
+
/** One entry per table column, in column order (max 10). */
|
|
19
|
+
columns: ColumnMenuColumn[];
|
|
20
|
+
/** The table (`.oc-table`) the menu controls. */
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
triggerLabel?: ReactNode;
|
|
23
|
+
title?: ReactNode;
|
|
24
|
+
resetLabel?: ReactNode;
|
|
25
|
+
/** Called with the column number (1-based) and its new visibility. */
|
|
26
|
+
onColumnToggle?: (column: number, visible: boolean) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `.oc-column-menu-scope` with a `.oc-column-menu[popover]` column checklist. */
|
|
30
|
+
export const ColumnMenu = forwardRef<HTMLDivElement, ColumnMenuProps>(function ColumnMenu(
|
|
31
|
+
{ columns, children, triggerLabel = "Columns", title = "Visible columns", resetLabel = "Reset", onColumnToggle, className, ...rest },
|
|
32
|
+
ref,
|
|
33
|
+
) {
|
|
34
|
+
const menuId = `oc-column-menu-${useId().replace(/:/g, "")}`;
|
|
35
|
+
return (
|
|
36
|
+
<div ref={ref} className={cx("oc-column-menu-scope", className)} {...rest}>
|
|
37
|
+
<div className="oc-table-toolbar">
|
|
38
|
+
<span className="oc-table-toolbar-spacer" />
|
|
39
|
+
<button className="oc-btn oc-btn--outline oc-btn--sm" type="button" {...attrs({ popovertarget: menuId })}>
|
|
40
|
+
{triggerLabel}
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
<div className="oc-column-menu" id={menuId} {...attrs({ popover: "" })}>
|
|
44
|
+
<p className="oc-column-menu-title">{title}</p>
|
|
45
|
+
<form className="oc-column-menu-list">
|
|
46
|
+
{columns.map((col, i) => (
|
|
47
|
+
<label className="oc-column-menu-item" key={i}>
|
|
48
|
+
<input
|
|
49
|
+
type="checkbox"
|
|
50
|
+
data-col={i + 1}
|
|
51
|
+
defaultChecked={col.locked || col.visible !== false}
|
|
52
|
+
disabled={col.locked}
|
|
53
|
+
onChange={(e) => onColumnToggle?.(i + 1, e.currentTarget.checked)}
|
|
54
|
+
/>{" "}
|
|
55
|
+
{col.label}
|
|
56
|
+
</label>
|
|
57
|
+
))}
|
|
58
|
+
<div className="oc-column-menu-footer">
|
|
59
|
+
<button className="oc-btn oc-btn--ghost oc-btn--sm" type="reset">{resetLabel}</button>
|
|
60
|
+
</div>
|
|
61
|
+
</form>
|
|
62
|
+
</div>
|
|
63
|
+
{children}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useId, useMemo, useState } from "react";
|
|
4
|
+
import type { HTMLAttributes, KeyboardEvent, ReactNode } from "react";
|
|
5
|
+
import { cx } from "../utils";
|
|
6
|
+
|
|
7
|
+
export interface CommandItemData {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
/** Right-aligned keyboard hint. */
|
|
11
|
+
shortcut?: string;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
/** Extra search terms. */
|
|
14
|
+
keywords?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CommandGroupData {
|
|
18
|
+
label?: string;
|
|
19
|
+
items: CommandItemData[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CommandProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
|
23
|
+
groups: CommandGroupData[];
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
emptyText?: ReactNode;
|
|
26
|
+
/** Search value (controlled). */
|
|
27
|
+
query?: string;
|
|
28
|
+
onQueryChange?: (query: string) => void;
|
|
29
|
+
onSelect?: (item: CommandItemData) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const matches = (item: CommandItemData, q: string) =>
|
|
33
|
+
!q || [item.label, ...(item.keywords ?? [])].some((s) => s.toLowerCase().includes(q));
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Command palette. The list is filtered by the search input (non-matching rows get `hidden`),
|
|
37
|
+
* the active row carries `aria-selected="true"`; arrow keys move it and Enter selects.
|
|
38
|
+
*/
|
|
39
|
+
export const Command = forwardRef<HTMLDivElement, CommandProps>(function Command(
|
|
40
|
+
{ groups, placeholder = "Type a command or search...", emptyText = "No results found.", query, onQueryChange, onSelect, className, ...rest },
|
|
41
|
+
ref,
|
|
42
|
+
) {
|
|
43
|
+
const listId = useId();
|
|
44
|
+
const [innerQuery, setInnerQuery] = useState("");
|
|
45
|
+
const q = (query ?? innerQuery).trim().toLowerCase();
|
|
46
|
+
const [activeId, setActiveId] = useState<string | null>(null);
|
|
47
|
+
|
|
48
|
+
const visible = useMemo(
|
|
49
|
+
() => groups.flatMap((g) => g.items).filter((i) => !i.disabled && matches(i, q)),
|
|
50
|
+
[groups, q],
|
|
51
|
+
);
|
|
52
|
+
const active = visible.find((i) => i.id === activeId) ?? visible[0];
|
|
53
|
+
|
|
54
|
+
const move = (delta: number) => {
|
|
55
|
+
if (!visible.length) return;
|
|
56
|
+
const idx = Math.max(0, visible.findIndex((i) => i.id === active?.id));
|
|
57
|
+
setActiveId(visible[(idx + delta + visible.length) % visible.length].id);
|
|
58
|
+
};
|
|
59
|
+
const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
60
|
+
if (e.key === "ArrowDown") { e.preventDefault(); move(1); }
|
|
61
|
+
else if (e.key === "ArrowUp") { e.preventDefault(); move(-1); }
|
|
62
|
+
else if (e.key === "Enter" && active) { e.preventDefault(); onSelect?.(active); }
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div ref={ref} className={cx("oc-command", className)} role="combobox" aria-expanded="true" aria-haspopup="listbox" aria-controls={listId} {...rest}>
|
|
67
|
+
<input
|
|
68
|
+
className="oc-command-input"
|
|
69
|
+
type="search"
|
|
70
|
+
placeholder={placeholder}
|
|
71
|
+
aria-label="Search commands"
|
|
72
|
+
value={query ?? innerQuery}
|
|
73
|
+
onChange={(e) => {
|
|
74
|
+
setInnerQuery(e.target.value);
|
|
75
|
+
setActiveId(null);
|
|
76
|
+
onQueryChange?.(e.target.value);
|
|
77
|
+
}}
|
|
78
|
+
onKeyDown={onKeyDown}
|
|
79
|
+
/>
|
|
80
|
+
<ul className="oc-command-list" role="listbox" id={listId}>
|
|
81
|
+
{groups.map((group, gi) => {
|
|
82
|
+
const anyVisible = group.items.some((i) => matches(i, q));
|
|
83
|
+
return [
|
|
84
|
+
gi > 0 && (
|
|
85
|
+
<li key={`sep-${gi}`} className="oc-command-separator" role="separator" hidden={!anyVisible} />
|
|
86
|
+
),
|
|
87
|
+
group.label && (
|
|
88
|
+
<li key={`label-${gi}`} className="oc-command-group-label" role="presentation" hidden={!anyVisible}>{group.label}</li>
|
|
89
|
+
),
|
|
90
|
+
...group.items.map((item) => (
|
|
91
|
+
<li
|
|
92
|
+
key={item.id}
|
|
93
|
+
className="oc-command-item"
|
|
94
|
+
role="option"
|
|
95
|
+
hidden={!matches(item, q)}
|
|
96
|
+
aria-selected={active?.id === item.id ? true : undefined}
|
|
97
|
+
aria-disabled={item.disabled ? true : undefined}
|
|
98
|
+
onMouseMove={() => !item.disabled && setActiveId(item.id)}
|
|
99
|
+
onClick={() => !item.disabled && onSelect?.(item)}
|
|
100
|
+
>
|
|
101
|
+
{item.label}
|
|
102
|
+
{item.shortcut && <> <span className="oc-command-shortcut">{item.shortcut}</span></>}
|
|
103
|
+
</li>
|
|
104
|
+
)),
|
|
105
|
+
];
|
|
106
|
+
})}
|
|
107
|
+
</ul>
|
|
108
|
+
<div className="oc-command-empty" hidden={groups.some((g) => g.items.some((i) => matches(i, q)))}>
|
|
109
|
+
{emptyText}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
});
|