@linxin666/dsh-client-ui-task-board 0.1.1
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/LICENSE +29 -0
- package/README.md +211 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +2592 -0
- package/lib/index.js +54 -0
- package/lib/invariant.js +6 -0
- package/lib/types/client/PluginSettingsCard.d.ts +77 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +51 -0
- package/lib/types/client/board/ConfirmDialog.d.ts +12 -0
- package/lib/types/client/board/NewTaskModal.d.ts +6 -0
- package/lib/types/client/board/TaskBoard.d.ts +5 -0
- package/lib/types/client/board/TaskCard.d.ts +12 -0
- package/lib/types/client/board/TaskDetail.d.ts +7 -0
- package/lib/types/client/board-mount.d.ts +10 -0
- package/lib/types/client/index.d.ts +43 -0
- package/lib/types/client/locales.d.ts +105 -0
- package/lib/types/client/settings-form.d.ts +118 -0
- package/lib/types/client/sidebar-entry.d.ts +25 -0
- package/lib/types/core/controller.d.ts +129 -0
- package/lib/types/core/execution.d.ts +136 -0
- package/lib/types/core/schedule.d.ts +40 -0
- package/lib/types/core/scheduler.d.ts +49 -0
- package/lib/types/core/store.d.ts +36 -0
- package/lib/types/core/tasks.d.ts +111 -0
- package/lib/types/index.d.ts +44 -0
- package/lib/types/invariant.d.ts +3 -0
- package/package.json +85 -0
- package/src/client/PluginSettingsCard.tsx +207 -0
- package/src/client/TaskBoardSettingsCard.tsx +124 -0
- package/src/client/board/ConfirmDialog.tsx +40 -0
- package/src/client/board/NewTaskModal.tsx +82 -0
- package/src/client/board/TaskBoard.tsx +107 -0
- package/src/client/board/TaskCard.tsx +62 -0
- package/src/client/board/TaskDetail.tsx +286 -0
- package/src/client/board-mount.tsx +74 -0
- package/src/client/board.module.css +849 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/index.ts +167 -0
- package/src/client/locales.ts +211 -0
- package/src/client/settings-card.module.css +261 -0
- package/src/client/settings-form.ts +294 -0
- package/src/client/sidebar-entry.ts +118 -0
- package/src/core/controller.ts +379 -0
- package/src/core/execution.ts +259 -0
- package/src/core/schedule.ts +140 -0
- package/src/core/scheduler.ts +104 -0
- package/src/core/store.ts +183 -0
- package/src/core/tasks.ts +204 -0
- package/src/index.ts +94 -0
- package/src/invariant.ts +4 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task-board client plugin: wires the framework-free core (controller,
|
|
3
|
+
* execution service, store) to the real client runtime and mounts the two
|
|
4
|
+
* DOM surfaces — the sidebar entry row and the board view in the center
|
|
5
|
+
* column.
|
|
6
|
+
*
|
|
7
|
+
* Failure policy: DOM mounting problems are logged, never thrown — the web
|
|
8
|
+
* shell fails the whole boot when a plugin apply throws, and an external
|
|
9
|
+
* plugin must not take the GUI down.
|
|
10
|
+
*/
|
|
11
|
+
import type { ClientContext, SessionId, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
|
|
12
|
+
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
|
13
|
+
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
14
|
+
// Type-only: pulls the locale plugin's Context merge (ctx.locale) and its
|
|
15
|
+
// LocaleNamespaceMap merge table.
|
|
16
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
17
|
+
// Type-only: pulls the settings-surface Context merge (ctx.settingsScope).
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
19
|
+
import { BoardController } from '../core/controller.ts'
|
|
20
|
+
import { ExecutionService } from '../core/execution.ts'
|
|
21
|
+
import { SchedulerService } from '../core/scheduler.ts'
|
|
22
|
+
import { LocalStorageTaskStore } from '../core/store.ts'
|
|
23
|
+
import { mountBoard } from './board-mount.tsx'
|
|
24
|
+
import { mountSidebarEntry } from './sidebar-entry.ts'
|
|
25
|
+
import { TaskBoardSettingsCard, TaskBoardSettingsCardController, type TaskBoardSettings } from './TaskBoardSettingsCard.tsx'
|
|
26
|
+
import { en, zh, type TaskBoardKey } from './locales.ts'
|
|
27
|
+
|
|
28
|
+
/** Locale namespace this plugin owns. */
|
|
29
|
+
const NS = 'task-board'
|
|
30
|
+
|
|
31
|
+
/** Settings namespace the settings card edits (the Host plugin registers it). */
|
|
32
|
+
const TASK_BOARD_NS = 'task-board'
|
|
33
|
+
|
|
34
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
35
|
+
interface LocaleNamespaceMap {
|
|
36
|
+
/** Task-board surface copy. */
|
|
37
|
+
'task-board': TaskBoardKey
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SlotMap {
|
|
41
|
+
/**
|
|
42
|
+
* The child slot the Web UI plugin group declares; this card registers
|
|
43
|
+
* into the group instead of the top-level `settings.plugin.item` list.
|
|
44
|
+
* Spelled here with the same shape so this package can register without
|
|
45
|
+
* depending on the sibling UI package.
|
|
46
|
+
*/
|
|
47
|
+
'web-ui.plugin.item': { kind: 'list'; scope: 'root'; owner: SettingsPluginItemOwnerProps }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Owner share of a plugin card (the section supplies nothing). */
|
|
52
|
+
export interface SettingsPluginItemOwnerProps {
|
|
53
|
+
/** Marker field: card owner props are intentionally empty. */
|
|
54
|
+
children?: never
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Required services (fiber inject waiting — the runtime must be up first). */
|
|
58
|
+
export const inject = ['slots', 'sessions', 'workspaces', 'connection', 'settingsScope', 'locale', 'remote']
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Mount the task board.
|
|
62
|
+
* @param ctx - client root context (services: sessions, workspaces).
|
|
63
|
+
*/
|
|
64
|
+
export function apply(ctx: ClientContext): void {
|
|
65
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'task-board: dictionaries')
|
|
66
|
+
|
|
67
|
+
// Plugin configuration card: one staged form over the `task-board` settings
|
|
68
|
+
// namespace, contributed to the Web UI plugin group.
|
|
69
|
+
const settingsScope = ctx.settingsScope.bind<TaskBoardSettings>({ namespace: TASK_BOARD_NS })
|
|
70
|
+
const settingsCard = new TaskBoardSettingsCardController(settingsScope)
|
|
71
|
+
ctx.slots.inject('web-ui.plugin.item', () => ctx.slots.register({
|
|
72
|
+
name: 'web-ui.plugin.item',
|
|
73
|
+
id: 'task-board',
|
|
74
|
+
order: 110,
|
|
75
|
+
locale: NS,
|
|
76
|
+
inject: () => settingsCard.inject(),
|
|
77
|
+
}, TaskBoardSettingsCard))
|
|
78
|
+
|
|
79
|
+
// The sidebar entry and board view mount once the settings scope settles;
|
|
80
|
+
// while the scope is still loading, the composition default is unknown, so
|
|
81
|
+
// nothing mounts yet. Only an unavailable scope (no settings surface served)
|
|
82
|
+
// falls back to the composition default (enabled).
|
|
83
|
+
let uiDisposer: (() => void) | undefined
|
|
84
|
+
const mountUi = (): void => {
|
|
85
|
+
if (uiDisposer !== undefined) return
|
|
86
|
+
const sessions = ctx.sessions
|
|
87
|
+
const workspaces = ctx.workspaces
|
|
88
|
+
const connection = ctx.get('connection') as ConnectionHandle
|
|
89
|
+
|
|
90
|
+
// Core wiring: real runtime faces into the framework-free services.
|
|
91
|
+
const store = new LocalStorageTaskStore()
|
|
92
|
+
const exec = new ExecutionService({
|
|
93
|
+
sessions: {
|
|
94
|
+
list: sessions.list,
|
|
95
|
+
binding: id => sessions.binding(id as SessionId),
|
|
96
|
+
},
|
|
97
|
+
workspaces: {
|
|
98
|
+
list: workspaces.list,
|
|
99
|
+
connectWorkspace: id => workspaces.connectWorkspace(id as WorkspaceId),
|
|
100
|
+
},
|
|
101
|
+
history: {
|
|
102
|
+
loadTail: async sessionId => {
|
|
103
|
+
const response = await connection.api.sessions.history({
|
|
104
|
+
sessionId: sessionId as SessionId,
|
|
105
|
+
maxMessages: 20,
|
|
106
|
+
})
|
|
107
|
+
return response.result.ok
|
|
108
|
+
? { events: response.result.value.events.map(entry => entry.event) }
|
|
109
|
+
: undefined
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
const controller = new BoardController({
|
|
114
|
+
store,
|
|
115
|
+
exec,
|
|
116
|
+
sessions: {
|
|
117
|
+
list: sessions.list,
|
|
118
|
+
open: id => sessions.open(id as SessionId),
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
controller.start()
|
|
122
|
+
|
|
123
|
+
// Scheduled runs: a browser-side heartbeat that triggers due tasks through
|
|
124
|
+
// the same run path as the manual Run button. The first tick is gated on
|
|
125
|
+
// the session list baseline so a page-load catch-up never fires into a
|
|
126
|
+
// not-yet-ready runtime; tab visibility recovery ticks immediately.
|
|
127
|
+
const scheduler = new SchedulerService({
|
|
128
|
+
tasks: () => controller.getSnapshot().tasks,
|
|
129
|
+
now: () => Date.now(),
|
|
130
|
+
runTask: id => controller.runTask(id),
|
|
131
|
+
applySchedule: (id, nextRunAt, lastTriggeredAt) =>
|
|
132
|
+
controller.applyScheduleNextRun(id, nextRunAt, lastTriggeredAt),
|
|
133
|
+
ready: () => sessions.list.getSnapshot().phase === 'ready',
|
|
134
|
+
environment: {
|
|
135
|
+
addEventListener: (type, listener) => document.addEventListener(type, listener),
|
|
136
|
+
removeEventListener: (type, listener) => document.removeEventListener(type, listener),
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
scheduler.start()
|
|
140
|
+
|
|
141
|
+
const disposers: Array<() => void> = []
|
|
142
|
+
try {
|
|
143
|
+
disposers.push(mountSidebarEntry(controller))
|
|
144
|
+
disposers.push(mountBoard(controller))
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// DOM failures degrade the board, never the GUI.
|
|
147
|
+
console.error('[dsh-task-board] mount failed:', error)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
uiDisposer = () => {
|
|
151
|
+
for (const dispose of disposers.splice(0)) dispose()
|
|
152
|
+
scheduler.dispose()
|
|
153
|
+
controller.dispose()
|
|
154
|
+
uiDisposer = undefined
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const syncEnabled = (): void => {
|
|
158
|
+
const snapshot = settingsScope.getSnapshot()
|
|
159
|
+
const enabled = snapshot.status === 'ready'
|
|
160
|
+
? snapshot.value?.enabled ?? true
|
|
161
|
+
: snapshot.status === 'unavailable'
|
|
162
|
+
if (enabled) mountUi()
|
|
163
|
+
else uiDisposer?.()
|
|
164
|
+
}
|
|
165
|
+
settingsScope.subscribe(syncEnabled)
|
|
166
|
+
syncEnabled()
|
|
167
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task-board copy: zh-first dictionaries with an English fallback, selected
|
|
3
|
+
* by the document language. Kept dependency-free (no dsh locale service) so
|
|
4
|
+
* the DOM-injected entry row and the standalone board tree share one tiny
|
|
5
|
+
* lookup.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** zh dictionary (key-set source of truth). */
|
|
9
|
+
export const zh = {
|
|
10
|
+
'entry.label': '任务看板',
|
|
11
|
+
'board.title': '任务看板',
|
|
12
|
+
'board.close': '返回对话',
|
|
13
|
+
'board.new': '新建任务',
|
|
14
|
+
'board.search': '筛选任务…',
|
|
15
|
+
'board.empty': '这个状态还没有任务',
|
|
16
|
+
'board.filterAll': '全部',
|
|
17
|
+
'board.status': '状态',
|
|
18
|
+
'board.status.backlog': '待规划',
|
|
19
|
+
'board.status.todo': '待办',
|
|
20
|
+
'board.status.running': '进行中',
|
|
21
|
+
'board.status.done': '已完成',
|
|
22
|
+
'board.status.failed': '已失败',
|
|
23
|
+
'board.runs': '次执行',
|
|
24
|
+
'board.updated': '更新于',
|
|
25
|
+
'board.created': '创建于',
|
|
26
|
+
'new.title': '标题',
|
|
27
|
+
'new.titlePlaceholder': '一句话描述要做什么',
|
|
28
|
+
'new.description': '描述',
|
|
29
|
+
'new.descriptionPlaceholder': '补充背景、范围与验收(可选)',
|
|
30
|
+
'new.prompt': '执行 Prompt',
|
|
31
|
+
'new.promptPlaceholder': '发给 agent 的完整指令(留空则使用标题)',
|
|
32
|
+
'new.submit': '创建',
|
|
33
|
+
'new.cancel': '取消',
|
|
34
|
+
'new.required': '标题不能为空',
|
|
35
|
+
'detail.title': '任务详情',
|
|
36
|
+
'detail.close': '关闭',
|
|
37
|
+
'detail.prompt': '执行 Prompt',
|
|
38
|
+
'detail.description': '描述',
|
|
39
|
+
'detail.execution': '执行记录',
|
|
40
|
+
'detail.noExecution': '尚未执行',
|
|
41
|
+
'detail.run': '执行',
|
|
42
|
+
'detail.rerun': '重新执行',
|
|
43
|
+
'detail.delete': '删除',
|
|
44
|
+
'detail.viewSession': '查看会话',
|
|
45
|
+
'detail.noSession': '暂无会话',
|
|
46
|
+
'detail.executionStarted': '已启动',
|
|
47
|
+
'detail.executionEnded': '已结束',
|
|
48
|
+
'detail.result.succeeded': '成功',
|
|
49
|
+
'detail.result.failed': '失败',
|
|
50
|
+
'detail.result.cancelled': '已取消',
|
|
51
|
+
'detail.result.running': '进行中',
|
|
52
|
+
'delete.title': '删除任务',
|
|
53
|
+
'delete.confirm': '确定删除「{name}」吗?删除后不可恢复。',
|
|
54
|
+
'delete.ok': '删除',
|
|
55
|
+
'delete.cancel': '取消',
|
|
56
|
+
'status.move.backlog': '移到待规划',
|
|
57
|
+
'status.move.todo': '移到待办',
|
|
58
|
+
'exec.error.noWorkspace': '没有可用工作区,无法执行任务',
|
|
59
|
+
'exec.error.promptRejected': 'Prompt 被拒绝',
|
|
60
|
+
'run.failed': '执行失败:{error}',
|
|
61
|
+
'time.justNow': '刚刚',
|
|
62
|
+
'detail.schedule': '定时运行',
|
|
63
|
+
'detail.schedule.enable': '启用定时执行',
|
|
64
|
+
'detail.schedule.cron': 'Cron 表达式',
|
|
65
|
+
'detail.schedule.presets': '预设',
|
|
66
|
+
'detail.schedule.preset.daily9': '每天 09:00',
|
|
67
|
+
'detail.schedule.preset.hourly': '每小时',
|
|
68
|
+
'detail.schedule.preset.tenMin': '每 10 分钟',
|
|
69
|
+
'detail.schedule.preset.weeklyMon9': '每周一 09:00',
|
|
70
|
+
'detail.schedule.nextRun': '下次运行',
|
|
71
|
+
'detail.schedule.lastTriggered': '上次触发',
|
|
72
|
+
'detail.schedule.invalid': 'Cron 表达式无效',
|
|
73
|
+
'detail.schedule.notScheduled': '尚未排程',
|
|
74
|
+
'detail.schedule.dueSoon': '即将运行',
|
|
75
|
+
'card.scheduled': '定时',
|
|
76
|
+
// 插件设置卡片(settings.plugin.item 席位)。
|
|
77
|
+
'settings.title': '任务看板',
|
|
78
|
+
'settings.description': '控制看板在 agent 系统提示中的播报行为。',
|
|
79
|
+
'settings.enabled': '启用任务看板',
|
|
80
|
+
'settings.enabledHint': '关闭后隐藏侧边栏入口与看板视图。',
|
|
81
|
+
'settings.announceToAgent': '向 agent 播报任务看板',
|
|
82
|
+
'settings.announceToAgentHint': '开启:每条 agent 系统提示都会包含本看板的说明;关闭:不播报,agent 仅在用户主动提及时了解看板。',
|
|
83
|
+
'settings.inherit': '继承',
|
|
84
|
+
'settings.on': '开',
|
|
85
|
+
'settings.off': '关',
|
|
86
|
+
'settings.overridden': '已覆盖',
|
|
87
|
+
'settings.reset': '恢复默认',
|
|
88
|
+
'settings.readOnly': '当前部署的设置只读。',
|
|
89
|
+
'settings.expand': '展开设置',
|
|
90
|
+
'settings.collapse': '收起设置',
|
|
91
|
+
'settings.save': '保存',
|
|
92
|
+
'settings.saving': '保存中…',
|
|
93
|
+
'settings.discard': '放弃',
|
|
94
|
+
'settings.unsaved': '未保存',
|
|
95
|
+
'settings.saveFailed': '部署未接受这些值,已保留供你修改。',
|
|
96
|
+
'settings.invalidNumber': '请输入数字,留空则使用默认值。',
|
|
97
|
+
} satisfies Record<string, string>
|
|
98
|
+
|
|
99
|
+
/** en dictionary, complete against the zh key set. */
|
|
100
|
+
export const en: Record<keyof typeof zh, string> = {
|
|
101
|
+
'entry.label': 'Task Board',
|
|
102
|
+
'board.title': 'Task Board',
|
|
103
|
+
'board.close': 'Back to chat',
|
|
104
|
+
'board.new': 'New Task',
|
|
105
|
+
'board.search': 'Filter tasks…',
|
|
106
|
+
'board.empty': 'No tasks in this column',
|
|
107
|
+
'board.filterAll': 'All',
|
|
108
|
+
'board.status': 'Status',
|
|
109
|
+
'board.status.backlog': 'Backlog',
|
|
110
|
+
'board.status.todo': 'To Do',
|
|
111
|
+
'board.status.running': 'In Progress',
|
|
112
|
+
'board.status.done': 'Done',
|
|
113
|
+
'board.status.failed': 'Failed',
|
|
114
|
+
'board.runs': 'runs',
|
|
115
|
+
'board.updated': 'Updated',
|
|
116
|
+
'board.created': 'Created',
|
|
117
|
+
'new.title': 'Title',
|
|
118
|
+
'new.titlePlaceholder': 'What should be done, in one line',
|
|
119
|
+
'new.description': 'Description',
|
|
120
|
+
'new.descriptionPlaceholder': 'Background, scope, acceptance criteria (optional)',
|
|
121
|
+
'new.prompt': 'Run Prompt',
|
|
122
|
+
'new.promptPlaceholder': 'The full instruction sent to the agent (title is used when blank)',
|
|
123
|
+
'new.submit': 'Create',
|
|
124
|
+
'new.cancel': 'Cancel',
|
|
125
|
+
'new.required': 'Title is required',
|
|
126
|
+
'detail.title': 'Task Detail',
|
|
127
|
+
'detail.close': 'Close',
|
|
128
|
+
'detail.prompt': 'Run Prompt',
|
|
129
|
+
'detail.description': 'Description',
|
|
130
|
+
'detail.execution': 'Execution History',
|
|
131
|
+
'detail.noExecution': 'Not executed yet',
|
|
132
|
+
'detail.run': 'Run',
|
|
133
|
+
'detail.rerun': 'Run Again',
|
|
134
|
+
'detail.delete': 'Delete',
|
|
135
|
+
'detail.viewSession': 'View Session',
|
|
136
|
+
'detail.noSession': 'No session',
|
|
137
|
+
'detail.executionStarted': 'Started',
|
|
138
|
+
'detail.executionEnded': 'Ended',
|
|
139
|
+
'detail.result.succeeded': 'Succeeded',
|
|
140
|
+
'detail.result.failed': 'Failed',
|
|
141
|
+
'detail.result.cancelled': 'Cancelled',
|
|
142
|
+
'detail.result.running': 'Running',
|
|
143
|
+
'delete.title': 'Delete Task',
|
|
144
|
+
'delete.confirm': 'Delete "{name}"? This cannot be undone.',
|
|
145
|
+
'delete.ok': 'Delete',
|
|
146
|
+
'delete.cancel': 'Cancel',
|
|
147
|
+
'status.move.backlog': 'Move to Backlog',
|
|
148
|
+
'status.move.todo': 'Move to To Do',
|
|
149
|
+
'exec.error.noWorkspace': 'No workspace is available to run the task',
|
|
150
|
+
'exec.error.promptRejected': 'Prompt rejected',
|
|
151
|
+
'run.failed': 'Run failed: {error}',
|
|
152
|
+
'time.justNow': 'just now',
|
|
153
|
+
'detail.schedule': 'Scheduled Runs',
|
|
154
|
+
'detail.schedule.enable': 'Enable scheduled runs',
|
|
155
|
+
'detail.schedule.cron': 'Cron expression',
|
|
156
|
+
'detail.schedule.presets': 'Presets',
|
|
157
|
+
'detail.schedule.preset.daily9': 'Every day 09:00',
|
|
158
|
+
'detail.schedule.preset.hourly': 'Every hour',
|
|
159
|
+
'detail.schedule.preset.tenMin': 'Every 10 minutes',
|
|
160
|
+
'detail.schedule.preset.weeklyMon9': 'Every Monday 09:00',
|
|
161
|
+
'detail.schedule.nextRun': 'Next run',
|
|
162
|
+
'detail.schedule.lastTriggered': 'Last triggered',
|
|
163
|
+
'detail.schedule.invalid': 'Invalid cron expression',
|
|
164
|
+
'detail.schedule.notScheduled': 'Not scheduled yet',
|
|
165
|
+
'detail.schedule.dueSoon': 'Due soon',
|
|
166
|
+
'card.scheduled': 'scheduled',
|
|
167
|
+
// Plugin settings card (the `settings.plugin.item` seat).
|
|
168
|
+
'settings.title': 'Task Board',
|
|
169
|
+
'settings.description': 'How the board announces itself in each agent system prompt.',
|
|
170
|
+
'settings.enabled': 'Enable the task board',
|
|
171
|
+
'settings.enabledHint': 'When off, the sidebar entry and board view are hidden.',
|
|
172
|
+
'settings.announceToAgent': 'Announce the task board to agents',
|
|
173
|
+
'settings.announceToAgentHint': 'On: every agent system prompt includes a note about this board. Off: no announcement; agents learn about the board only when you mention it.',
|
|
174
|
+
'settings.inherit': 'Inherit',
|
|
175
|
+
'settings.on': 'On',
|
|
176
|
+
'settings.off': 'Off',
|
|
177
|
+
'settings.overridden': 'Overridden',
|
|
178
|
+
'settings.reset': 'Reset to default',
|
|
179
|
+
'settings.readOnly': 'This deployment stores settings read-only.',
|
|
180
|
+
'settings.expand': 'Show settings',
|
|
181
|
+
'settings.collapse': 'Hide settings',
|
|
182
|
+
'settings.save': 'Save',
|
|
183
|
+
'settings.saving': 'Saving…',
|
|
184
|
+
'settings.discard': 'Discard',
|
|
185
|
+
'settings.unsaved': 'Unsaved',
|
|
186
|
+
'settings.saveFailed': 'The deployment did not accept these values; they were left for you to correct.',
|
|
187
|
+
'settings.invalidNumber': 'Enter a number, or leave blank to use the default.',
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The dictionary key union. */
|
|
191
|
+
export type TaskBoardKey = keyof typeof zh
|
|
192
|
+
|
|
193
|
+
/** The settings-card slice of the task-board dictionary. */
|
|
194
|
+
export type SettingsCardKey = TaskBoardKey
|
|
195
|
+
|
|
196
|
+
/** Active dictionary, picked by the document language at call time. */
|
|
197
|
+
export function dictionary(): Record<TaskBoardKey, string> {
|
|
198
|
+
const lang = typeof document !== 'undefined' ? document.documentElement.lang : 'zh'
|
|
199
|
+
return lang.toLowerCase().startsWith('en') ? en : zh
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Translate a key with optional {name} template params. */
|
|
203
|
+
export function t(key: TaskBoardKey, params?: Record<string, string>): string {
|
|
204
|
+
let text: string = dictionary()[key]
|
|
205
|
+
if (params !== undefined) {
|
|
206
|
+
for (const [name, value] of Object.entries(params)) {
|
|
207
|
+
text = text.replaceAll(`{${name}}`, value)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return text
|
|
211
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
.card {
|
|
2
|
+
list-style: none;
|
|
3
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
4
|
+
border-radius: 8px;
|
|
5
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
transition: border-color 0.16s, background 0.16s;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.cardOpen {
|
|
11
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
12
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.header {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: 8px;
|
|
19
|
+
width: 100%;
|
|
20
|
+
padding: 10px 14px;
|
|
21
|
+
border: 0;
|
|
22
|
+
background: transparent;
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
text-align: left;
|
|
25
|
+
font: inherit;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.header:hover {
|
|
29
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.headText {
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
gap: 2px;
|
|
36
|
+
flex: 1;
|
|
37
|
+
min-width: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.name {
|
|
41
|
+
font-weight: 600;
|
|
42
|
+
color: var(--dsw-alias-label-primary);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.description {
|
|
46
|
+
font-size: 12px;
|
|
47
|
+
color: var(--dsw-alias-label-tertiary);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.pending {
|
|
51
|
+
font-size: 12px;
|
|
52
|
+
color: var(--dsw-alias-state-warn-primary);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.chevron {
|
|
56
|
+
transition: transform 120ms ease;
|
|
57
|
+
color: var(--dsw-alias-label-tertiary);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.chevronOpen {
|
|
61
|
+
transform: rotate(180deg);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.body {
|
|
65
|
+
padding: 0 14px 14px;
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
gap: 14px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.readOnly {
|
|
72
|
+
margin: 0;
|
|
73
|
+
font-size: 12px;
|
|
74
|
+
color: var(--dsw-alias-label-tertiary);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.footer {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: flex-end;
|
|
81
|
+
gap: 8px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.failed {
|
|
85
|
+
margin: 0 auto 0 0;
|
|
86
|
+
font-size: 12px;
|
|
87
|
+
color: var(--dsw-alias-state-error-primary);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.discard,
|
|
91
|
+
.save {
|
|
92
|
+
padding: 5px 12px;
|
|
93
|
+
border-radius: 6px;
|
|
94
|
+
font: inherit;
|
|
95
|
+
font-size: 13px;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.discard {
|
|
100
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
101
|
+
background: transparent;
|
|
102
|
+
color: var(--dsw-alias-label-secondary);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.discard:hover:not(:disabled) {
|
|
106
|
+
color: var(--dsw-alias-label-primary);
|
|
107
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.save {
|
|
111
|
+
border: 1px solid var(--dsw-alias-button-info-fill);
|
|
112
|
+
background: var(--dsw-alias-button-info-fill);
|
|
113
|
+
color: var(--dsw-alias-label-primary-foreground);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.save:hover:not(:disabled) {
|
|
117
|
+
border-color: var(--dsw-alias-button-info-hover);
|
|
118
|
+
background: var(--dsw-alias-button-info-hover);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.discard:active:not(:disabled),
|
|
122
|
+
.save:active:not(:disabled) {
|
|
123
|
+
transform: translateY(1px);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.discard:focus-visible:not(:disabled),
|
|
127
|
+
.save:focus-visible:not(:disabled) {
|
|
128
|
+
outline: 2px solid var(--dsw-alias-state-business-primary);
|
|
129
|
+
outline-offset: -2px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.discard:disabled,
|
|
133
|
+
.save:disabled {
|
|
134
|
+
opacity: 0.5;
|
|
135
|
+
cursor: default;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.field {
|
|
139
|
+
display: flex;
|
|
140
|
+
flex-direction: column;
|
|
141
|
+
gap: 4px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.head {
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
gap: 8px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.label {
|
|
151
|
+
font-size: 13px;
|
|
152
|
+
font-weight: 500;
|
|
153
|
+
color: var(--dsw-alias-label-primary);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.badges {
|
|
157
|
+
display: flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
gap: 6px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.badge {
|
|
163
|
+
font-size: 11px;
|
|
164
|
+
padding: 1px 6px;
|
|
165
|
+
border-radius: 999px;
|
|
166
|
+
background: var(--dsw-alias-interactive-bg-hover-accent);
|
|
167
|
+
color: var(--dsw-alias-state-business-primary);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.reset {
|
|
171
|
+
font-size: 11px;
|
|
172
|
+
border: 0;
|
|
173
|
+
background: transparent;
|
|
174
|
+
color: var(--dsw-alias-state-business-primary);
|
|
175
|
+
cursor: pointer;
|
|
176
|
+
padding: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.reset:hover:not(:disabled) {
|
|
180
|
+
text-decoration: underline;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.reset:active:not(:disabled) {
|
|
184
|
+
text-decoration: underline;
|
|
185
|
+
opacity: 0.8;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.reset:focus-visible {
|
|
189
|
+
outline: 2px solid var(--dsw-alias-state-business-primary);
|
|
190
|
+
outline-offset: -2px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.reset:disabled {
|
|
194
|
+
opacity: 0.5;
|
|
195
|
+
cursor: default;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.input,
|
|
199
|
+
.select {
|
|
200
|
+
padding: 6px 8px;
|
|
201
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
202
|
+
border-radius: 6px;
|
|
203
|
+
font: inherit;
|
|
204
|
+
font-size: 13px;
|
|
205
|
+
color: var(--dsw-alias-label-primary);
|
|
206
|
+
background: var(--dsw-specific-input-major);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.inputInvalid {
|
|
210
|
+
padding: 6px 8px;
|
|
211
|
+
border: 1px solid var(--dsw-alias-state-error-primary);
|
|
212
|
+
border-radius: 6px;
|
|
213
|
+
font: inherit;
|
|
214
|
+
font-size: 13px;
|
|
215
|
+
color: var(--dsw-alias-label-primary);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.input:disabled,
|
|
219
|
+
.select:disabled {
|
|
220
|
+
opacity: 0.6;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.hint {
|
|
224
|
+
margin: 0;
|
|
225
|
+
font-size: 12px;
|
|
226
|
+
color: var(--dsw-alias-label-secondary);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.invalid {
|
|
230
|
+
margin: 0;
|
|
231
|
+
font-size: 12px;
|
|
232
|
+
color: var(--dsw-alias-state-error-primary);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* --- polish: header keyboard focus / press, unified motion, reduced-motion --- */
|
|
236
|
+
.header:focus-visible {
|
|
237
|
+
outline: 2px solid var(--dsw-alias-state-business-primary);
|
|
238
|
+
outline-offset: -2px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.header:active {
|
|
242
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.header,
|
|
246
|
+
.discard,
|
|
247
|
+
.save,
|
|
248
|
+
.reset {
|
|
249
|
+
transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease, transform 120ms ease;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@media (prefers-reduced-motion: reduce) {
|
|
253
|
+
.card,
|
|
254
|
+
.header,
|
|
255
|
+
.chevron,
|
|
256
|
+
.discard,
|
|
257
|
+
.save,
|
|
258
|
+
.reset {
|
|
259
|
+
transition: none;
|
|
260
|
+
}
|
|
261
|
+
}
|