@dsh-workbench/shared 0.2.12 → 0.2.14

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 (53) hide show
  1. package/dist/components/ChoiceGroup.css +53 -3
  2. package/dist/components/ChoiceGroup.d.ts +5 -0
  3. package/dist/components/ChoiceGroup.js +15 -5
  4. package/dist/components/FormActions.css +13 -0
  5. package/dist/components/FormActions.d.ts +15 -0
  6. package/dist/components/FormActions.js +38 -0
  7. package/dist/components/FormRows.css +69 -0
  8. package/dist/components/FormRows.d.ts +14 -0
  9. package/dist/components/FormRows.js +11 -0
  10. package/dist/components/SaveBar.d.ts +0 -1
  11. package/dist/components/SaveBar.js +13 -5
  12. package/dist/components/SelectField.css +12 -4
  13. package/dist/components/SelectField.d.ts +1 -0
  14. package/dist/components/SelectField.js +9 -7
  15. package/dist/components/Spinner.css +6 -0
  16. package/dist/components/Spinner.d.ts +6 -0
  17. package/dist/components/Spinner.js +7 -0
  18. package/dist/components/TextField.css +2 -2
  19. package/dist/host.d.ts +3 -3
  20. package/dist/host.js +3 -3
  21. package/dist/i18n/en.js +9 -0
  22. package/dist/i18n/index.d.ts +10 -1
  23. package/dist/i18n/zh.d.ts +9 -0
  24. package/dist/i18n/zh.js +9 -0
  25. package/dist/index.d.ts +8 -4
  26. package/dist/index.js +8 -4
  27. package/dist/lib/form-problem.d.ts +2 -0
  28. package/dist/lib/form-problem.js +20 -0
  29. package/package.json +2 -1
  30. package/src/components/ChoiceGroup.css +53 -3
  31. package/src/components/ChoiceGroup.test.tsx +88 -0
  32. package/src/components/ChoiceGroup.tsx +70 -28
  33. package/src/components/FormActions.css +13 -0
  34. package/src/components/FormActions.test.tsx +157 -0
  35. package/src/components/FormActions.tsx +75 -0
  36. package/src/components/FormRows.css +69 -0
  37. package/src/components/FormRows.test.tsx +138 -0
  38. package/src/components/FormRows.tsx +39 -0
  39. package/src/components/SaveBar.tsx +14 -18
  40. package/src/components/SelectField.css +12 -4
  41. package/src/components/SelectField.test.tsx +20 -2
  42. package/src/components/SelectField.tsx +11 -3
  43. package/src/components/Spinner.css +6 -0
  44. package/src/components/Spinner.test.tsx +35 -0
  45. package/src/components/Spinner.tsx +19 -0
  46. package/src/components/TextField.css +2 -2
  47. package/src/host.ts +3 -3
  48. package/src/i18n/en.ts +9 -0
  49. package/src/i18n/zh.ts +9 -0
  50. package/src/index.ts +8 -4
  51. package/src/lib/form-problem.test.ts +76 -0
  52. package/src/lib/form-problem.ts +22 -0
  53. package/src/components/SaveBar.css +0 -7
@@ -3,10 +3,13 @@
3
3
  display: flex;
4
4
  flex-direction: column;
5
5
  gap: var(--wb-len-2);
6
+ background: transparent;
7
+ }
8
+
9
+ .wb-choiceMenu[data-variant='boxed'] {
6
10
  padding: var(--wb-len-4);
7
11
  border: 0.5px solid var(--dsw-alias-border-l3);
8
12
  border-radius: var(--dsw-radius-md);
9
- background: transparent;
10
13
  }
11
14
 
12
15
  .wb-choiceOption {
@@ -14,8 +17,8 @@
14
17
  display: flex;
15
18
  align-items: center;
16
19
  gap: var(--wb-len-6);
17
- min-height: var(--wb-len-30);
18
- padding: var(--wb-len-6) var(--wb-len-8);
20
+ min-height: var(--wb-len-28);
21
+ padding: var(--wb-len-4) var(--wb-len-8);
19
22
  border-radius: var(--dsw-radius-md);
20
23
  cursor: pointer;
21
24
  font-size: var(--dsw-font-xs-13-font-size);
@@ -87,12 +90,59 @@
87
90
 
88
91
  .wb-choiceLabel {
89
92
  flex: 1;
93
+ min-width: 0;
94
+ display: flex;
95
+ flex-direction: column;
96
+ overflow: hidden;
97
+ }
98
+
99
+ .wb-choiceLabelText {
100
+ overflow: hidden;
101
+ text-overflow: ellipsis;
102
+ white-space: nowrap;
103
+ }
104
+
105
+ .wb-choiceLabel:has(.wb-choiceDescription) .wb-choiceLabelText {
106
+ overflow: visible;
107
+ white-space: normal;
108
+ overflow-wrap: anywhere;
109
+ }
110
+
111
+ .wb-choiceDescription {
112
+ font-size: var(--dsw-font-xxs-12-font-size);
113
+ line-height: var(--wb-line-18);
114
+ color: var(--dsw-alias-label-tertiary);
115
+ overflow-wrap: anywhere;
116
+ }
117
+
118
+ .wb-choiceLoading {
119
+ display: flex;
120
+ align-items: center;
121
+ gap: var(--wb-len-6);
122
+ min-height: var(--wb-len-28);
123
+ padding: var(--wb-len-4) var(--wb-len-8);
124
+ font-size: var(--dsw-font-xs-13-font-size);
125
+ line-height: var(--wb-line-20);
126
+ color: var(--dsw-alias-label-secondary);
127
+ }
128
+
129
+ .wb-choiceLoadingText {
90
130
  min-width: 0;
91
131
  overflow: hidden;
92
132
  text-overflow: ellipsis;
93
133
  white-space: nowrap;
94
134
  }
95
135
 
136
+ .wb-choiceEmpty {
137
+ display: flex;
138
+ align-items: center;
139
+ min-height: var(--wb-len-28);
140
+ padding: var(--wb-len-4) var(--wb-len-8);
141
+ font-size: var(--dsw-font-xs-13-font-size);
142
+ line-height: var(--wb-line-20);
143
+ color: var(--dsw-alias-label-tertiary);
144
+ }
145
+
96
146
  .wb-radio {
97
147
  appearance: none;
98
148
  -webkit-appearance: none;
@@ -3,12 +3,17 @@ import './ChoiceGroup.css';
3
3
  export interface ChoiceGroupOption {
4
4
  readonly value: string;
5
5
  readonly label: ReactNode;
6
+ readonly description?: ReactNode | undefined;
6
7
  }
7
8
  interface ChoiceGroupCommon {
8
9
  readonly label: string;
9
10
  readonly name: string;
10
11
  readonly options: readonly ChoiceGroupOption[];
11
12
  readonly disabled?: boolean | undefined;
13
+ readonly variant?: 'boxed' | 'ghost' | undefined;
14
+ readonly loading?: boolean | undefined;
15
+ readonly loadingText?: ReactNode | undefined;
16
+ readonly emptyText?: ReactNode | undefined;
12
17
  }
13
18
  export interface ChoiceGroupSingleProps extends ChoiceGroupCommon {
14
19
  readonly multi?: false | undefined;
@@ -1,15 +1,21 @@
1
1
  import { Fragment, h } from '../lib/react.js';
2
2
  import { IconCheckOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives';
3
+ import { Spinner } from './Spinner.js';
3
4
  import './ChoiceGroup.css';
4
5
  function ChoiceMenu(input) {
5
- return (h("div", { className: "wb-choiceMenu", role: input.multi ? 'group' : 'radiogroup', "aria-label": input.label }, input.options.map((option) => {
6
+ const body = input.loading ? (h("span", { className: "wb-choiceLoading", role: "status" },
7
+ h(Spinner, { size: 14 }),
8
+ h("span", { className: "wb-choiceLoadingText" }, input.loadingText ?? '…'))) : input.options.length === 0 ? (input.emptyText === undefined ? null : (h("span", { className: "wb-choiceEmpty" }, input.emptyText))) : (input.options.map((option) => {
6
9
  const row = input.rowOf(option.value);
7
10
  return (h(Fragment, { key: option.value },
8
11
  h("label", { className: `wb-choiceOption${row.checked ? ' wb-choiceOptionOn' : ''}` },
9
12
  h("input", { className: input.multi ? 'wb-checkbox' : 'wb-radio', type: input.multi ? 'checkbox' : 'radio', name: input.name, checked: row.checked, disabled: input.disabled, onChange: row.onSelect }),
10
13
  input.multi ? (h("span", { className: "wb-checkboxBox", "aria-hidden": "true" }, row.checked ? h(IconCheckOutlineRegular, { size: 12 }) : null)) : null,
11
- h("span", { className: "wb-choiceLabel" }, option.label))));
12
- })));
14
+ h("span", { className: "wb-choiceLabel" },
15
+ h("span", { className: "wb-choiceLabelText" }, option.label),
16
+ option.description === undefined ? null : (h("span", { className: "wb-choiceDescription" }, option.description))))));
17
+ }));
18
+ return (h("div", { className: "wb-choiceMenu", "data-variant": input.variant, role: input.multi ? 'group' : 'radiogroup', "aria-label": input.label, "aria-busy": input.loading ? true : undefined }, body));
13
19
  }
14
20
  export function ChoiceGroup(props) {
15
21
  const common = {
@@ -17,11 +23,15 @@ export function ChoiceGroup(props) {
17
23
  name: props.name,
18
24
  options: props.options,
19
25
  disabled: props.disabled === true,
26
+ variant: props.variant ?? 'boxed',
27
+ loading: props.loading === true,
28
+ loadingText: props.loadingText,
29
+ emptyText: props.emptyText,
20
30
  };
21
31
  if (props.multi === true) {
22
32
  const values = props.values;
23
33
  const onChange = props.onChange;
24
- return (h(ChoiceMenu, { label: common.label, name: common.name, options: common.options, disabled: common.disabled, multi: true, rowOf: (value) => ({
34
+ return (h(ChoiceMenu, { label: common.label, name: common.name, options: common.options, disabled: common.disabled, multi: true, variant: common.variant, loading: common.loading, loadingText: common.loadingText, emptyText: common.emptyText, rowOf: (value) => ({
25
35
  checked: values.includes(value),
26
36
  onSelect: () => {
27
37
  onChange(values.includes(value)
@@ -32,7 +42,7 @@ export function ChoiceGroup(props) {
32
42
  }
33
43
  const value = props.value;
34
44
  const onChange = props.onChange;
35
- return (h(ChoiceMenu, { label: common.label, name: common.name, options: common.options, disabled: common.disabled, multi: false, rowOf: (option) => ({
45
+ return (h(ChoiceMenu, { label: common.label, name: common.name, options: common.options, disabled: common.disabled, multi: false, variant: common.variant, loading: common.loading, loadingText: common.loadingText, emptyText: common.emptyText, rowOf: (option) => ({
36
46
  checked: option === value,
37
47
  onSelect: () => {
38
48
  onChange(option);
@@ -0,0 +1,13 @@
1
+ .wb-formActions {
2
+ display: flex;
3
+ flex-wrap: wrap;
4
+ align-items: center;
5
+ justify-content: flex-end;
6
+ gap: var(--wb-len-8);
7
+ }
8
+
9
+ .wb-formActionDanger {
10
+ color: var(--dsw-alias-state-error-primary);
11
+ border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent);
12
+ --dsw-alias-interactive-bg-hover: color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent);
13
+ }
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from '../lib/react.js';
2
+ import './FormActions.css';
3
+ export type FormActionKind = 'default' | 'danger' | 'primary';
4
+ export interface FormAction {
5
+ readonly key: string;
6
+ readonly label: ReactNode;
7
+ readonly kind?: FormActionKind | undefined;
8
+ readonly onClick: () => void;
9
+ readonly disabled?: boolean | undefined;
10
+ readonly busy?: boolean | undefined;
11
+ }
12
+ export interface FormActionsProps {
13
+ readonly actions: readonly FormAction[];
14
+ }
15
+ export declare function FormActions({ actions }: FormActionsProps): JSX.Element | null;
@@ -0,0 +1,38 @@
1
+ import { h } from '../lib/react.js';
2
+ import { Button } from '@deepseek-ai/dsh-client-ui-primitives';
3
+ import { Spinner } from './Spinner.js';
4
+ import './FormActions.css';
5
+ const RANK = { default: 0, danger: 1, primary: 2 };
6
+ function kindOf(action) {
7
+ return action.kind ?? 'default';
8
+ }
9
+ function ordered(actions) {
10
+ return actions
11
+ .map((action, index) => ({ action, index, rank: RANK[kindOf(action)] }))
12
+ .sort((left, right) => left.rank - right.rank || left.index - right.index)
13
+ .map((one) => one.action);
14
+ }
15
+ function reportExtraPrimaries(actions) {
16
+ const keys = actions.filter((one) => kindOf(one) === 'primary').map((one) => one.key);
17
+ if (keys.length < 2)
18
+ return;
19
+ console.error(`[dsh-workbench] FormActions 一个区里出现了 ${keys.length} 颗 primary(${keys.join('、')})—— ` +
20
+ '一个区只许一颗主操作(ADR-0054 §2 规则二):多余的这颗该改成 default');
21
+ }
22
+ function variantOf(kind) {
23
+ if (kind === 'primary')
24
+ return 'primary';
25
+ if (kind === 'danger')
26
+ return 'outline';
27
+ return 'ghost';
28
+ }
29
+ export function FormActions({ actions }) {
30
+ if (actions.length === 0)
31
+ return null;
32
+ reportExtraPrimaries(actions);
33
+ return (h("div", { className: "wb-formActions" }, ordered(actions).map((action) => {
34
+ const kind = kindOf(action);
35
+ const busy = action.busy === true;
36
+ return (h(Button, { key: action.key, type: "button", size: "sm", variant: variantOf(kind), className: kind === 'danger' ? 'wb-formActionDanger' : undefined, disabled: busy || action.disabled === true, icon: busy ? h(Spinner, { size: 12 }) : undefined, onClick: action.onClick }, action.label));
37
+ })));
38
+ }
@@ -0,0 +1,69 @@
1
+ .wb-formRows {
2
+ display: grid;
3
+ grid-template-columns: minmax(72px, max-content) minmax(0, 1fr);
4
+ column-gap: var(--wb-len-8);
5
+ row-gap: var(--wb-len-12);
6
+ }
7
+
8
+ .wb-formRows > :not(.wb-formRow) {
9
+ grid-column: 1 / -1;
10
+ }
11
+
12
+ .wb-formRow {
13
+ display: grid;
14
+ grid-column: 1 / -1;
15
+ grid-template-columns: minmax(72px, max-content) minmax(0, 1fr);
16
+ align-items: center;
17
+ column-gap: var(--wb-len-8);
18
+ row-gap: var(--wb-len-8);
19
+ }
20
+
21
+ .wb-formRows > .wb-formRow {
22
+ grid-template-columns: subgrid;
23
+ }
24
+
25
+ .wb-formRow[data-layout='stacked'] {
26
+ grid-template-columns: minmax(0, 1fr);
27
+ align-items: stretch;
28
+ gap: var(--wb-len-6);
29
+ }
30
+
31
+ .wb-formLabel {
32
+ min-width: 0;
33
+ font-size: var(--dsw-font-xs-13-font-size);
34
+ color: var(--dsw-alias-label-secondary);
35
+ overflow-wrap: anywhere;
36
+ white-space: nowrap;
37
+ }
38
+
39
+ .wb-formRow[data-layout='stacked'] .wb-formLabel {
40
+ white-space: normal;
41
+ }
42
+
43
+ .wb-formControl {
44
+ display: flex;
45
+ align-items: center;
46
+ min-width: 0;
47
+ }
48
+
49
+ .wb-formHint {
50
+ grid-column: 2;
51
+ font-size: var(--dsw-font-xxs-12-font-size);
52
+ line-height: var(--wb-line-18);
53
+ color: var(--dsw-alias-label-tertiary);
54
+ }
55
+
56
+ .wb-formProblem {
57
+ grid-column: 2;
58
+ font-size: var(--dsw-font-xxs-12-font-size);
59
+ line-height: var(--wb-line-18);
60
+ color: var(--dsw-alias-state-error-primary);
61
+ }
62
+
63
+ .wb-formRow:has(.wb-formProblem) .wb-formLabel {
64
+ color: var(--dsw-alias-state-error-primary);
65
+ }
66
+
67
+ .wb-formRow:has(:disabled) .wb-formLabel {
68
+ opacity: 0.4;
69
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from '../lib/react.js';
2
+ import './FormRows.css';
3
+ export interface FormRowsProps {
4
+ readonly children?: ReactNode | undefined;
5
+ }
6
+ export declare function FormRows({ children }: FormRowsProps): JSX.Element;
7
+ export interface FormRowProps {
8
+ readonly label: ReactNode;
9
+ readonly hint?: ReactNode | undefined;
10
+ readonly problem?: ReactNode | undefined;
11
+ readonly layout?: 'inline' | 'stacked' | undefined;
12
+ readonly children?: ReactNode | undefined;
13
+ }
14
+ export declare function FormRow({ label, hint, problem, layout, children }: FormRowProps): JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { h } from '../lib/react.js';
2
+ import './FormRows.css';
3
+ export function FormRows({ children }) {
4
+ return h("div", { className: "wb-formRows" }, children);
5
+ }
6
+ export function FormRow({ label, hint, problem, layout = 'inline', children }) {
7
+ return (h("label", { className: "wb-formRow", "data-layout": layout, "data-wb-problem": problem === undefined ? undefined : '' },
8
+ h("span", { className: "wb-formLabel" }, label),
9
+ h("span", { className: "wb-formControl" }, children),
10
+ problem === undefined ? (hint === undefined ? null : (h("span", { className: "wb-formHint" }, hint))) : (h("span", { className: "wb-formProblem" }, problem))));
11
+ }
@@ -1,4 +1,3 @@
1
- import './SaveBar.css';
2
1
  export interface SaveBarProps {
3
2
  readonly busy: boolean;
4
3
  readonly disabled?: boolean | undefined;
@@ -1,8 +1,16 @@
1
- import { Button } from '@deepseek-ai/dsh-client-ui-primitives';
2
1
  import { h } from '../lib/react.js';
3
- import './SaveBar.css';
2
+ import { FormActions } from './FormActions.js';
4
3
  export function SaveBar({ busy, disabled, labels, onCancel, onSave }) {
5
- return (h("div", { className: "wb-formActions" },
6
- h(Button, { type: "button", size: "sm", variant: "ghost", onClick: onCancel, disabled: busy }, labels.cancel),
7
- h(Button, { type: "button", size: "sm", variant: "primary", onClick: onSave, disabled: busy || disabled === true }, busy ? labels.saving : labels.save)));
4
+ const actions = [
5
+ { key: 'cancel', label: labels.cancel, kind: 'default', onClick: onCancel, disabled: busy },
6
+ {
7
+ key: 'save',
8
+ label: busy ? labels.saving : labels.save,
9
+ kind: 'primary',
10
+ onClick: onSave,
11
+ disabled: busy || disabled === true,
12
+ busy,
13
+ },
14
+ ];
15
+ return h(FormActions, { actions: actions });
8
16
  }
@@ -4,8 +4,8 @@
4
4
  align-items: center;
5
5
  gap: var(--wb-len-8);
6
6
  width: 100%;
7
- min-height: var(--wb-len-32);
8
- padding: var(--wb-len-6) var(--wb-len-12);
7
+ min-height: var(--wb-len-28);
8
+ padding: var(--wb-len-4) var(--wb-len-10);
9
9
  border: 0.5px solid var(--dsw-alias-border-l4);
10
10
  border-radius: var(--dsw-radius-md);
11
11
  background: var(--dsw-alias-bg-layer-3);
@@ -21,6 +21,14 @@
21
21
  box-shadow: inset 0 0 0 0.5px var(--dsw-alias-state-business-primary);
22
22
  }
23
23
 
24
+ .wb-selectTrigger[aria-invalid='true'] {
25
+ border-color: var(--dsw-alias-state-error-primary);
26
+ }
27
+
28
+ .wb-selectTrigger[aria-invalid='true']:focus-visible {
29
+ box-shadow: inset 0 0 0 0.5px var(--dsw-alias-state-error-primary);
30
+ }
31
+
24
32
  .wb-selectTrigger:disabled {
25
33
  cursor: not-allowed;
26
34
  opacity: 0.4;
@@ -100,8 +108,8 @@
100
108
  align-items: center;
101
109
  gap: var(--wb-len-8);
102
110
  width: 100%;
103
- min-height: var(--wb-len-32);
104
- padding: var(--wb-len-6) var(--wb-len-12);
111
+ min-height: var(--wb-len-28);
112
+ padding: var(--wb-len-4) var(--wb-len-10);
105
113
  border: 0.5px solid var(--dsw-alias-border-l4);
106
114
  border-radius: var(--dsw-radius-md);
107
115
  background: var(--dsw-alias-bg-layer-3);
@@ -34,6 +34,7 @@ export interface SelectFieldProps {
34
34
  readonly open?: boolean | undefined;
35
35
  readonly onOpenChange?: ((open: boolean) => void) | undefined;
36
36
  readonly disabled?: boolean | undefined;
37
+ readonly invalid?: boolean | undefined;
37
38
  readonly loading?: boolean | undefined;
38
39
  readonly loadingText?: string | undefined;
39
40
  readonly fallback?: SelectFieldFallback | undefined;
@@ -1,7 +1,8 @@
1
1
  import { h, useEffect, useMemo, useRef, useState } from '../lib/react.js';
2
- import { IconChevronDownOutlineRegular, IconSearchOutlineRegular, Menu, StateDot, TextShimmer, } from '@deepseek-ai/dsh-client-ui-primitives';
2
+ import { IconChevronDownOutlineRegular, IconSearchOutlineRegular, Menu, TextShimmer, } from '@deepseek-ai/dsh-client-ui-primitives';
3
3
  import { Field } from './Field.js';
4
4
  import { TextField } from './TextField.js';
5
+ import { Spinner } from './Spinner.js';
5
6
  import './SelectField.css';
6
7
  const FOOTER_ID = '__wb-select-footer__';
7
8
  const EMPTY_ID = '__wb-select-empty__';
@@ -17,18 +18,18 @@ function itemOf(option) {
17
18
  };
18
19
  }
19
20
  function triggerNode(args) {
20
- return (h("button", { type: "button", id: args.id, className: args.className, disabled: args.disabled, "aria-haspopup": "listbox", "aria-expanded": args.open, "aria-label": args.ariaLabel, title: args.title, onClick: args.onOpen },
21
+ return (h("button", { type: "button", id: args.id, className: args.className, disabled: args.disabled, "aria-haspopup": "listbox", "aria-expanded": args.open, "aria-invalid": args.invalid || undefined, "aria-label": args.ariaLabel, title: args.title, onClick: args.onOpen },
21
22
  args.icon === undefined ? null : h("span", { className: "wb-selectIcon" }, args.icon),
22
23
  h("span", { className: "wb-selectValue" }, args.value),
23
24
  h("span", { className: "wb-selectChevron" },
24
25
  h(IconChevronDownOutlineRegular, { size: 14 }))));
25
26
  }
26
27
  function loadingNode(args) {
27
- return (h("button", { type: "button", id: args.id, className: `${args.className} wb-selectLoading`, disabled: true, "aria-busy": "true", "aria-label": args.ariaLabel, title: args.loadingText },
28
+ return (h("button", { type: "button", id: args.id, className: `${args.className} wb-selectLoading`, disabled: true, "aria-busy": "true", "aria-invalid": args.invalid || undefined, "aria-label": args.ariaLabel, title: args.loadingText },
28
29
  h("span", { className: "wb-selectValue" },
29
30
  h(TextShimmer, { active: true }, args.loadingText ?? '…')),
30
31
  h("span", { className: "wb-selectChevron" },
31
- h(StateDot, { state: "ongoing", size: 14 }))));
32
+ h(Spinner, { size: 14 }))));
32
33
  }
33
34
  function searchNode(args) {
34
35
  return (h("span", { ref: args.boxRef, className: args.className },
@@ -154,21 +155,22 @@ function useSelectState(props) {
154
155
  };
155
156
  }
156
157
  export function SelectField(props) {
157
- const { value, options, onChange, label, hint, icon, renderValue, placeholder, search = true, searchLabel, searchPlaceholder, footer, onFooter, disabled = false, loading = false, loadingText, fallback, selection = 'fill', variant = 'boxed', ariaLabel, title, id, name, className, menuClassName, } = props;
158
+ const { value, options, onChange, label, hint, icon, renderValue, placeholder, search = true, searchLabel, searchPlaceholder, footer, onFooter, disabled = false, invalid = false, loading = false, loadingText, fallback, selection = 'fill', variant = 'boxed', ariaLabel, title, id, name, className, menuClassName, } = props;
158
159
  const state = useSelectState(props);
159
160
  const current = options.find((option) => option.value === value);
160
161
  const variantClass = variant === 'ghost' ? 'wb-selectGhost' : undefined;
161
162
  const triggerClass = mergeClass('wb-selectTrigger', variantClass);
162
163
  if (loading) {
163
- return framed(label, hint, className, loadingNode({ id, className: triggerClass, ariaLabel, loadingText }));
164
+ return framed(label, hint, className, loadingNode({ id, className: triggerClass, invalid, ariaLabel, loadingText }));
164
165
  }
165
166
  if (fallback !== undefined && options.length === 0) {
166
- return framed(label, hint, className, h(TextField, { id: id, name: name, className: "wb-selectFallback", "aria-label": ariaLabel, value: fallback.value, onChange: (event) => fallback.onChange(event.currentTarget.value) }));
167
+ return framed(label, hint, className, h(TextField, { id: id, name: name, className: "wb-selectFallback", invalid: invalid, "aria-label": ariaLabel, value: fallback.value, onChange: (event) => fallback.onChange(event.currentTarget.value) }));
167
168
  }
168
169
  const trigger = triggerNode({
169
170
  id,
170
171
  className: triggerClass,
171
172
  disabled,
173
+ invalid,
172
174
  open: state.open,
173
175
  ariaLabel,
174
176
  title,
@@ -0,0 +1,6 @@
1
+
2
+ .wb-spinner {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ flex: none;
6
+ }
@@ -0,0 +1,6 @@
1
+ import './Spinner.css';
2
+ export interface SpinnerProps {
3
+ readonly size?: number;
4
+ readonly label?: string | undefined;
5
+ }
6
+ export declare function Spinner({ size, label }: SpinnerProps): JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { StateDot } from '@deepseek-ai/dsh-client-ui-primitives';
2
+ import { h } from '../lib/react.js';
3
+ import './Spinner.css';
4
+ export function Spinner({ size = 14, label }) {
5
+ return (h("span", { className: "wb-spinner", ...(label === undefined ? { 'aria-hidden': true } : { role: 'status', 'aria-label': label }) },
6
+ h(StateDot, { state: "ongoing", size: size })));
7
+ }
@@ -27,6 +27,6 @@
27
27
  }
28
28
 
29
29
  :where(.wb-textField) {
30
- min-height: var(--wb-len-32);
31
- padding: var(--wb-len-6) var(--wb-len-12);
30
+ min-height: var(--wb-len-28);
31
+ padding: var(--wb-len-4) var(--wb-len-10);
32
32
  }
package/dist/host.d.ts CHANGED
@@ -8,17 +8,17 @@ export * from './lib/text.js';
8
8
  export * from './lib/ui.js';
9
9
  export * from './lib/panel-state.js';
10
10
  export * from './api/blocker-table.js';
11
+ export * from './host/browser-find.js';
12
+ export * from './host/browser-read.js';
11
13
  export * from './host/cli-error.js';
12
14
  export * from './host/data-root.js';
13
- export * from './host/browser-read.js';
14
- export * from './host/browser-find.js';
15
15
  export * from './host/facts.js';
16
16
  export * from './host/git.js';
17
17
  export * from './host/http.js';
18
18
  export * from './host/long-task.js';
19
19
  export * from './host/opencli-runtime.js';
20
- export * from './host/opencli.js';
21
20
  export * from './host/opencli-window.js';
21
+ export * from './host/opencli.js';
22
22
  export * from './host/plugin-ledger.js';
23
23
  export * from './host/plugin-source.js';
24
24
  export * from './host/provision.js';
package/dist/host.js CHANGED
@@ -8,17 +8,17 @@ export * from './lib/text.js';
8
8
  export * from './lib/ui.js';
9
9
  export * from './lib/panel-state.js';
10
10
  export * from './api/blocker-table.js';
11
+ export * from './host/browser-find.js';
12
+ export * from './host/browser-read.js';
11
13
  export * from './host/cli-error.js';
12
14
  export * from './host/data-root.js';
13
- export * from './host/browser-read.js';
14
- export * from './host/browser-find.js';
15
15
  export * from './host/facts.js';
16
16
  export * from './host/git.js';
17
17
  export * from './host/http.js';
18
18
  export * from './host/long-task.js';
19
19
  export * from './host/opencli-runtime.js';
20
- export * from './host/opencli.js';
21
20
  export * from './host/opencli-window.js';
21
+ export * from './host/opencli.js';
22
22
  export * from './host/plugin-ledger.js';
23
23
  export * from './host/plugin-source.js';
24
24
  export * from './host/provision.js';
package/dist/i18n/en.js CHANGED
@@ -45,6 +45,15 @@ export const en = {
45
45
  'status.completed': 'Completed',
46
46
  'status.subagentsRunning.one': '{n} subagent running',
47
47
  'status.subagentsRunning.other': '{n} subagents running',
48
+ 'project.attention.warning.one': '1 session waiting for you',
49
+ 'project.attention.warning.other': '{n} sessions waiting for you',
50
+ 'project.attention.running.one': '1 session running',
51
+ 'project.attention.running.other': '{n} sessions running',
52
+ 'project.attention.done.one': '1 session finished, unread',
53
+ 'project.attention.done.other': '{n} sessions finished, unread',
54
+ 'project.attention.state.warning': 'Approval',
55
+ 'project.attention.state.running': 'Running',
56
+ 'project.attention.state.done': 'New',
48
57
  'hover.copied': 'Copied',
49
58
  'time.now': 'just now',
50
59
  'time.minutes': '{n} min ago',