@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.
- package/dist/components/FormActions.css +13 -0
- package/dist/components/FormActions.d.ts +15 -0
- package/dist/components/FormActions.js +38 -0
- package/dist/components/FormRows.css +21 -5
- package/dist/components/SaveBar.d.ts +0 -1
- package/dist/components/SaveBar.js +13 -5
- package/dist/i18n/en.js +9 -0
- package/dist/i18n/index.d.ts +10 -1
- package/dist/i18n/zh.d.ts +9 -0
- package/dist/i18n/zh.js +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/src/components/FormActions.css +13 -0
- package/src/components/FormActions.test.tsx +157 -0
- package/src/components/FormActions.tsx +75 -0
- package/src/components/FormRows.css +21 -5
- package/src/components/FormRows.test.tsx +56 -0
- package/src/components/SaveBar.tsx +14 -18
- package/src/i18n/en.ts +9 -0
- package/src/i18n/zh.ts +9 -0
- package/src/index.ts +1 -0
- package/src/components/SaveBar.css +0 -7
|
@@ -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:
|
|
3
|
-
|
|
4
|
-
gap: var(--wb-len-
|
|
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-
|
|
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,8 +1,16 @@
|
|
|
1
|
-
import { Button } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
2
1
|
import { h } from '../lib/react.js';
|
|
3
|
-
import './
|
|
2
|
+
import { FormActions } from './FormActions.js';
|
|
4
3
|
export function SaveBar({ busy, disabled, labels, onCancel, onSave }) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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',
|
package/dist/i18n/index.d.ts
CHANGED
|
@@ -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
|
@@ -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:
|
|
3
|
-
|
|
4
|
-
gap: var(--wb-len-
|
|
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-
|
|
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 './
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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';
|