@dsh-workbench/shared 0.2.13 → 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.
@@ -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
+ }
@@ -1,14 +1,25 @@
1
1
  .wb-formRows {
2
- display: flex;
3
- flex-direction: column;
4
- gap: var(--wb-len-12);
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;
5
10
  }
6
11
 
7
12
  .wb-formRow {
8
13
  display: grid;
9
- grid-template-columns: 72px minmax(0, 1fr);
14
+ grid-column: 1 / -1;
15
+ grid-template-columns: minmax(72px, max-content) minmax(0, 1fr);
10
16
  align-items: center;
11
- gap: var(--wb-len-8);
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;
12
23
  }
13
24
 
14
25
  .wb-formRow[data-layout='stacked'] {
@@ -22,6 +33,11 @@
22
33
  font-size: var(--dsw-font-xs-13-font-size);
23
34
  color: var(--dsw-alias-label-secondary);
24
35
  overflow-wrap: anywhere;
36
+ white-space: nowrap;
37
+ }
38
+
39
+ .wb-formRow[data-layout='stacked'] .wb-formLabel {
40
+ white-space: normal;
25
41
  }
26
42
 
27
43
  .wb-formControl {
@@ -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
  }
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',
@@ -47,6 +47,15 @@ export declare const dictionaries: {
47
47
  readonly 'status.completed': "已完成";
48
48
  readonly 'status.subagentsRunning.one': "{n} 个子代理运行中";
49
49
  readonly 'status.subagentsRunning.other': "{n} 个子代理运行中";
50
+ readonly 'project.attention.warning.one': "1 个会话在等你点头";
51
+ readonly 'project.attention.warning.other': "{n} 个会话在等你点头";
52
+ readonly 'project.attention.running.one': "1 个会话在跑";
53
+ readonly 'project.attention.running.other': "{n} 个会话在跑";
54
+ readonly 'project.attention.done.one': "1 个会话跑完没看";
55
+ readonly 'project.attention.done.other': "{n} 个会话跑完没看";
56
+ readonly 'project.attention.state.warning': "待审批";
57
+ readonly 'project.attention.state.running': "在跑";
58
+ readonly 'project.attention.state.done': "新消息";
50
59
  readonly 'hover.copied': "已复制";
51
60
  readonly 'time.now': "刚刚";
52
61
  readonly 'time.minutes': "{n} 分钟前";
@@ -400,7 +409,7 @@ export declare const dictionaries: {
400
409
  readonly 'configSync.emptyProviders': "工作台还没有任何模型供应商 —— 去「配置同步」从某个 profile 把 llm- 那几行搬过来,或自己去「模型」里配。";
401
410
  readonly 'configSync.failed': "同步没成功,再试一次。";
402
411
  };
403
- en: Record<"time.now" | "time.ago" | "time.minutes" | "time.hours" | "time.days" | "blocker.retry" | "blocker.copy" | "blocker.unknown.title" | "blocker.login.go" | "blocker.login.titleGeneric" | "blocker.login.title" | "blocker.login.manual" | "blocker.login.hint" | "blocker.forbidden.open" | "blocker.forbidden.titleGeneric" | "blocker.forbidden.title" | "blocker.forbidden.hint" | "blocker.timeout.hint" | "blocker.exec.hint" | "blocker.fix.install" | "blocker.fix.update" | "blocker.market.open" | "env.unit.failed" | "env.unit.failedHint" | "env.unit.missingPlugin" | "env.unit.hint" | "env.unit.missingCommands" | "env.unit.missingCommandsHint" | "env.unit.unchecked" | "env.unit.uncheckedHint" | "brand.name" | "toggle.open" | "toggle.collapse" | "project.label" | "workspace.section" | "project.none" | "project.empty" | "project.add" | "project.add.busy" | "project.add.failed" | "project.search.label" | "project.search.placeholder" | "project.search.empty" | "project.group.workspaces" | "project.group.defaultWorkspace" | "project.group.workshop" | "project.rename.row" | "project.rename.current" | "project.rename.title" | "project.rename.description" | "project.rename.field" | "project.rename.placeholder" | "project.rename.hint" | "project.rename.save" | "project.rename.saving" | "project.rename.cancel" | "project.rename.failed" | "session.new" | "session.new.label" | "session.empty" | "session.noProject" | "session.list.label" | "session.copy" | "session.pinned" | "session.actions" | "status.idle" | "status.running" | "status.waitingApproval" | "status.planReview" | "status.waitingAnswer" | "status.compact.approval" | "status.compact.planReview" | "status.compact.answer" | "status.completed" | "status.subagentsRunning.one" | "status.subagentsRunning.other" | "hover.copied" | "time.short.minutes" | "time.short.hours" | "time.short.days" | "time.short.months" | "time.short.years" | "panels.title" | "panels.groupBoardless" | "panels.groupPlugins" | "panels.market.defaultHint" | "panels.market.defaultLabel" | "panels.market.defaultLocked" | "panels.followAll" | "panels.followAllConfirmTitle" | "panels.followAllConfirmDetail" | "panels.followAllConfirmAck" | "panels.followAllConfirmYes" | "panels.followAllConfirmNo" | "panels.enabled" | "panels.disabled" | "panels.choose" | "panels.buildModule" | "panels.failed" | "panels.dragHint" | "panels.unknownStep" | "panels.labelWithHint" | "panels.dep.failed" | "panels.dep.missingPlugin" | "panels.dep.missingCommands" | "panels.dep.unchecked" | "panels.tab.modules" | "panels.tab.market" | "panels.browseMarket" | "panels.noneInstalled" | "panels.noneInstalledHint" | "panels.market.installed" | "panels.market.notActive" | "panels.market.notUsable" | "panels.market.broken" | "panels.market.hasNew" | "panels.market.missing" | "panels.market.unchecked" | "panels.market.checking" | "panels.market.sectionInstalled" | "panels.market.sectionPending" | "panels.market.sectionShared" | "panels.market.sharedUsedBy" | "panels.market.listSeparator" | "panels.market.sharedMissing" | "panels.market.sharedOutdated" | "panels.market.sharedUnsatisfied" | "panels.market.sharedOk" | "panels.market.versionArrow" | "panels.market.lead" | "panels.market.install" | "panels.market.update" | "panels.market.swapSlow" | "panels.market.uninstall" | "panels.market.uninstallLabel" | "panels.market.installing" | "panels.market.updating" | "panels.market.uninstalling" | "panels.market.confirmUninstallTitle" | "panels.market.confirmUninstallDetail" | "panels.market.confirmUninstallAck" | "panels.market.confirmYes" | "panels.market.confirmNo" | "panels.market.failed" | "panels.market.needEnv" | "panels.market.needCommands" | "panels.market.localMark" | "panels.market.localHint" | "panels.market.noSource" | "panels.market.badSource" | "panels.market.empty" | "panels.market.sectionPlugins" | "panels.market.moduleOutdated" | "panels.market.moduleRefused" | "panels.market.moduleRefusedDetail" | "panels.market.moduleRestartHint" | "panels.market.pendingRestart" | "panels.market.workbenchName" | "panels.market.workbenchLocal" | "panels.market.pluginKeptInUse" | "panels.market.pluginKeptUnsure" | "panels.market.pluginKeptHeld" | "panels.market.pluginKeptUnnamed" | "panels.market.pluginBroken" | "panels.market.restartNeeded" | "panels.market.foot" | "panels.market.recheck" | "panels.market.checkUpdates" | "panels.market.checkedAt" | "panels.market.justNow" | "panels.market.minutesAgo" | "panels.market.hoursAgo" | "panels.market.add.description" | "panels.market.add.specPlaceholder" | "panels.market.add.guideToggle" | "panels.market.add.guideHide" | "panels.market.add.guideIdTitle" | "panels.market.add.guideIdExample" | "panels.market.add.guideIdHint" | "panels.market.add.guideGitTitle" | "panels.market.add.guideGitExample" | "panels.market.add.guideGitHint" | "panels.market.add.guidePathTitle" | "panels.market.add.guidePathExample" | "panels.market.add.guidePathHint" | "panels.market.add.guideExampleLabel" | "panels.market.add.guideFill" | "panels.market.add.guideSafety" | "panels.market.add.registryLegend" | "panels.market.add.registryOfficial" | "panels.market.add.registryMirror" | "panels.market.add.registryWithHost" | "panels.market.add.registryToggle" | "panels.market.add.installVersion" | "panels.market.add.registryDefault" | "panels.market.add.registryCustom" | "panels.market.add.registryCustomPlaceholder" | "panels.market.add.registryCustomHint" | "panels.market.add.registryCustomInvalid" | "panels.market.add.run" | "panels.market.add.edit" | "panels.market.add.changeRegistry" | "panels.market.add.installingTitle" | "panels.market.add.installedTitle" | "panels.market.add.failedTitle" | "panels.market.add.cancelledTitle" | "panels.market.add.failureNetworkHost" | "panels.market.add.problemInvalid" | "panels.market.add.problemInstalled" | "panels.market.add.problemNotFound" | "panels.market.add.problemNotPackage" | "panels.market.add.problemNotBundle" | "panels.market.add.problemNotWorkbenchModule" | "panels.market.add.problemNotModuleOrPlugin" | "panels.market.add.failureNotModuleOrPlugin" | "panels.market.add.installedPlugin" | "panels.market.add.pluginPreview" | "panels.market.add.pluginAlias" | "panels.market.add.problemNetwork" | "panels.market.add.problemUnknown" | "panels.market.add.failureNetwork" | "panels.market.add.failureNotFound" | "panels.market.add.failureNoMatchingVersion" | "panels.market.add.failureDiskFull" | "panels.market.add.failurePermission" | "panels.market.add.failureBuildBlocked" | "panels.market.add.failureIntegrity" | "panels.market.add.failureTimeout" | "panels.market.add.failurePnpmMissing" | "panels.market.add.failureInstalled" | "panels.market.add.failureGeneric" | "panels.market.add.approveTitle" | "panels.market.add.approveCaution" | "panels.market.add.approveRun" | "panels.market.add.restart" | "panels.market.add.tookEffect" | "panels.market.add.details" | "panels.market.add.open" | "panels.market.add.title" | "panels.market.add.intro" | "panels.market.add.specLabel" | "panels.market.add.specHint" | "panels.market.add.exampleTitle" | "panels.market.add.exampleFill" | "panels.market.add.registryLabel" | "panels.market.add.registryProfile" | "panels.market.add.registryPnpm" | "panels.market.add.check" | "panels.market.add.checking" | "panels.market.add.classifying" | "panels.market.add.change" | "panels.market.add.previewTitle" | "panels.market.add.fieldName" | "panels.market.add.fieldVersion" | "panels.market.add.fieldDescription" | "panels.market.add.fieldSource" | "panels.market.add.install" | "panels.market.add.installing" | "panels.market.add.cancel" | "panels.market.add.close" | "panels.market.add.retry" | "panels.market.add.changeSource" | "panels.market.add.approveBuilds" | "panels.market.add.approveHint" | "panels.market.add.doneApplied" | "panels.market.add.doneRestart" | "panels.market.add.restartWhy" | "panels.market.add.restartWhy.no-hmr" | "panels.market.add.restartWhy.package-moved" | "panels.market.add.restartWhy.host-module" | "panels.market.add.restartWhy.unmanaged-plugin" | "panels.market.add.restartWhy.module-comparison" | "panels.market.add.cancelled" | "panels.market.add.failed" | "panels.market.add.removed" | "panels.market.add.supplyPending" | "panels.market.add.supplyFailed" | "panels.market.add.notModule" | "panels.market.add.removedRestart" | "panels.market.add.remove" | "panels.market.add.problem.invalidSpec" | "panels.market.add.problem.alreadyInstalled" | "panels.market.add.problem.notFound" | "panels.market.add.problem.notAPackage" | "panels.market.add.problem.notABundle" | "panels.market.add.problem.network" | "panels.market.add.problem.unknown" | "panels.market.add.reason.invalidSpec" | "panels.market.add.reason.incompatible" | "panels.market.add.reason.notBundle" | "panels.market.add.reason.notRemovable" | "panels.market.add.reason.bundleInUse" | "panels.market.add.reason.stopProfile" | "panels.market.add.reason.staleApproval" | "panels.market.add.reason.operationError" | "panels.market.add.reason.buildBlocked" | "panels.market.add.reason.pnpmMissing" | "panels.market.add.reason.noMatchingVersion" | "panels.market.add.reason.diskFull" | "panels.market.add.reason.permission" | "panels.market.add.reason.integrity" | "panels.market.add.reason.timeout" | "panels.market.add.reason.network" | "panels.market.add.reason.notFound" | "panels.market.add.reason.unknown" | "blocker.fix.running" | "blocker.fix.failed" | "blocker.retry.running" | "panels.reading" | "panels.cardBroken" | "panels.cardRetry" | "panels.empty" | "panels.emptyHint" | "env.title" | "env.checking" | "env.installing" | "env.failed" | "env.elapsed" | "env.stepFailed.install" | "env.stepFailed.path" | "env.stepFailed.shell" | "env.bridge.ok" | "env.bridge.short" | "env.bridge.why" | "env.bridge.daemon" | "env.bridge.extension" | "env.bridge.extensionLink" | "env.bridge.profile" | "env.bridge.stale" | "env.bridge.unknown" | "env.fix.title" | "env.fix.why" | "env.fix.whyPath" | "env.fix.whyShell" | "env.fix.step" | "env.fix.stepPath" | "env.fix.stepShell" | "env.fix.permission" | "env.fix.network" | "env.fix.details" | "env.retry" | "env.copyCommand" | "env.copied" | "env.step.opencli" | "env.step.codingPlugin" | "env.step.bridge" | "env.step.warm" | "env.warm.done" | "env.warm.short" | "rightbar.label" | "rightbar.expand" | "rightbar.collapse" | "rightbar.column.label" | "rightbar.column.workbench" | "rightbar.column.dsh" | "rightbar.refresh" | "rightbar.refreshing" | "rightbar.updatedAt" | "rightbar.noProject" | "rightbar.openPlatform" | "coding.notARepo.title" | "error.unknown" | "board.title" | "panel.conversation" | "panels.label" | "configSync.nav" | "configSync.lead" | "configSync.source" | "configSync.sourceBroken" | "configSync.sourceRows" | "configSync.sourceDead" | "configSync.refresh" | "configSync.loading" | "configSync.readHint" | "configSync.tag.new" | "configSync.tag.differs" | "configSync.conflictNote" | "configSync.code.lang" | "configSync.code.copy" | "configSync.code.copied" | "configSync.code.wrap" | "configSync.code.unwrap" | "configSync.code.collapse" | "configSync.code.collapseAria" | "configSync.code.expandAria" | "configSync.code.expand" | "configSync.missing" | "configSync.apply" | "configSync.applyNone" | "configSync.applying" | "configSync.applied" | "configSync.appliedIds" | "configSync.skippedIds" | "configSync.nothing" | "configSync.unreadable" | "configSync.noSource" | "configSync.emptyProviders" | "configSync.failed", string>;
412
+ en: Record<"time.now" | "time.ago" | "time.minutes" | "time.hours" | "time.days" | "blocker.retry" | "blocker.copy" | "blocker.unknown.title" | "blocker.login.go" | "blocker.login.titleGeneric" | "blocker.login.title" | "blocker.login.manual" | "blocker.login.hint" | "blocker.forbidden.open" | "blocker.forbidden.titleGeneric" | "blocker.forbidden.title" | "blocker.forbidden.hint" | "blocker.timeout.hint" | "blocker.exec.hint" | "blocker.fix.install" | "blocker.fix.update" | "blocker.market.open" | "env.unit.failed" | "env.unit.failedHint" | "env.unit.missingPlugin" | "env.unit.hint" | "env.unit.missingCommands" | "env.unit.missingCommandsHint" | "env.unit.unchecked" | "env.unit.uncheckedHint" | "brand.name" | "toggle.open" | "toggle.collapse" | "project.label" | "workspace.section" | "project.none" | "project.empty" | "project.add" | "project.add.busy" | "project.add.failed" | "project.search.label" | "project.search.placeholder" | "project.search.empty" | "project.group.workspaces" | "project.group.defaultWorkspace" | "project.group.workshop" | "project.rename.row" | "project.rename.current" | "project.rename.title" | "project.rename.description" | "project.rename.field" | "project.rename.placeholder" | "project.rename.hint" | "project.rename.save" | "project.rename.saving" | "project.rename.cancel" | "project.rename.failed" | "session.new" | "session.new.label" | "session.empty" | "session.noProject" | "session.list.label" | "session.copy" | "session.pinned" | "session.actions" | "status.idle" | "status.running" | "status.waitingApproval" | "status.planReview" | "status.waitingAnswer" | "status.compact.approval" | "status.compact.planReview" | "status.compact.answer" | "status.completed" | "status.subagentsRunning.one" | "status.subagentsRunning.other" | "project.attention.warning.one" | "project.attention.warning.other" | "project.attention.running.one" | "project.attention.running.other" | "project.attention.done.one" | "project.attention.done.other" | "project.attention.state.warning" | "project.attention.state.running" | "project.attention.state.done" | "hover.copied" | "time.short.minutes" | "time.short.hours" | "time.short.days" | "time.short.months" | "time.short.years" | "panels.title" | "panels.groupBoardless" | "panels.groupPlugins" | "panels.market.defaultHint" | "panels.market.defaultLabel" | "panels.market.defaultLocked" | "panels.followAll" | "panels.followAllConfirmTitle" | "panels.followAllConfirmDetail" | "panels.followAllConfirmAck" | "panels.followAllConfirmYes" | "panels.followAllConfirmNo" | "panels.enabled" | "panels.disabled" | "panels.choose" | "panels.buildModule" | "panels.failed" | "panels.dragHint" | "panels.unknownStep" | "panels.labelWithHint" | "panels.dep.failed" | "panels.dep.missingPlugin" | "panels.dep.missingCommands" | "panels.dep.unchecked" | "panels.tab.modules" | "panels.tab.market" | "panels.browseMarket" | "panels.noneInstalled" | "panels.noneInstalledHint" | "panels.market.installed" | "panels.market.notActive" | "panels.market.notUsable" | "panels.market.broken" | "panels.market.hasNew" | "panels.market.missing" | "panels.market.unchecked" | "panels.market.checking" | "panels.market.sectionInstalled" | "panels.market.sectionPending" | "panels.market.sectionShared" | "panels.market.sharedUsedBy" | "panels.market.listSeparator" | "panels.market.sharedMissing" | "panels.market.sharedOutdated" | "panels.market.sharedUnsatisfied" | "panels.market.sharedOk" | "panels.market.versionArrow" | "panels.market.lead" | "panels.market.install" | "panels.market.update" | "panels.market.swapSlow" | "panels.market.uninstall" | "panels.market.uninstallLabel" | "panels.market.installing" | "panels.market.updating" | "panels.market.uninstalling" | "panels.market.confirmUninstallTitle" | "panels.market.confirmUninstallDetail" | "panels.market.confirmUninstallAck" | "panels.market.confirmYes" | "panels.market.confirmNo" | "panels.market.failed" | "panels.market.needEnv" | "panels.market.needCommands" | "panels.market.localMark" | "panels.market.localHint" | "panels.market.noSource" | "panels.market.badSource" | "panels.market.empty" | "panels.market.sectionPlugins" | "panels.market.moduleOutdated" | "panels.market.moduleRefused" | "panels.market.moduleRefusedDetail" | "panels.market.moduleRestartHint" | "panels.market.pendingRestart" | "panels.market.workbenchName" | "panels.market.workbenchLocal" | "panels.market.pluginKeptInUse" | "panels.market.pluginKeptUnsure" | "panels.market.pluginKeptHeld" | "panels.market.pluginKeptUnnamed" | "panels.market.pluginBroken" | "panels.market.restartNeeded" | "panels.market.foot" | "panels.market.recheck" | "panels.market.checkUpdates" | "panels.market.checkedAt" | "panels.market.justNow" | "panels.market.minutesAgo" | "panels.market.hoursAgo" | "panels.market.add.description" | "panels.market.add.specPlaceholder" | "panels.market.add.guideToggle" | "panels.market.add.guideHide" | "panels.market.add.guideIdTitle" | "panels.market.add.guideIdExample" | "panels.market.add.guideIdHint" | "panels.market.add.guideGitTitle" | "panels.market.add.guideGitExample" | "panels.market.add.guideGitHint" | "panels.market.add.guidePathTitle" | "panels.market.add.guidePathExample" | "panels.market.add.guidePathHint" | "panels.market.add.guideExampleLabel" | "panels.market.add.guideFill" | "panels.market.add.guideSafety" | "panels.market.add.registryLegend" | "panels.market.add.registryOfficial" | "panels.market.add.registryMirror" | "panels.market.add.registryWithHost" | "panels.market.add.registryToggle" | "panels.market.add.installVersion" | "panels.market.add.registryDefault" | "panels.market.add.registryCustom" | "panels.market.add.registryCustomPlaceholder" | "panels.market.add.registryCustomHint" | "panels.market.add.registryCustomInvalid" | "panels.market.add.run" | "panels.market.add.edit" | "panels.market.add.changeRegistry" | "panels.market.add.installingTitle" | "panels.market.add.installedTitle" | "panels.market.add.failedTitle" | "panels.market.add.cancelledTitle" | "panels.market.add.failureNetworkHost" | "panels.market.add.problemInvalid" | "panels.market.add.problemInstalled" | "panels.market.add.problemNotFound" | "panels.market.add.problemNotPackage" | "panels.market.add.problemNotBundle" | "panels.market.add.problemNotWorkbenchModule" | "panels.market.add.problemNotModuleOrPlugin" | "panels.market.add.failureNotModuleOrPlugin" | "panels.market.add.installedPlugin" | "panels.market.add.pluginPreview" | "panels.market.add.pluginAlias" | "panels.market.add.problemNetwork" | "panels.market.add.problemUnknown" | "panels.market.add.failureNetwork" | "panels.market.add.failureNotFound" | "panels.market.add.failureNoMatchingVersion" | "panels.market.add.failureDiskFull" | "panels.market.add.failurePermission" | "panels.market.add.failureBuildBlocked" | "panels.market.add.failureIntegrity" | "panels.market.add.failureTimeout" | "panels.market.add.failurePnpmMissing" | "panels.market.add.failureInstalled" | "panels.market.add.failureGeneric" | "panels.market.add.approveTitle" | "panels.market.add.approveCaution" | "panels.market.add.approveRun" | "panels.market.add.restart" | "panels.market.add.tookEffect" | "panels.market.add.details" | "panels.market.add.open" | "panels.market.add.title" | "panels.market.add.intro" | "panels.market.add.specLabel" | "panels.market.add.specHint" | "panels.market.add.exampleTitle" | "panels.market.add.exampleFill" | "panels.market.add.registryLabel" | "panels.market.add.registryProfile" | "panels.market.add.registryPnpm" | "panels.market.add.check" | "panels.market.add.checking" | "panels.market.add.classifying" | "panels.market.add.change" | "panels.market.add.previewTitle" | "panels.market.add.fieldName" | "panels.market.add.fieldVersion" | "panels.market.add.fieldDescription" | "panels.market.add.fieldSource" | "panels.market.add.install" | "panels.market.add.installing" | "panels.market.add.cancel" | "panels.market.add.close" | "panels.market.add.retry" | "panels.market.add.changeSource" | "panels.market.add.approveBuilds" | "panels.market.add.approveHint" | "panels.market.add.doneApplied" | "panels.market.add.doneRestart" | "panels.market.add.restartWhy" | "panels.market.add.restartWhy.no-hmr" | "panels.market.add.restartWhy.package-moved" | "panels.market.add.restartWhy.host-module" | "panels.market.add.restartWhy.unmanaged-plugin" | "panels.market.add.restartWhy.module-comparison" | "panels.market.add.cancelled" | "panels.market.add.failed" | "panels.market.add.removed" | "panels.market.add.supplyPending" | "panels.market.add.supplyFailed" | "panels.market.add.notModule" | "panels.market.add.removedRestart" | "panels.market.add.remove" | "panels.market.add.problem.invalidSpec" | "panels.market.add.problem.alreadyInstalled" | "panels.market.add.problem.notFound" | "panels.market.add.problem.notAPackage" | "panels.market.add.problem.notABundle" | "panels.market.add.problem.network" | "panels.market.add.problem.unknown" | "panels.market.add.reason.invalidSpec" | "panels.market.add.reason.incompatible" | "panels.market.add.reason.notBundle" | "panels.market.add.reason.notRemovable" | "panels.market.add.reason.bundleInUse" | "panels.market.add.reason.stopProfile" | "panels.market.add.reason.staleApproval" | "panels.market.add.reason.operationError" | "panels.market.add.reason.buildBlocked" | "panels.market.add.reason.pnpmMissing" | "panels.market.add.reason.noMatchingVersion" | "panels.market.add.reason.diskFull" | "panels.market.add.reason.permission" | "panels.market.add.reason.integrity" | "panels.market.add.reason.timeout" | "panels.market.add.reason.network" | "panels.market.add.reason.notFound" | "panels.market.add.reason.unknown" | "blocker.fix.running" | "blocker.fix.failed" | "blocker.retry.running" | "panels.reading" | "panels.cardBroken" | "panels.cardRetry" | "panels.empty" | "panels.emptyHint" | "env.title" | "env.checking" | "env.installing" | "env.failed" | "env.elapsed" | "env.stepFailed.install" | "env.stepFailed.path" | "env.stepFailed.shell" | "env.bridge.ok" | "env.bridge.short" | "env.bridge.why" | "env.bridge.daemon" | "env.bridge.extension" | "env.bridge.extensionLink" | "env.bridge.profile" | "env.bridge.stale" | "env.bridge.unknown" | "env.fix.title" | "env.fix.why" | "env.fix.whyPath" | "env.fix.whyShell" | "env.fix.step" | "env.fix.stepPath" | "env.fix.stepShell" | "env.fix.permission" | "env.fix.network" | "env.fix.details" | "env.retry" | "env.copyCommand" | "env.copied" | "env.step.opencli" | "env.step.codingPlugin" | "env.step.bridge" | "env.step.warm" | "env.warm.done" | "env.warm.short" | "rightbar.label" | "rightbar.expand" | "rightbar.collapse" | "rightbar.column.label" | "rightbar.column.workbench" | "rightbar.column.dsh" | "rightbar.refresh" | "rightbar.refreshing" | "rightbar.updatedAt" | "rightbar.noProject" | "rightbar.openPlatform" | "coding.notARepo.title" | "error.unknown" | "board.title" | "panel.conversation" | "panels.label" | "configSync.nav" | "configSync.lead" | "configSync.source" | "configSync.sourceBroken" | "configSync.sourceRows" | "configSync.sourceDead" | "configSync.refresh" | "configSync.loading" | "configSync.readHint" | "configSync.tag.new" | "configSync.tag.differs" | "configSync.conflictNote" | "configSync.code.lang" | "configSync.code.copy" | "configSync.code.copied" | "configSync.code.wrap" | "configSync.code.unwrap" | "configSync.code.collapse" | "configSync.code.collapseAria" | "configSync.code.expandAria" | "configSync.code.expand" | "configSync.missing" | "configSync.apply" | "configSync.applyNone" | "configSync.applying" | "configSync.applied" | "configSync.appliedIds" | "configSync.skippedIds" | "configSync.nothing" | "configSync.unreadable" | "configSync.noSource" | "configSync.emptyProviders" | "configSync.failed", string>;
404
413
  };
405
414
  export type { WorkbenchMessages };
406
415
  export interface Dictionaries {
package/dist/i18n/zh.d.ts CHANGED
@@ -45,6 +45,15 @@ export declare const zh: {
45
45
  readonly 'status.completed': "已完成";
46
46
  readonly 'status.subagentsRunning.one': "{n} 个子代理运行中";
47
47
  readonly 'status.subagentsRunning.other': "{n} 个子代理运行中";
48
+ readonly 'project.attention.warning.one': "1 个会话在等你点头";
49
+ readonly 'project.attention.warning.other': "{n} 个会话在等你点头";
50
+ readonly 'project.attention.running.one': "1 个会话在跑";
51
+ readonly 'project.attention.running.other': "{n} 个会话在跑";
52
+ readonly 'project.attention.done.one': "1 个会话跑完没看";
53
+ readonly 'project.attention.done.other': "{n} 个会话跑完没看";
54
+ readonly 'project.attention.state.warning': "待审批";
55
+ readonly 'project.attention.state.running': "在跑";
56
+ readonly 'project.attention.state.done': "新消息";
48
57
  readonly 'hover.copied': "已复制";
49
58
  readonly 'time.now': "刚刚";
50
59
  readonly 'time.minutes': "{n} 分钟前";
package/dist/i18n/zh.js CHANGED
@@ -45,6 +45,15 @@ export const zh = {
45
45
  'status.completed': '已完成',
46
46
  'status.subagentsRunning.one': '{n} 个子代理运行中',
47
47
  'status.subagentsRunning.other': '{n} 个子代理运行中',
48
+ 'project.attention.warning.one': '1 个会话在等你点头',
49
+ 'project.attention.warning.other': '{n} 个会话在等你点头',
50
+ 'project.attention.running.one': '1 个会话在跑',
51
+ 'project.attention.running.other': '{n} 个会话在跑',
52
+ 'project.attention.done.one': '1 个会话跑完没看',
53
+ 'project.attention.done.other': '{n} 个会话跑完没看',
54
+ 'project.attention.state.warning': '待审批',
55
+ 'project.attention.state.running': '在跑',
56
+ 'project.attention.state.done': '新消息',
48
57
  'hover.copied': '已复制',
49
58
  'time.now': '刚刚',
50
59
  'time.minutes': '{n} 分钟前',
package/dist/index.d.ts CHANGED
@@ -20,6 +20,7 @@ export * from './components/CardBoundary.js';
20
20
  export * from './components/CardHead.js';
21
21
  export * from './components/ChoiceGroup.js';
22
22
  export * from './components/Field.js';
23
+ export * from './components/FormActions.js';
23
24
  export * from './components/FormRows.js';
24
25
  export * from './components/Link.js';
25
26
  export * from './components/LongTaskRow.js';
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ export * from './components/CardBoundary.js';
20
20
  export * from './components/CardHead.js';
21
21
  export * from './components/ChoiceGroup.js';
22
22
  export * from './components/Field.js';
23
+ export * from './components/FormActions.js';
23
24
  export * from './components/FormRows.js';
24
25
  export * from './components/Link.js';
25
26
  export * from './components/LongTaskRow.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/shared",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "工作台与模块共享的纯代码与契约类型:类型、纯逻辑、哑组件、平台解析。宿主侧的东西从 /host 进(不影响浏览器产物)。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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,157 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { FormActions, type FormAction, type FormActionKind } from './FormActions.js';
4
+ import { SaveBar } from './SaveBar.js';
5
+
6
+ function action(key: string, kind?: FormActionKind): FormAction {
7
+ return { key, label: key, ...(kind === undefined ? {} : { kind }), onClick: () => {} };
8
+ }
9
+
10
+ function buttonsOf(node: unknown): { props: Record<string, unknown> }[] {
11
+ return (node as { props: { children: unknown[] } }).props.children as { props: Record<string, unknown> }[];
12
+ }
13
+
14
+ function labelsOf(node: unknown): unknown[] {
15
+ return buttonsOf(node).map((one) => one.props['children']);
16
+ }
17
+
18
+ describe('FormActions', () => {
19
+ it('★ 顺序由组件决定:给乱序数组,primary 仍在末尾、danger 紧挨它左侧、其余保持调用点顺序', () => {
20
+ const node = FormActions({
21
+ actions: [action('save', 'primary'), action('retry'), action('remove', 'danger'), action('edit')],
22
+ });
23
+
24
+ expect(labelsOf(node)).toEqual(['retry', 'edit', 'remove', 'save']);
25
+ expect(buttonsOf(node).map((one) => one.props['variant'])).toEqual([
26
+ 'ghost',
27
+ 'ghost',
28
+ 'outline',
29
+ 'primary',
30
+ ]);
31
+ });
32
+
33
+ it('没有 primary 时 danger 仍是最后一个(固定槽位,不随调用点顺序漂)', () => {
34
+ const node = FormActions({ actions: [action('remove', 'danger'), action('reset')] });
35
+
36
+ expect(labelsOf(node)).toEqual(['reset', 'remove']);
37
+ });
38
+
39
+ it('★ 两颗 primary 给可诊断信号(点名 key),但不吞、不挪、不丢', () => {
40
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
41
+ try {
42
+ const node = FormActions({ actions: [action('keep', 'primary'), action('also', 'primary')] });
43
+
44
+ expect(spy).toHaveBeenCalledTimes(1);
45
+ const message = String(spy.mock.calls[0]?.[0]);
46
+ expect(message).toContain('[dsh-workbench]');
47
+ expect(message).toContain('keep');
48
+ expect(message).toContain('also');
49
+ expect(labelsOf(node)).toEqual(['keep', 'also']);
50
+ } finally {
51
+ spy.mockRestore();
52
+ }
53
+ });
54
+
55
+ it('一颗 primary 不报', () => {
56
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
57
+ try {
58
+ FormActions({ actions: [action('save', 'primary'), action('cancel')] });
59
+ expect(spy).not.toHaveBeenCalled();
60
+ } finally {
61
+ spy.mockRestore();
62
+ }
63
+ });
64
+
65
+ it('★ 空数组不渲空壳', () => {
66
+ expect(FormActions({ actions: [] })).toBeNull();
67
+ });
68
+
69
+ it('danger 是 outline + 危险类,绝不是 primary 填充', () => {
70
+ const node = FormActions({ actions: [action('remove', 'danger')] });
71
+ const only = buttonsOf(node)[0];
72
+
73
+ expect(only?.props['variant']).toBe('outline');
74
+ expect(only?.props['className']).toBe('wb-formActionDanger');
75
+ });
76
+
77
+ it('busy 那颗禁用并带 Spinner;disabled 单独算', () => {
78
+ const node = FormActions({
79
+ actions: [
80
+ { key: 'save', label: '保存', kind: 'primary', onClick: () => {}, busy: true },
81
+ { key: 'cancel', label: '取消', onClick: () => {}, disabled: true },
82
+ ],
83
+ });
84
+ const [cancel, save] = buttonsOf(node);
85
+
86
+ expect(save?.props['disabled']).toBe(true);
87
+ expect(save?.props['icon']).toBeTruthy();
88
+ expect(cancel?.props['disabled']).toBe(true);
89
+ expect(cancel?.props['icon']).toBeUndefined();
90
+ });
91
+ });
92
+
93
+ const CSS = readFileSync(new URL('./FormActions.css', import.meta.url), 'utf8');
94
+
95
+ function declarations(selector: string): string[] {
96
+ for (const block of CSS.split('}')) {
97
+ const [head, ...rest] = block.split('{');
98
+ if (rest.length === 0 || head === undefined) continue;
99
+ if (head.trim() !== selector) continue;
100
+ return rest
101
+ .join('{')
102
+ .split(';')
103
+ .map((one) => one.replace(/\s+/g, ' ').trim())
104
+ .filter((one) => one !== '');
105
+ }
106
+ throw new Error(`FormActions.css 里没有这条规则:${selector}`);
107
+ }
108
+
109
+ describe('操作区的版式(ADR-0054 §2 规则四)', () => {
110
+ it('★ 窄容器换行 + 整组右对齐 + 唯一一档间距', () => {
111
+ const rule = declarations('.wb-formActions');
112
+
113
+ expect(rule).toContain('flex-wrap: wrap');
114
+ expect(rule).toContain('justify-content: flex-end');
115
+ expect(rule).toContain('gap: var(--wb-len-8)');
116
+ });
117
+
118
+ it('★ 危险色走现成令牌(文字 / 描边 / 悬停)', () => {
119
+ const rule = declarations('.wb-formActionDanger');
120
+
121
+ expect(rule).toContain('color: var(--dsw-alias-state-error-primary)');
122
+ expect(rule.some((one) => one.startsWith('border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary)'))).toBe(
123
+ true,
124
+ );
125
+ expect(rule.some((one) => one.startsWith('--dsw-alias-interactive-bg-hover:'))).toBe(true);
126
+ });
127
+ });
128
+
129
+ describe('SaveBar 自吃狗粮(ADR-0054 §10)', () => {
130
+ it('两个动作喂给 FormActions:取消是 default、保存是 primary(顺序不再由它摆)', () => {
131
+ const node = SaveBar({
132
+ busy: false,
133
+ labels: { cancel: '取消', save: '保存', saving: '保存中…' },
134
+ onCancel: () => {},
135
+ onSave: () => {},
136
+ }) as unknown as { props: { actions: readonly FormAction[] } };
137
+
138
+ expect(node.props.actions.map((one) => [one.key, one.kind])).toEqual([
139
+ ['cancel', 'default'],
140
+ ['save', 'primary'],
141
+ ]);
142
+ });
143
+
144
+ it('busy 时保存那颗显示 saving 文案、禁用且带忙碌标记', () => {
145
+ const node = SaveBar({
146
+ busy: true,
147
+ labels: { cancel: '取消', save: '保存', saving: '保存中…' },
148
+ onCancel: () => {},
149
+ onSave: () => {},
150
+ }) as unknown as { props: { actions: readonly FormAction[] } };
151
+ const save = node.props.actions[1];
152
+
153
+ expect(save?.label).toBe('保存中…');
154
+ expect(save?.disabled).toBe(true);
155
+ expect(save?.busy).toBe(true);
156
+ });
157
+ });
@@ -0,0 +1,75 @@
1
+ import type { ReactNode } from '../lib/react.js';
2
+ import { h } from '../lib/react.js';
3
+ import { Button } from '@deepseek-ai/dsh-client-ui-primitives';
4
+ import { Spinner } from './Spinner.js';
5
+ import './FormActions.css';
6
+
7
+ export type FormActionKind = 'default' | 'danger' | 'primary';
8
+
9
+ export interface FormAction {
10
+ readonly key: string;
11
+ readonly label: ReactNode;
12
+ readonly kind?: FormActionKind | undefined;
13
+ readonly onClick: () => void;
14
+ readonly disabled?: boolean | undefined;
15
+ readonly busy?: boolean | undefined;
16
+ }
17
+
18
+ export interface FormActionsProps {
19
+ readonly actions: readonly FormAction[];
20
+ }
21
+
22
+ const RANK: Record<FormActionKind, number> = { default: 0, danger: 1, primary: 2 };
23
+
24
+ function kindOf(action: FormAction): FormActionKind {
25
+ return action.kind ?? 'default';
26
+ }
27
+
28
+ function ordered(actions: readonly FormAction[]): readonly FormAction[] {
29
+ return actions
30
+ .map((action, index) => ({ action, index, rank: RANK[kindOf(action)] }))
31
+ .sort((left, right) => left.rank - right.rank || left.index - right.index)
32
+ .map((one) => one.action);
33
+ }
34
+
35
+ function reportExtraPrimaries(actions: readonly FormAction[]): void {
36
+ const keys = actions.filter((one) => kindOf(one) === 'primary').map((one) => one.key);
37
+ if (keys.length < 2) return;
38
+ console.error(
39
+ `[dsh-workbench] FormActions 一个区里出现了 ${keys.length} 颗 primary(${keys.join('、')})—— ` +
40
+ '一个区只许一颗主操作(ADR-0054 §2 规则二):多余的这颗该改成 default',
41
+ );
42
+ }
43
+
44
+ function variantOf(kind: FormActionKind): 'ghost' | 'outline' | 'primary' {
45
+ if (kind === 'primary') return 'primary';
46
+ if (kind === 'danger') return 'outline';
47
+ return 'ghost';
48
+ }
49
+
50
+ export function FormActions({ actions }: FormActionsProps) {
51
+ if (actions.length === 0) return null;
52
+ reportExtraPrimaries(actions);
53
+ return (
54
+ <div className="wb-formActions">
55
+ {ordered(actions).map((action) => {
56
+ const kind = kindOf(action);
57
+ const busy = action.busy === true;
58
+ return (
59
+ <Button
60
+ key={action.key}
61
+ type="button"
62
+ size="sm"
63
+ variant={variantOf(kind)}
64
+ className={kind === 'danger' ? 'wb-formActionDanger' : undefined}
65
+ disabled={busy || action.disabled === true}
66
+ icon={busy ? <Spinner size={12} /> : undefined}
67
+ onClick={action.onClick}
68
+ >
69
+ {action.label}
70
+ </Button>
71
+ );
72
+ })}
73
+ </div>
74
+ );
75
+ }
@@ -1,14 +1,25 @@
1
1
  .wb-formRows {
2
- display: flex;
3
- flex-direction: column;
4
- gap: var(--wb-len-12);
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;
5
10
  }
6
11
 
7
12
  .wb-formRow {
8
13
  display: grid;
9
- grid-template-columns: 72px minmax(0, 1fr);
14
+ grid-column: 1 / -1;
15
+ grid-template-columns: minmax(72px, max-content) minmax(0, 1fr);
10
16
  align-items: center;
11
- gap: var(--wb-len-8);
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;
12
23
  }
13
24
 
14
25
  .wb-formRow[data-layout='stacked'] {
@@ -22,6 +33,11 @@
22
33
  font-size: var(--dsw-font-xs-13-font-size);
23
34
  color: var(--dsw-alias-label-secondary);
24
35
  overflow-wrap: anywhere;
36
+ white-space: nowrap;
37
+ }
38
+
39
+ .wb-formRow[data-layout='stacked'] .wb-formLabel {
40
+ white-space: normal;
25
41
  }
26
42
 
27
43
  .wb-formControl {
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from 'node:fs';
1
2
  import { describe, expect, it } from 'vitest';
2
3
  import { FormRow, FormRows } from './FormRows.js';
3
4
 
@@ -80,3 +81,58 @@ describe('FormRow', () => {
80
81
  expect(texts(node)).toEqual(['两行']);
81
82
  });
82
83
  });
84
+
85
+ const FORM_CSS = readFileSync(new URL('./FormRows.css', import.meta.url), 'utf8');
86
+
87
+ function declarations(selector: string): string[] {
88
+ for (const block of FORM_CSS.split('}')) {
89
+ const [head, ...rest] = block.split('{');
90
+ if (rest.length === 0 || head === undefined) continue;
91
+ if (head.trim() !== selector) continue;
92
+ return rest
93
+ .join('{')
94
+ .split(';')
95
+ .map((one) => one.replace(/\s+/g, ' ').trim())
96
+ .filter((one) => one !== '');
97
+ }
98
+ throw new Error(`FormRows.css 里没有这条规则:${selector}`);
99
+ }
100
+
101
+ describe('标签列:同一个 FormRows 共用一列(ADR-0052 §11)', () => {
102
+ it('★ FormRows 自己是一个 grid:标签列 minmax(72px, max-content),行距还是那一档', () => {
103
+ const rows = declarations('.wb-formRows');
104
+
105
+ expect(rows).toContain('display: grid');
106
+ expect(rows).toContain('grid-template-columns: minmax(72px, max-content) minmax(0, 1fr)');
107
+ expect(rows).toContain('row-gap: var(--wb-len-12)');
108
+ });
109
+
110
+ it('★ 行先留回退两列,再由 subgrid 继承父网格那两列(老浏览器丢掉 subgrid 仍有回退)', () => {
111
+ expect(declarations('.wb-formRow')).toContain(
112
+ 'grid-template-columns: minmax(72px, max-content) minmax(0, 1fr)',
113
+ );
114
+ expect(declarations('.wb-formRows > .wb-formRow')).toContain('grid-template-columns: subgrid');
115
+ });
116
+
117
+ it('★ 行自己的列距与 FormRows 那一档同值(不同值就是控件起点参差)', () => {
118
+ const rowGap = declarations('.wb-formRow').find((one) => one.startsWith('column-gap:'));
119
+ const rowsGap = declarations('.wb-formRows').find((one) => one.startsWith('column-gap:'));
120
+
121
+ expect(rowGap).toBe('column-gap: var(--wb-len-8)');
122
+ expect(rowGap).toBe(rowsGap);
123
+ });
124
+
125
+ it('★ 标签不折行;stacked 还是单列、标签照常换行', () => {
126
+ expect(declarations('.wb-formLabel')).toContain('white-space: nowrap');
127
+ expect(declarations(".wb-formRow[data-layout='stacked'] .wb-formLabel")).toContain(
128
+ 'white-space: normal',
129
+ );
130
+ expect(declarations(".wb-formRow[data-layout='stacked']")).toContain(
131
+ 'grid-template-columns: minmax(0, 1fr)',
132
+ );
133
+ });
134
+
135
+ it('★ 不是 FormRow 的子项(分组标题那种)通栏,别被塞进标签那一列', () => {
136
+ expect(declarations('.wb-formRows > :not(.wb-formRow)')).toContain('grid-column: 1 / -1');
137
+ });
138
+ });
@@ -1,6 +1,6 @@
1
- import { Button } from '@deepseek-ai/dsh-client-ui-primitives';
2
1
  import { h } from '../lib/react.js';
3
- import './SaveBar.css';
2
+ import type { FormAction } from './FormActions.js';
3
+ import { FormActions } from './FormActions.js';
4
4
 
5
5
  export interface SaveBarProps {
6
6
  readonly busy: boolean;
@@ -15,20 +15,16 @@ export interface SaveBarProps {
15
15
  }
16
16
 
17
17
  export function SaveBar({ busy, disabled, labels, onCancel, onSave }: SaveBarProps) {
18
- return (
19
- <div className="wb-formActions">
20
- <Button type="button" size="sm" variant="ghost" onClick={onCancel} disabled={busy}>
21
- {labels.cancel}
22
- </Button>
23
- <Button
24
- type="button"
25
- size="sm"
26
- variant="primary"
27
- onClick={onSave}
28
- disabled={busy || disabled === true}
29
- >
30
- {busy ? labels.saving : labels.save}
31
- </Button>
32
- </div>
33
- );
18
+ const actions: readonly FormAction[] = [
19
+ { key: 'cancel', label: labels.cancel, kind: 'default', onClick: onCancel, disabled: busy },
20
+ {
21
+ key: 'save',
22
+ label: busy ? labels.saving : labels.save,
23
+ kind: 'primary',
24
+ onClick: onSave,
25
+ disabled: busy || disabled === true,
26
+ busy,
27
+ },
28
+ ];
29
+ return <FormActions actions={actions} />;
34
30
  }
package/src/i18n/en.ts CHANGED
@@ -48,6 +48,15 @@ export const en: Record<keyof WorkbenchMessages, string> = {
48
48
  'status.completed': 'Completed',
49
49
  'status.subagentsRunning.one': '{n} subagent running',
50
50
  'status.subagentsRunning.other': '{n} subagents running',
51
+ 'project.attention.warning.one': '1 session waiting for you',
52
+ 'project.attention.warning.other': '{n} sessions waiting for you',
53
+ 'project.attention.running.one': '1 session running',
54
+ 'project.attention.running.other': '{n} sessions running',
55
+ 'project.attention.done.one': '1 session finished, unread',
56
+ 'project.attention.done.other': '{n} sessions finished, unread',
57
+ 'project.attention.state.warning': 'Approval',
58
+ 'project.attention.state.running': 'Running',
59
+ 'project.attention.state.done': 'New',
51
60
 
52
61
  'hover.copied': 'Copied',
53
62
  'time.now': 'just now',
package/src/i18n/zh.ts CHANGED
@@ -46,6 +46,15 @@ export const zh = {
46
46
  'status.completed': '已完成',
47
47
  'status.subagentsRunning.one': '{n} 个子代理运行中',
48
48
  'status.subagentsRunning.other': '{n} 个子代理运行中',
49
+ 'project.attention.warning.one': '1 个会话在等你点头',
50
+ 'project.attention.warning.other': '{n} 个会话在等你点头',
51
+ 'project.attention.running.one': '1 个会话在跑',
52
+ 'project.attention.running.other': '{n} 个会话在跑',
53
+ 'project.attention.done.one': '1 个会话跑完没看',
54
+ 'project.attention.done.other': '{n} 个会话跑完没看',
55
+ 'project.attention.state.warning': '待审批',
56
+ 'project.attention.state.running': '在跑',
57
+ 'project.attention.state.done': '新消息',
49
58
 
50
59
  'hover.copied': '已复制',
51
60
  'time.now': '刚刚',
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ export * from './components/CardBoundary.js';
20
20
  export * from './components/CardHead.js';
21
21
  export * from './components/ChoiceGroup.js';
22
22
  export * from './components/Field.js';
23
+ export * from './components/FormActions.js';
23
24
  export * from './components/FormRows.js';
24
25
  export * from './components/Link.js';
25
26
  export * from './components/LongTaskRow.js';
@@ -1,7 +0,0 @@
1
-
2
- .wb-formActions {
3
- display: flex;
4
- align-items: center;
5
- justify-content: flex-end;
6
- gap: var(--wb-len-8);
7
- }