@robert-stackflow/oneui 0.1.0 → 0.2.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +28 -1
  3. package/components/AppShell.module.css +3 -3
  4. package/components/AppearanceSettings.js +1 -1
  5. package/components/Button.d.ts +9 -2
  6. package/components/Button.js +30 -7
  7. package/components/Button.module.css +16 -7
  8. package/components/Card.d.ts +2 -1
  9. package/components/Card.js +13 -3
  10. package/components/Card.module.css +19 -0
  11. package/components/Checkbox.module.css +3 -3
  12. package/components/Combobox.d.ts +4 -2
  13. package/components/Combobox.js +162 -76
  14. package/components/Combobox.module.css +5 -7
  15. package/components/Control.module.css +18 -5
  16. package/components/Dialog.d.ts +6 -1
  17. package/components/Dialog.js +19 -6
  18. package/components/Dialog.module.css +17 -0
  19. package/components/DialogVariants.d.ts +27 -0
  20. package/components/DialogVariants.js +161 -0
  21. package/components/DialogVariants.module.css +29 -0
  22. package/components/HierarchyPicker.module.css +35 -0
  23. package/components/IconButton.module.css +5 -2
  24. package/components/Indicator.module.css +1 -1
  25. package/components/Input.d.ts +2 -1
  26. package/components/Input.js +12 -4
  27. package/components/Input.module.css +18 -1
  28. package/components/LoadingState.d.ts +3 -1
  29. package/components/LoadingState.js +26 -15
  30. package/components/LoadingState.module.css +3 -0
  31. package/components/MenuItem.d.ts +2 -1
  32. package/components/MenuItem.js +2 -0
  33. package/components/MenuItem.module.css +9 -0
  34. package/components/MultiSelect.d.ts +2 -1
  35. package/components/MultiSelect.js +66 -2
  36. package/components/MultiSelect.module.css +38 -0
  37. package/components/NumberInput.js +1 -0
  38. package/components/OptionList.module.css +12 -0
  39. package/components/PasswordField.js +1 -0
  40. package/components/PinInput.js +1 -0
  41. package/components/Popover.d.ts +4 -1
  42. package/components/Popover.js +11 -5
  43. package/components/Popover.module.css +3 -0
  44. package/components/RadioGroup.module.css +2 -2
  45. package/components/SearchField.js +1 -0
  46. package/components/Select.js +1 -0
  47. package/components/Select.module.css +3 -0
  48. package/components/SelectionTag.d.ts +8 -0
  49. package/components/SelectionTag.js +39 -0
  50. package/components/SettingsLayout.d.ts +3 -1
  51. package/components/SettingsLayout.js +19 -3
  52. package/components/SettingsLayout.module.css +20 -0
  53. package/components/Switch.module.css +8 -5
  54. package/components/Table.d.ts +6 -2
  55. package/components/Table.js +14 -4
  56. package/components/TagInput.js +11 -23
  57. package/components/TagInput.module.css +1 -1
  58. package/components/ThemeProvider.js +17 -3
  59. package/components/TreeGuides.d.ts +9 -0
  60. package/components/TreeGuides.js +39 -0
  61. package/components/TreeGuides.module.css +45 -0
  62. package/components/TreeSelect.d.ts +3 -0
  63. package/components/TreeSelect.js +49 -21
  64. package/components/TreeTable.d.ts +15 -0
  65. package/components/TreeTable.js +151 -0
  66. package/components/TreeTable.module.css +53 -0
  67. package/components/VirtualPickerList.module.css +4 -0
  68. package/components/appearance.d.ts +1 -0
  69. package/components/appearance.js +15 -6
  70. package/components/hierarchy.d.ts +1 -0
  71. package/components/index.d.ts +3 -1
  72. package/components/index.js +6 -0
  73. package/components/usePopover.d.ts +2 -2
  74. package/package.json +30 -3
  75. package/styles/base.css +20 -3
  76. package/styles/blue.css +49 -35
  77. package/styles/minimal.css +1 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-10-28
4
+
5
+ - 新增 MessageDialog、ConfirmDialog、FormDialog 与统一 LoadingState,复用表单校验、异步提交、错误反馈和焦点管理。
6
+ - TreeSelect 新增 category 图标、数量、层级连线样式;保留 tree 风格,支持父子联动多选。
7
+ - 新增 TreeTable,复用 Table 列定义,支持受控展开、多级连线和键盘导航;列支持 align、width。
8
+ - Combobox 新增 mode="input" 触发框补全;保留弹层搜索,支持异步加载、取消、创建与失败重试。
9
+ - MultiSelect 新增 display="tags",与 TagInput 共用标签实现;保留摘要模式及选择限制。
10
+ - 蓝白主题采用 Moment 对应色板、输入 hover 混色和深色参数;修复默认色板迁移与选择器分类栏底色。
11
+ - 统一菜单项 1px 间距,修复快捷键右对齐、宿主 kbd 边框干扰、关闭的 Popover 被布局样式显示等问题。
12
+ - 树选择器底部清空/完成操作使用独立的 8px 按钮间距。
13
+ - 共享应用外壳与示例布局统一,修复图标尺寸、链接文字、动效示例、表单间距、弹层和移动布局。
14
+ - 发布构建使用干净输出并检查包内引用;增加独立 tarball 消费项目的类型、生产与交互验证。
15
+
16
+ ## 0.1.0 — 2026-10-28
17
+
18
+ - 首次发布 React 组件、应用外壳、极简/蓝白主题与图标字符资源。
package/README.md CHANGED
@@ -30,17 +30,44 @@ export function App() {
30
30
 
31
31
  `ThemeProvider` 管理当前文档的主题,应用根部放置一个即可。颜色模式、动效、字体、密度、圆角和自定义配色均由共享层应用。
32
32
 
33
+ 蓝白默认色板取自 Moment 的 `admin/src/styles.css` 最终变量及 `admin/src/main.tsx` 的 victoria 色阶:浅色背景 `#F5F7FB`、面板 `#FFFFFF`、正文 `#25324A`、次级文字 `#8592A8`、边框 `#E3E8F1`。导航/焦点蓝为 `#5067B8`,主按钮为 `#4059AA`,开关为 `#526EDB`;深色同样使用 Moment 的对应值。各颜色用途分开配置,不再共用一种近似蓝。
34
+
35
+ `normalizeAppearance` 使用 `paletteVersion: 2` 兼容旧蓝白默认值;非默认自定义配色保留,已带版本的显式颜色也不会被迁移。Desktop 与 Web 共用该规范化函数。参考值和源码摘要保存在 `tests/fixtures/moment-palette.json`,验证不依赖 Moment 或 Mantine 安装。
36
+
37
+ 蓝白输入与选择控件的 hover 边框复用 Moment 的混色规则:强调色 35% 与基础边框色混合。聚焦或展开时使用完整强调色,错误状态保持错误色;禁用、只读控件不响应 hover。图标、Emoji 和符号选择器的分类栏使用透明背景与主题选中块,不继承普通块状标签页的底板和投影。
38
+
33
39
  ## 边界
34
40
 
35
41
  - `AppShell` 通过导航数据和 `onNavigate` 接入任何路由或状态机制。
36
42
  - `headerActions`、`sidebarFooter`、`windowControls` 为应用能力插槽;`draggable` 仅标记 header 的拖动区域。
37
43
  - `UIProvider` 配置 Toast 位置、Tooltip 位置和必填标记;组件不读取宿主设置。
38
44
  - 普通组件也支持子路径导入,例如 `@robert-stackflow/oneui/Input`。
45
+ - 页面直接复用 Form / FormField / useFormState 的校验与提交状态、Alert 的错误反馈和 ConfirmDialog 的确认流程;避免另写原生表单、按钮样式或错误提示。
46
+ - Button 支持 `href`,渲染标准链接并沿用按钮样式;禁用或加载时阻止导航。Card 传入 `onClick` 后渲染可通过键盘激活的按钮卡片。导航与业务回调由消费应用提供。
39
47
  - CSS Modules 随组件分发,由消费项目的构建工具处理。图标/字符目录通过标准 `new URL(..., import.meta.url)` 加载随包资源。
40
48
  - React 和 React DOM 为 peer dependencies,不打入组件包。当前验证版本为 React 19.3.1,浏览器验证使用 Chromium。
41
49
 
50
+ ## 弹窗与加载状态
51
+
52
+ `Dialog` 用于自定义内容,支持 small / medium / large。`MessageDialog` 提供纯文字消息与确认按钮,`ConfirmDialog` 提供普通、危险操作确认,`FormDialog` 提供有统一字段间距的表单及保存操作。三者均可从根入口或对应组件子路径导入。
53
+
54
+ 确认与表单支持异步回调,等待期间禁止重复提交及关闭,失败时保留内容并显示错误;回调返回 `false` 可保留弹窗。`FormDialog` 接收共享 `useFormState` 的 controller,字段使用 `FormField`,`onSubmit` 接收校验后的 values,错误样式与普通 Form 一致。
55
+
56
+ `LoadingState` 复用 Spinner;`showLabel={false}` 仅显示动画,`fullPage` 用于页面内容区域。应用自行放置等待边界,保留路由外壳。
57
+
58
+ 以上 API 从 0.2.0 起提供。
59
+
60
+ ## 层级选择、补全与标签
61
+
62
+ - `TreeSelect variant="category"` 在原有树选择交互上增加图标底块、子项数量与层级连线;默认 `variant="tree"` 保留简洁样式。两种样式均支持搜索、父子联动、多选、禁用、异步加载及键盘操作。`HierarchyOption.count` 可单独提供数量。
63
+ - `TreeTable` 复用 `Table` 的列定义,接收 `rows`、`rowKey`、`getChildren`、`rowLabel`;通过 `expanded` / `onExpandedChange` 控制展开,或使用 `defaultExpanded` / `defaultExpandAll`。`treeColumn` 默认为第一列,支持多级连线、方向键导航及独立行操作。`Column.align`、`Column.width` 可用于对齐和列宽。
64
+ - `Combobox mode="input"` 直接在触发框输入并在下方显示候选,保留 `loadOptions`、`onCreate` 和键盘选择。输入中的文本只用于搜索;选择候选或创建成功才提交选项值,关闭时恢复已选标签。默认 `mode="panel"` 继续使用弹层内搜索。
65
+ - `MultiSelect display="tags"` 在触发框内展示可移除标签,复用 `TagInput` 的标签样式;默认 `display="summary"` 保留文字摘要。两种模式共享连续选择、搜索、数量限制和表单值。只读标签不可移除,禁用选项保持不可操作。
66
+
67
+ 菜单项间距统一为 `--menu-item-spacing: 1px`,菜单快捷键为靠右的纯文字提示,不预留空的指示器位置。Desktop 与 Web 共用示例:`/pickers?view=hierarchy`、`/forms?view=advanced`、`/data?view=hierarchy`;层级表格中的增删改仅更新示例内存数据。以上扩展从 0.2.0 起提供。
68
+
42
69
  ## 构建与打包
43
70
 
44
- 在 General 根目录运行 `npm run pack:ui`。源码工作区供两端即时开发使用;发布内容单独生成到本目录 `dist`,含 ESM、TypeScript 声明、CSS 和资源。`dist/package.json` 只导出生成内容。
71
+ 在仓库根目录运行 `npm run pack:ui`。源码工作区供两端即时开发使用;发布内容单独生成到本目录 `dist`,含 ESM、TypeScript 声明、CSS 和资源。构建使用干净的临时目录并检查所有包内引用,成功后替换 dist,避免旧文件混入。`dist/package.json` 只导出生成内容。
45
72
 
46
73
  发布内容已通过独立 Vite 消费项目验证。包的许可为 UNLICENSED;资源目录内保留图标许可文件。
@@ -83,7 +83,7 @@
83
83
  .bottom {
84
84
  display: flex;
85
85
  flex-direction: column;
86
- gap: 16px;
86
+ gap: 8px;
87
87
  margin-top: auto;
88
88
  flex-shrink: 0;
89
89
  }
@@ -100,7 +100,7 @@
100
100
  align-items: center;
101
101
  justify-content: space-between;
102
102
  gap: 16px;
103
- padding: 0 12px;
103
+ padding: 0 8px;
104
104
  border-bottom: 1px solid var(--line);
105
105
  background: var(--surface);
106
106
  color: var(--fg);
@@ -235,5 +235,5 @@
235
235
  }
236
236
 
237
237
  .header[data-window-controls="true"] {
238
- padding-right: 4px;
238
+ padding-right: 8px;
239
239
  }
@@ -55,7 +55,7 @@ function AppearanceSettings({
55
55
  ),
56
56
  /* @__PURE__ */ jsxs(SettingsCard, { children: [
57
57
  /* @__PURE__ */ jsx(SettingRow, { title: "\u5F3A\u8C03\u8272", children: /* @__PURE__ */ jsx("div", { className: s.swatches, children: [
58
- settings.designTheme === "blue" ? "#5269b6" : "#303030",
58
+ appearanceDefaults(settings.designTheme).accent,
59
59
  "#4479d8",
60
60
  "#6d61bd",
61
61
  settings.designTheme === "blue" ? "#397c67" : "#41886c",
@@ -1,6 +1,13 @@
1
1
  import type { ComponentPropsWithRef } from "react";
2
- export declare function Button({ variant, size, loading, children, className, ...props }: ComponentPropsWithRef<"button"> & {
2
+ type ButtonProps = {
3
3
  variant?: "primary" | "secondary" | "ghost" | "danger";
4
4
  size?: "normal" | "small";
5
5
  loading?: boolean;
6
- }): import("react").JSX.Element;
6
+ } & ((ComponentPropsWithRef<"button"> & {
7
+ href?: never;
8
+ }) | (ComponentPropsWithRef<"a"> & {
9
+ href: string;
10
+ disabled?: boolean;
11
+ }));
12
+ export declare function Button({ variant, size, loading, children, className, ...props }: ButtonProps): import("react").JSX.Element;
13
+ export {};
@@ -1,4 +1,4 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { Spinner } from "./Spinner.js";
3
3
  import s from "./Button.module.css";
4
4
  function Button({
@@ -9,18 +9,41 @@ function Button({
9
9
  className = "",
10
10
  ...props
11
11
  }) {
12
- return /* @__PURE__ */ jsxs(
12
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
13
+ /* @__PURE__ */ jsx("span", { className: s.content, style: { opacity: loading ? 0 : 1 }, children }),
14
+ loading && /* @__PURE__ */ jsx("span", { className: s.loader, children: /* @__PURE__ */ jsx(Spinner, { decorative: true }) })
15
+ ] });
16
+ const classes = `${s.button} ${s[variant]} ${size === "small" ? s.small : ""} ${className}`;
17
+ if (typeof props.href === "string") {
18
+ const { disabled, onClick, ...link } = props;
19
+ const blocked = disabled || loading;
20
+ return /* @__PURE__ */ jsx(
21
+ "a",
22
+ {
23
+ ...link,
24
+ className: classes,
25
+ "aria-disabled": blocked || void 0,
26
+ "aria-busy": loading || void 0,
27
+ tabIndex: blocked ? -1 : link.tabIndex,
28
+ onClick: (event) => {
29
+ if (blocked) {
30
+ event.preventDefault();
31
+ event.stopPropagation();
32
+ } else onClick?.(event);
33
+ },
34
+ children: content
35
+ }
36
+ );
37
+ }
38
+ return /* @__PURE__ */ jsx(
13
39
  "button",
14
40
  {
15
41
  type: "button",
16
42
  ...props,
17
43
  disabled: props.disabled || loading,
18
44
  "aria-busy": loading || void 0,
19
- className: `${s.button} ${s[variant]} ${size === "small" ? s.small : ""} ${className}`,
20
- children: [
21
- /* @__PURE__ */ jsx("span", { className: s.content, style: { opacity: loading ? 0 : 1 }, children }),
22
- loading && /* @__PURE__ */ jsx("span", { className: s.loader, children: /* @__PURE__ */ jsx(Spinner, { decorative: true }) })
23
- ]
45
+ className: classes,
46
+ children: content
24
47
  }
25
48
  );
26
49
  }
@@ -14,21 +14,30 @@
14
14
  transform var(--motion-hover),
15
15
  border-color var(--motion-hover);
16
16
  font-size: 12px;
17
+ text-decoration: none;
18
+ color: var(--fg);
17
19
  }
18
- .button:hover:not(:disabled) {
20
+ .button[aria-disabled="true"] {
21
+ opacity: 0.5;
22
+ cursor: not-allowed;
23
+ }
24
+ .button:hover:not(:disabled):not([aria-disabled="true"]) {
19
25
  background: var(--hover);
20
26
  border-color: color-mix(in srgb, var(--fg) 18%, var(--surface));
21
27
  }
22
- .button:active:not(:disabled) {
28
+ .button:active:not(:disabled):not([aria-disabled="true"]) {
23
29
  transform: translateY(1px);
24
30
  }
25
31
  .primary {
26
- background: var(--accent);
27
- border-color: var(--accent);
28
- color: var(--on-accent);
32
+ background: var(--accent-fill, var(--accent));
33
+ border-color: var(--accent-fill, var(--accent));
34
+ color: var(--on-accent-fill, var(--on-accent));
29
35
  }
30
- .primary:hover:not(:disabled) {
31
- background: color-mix(in srgb, var(--accent) 86%, var(--surface));
36
+ .primary:hover:not(:disabled):not([aria-disabled="true"]) {
37
+ background: var(
38
+ --accent-fill-hover,
39
+ color-mix(in srgb, var(--accent) 86%, var(--surface))
40
+ );
32
41
  border-color: transparent;
33
42
  }
34
43
  .secondary {
@@ -1,7 +1,8 @@
1
1
  import type { ReactNode } from "react";
2
- export declare function Card({ title, action, children, className, }: {
2
+ export declare function Card({ title, action, children, className, onClick, }: {
3
3
  title?: string;
4
4
  action?: ReactNode;
5
5
  children: ReactNode;
6
6
  className?: string;
7
+ onClick?(): void;
7
8
  }): import("react").JSX.Element;
@@ -1,18 +1,28 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import s from "./Card.module.css";
3
3
  function Card({
4
4
  title,
5
5
  action,
6
6
  children,
7
- className = ""
7
+ className = "",
8
+ onClick
8
9
  }) {
9
- return /* @__PURE__ */ jsxs("section", { className: s.card + " " + className, children: [
10
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
10
11
  (title || action) && /* @__PURE__ */ jsxs("header", { className: s.header, children: [
11
12
  /* @__PURE__ */ jsx("h2", { children: title }),
12
13
  action
13
14
  ] }),
14
15
  /* @__PURE__ */ jsx("div", { className: s.body, "data-card-body": true, children })
15
16
  ] });
17
+ return onClick ? /* @__PURE__ */ jsx(
18
+ "button",
19
+ {
20
+ type: "button",
21
+ className: `${s.card} ${s.interactive} ${className}`,
22
+ onClick,
23
+ children: content
24
+ }
25
+ ) : /* @__PURE__ */ jsx("section", { className: s.card + " " + className, children: content });
16
26
  }
17
27
  export {
18
28
  Card
@@ -5,6 +5,25 @@
5
5
  min-width: 0;
6
6
  box-shadow: var(--card-shadow, none);
7
7
  }
8
+ .interactive {
9
+ display: block;
10
+ width: 100%;
11
+ padding: 0;
12
+ color: var(--fg);
13
+ font: inherit;
14
+ text-align: left;
15
+ cursor: pointer;
16
+ transition:
17
+ box-shadow var(--motion-hover),
18
+ transform var(--motion-hover);
19
+ }
20
+ .interactive:hover {
21
+ box-shadow: var(--shadow);
22
+ transform: translateY(-2px);
23
+ }
24
+ .interactive:active {
25
+ transform: none;
26
+ }
8
27
  .header {
9
28
  display: flex;
10
29
  align-items: center;
@@ -54,9 +54,9 @@
54
54
  transform: scale(0.92);
55
55
  }
56
56
  .checkbox input:checked + .checkMark {
57
- background: var(--accent);
58
- color: var(--on-accent);
59
- border-color: var(--accent);
57
+ background: var(--accent-fill, var(--accent));
58
+ color: var(--on-accent-fill, var(--on-accent));
59
+ border-color: var(--accent-fill, var(--accent));
60
60
  }
61
61
  .checkbox input:checked + .checkMark path {
62
62
  stroke-dashoffset: 0;
@@ -1,6 +1,6 @@
1
1
  import { type FieldOptions } from "./field";
2
2
  import type { SelectOption } from "./types";
3
- export declare function Combobox({ value, onChange, options, loadOptions, onCreate, label, placeholder, ...props }: FieldOptions & {
3
+ export type ComboboxProps = FieldOptions & {
4
4
  value: string;
5
5
  onChange(value: string): void;
6
6
  options?: SelectOption[];
@@ -8,4 +8,6 @@ export declare function Combobox({ value, onChange, options, loadOptions, onCrea
8
8
  onCreate?(label: string): string | void | Promise<string | void>;
9
9
  label?: string;
10
10
  placeholder?: string;
11
- }): import("react").JSX.Element;
11
+ mode?: "panel" | "input";
12
+ };
13
+ export declare function Combobox({ value, onChange, options, loadOptions, onCreate, label, placeholder, mode, ...props }: ComboboxProps): import("react").JSX.Element;
@@ -1,12 +1,26 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useId, useRef, useState } from "react";
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useId,
6
+ useRef,
7
+ useState
8
+ } from "react";
3
9
  import { Plus } from "lucide-react";
4
- import { fieldTriggerProps, useFieldControl } from "./field.js";
10
+ import {
11
+ FieldContext,
12
+ fieldInputProps,
13
+ fieldTriggerProps,
14
+ useFieldControl
15
+ } from "./field.js";
5
16
  import { SelectTrigger } from "./SelectTrigger.js";
6
17
  import { Popover } from "./Popover.js";
7
18
  import { SearchField } from "./SearchField.js";
19
+ import { Input } from "./Input.js";
8
20
  import { MenuItem } from "./MenuItem.js";
9
21
  import { Spinner } from "./Spinner.js";
22
+ import { Button } from "./Button.js";
23
+ import { DisclosureChevron } from "./DisclosureChevron.js";
10
24
  import s from "./Combobox.module.css";
11
25
  function Combobox({
12
26
  value,
@@ -16,14 +30,24 @@ function Combobox({
16
30
  onCreate,
17
31
  label = "\u641C\u7D22\u9009\u62E9",
18
32
  placeholder = "\u8BF7\u9009\u62E9",
33
+ mode = "panel",
19
34
  ...props
20
35
  }) {
21
- const field = useFieldControl({ ...props, label }), anchor = useRef(null), list = useRef(null), id = useId(), [open, setOpen] = useState(false), [query, setQuery] = useState(""), [remote, setRemote] = useState([]), [loading, setLoading] = useState(false), [error, setError] = useState(""), [active, setActive] = useState(0), [created, setCreated] = useState(), [chosen, setChosen] = useState(), [creating, setCreating] = useState(false), [retry, setRetry] = useState(0);
22
- const generation = useRef(0);
36
+ const field = useFieldControl({ ...props, label }), anchor = useRef(null), list = useRef(null), id = useId();
37
+ const [open, setOpen] = useState(false), [query, setQuery] = useState(""), [editing, setEditing] = useState(false), [remote, setRemote] = useState([]), [loading, setLoading] = useState(false), [error, setError] = useState(""), [failure, setFailure] = useState("load"), [active, setActive] = useState(-1), [created, setCreated] = useState(), [chosen, setChosen] = useState(), [creating, setCreating] = useState(false), [retry, setRetry] = useState(0);
38
+ const generation = useRef(0), restoring = useRef(false), loader = useRef(loadOptions);
39
+ loader.current = loadOptions;
40
+ const inline = mode === "input", async = !!loadOptions;
23
41
  const close = useCallback(() => {
24
42
  generation.current++;
25
43
  setOpen(false);
44
+ setEditing(false);
26
45
  setCreating(false);
46
+ setLoading(false);
47
+ restoring.current = true;
48
+ queueMicrotask(() => {
49
+ restoring.current = false;
50
+ });
27
51
  }, []);
28
52
  useEffect(
29
53
  () => () => {
@@ -31,28 +55,21 @@ function Combobox({
31
55
  },
32
56
  []
33
57
  );
34
- const loader = useRef(loadOptions);
35
- loader.current = loadOptions;
36
- const async = !!loadOptions;
37
58
  useEffect(() => {
38
59
  if (!open || !async) return;
39
- const controller = new AbortController();
60
+ const controller = new AbortController(), revision = generation.current;
40
61
  setLoading(true);
41
62
  setError("");
42
63
  const timer = setTimeout(() => {
43
64
  Promise.resolve().then(() => loader.current(query, controller.signal)).then((items) => {
44
- if (!controller.signal.aborted) {
65
+ if (!controller.signal.aborted && revision === generation.current) {
45
66
  setRemote(items);
46
67
  setLoading(false);
47
- setActive(
48
- Math.max(
49
- 0,
50
- items.findIndex((item) => !item.disabled)
51
- )
52
- );
68
+ setActive(inline ? -1 : items.findIndex((item) => !item.disabled));
53
69
  }
54
70
  }).catch(() => {
55
- if (!controller.signal.aborted) {
71
+ if (!controller.signal.aborted && revision === generation.current) {
72
+ setFailure("load");
56
73
  setError("\u9009\u9879\u52A0\u8F7D\u5931\u8D25");
57
74
  setLoading(false);
58
75
  }
@@ -62,10 +79,10 @@ function Combobox({
62
79
  clearTimeout(timer);
63
80
  controller.abort();
64
81
  };
65
- }, [open, query, async, retry]);
82
+ }, [open, query, async, retry, inline]);
66
83
  useEffect(() => {
67
84
  if (field.disabled || field.readOnly) close();
68
- }, [field.disabled, field.readOnly]);
85
+ }, [field.disabled, field.readOnly, close]);
69
86
  const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean), filtered = async ? remote : options.filter(
70
87
  (item) => terms.every(
71
88
  (term) => (item.label + " " + (item.description || "") + " " + item.value).toLowerCase().includes(term)
@@ -73,7 +90,8 @@ function Combobox({
73
90
  );
74
91
  const selected = [...options, ...remote, created, chosen].find(
75
92
  (item) => item?.value === value
76
- );
93
+ ), first = filtered.findIndex((item) => !item.disabled);
94
+ const activeIndex = active >= 0 && filtered[active] && !filtered[active].disabled ? active : inline ? -1 : first;
77
95
  const choose = (item) => {
78
96
  if (item.disabled || field.disabled || field.readOnly) return;
79
97
  setChosen(item);
@@ -82,19 +100,116 @@ function Combobox({
82
100
  anchor.current?.focus();
83
101
  };
84
102
  const show = () => {
103
+ if (field.disabled || field.readOnly || creating) return;
104
+ generation.current++;
105
+ setLoading(async);
85
106
  setQuery("");
86
- setActive(0);
107
+ setEditing(false);
108
+ setActive(inline ? -1 : 0);
109
+ setError("");
110
+ setOpen(true);
111
+ };
112
+ const search = (text) => {
113
+ if (field.disabled || field.readOnly || creating) return;
114
+ generation.current++;
115
+ setLoading(async);
116
+ setQuery(text);
117
+ setEditing(true);
118
+ setActive(inline ? -1 : 0);
87
119
  setError("");
88
120
  setOpen(true);
89
121
  };
90
122
  useEffect(() => {
91
123
  list.current?.querySelector('[data-active="true"]')?.scrollIntoView({ block: "nearest" });
92
- }, [active, query]);
124
+ }, [activeIndex, query, loading]);
93
125
  const canCreate = !!onCreate && !!query.trim() && !filtered.some(
94
126
  (item) => item.label.toLowerCase() === query.trim().toLowerCase()
95
127
  );
128
+ const create = async () => {
129
+ if (!onCreate || creating || loading || !canCreate) return;
130
+ const revision = generation.current, text = query.trim();
131
+ setCreating(true);
132
+ setError("");
133
+ try {
134
+ const result = await onCreate(text);
135
+ if (revision !== generation.current) return;
136
+ const item = { value: result || text, label: text };
137
+ setCreated(item);
138
+ choose(item);
139
+ } catch {
140
+ if (revision === generation.current) {
141
+ setFailure("create");
142
+ setError("\u521B\u5EFA\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002");
143
+ }
144
+ } finally {
145
+ if (revision === generation.current) setCreating(false);
146
+ }
147
+ };
148
+ const navigate = (event) => {
149
+ if (event.nativeEvent.isComposing || event.keyCode === 229 || field.readOnly || field.disabled)
150
+ return;
151
+ if (event.key === "Escape" && open) {
152
+ event.preventDefault();
153
+ event.stopPropagation();
154
+ close();
155
+ anchor.current?.focus();
156
+ return;
157
+ }
158
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
159
+ event.preventDefault();
160
+ if (!open) {
161
+ show();
162
+ return;
163
+ }
164
+ const direction = event.key === "ArrowDown" ? 1 : -1, start = activeIndex < 0 ? direction === 1 ? -1 : 0 : activeIndex;
165
+ for (let step = 1; step <= filtered.length; step++) {
166
+ const next = (start + direction * step + filtered.length * 2) % filtered.length;
167
+ if (!filtered[next].disabled) {
168
+ setActive(next);
169
+ break;
170
+ }
171
+ }
172
+ } else if (event.key === "Enter" && open) {
173
+ event.preventDefault();
174
+ if (!loading && !creating && !error) {
175
+ const item = filtered[activeIndex < 0 ? first : activeIndex];
176
+ if (item) choose(item);
177
+ else void create();
178
+ }
179
+ }
180
+ };
181
+ const activeId = open && !loading && !error && activeIndex >= 0 ? id + "-" + activeIndex : void 0;
96
182
  return /* @__PURE__ */ jsxs(Fragment, { children: [
97
- /* @__PURE__ */ jsx(
183
+ inline ? /* @__PURE__ */ jsx(FieldContext, { value: { ...field, name: void 0 }, children: /* @__PURE__ */ jsx(
184
+ Input,
185
+ {
186
+ ...fieldInputProps(field),
187
+ name: void 0,
188
+ ref: anchor,
189
+ role: "combobox",
190
+ "aria-label": field.label,
191
+ "aria-expanded": open,
192
+ "aria-busy": loading || creating,
193
+ "aria-controls": id + "-list",
194
+ "aria-autocomplete": "list",
195
+ "aria-activedescendant": activeId,
196
+ autoComplete: "off",
197
+ disabled: field.disabled || creating,
198
+ className: s.inputTrigger,
199
+ value: open && editing ? query : selected?.label || value,
200
+ placeholder,
201
+ leading: selected?.icon,
202
+ trailing: loading || creating ? /* @__PURE__ */ jsx(Spinner, { size: 16, decorative: true }) : /* @__PURE__ */ jsx(DisclosureChevron, { expanded: open }),
203
+ onChange: (event) => search(event.target.value),
204
+ onFocus: () => {
205
+ if (!restoring.current && !open) show();
206
+ },
207
+ onClick: () => {
208
+ if (!open) show();
209
+ },
210
+ onKeyDown: navigate
211
+ }
212
+ ) }) : /* @__PURE__ */ jsx(
98
213
  SelectTrigger,
99
214
  {
100
215
  ...fieldTriggerProps(field),
@@ -107,12 +222,7 @@ function Combobox({
107
222
  onClick: () => {
108
223
  if (!field.readOnly) open ? close() : show();
109
224
  },
110
- onKeyDown: (event) => {
111
- if (!field.readOnly && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
112
- event.preventDefault();
113
- show();
114
- }
115
- },
225
+ onKeyDown: navigate,
116
226
  children: /* @__PURE__ */ jsx("span", { children: selected?.label || value || placeholder })
117
227
  }
118
228
  ),
@@ -134,36 +244,21 @@ function Combobox({
134
244
  anchor,
135
245
  label: field.label + "\u9009\u9879",
136
246
  width: 300,
247
+ autoFocus: !inline,
248
+ matchAnchorWidth: inline,
137
249
  children: /* @__PURE__ */ jsxs("div", { className: s.panel, children: [
138
- /* @__PURE__ */ jsx(
250
+ !inline && /* @__PURE__ */ jsx(
139
251
  SearchField,
140
252
  {
141
253
  label: "\u641C\u7D22" + field.label,
142
254
  placeholder: "\u641C\u7D22\u9009\u9879\u2026",
143
255
  value: query,
144
- onChange: (value2) => {
145
- setQuery(value2);
146
- setActive(0);
147
- },
256
+ onChange: search,
148
257
  loading,
258
+ disabled: creating,
149
259
  "aria-controls": id + "-list",
150
- "aria-activedescendant": filtered[active] ? id + "-" + active : void 0,
151
- onKeyDown: (event) => {
152
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
153
- event.preventDefault();
154
- const direction = event.key === "ArrowDown" ? 1 : -1;
155
- for (let step = 1; step <= filtered.length; step++) {
156
- const next = (active + direction * step + filtered.length * 2) % filtered.length;
157
- if (!filtered[next].disabled) {
158
- setActive(next);
159
- break;
160
- }
161
- }
162
- } else if (event.key === "Enter") {
163
- event.preventDefault();
164
- if (!loading && filtered[active]) choose(filtered[active]);
165
- }
166
- }
260
+ "aria-activedescendant": activeId,
261
+ onKeyDown: navigate
167
262
  }
168
263
  ),
169
264
  /* @__PURE__ */ jsx(
@@ -175,13 +270,22 @@ function Combobox({
175
270
  "aria-label": field.label,
176
271
  className: s.list,
177
272
  "aria-busy": loading,
178
- children: error ? /* @__PURE__ */ jsxs("div", { className: s.empty, children: [
273
+ children: error ? /* @__PURE__ */ jsxs("div", { className: s.empty, role: "alert", children: [
179
274
  /* @__PURE__ */ jsx("span", { children: error }),
180
275
  /* @__PURE__ */ jsx(
181
- "button",
276
+ Button,
182
277
  {
183
- type: "button",
184
- onClick: () => setRetry((value2) => value2 + 1),
278
+ size: "small",
279
+ variant: "ghost",
280
+ onClick: () => {
281
+ setError("");
282
+ if (failure === "create") void create();
283
+ else {
284
+ generation.current++;
285
+ setLoading(true);
286
+ setRetry((value2) => value2 + 1);
287
+ }
288
+ },
185
289
  children: "\u91CD\u8BD5"
186
290
  }
187
291
  )
@@ -196,9 +300,9 @@ function Combobox({
196
300
  icon: item.icon,
197
301
  description: item.description,
198
302
  shortcut: item.shortcut,
199
- reserveIcon: filtered.some((item2) => !!item2.icon),
303
+ reserveIcon: filtered.some((option) => !!option.icon),
200
304
  selected: value === item.value,
201
- active: active === index,
305
+ active: activeIndex === index,
202
306
  disabled: item.disabled,
203
307
  "aria-selected": value === item.value,
204
308
  selection: "check",
@@ -220,26 +324,8 @@ function Combobox({
220
324
  label: "\u521B\u5EFA\u201C" + query.trim() + "\u201D",
221
325
  icon: /* @__PURE__ */ jsx(Plus, {}),
222
326
  disabled: creating,
223
- onClick: async () => {
224
- const revision = generation.current;
225
- setCreating(true);
226
- setError("");
227
- try {
228
- const result = await onCreate(query.trim());
229
- if (revision !== generation.current) return;
230
- const item = {
231
- value: result || query.trim(),
232
- label: query.trim()
233
- };
234
- setCreated(item);
235
- choose(item);
236
- } catch {
237
- if (revision === generation.current)
238
- setError("\u521B\u5EFA\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002");
239
- } finally {
240
- if (revision === generation.current) setCreating(false);
241
- }
242
- }
327
+ onMouseDown: (event) => event.preventDefault(),
328
+ onClick: () => void create()
243
329
  }
244
330
  )
245
331
  ] })