@dsh-workbench/shared 0.2.11 → 0.2.13

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 (40) 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/FormRows.css +53 -0
  5. package/dist/components/FormRows.d.ts +14 -0
  6. package/dist/components/FormRows.js +11 -0
  7. package/dist/components/MockStrip.css +5 -1
  8. package/dist/components/SelectField.css +12 -4
  9. package/dist/components/SelectField.d.ts +1 -0
  10. package/dist/components/SelectField.js +9 -7
  11. package/dist/components/Spinner.css +6 -0
  12. package/dist/components/Spinner.d.ts +6 -0
  13. package/dist/components/Spinner.js +7 -0
  14. package/dist/components/TextField.css +2 -2
  15. package/dist/host.d.ts +3 -3
  16. package/dist/host.js +3 -3
  17. package/dist/index.d.ts +7 -5
  18. package/dist/index.js +7 -5
  19. package/dist/lib/form-problem.d.ts +2 -0
  20. package/dist/lib/form-problem.js +20 -0
  21. package/package.json +2 -1
  22. package/src/components/ChoiceGroup.css +53 -3
  23. package/src/components/ChoiceGroup.test.tsx +88 -0
  24. package/src/components/ChoiceGroup.tsx +70 -28
  25. package/src/components/FormRows.css +53 -0
  26. package/src/components/FormRows.test.tsx +82 -0
  27. package/src/components/FormRows.tsx +39 -0
  28. package/src/components/SelectField.css +12 -4
  29. package/src/components/SelectField.test.tsx +20 -2
  30. package/src/components/SelectField.tsx +11 -3
  31. package/src/components/Spinner.css +6 -0
  32. package/src/components/Spinner.test.tsx +35 -0
  33. package/src/components/Spinner.tsx +19 -0
  34. package/src/components/TextField.css +2 -2
  35. package/src/host.ts +3 -3
  36. package/src/index.ts +7 -5
  37. package/src/lib/form-problem.test.ts +76 -0
  38. package/src/lib/form-problem.ts +22 -0
  39. package/src/components/MockStrip.css +0 -45
  40. package/src/components/MockStrip.tsx +0 -41
@@ -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,53 @@
1
+ .wb-formRows {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--wb-len-12);
5
+ }
6
+
7
+ .wb-formRow {
8
+ display: grid;
9
+ grid-template-columns: 72px minmax(0, 1fr);
10
+ align-items: center;
11
+ gap: var(--wb-len-8);
12
+ }
13
+
14
+ .wb-formRow[data-layout='stacked'] {
15
+ grid-template-columns: minmax(0, 1fr);
16
+ align-items: stretch;
17
+ gap: var(--wb-len-6);
18
+ }
19
+
20
+ .wb-formLabel {
21
+ min-width: 0;
22
+ font-size: var(--dsw-font-xs-13-font-size);
23
+ color: var(--dsw-alias-label-secondary);
24
+ overflow-wrap: anywhere;
25
+ }
26
+
27
+ .wb-formControl {
28
+ display: flex;
29
+ align-items: center;
30
+ min-width: 0;
31
+ }
32
+
33
+ .wb-formHint {
34
+ grid-column: 2;
35
+ font-size: var(--dsw-font-xxs-12-font-size);
36
+ line-height: var(--wb-line-18);
37
+ color: var(--dsw-alias-label-tertiary);
38
+ }
39
+
40
+ .wb-formProblem {
41
+ grid-column: 2;
42
+ font-size: var(--dsw-font-xxs-12-font-size);
43
+ line-height: var(--wb-line-18);
44
+ color: var(--dsw-alias-state-error-primary);
45
+ }
46
+
47
+ .wb-formRow:has(.wb-formProblem) .wb-formLabel {
48
+ color: var(--dsw-alias-state-error-primary);
49
+ }
50
+
51
+ .wb-formRow:has(:disabled) .wb-formLabel {
52
+ opacity: 0.4;
53
+ }
@@ -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
+ }
@@ -42,4 +42,8 @@
42
42
  border-color: transparent;
43
43
  background: var(--dsw-alias-brand-primary);
44
44
  color: var(--dsw-alias-label-primary-foreground);
45
- }
45
+ }
46
+
47
+ .wb-panelBody.wb-panelMany .wb-mockStrip {
48
+ display: none;
49
+ }
@@ -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/index.d.ts CHANGED
@@ -7,8 +7,8 @@ export * from './api/facts.js';
7
7
  export * from './api/host-write.js';
8
8
  export * from './api/long-task.js';
9
9
  export * from './api/unit-board.js';
10
- export * from './api/unit.js';
11
10
  export * from './api/unit-declaration.js';
11
+ export * from './api/unit.js';
12
12
  export * from './api/workbench-client.js';
13
13
  export * from './api/workbench-contract.js';
14
14
  export * from './components/Activity.js';
@@ -19,19 +19,20 @@ export * from './components/CardAnchor.js';
19
19
  export * from './components/CardBoundary.js';
20
20
  export * from './components/CardHead.js';
21
21
  export * from './components/ChoiceGroup.js';
22
+ export * from './components/Field.js';
23
+ export * from './components/FormRows.js';
24
+ export * from './components/Link.js';
22
25
  export * from './components/LongTaskRow.js';
23
- export * from './components/MockStrip.js';
24
26
  export * from './components/Note.js';
25
27
  export * from './components/ResourceStates.js';
26
28
  export * from './components/Rows.js';
27
29
  export * from './components/SaveBar.js';
28
- export * from './components/SettingsSection.js';
29
30
  export * from './components/SelectField.js';
31
+ export * from './components/SettingsSection.js';
32
+ export * from './components/Spinner.js';
30
33
  export * from './components/Stack.js';
31
34
  export * from './components/StateBlock.js';
32
35
  export * from './components/TextField.js';
33
- export * from './components/Field.js';
34
- export * from './components/Link.js';
35
36
  export * from './components/board-module.js';
36
37
  export * from './components/icons.js';
37
38
  export * from './config/constants.js';
@@ -42,6 +43,7 @@ export * from './i18n/merge.js';
42
43
  export * from './i18n/zh.js';
43
44
  export * from './lib/blocker-action.js';
44
45
  export * from './lib/busy.js';
46
+ export * from './lib/form-problem.js';
45
47
  export * from './lib/panel-state.js';
46
48
  export * from './lib/react.js';
47
49
  export * from './lib/text.js';
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ export * from './api/facts.js';
7
7
  export * from './api/host-write.js';
8
8
  export * from './api/long-task.js';
9
9
  export * from './api/unit-board.js';
10
- export * from './api/unit.js';
11
10
  export * from './api/unit-declaration.js';
11
+ export * from './api/unit.js';
12
12
  export * from './api/workbench-client.js';
13
13
  export * from './api/workbench-contract.js';
14
14
  export * from './components/Activity.js';
@@ -19,19 +19,20 @@ export * from './components/CardAnchor.js';
19
19
  export * from './components/CardBoundary.js';
20
20
  export * from './components/CardHead.js';
21
21
  export * from './components/ChoiceGroup.js';
22
+ export * from './components/Field.js';
23
+ export * from './components/FormRows.js';
24
+ export * from './components/Link.js';
22
25
  export * from './components/LongTaskRow.js';
23
- export * from './components/MockStrip.js';
24
26
  export * from './components/Note.js';
25
27
  export * from './components/ResourceStates.js';
26
28
  export * from './components/Rows.js';
27
29
  export * from './components/SaveBar.js';
28
- export * from './components/SettingsSection.js';
29
30
  export * from './components/SelectField.js';
31
+ export * from './components/SettingsSection.js';
32
+ export * from './components/Spinner.js';
30
33
  export * from './components/Stack.js';
31
34
  export * from './components/StateBlock.js';
32
35
  export * from './components/TextField.js';
33
- export * from './components/Field.js';
34
- export * from './components/Link.js';
35
36
  export * from './components/board-module.js';
36
37
  export * from './components/icons.js';
37
38
  export * from './config/constants.js';
@@ -42,6 +43,7 @@ export * from './i18n/merge.js';
42
43
  export * from './i18n/zh.js';
43
44
  export * from './lib/blocker-action.js';
44
45
  export * from './lib/busy.js';
46
+ export * from './lib/form-problem.js';
45
47
  export * from './lib/panel-state.js';
46
48
  export * from './lib/react.js';
47
49
  export * from './lib/text.js';
@@ -0,0 +1,2 @@
1
+ export declare function firstProblemOf(root: ParentNode): Element | null;
2
+ export declare function revealFirstProblem(root: ParentNode): boolean;
@@ -0,0 +1,20 @@
1
+ const PROBLEM_SELECTOR = '[data-wb-problem]';
2
+ const FOCUSABLE_SELECTOR = [
3
+ 'input:not([disabled])',
4
+ 'select:not([disabled])',
5
+ 'textarea:not([disabled])',
6
+ 'button:not([disabled])',
7
+ 'a[href]',
8
+ '[tabindex]:not([tabindex="-1"])',
9
+ ].join(', ');
10
+ export function firstProblemOf(root) {
11
+ return root.querySelector(PROBLEM_SELECTOR);
12
+ }
13
+ export function revealFirstProblem(root) {
14
+ const problem = firstProblemOf(root);
15
+ if (problem === null)
16
+ return false;
17
+ problem.scrollIntoView?.({ block: 'nearest' });
18
+ problem.querySelector(FOCUSABLE_SELECTOR)?.focus?.();
19
+ return true;
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/shared",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "工作台与模块共享的纯代码与契约类型:类型、纯逻辑、哑组件、平台解析。宿主侧的东西从 /host 进(不影响浏览器产物)。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,6 +43,7 @@
43
43
  "typecheck": "tsc --noEmit",
44
44
  "test": "vitest run",
45
45
  "coverage": "vitest run --coverage",
46
+ "barrels": "node scripts/barrels.mjs",
46
47
  "selfcheck": "node scripts/check-state.mjs",
47
48
  "arch:report": "arch-guard --report-only",
48
49
  "arch": "arch-guard"
@@ -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;