dsh-notify-yimit 0.1.4 → 0.1.5

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/lib/client.js CHANGED
@@ -40,8 +40,6 @@ window.__ModuleLoader__.load({
40
40
  '.dn-seg{display:flex;gap:2px;padding:2px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);max-width:520px}',
41
41
  '.dn-seg-btn{flex:1;font:inherit;font-size:13px;color:var(--dsw-alias-label-secondary);border:0;border-radius:8px;padding:6px 10px;cursor:pointer;background:transparent;transition:background .15s ease,color .15s ease;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
42
42
  '.dn-seg-btn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}',
43
- // 选中态用主题主色 + 反色文字:浅色模式下与背景对比也足够清晰(此前浅色下
44
- // 白底+深灰与容器太接近,深色正常)。
45
43
  '.dn-seg-btn[data-on="true"]{background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-inverted);box-shadow:var(--dsw-shadow-lv1)}',
46
44
  '.dn-seg-btn[data-on="true"]:hover{background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary-inverted)}',
47
45
  '.dn-seg-btn:disabled{opacity:.45;cursor:not-allowed}',
@@ -52,17 +50,13 @@ window.__ModuleLoader__.load({
52
50
  '.dn-collapse>.dn-collapse-body>.dn-block{min-width:0}',
53
51
  '.dn-kind-list{display:flex;flex-direction:column;gap:8px}',
54
52
  '.dn-kind-row{display:flex;align-items:center;gap:12px;padding:8px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}',
55
- // 每类型的背景/文字/开关项右对齐(预览块留在左侧)
56
53
  '.dn-kind-row>.dn-kind-field:first-of-type{margin-left:auto}',
57
- // 样式预览:纯色块内嵌类型文字,始终用配置的背景/文字色(内联样式,不受主题影响)
58
54
  '.dn-kind-preview{flex:none;min-width:72px;text-align:center;font-size:12px;font-weight:600;line-height:22px;padding:2px 10px;border-radius:6px;white-space:nowrap;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}',
59
55
  '.dn-kind-field{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-tertiary);flex:none}',
60
56
  '.dn-input{font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:5px 10px;outline:none;max-width:120px}',
61
57
  '.dn-input:focus{border-color:var(--dsw-alias-state-business-primary)}',
62
58
  '.dn-input[type=color]{padding:2px 4px;width:48px;height:28px;cursor:pointer;max-width:none}',
63
59
  '.dn-input:disabled{opacity:.45;cursor:not-allowed}',
64
- // 浏览器选择器:原生 select 的展开列表为系统绘制、无法贴合主题,改为自绘下拉,
65
- // 触发器与弹层都走 --dsw-* 变量,深浅色模式下与设置页风格一致。
66
60
  '.dn-bselect{position:relative;width:280px;max-width:100%;flex:none}',
67
61
  '.dn-bselect-trigger{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:5px 10px;cursor:pointer;outline:none}',
68
62
  '.dn-bselect-trigger:hover:not(:disabled){border-color:var(--dsw-alias-border-l2)}',
@@ -577,11 +571,8 @@ window.__ModuleLoader__.load({
577
571
  }
578
572
 
579
573
  if (config.mode === 'custom') {
580
- if (typeof Notification === 'undefined' || Notification.permission !==
581
- 'granted') {
582
- ack();
583
- return
584
- }
574
+ ack();
575
+ return
585
576
  } else if (config.mode === 'system') {
586
577
  if (typeof Notification === 'undefined' || Notification.permission ===
587
578
  'denied') {
@@ -801,7 +792,8 @@ window.__ModuleLoader__.load({
801
792
  const controlsDisabled = !config.enabled
802
793
 
803
794
  const kindRows = KIND_ORDER.map((kind) => {
804
- const entry = config.colors?. [kind] ?? {}
795
+ // 修正:去掉 `?.` 和 `[kind]` 之间的空格
796
+ const entry = config.colors?.[kind] ?? {}
805
797
  const bg = entry.bg || config.bgColor || '#20272f'
806
798
  const fg = entry.fg || config.textColor || '#e6edf3'
807
799
  const enabled = entry.enabled !== false
@@ -1366,4 +1358,4 @@ window.__ModuleLoader__.load({
1366
1358
  exports.inject = inject
1367
1359
  return module.exports
1368
1360
  },
1369
- })
1361
+ })
package/lib/index.js CHANGED
@@ -30,39 +30,42 @@ const DEFAULT_CONFIG = Object.freeze({
30
30
  bgColor: '#20272f',
31
31
  textColor: '#e6edf3',
32
32
  locale: 'en',
33
- // 通知窗口样式:width 窗口宽度(编辑模式拖拽边缘调整);gap 通知间距(px);
34
- // position = null 表示默认右下贴边,{left,top} 为任意屏幕位置(工作区相对坐标)。
35
33
  toast: {
36
34
  width: 340,
37
35
  gap: 12,
38
- position: null,
36
+ position: null
39
37
  },
40
38
  colors: {
41
39
  completed: {
42
40
  bg: '#1f5138',
43
41
  fg: '#e6f4ec',
42
+ accent: '#34d399',
44
43
  enabled: true
45
- },
44
+ }, // 翠绿
46
45
  error: {
47
46
  bg: '#5c1f24',
48
47
  fg: '#fbe9ea',
48
+ accent: '#f87171',
49
49
  enabled: true
50
- },
50
+ }, // 珊瑚红
51
51
  running: {
52
52
  bg: '#203a5c',
53
53
  fg: '#e8f0fb',
54
+ accent: '#60a5fa',
54
55
  enabled: true
55
- },
56
+ }, // 天蓝
56
57
  approval: {
57
58
  bg: '#4a3a12',
58
59
  fg: '#f7efd8',
60
+ accent: '#fbbf24',
59
61
  enabled: true
60
- },
62
+ }, // 琥珀
61
63
  question: {
62
64
  bg: '#3d2a55',
63
65
  fg: '#f0e8fa',
66
+ accent: '#a78bfa',
64
67
  enabled: true
65
- },
68
+ }, // 雾紫
66
69
  },
67
70
  })
68
71
 
@@ -137,7 +140,11 @@ function normalizeConfig(input) {
137
140
  if (src.locale === 'zh' || src.locale === 'en') out.locale = src.locale
138
141
 
139
142
  // 通知窗口样式:width 260–520;gap 0–60;position 缺省 null(默认右下贴边),合法 {left,top} 才采纳。
140
- out.toast = { width: 340, gap: 12, position: null }
143
+ out.toast = {
144
+ width: 340,
145
+ gap: 12,
146
+ position: null
147
+ }
141
148
  const srcToast = src.toast !== null && typeof src.toast === 'object' && !Array.isArray(src.toast) ? src.toast : {}
142
149
  if (typeof srcToast.width === 'number' && Number.isFinite(srcToast.width)) {
143
150
  out.toast.width = Math.max(260, Math.min(520, Math.round(srcToast.width)))
@@ -149,7 +156,10 @@ function normalizeConfig(input) {
149
156
  if (pos !== null && typeof pos === 'object' && !Array.isArray(pos) &&
150
157
  typeof pos.left === 'number' && Number.isFinite(pos.left) &&
151
158
  typeof pos.top === 'number' && Number.isFinite(pos.top)) {
152
- out.toast.position = { left: Math.round(pos.left), top: Math.round(pos.top) }
159
+ out.toast.position = {
160
+ left: Math.round(pos.left),
161
+ top: Math.round(pos.top)
162
+ }
153
163
  }
154
164
 
155
165
  const srcColors = src.colors !== null && typeof src.colors === 'object' && !Array.isArray(src.colors) ? src.colors :
@@ -161,6 +171,7 @@ function normalizeConfig(input) {
161
171
  out.colors[kind] = {
162
172
  bg: isHex(entry?.bg) ? entry.bg : def.bg,
163
173
  fg: isHex(entry?.fg) ? entry.fg : def.fg,
174
+ accent: isHex(entry?.accent) ? entry.accent : def.accent, // 新增
164
175
  enabled: entry?.enabled !== false,
165
176
  }
166
177
  }
@@ -201,7 +212,8 @@ function mergeConfigPatch(base, patch) {
201
212
  if (patch !== null && typeof patch === 'object' && patch.toast !== null && typeof patch.toast === 'object' && !
202
213
  Array.isArray(patch.toast)) {
203
214
  out.toast = {
204
- ...(base.toast !== null && typeof base.toast === 'object' && !Array.isArray(base.toast) ? base.toast : {}),
215
+ ...(base.toast !== null && typeof base.toast === 'object' && !Array.isArray(base.toast) ? base.toast :
216
+ {}),
205
217
  ...patch.toast
206
218
  }
207
219
  }
@@ -258,7 +270,8 @@ function openConfig() {
258
270
  fs.writeFileSync(tmp, JSON.stringify(config, null, 2), 'utf8')
259
271
  fs.renameSync(tmp, file)
260
272
  } catch {
261
- /* 静默 */ }
273
+ /* 静默 */
274
+ }
262
275
  },
263
276
  }
264
277
  }
@@ -489,7 +502,8 @@ export function apply(ctx) {
489
502
  if (idx !== -1) toastOrder.splice(idx, 1)
490
503
  reflowToasts()
491
504
  }
492
- } else if (msg.type === 'geometry' && typeof msg.left === 'number' && typeof msg.top === 'number') {
505
+ } else if (msg.type === 'geometry' && typeof msg.left === 'number' && typeof msg.top ===
506
+ 'number') {
493
507
  // 编辑模式:样板窗拖拽/缩放结束 → 写回配置,并应用到在屏浮窗。
494
508
  const width = Math.max(260, Math.min(520, Math.round(Number(msg.width) || 340)))
495
509
  const left = Math.round(Number(msg.left))
@@ -497,11 +511,18 @@ export function apply(ctx) {
497
511
  store.set({
498
512
  toast: {
499
513
  width,
500
- position: { left, top }
514
+ position: {
515
+ left,
516
+ top
517
+ }
501
518
  }
502
519
  })
503
520
  for (const rec of activeToasts.values()) {
504
- sendCmd({ cmd: 'size', key: rec.key, width })
521
+ sendCmd({
522
+ cmd: 'size',
523
+ key: rec.key,
524
+ width
525
+ })
505
526
  }
506
527
  reflowToasts()
507
528
  }
@@ -585,9 +606,16 @@ export function apply(ctx) {
585
606
  const needsTop = rec.meta !== null && newTop !== rec.meta.top
586
607
  const needsLeft = rec.meta !== null && rec.meta.left !== pos.left
587
608
  if (needsTop || needsLeft) {
588
- const move = { cmd: 'move', key: rec.key, top: newTop }
609
+ const move = {
610
+ cmd: 'move',
611
+ key: rec.key,
612
+ top: newTop
613
+ }
589
614
  if (needsTop) rec.meta.top = newTop
590
- if (needsLeft) { move.left = pos.left; rec.meta.left = pos.left }
615
+ if (needsLeft) {
616
+ move.left = pos.left;
617
+ rec.meta.left = pos.left
618
+ }
591
619
  sendCmd(move)
592
620
  }
593
621
  prevTop = newTop
@@ -626,6 +654,23 @@ export function apply(ctx) {
626
654
  return `http://127.0.0.1:${ws !== null && ws !== undefined && typeof ws.port === 'number' ? ws.port : 3080}`
627
655
  }
628
656
 
657
+ const KIND_BADGES = {
658
+ zh: {
659
+ completed: '完成',
660
+ error: '出错',
661
+ running: '运行中',
662
+ approval: '待审批',
663
+ question: '待回答'
664
+ },
665
+ en: {
666
+ completed: 'Done',
667
+ error: 'Failed',
668
+ running: 'Running',
669
+ approval: 'Approval',
670
+ question: 'Question'
671
+ },
672
+ }
673
+
629
674
  function showDesktopToast(cfg, ev) {
630
675
  const maxVisible = Math.max(1, Math.min(20, Math.floor(Number(cfg.maxVisible) || 3)))
631
676
  const stickyKind = ev.kind === 'running' || ev.kind === 'approval' || ev.kind === 'question'
@@ -691,6 +736,8 @@ export function apply(ctx) {
691
736
  bg: colors?.bg ?? cfg.bgColor ?? '#20272f',
692
737
  fg: colors?.fg ?? cfg.textColor ?? '#e6edf3',
693
738
  durationSec: Math.max(1, Math.round((Number(cfg.durationMs) || 8000) / 1000)),
739
+ accent: colors?.accent ?? '#60a5fa', // 强调色
740
+ durationSec: Math.max(1, Math.round((Number(cfg.durationMs) || 8000) / 1000)),
694
741
  sticky: stickyKind,
695
742
  offsetY,
696
743
  sessionId: String(ev.sessionId),
@@ -736,7 +783,11 @@ export function apply(ctx) {
736
783
  if (typeof title !== 'string' || title.length === 0) return
737
784
  for (const rec of activeToasts.values()) {
738
785
  if (rec.sessionId === sessionId) {
739
- sendCmd({ cmd: 'title', key: rec.key, title })
786
+ sendCmd({
787
+ cmd: 'title',
788
+ key: rec.key,
789
+ title
790
+ })
740
791
  }
741
792
  }
742
793
  }
@@ -748,11 +799,13 @@ export function apply(ctx) {
748
799
  cmd: 'shutdown'
749
800
  })
750
801
  } catch {
751
- /* 忽略 */ }
802
+ /* 忽略 */
803
+ }
752
804
  try {
753
805
  host.kill()
754
806
  } catch {
755
- /* 已退出 */ }
807
+ /* 已退出 */
808
+ }
756
809
  }
757
810
  activeToasts.clear()
758
811
  toastOrder.length = 0
@@ -768,7 +821,7 @@ export function apply(ctx) {
768
821
  const at = Date.now()
769
822
  const cfg = store.get()
770
823
  // 每类型开关:该类型被关闭时完全不产生通知(桌面浮窗与系统通知都不发)。
771
- if (cfg.colors?.[kind]?.enabled === false) return
824
+ if (cfg.colors?. [kind]?.enabled === false) return
772
825
  const desktop = cfg.enabled === true && cfg.mode === 'custom'
773
826
  const lang = cfg.locale === 'zh' ? 'zh' : 'en'
774
827
  const l = TOAST_LABELS[lang]
@@ -878,7 +931,8 @@ export function apply(ctx) {
878
931
  if (typeof event.data?.title === 'string' && event.data.title.length > 0) {
879
932
  st.title = event.data.title
880
933
  for (let i = queueHead; i < queue.length; i++) {
881
- if (queue[i].sessionId === id && (queue[i].title.length === 0 || UNTITLED_NAMES.has(queue[i].title)))
934
+ if (queue[i].sessionId === id && (queue[i].title.length === 0 || UNTITLED_NAMES.has(
935
+ queue[i].title)))
882
936
  queue[i].title = st.title
883
937
  }
884
938
  // 已弹浮窗(运行中/待审批/待回答等)标题原地更新,不再一直顶着占位/旧标题。
@@ -928,7 +982,8 @@ export function apply(ctx) {
928
982
  push(id, 'question', first.question)
929
983
  }
930
984
  } catch {
931
- /* arguments 非 JSON */ }
985
+ /* arguments 非 JSON */
986
+ }
932
987
  }
933
988
  return
934
989
  }
@@ -1015,7 +1070,8 @@ export function apply(ctx) {
1015
1070
  `${L.approvalPrefix} ${toolName}` : L.approvalFallback))
1016
1071
  }
1017
1072
  } catch {
1018
- /* 观察失败不影响审批链路 */ }
1073
+ /* 观察失败不影响审批链路 */
1074
+ }
1019
1075
  return next()
1020
1076
  })
1021
1077
 
@@ -1032,11 +1088,16 @@ export function apply(ctx) {
1032
1088
  const prev = store.get()
1033
1089
  const next = store.set(patch)
1034
1090
  // toast 几何(位置/宽度/间距)变更:立即应用到在屏浮窗,无需等待下一个浮窗。
1035
- const pToast = patch !== null && typeof patch === 'object' && !Array.isArray(patch) ? patch.toast : undefined
1091
+ const pToast = patch !== null && typeof patch === 'object' && !Array.isArray(patch) ? patch.toast :
1092
+ undefined
1036
1093
  if (pToast !== null && typeof pToast === 'object' && !Array.isArray(pToast)) {
1037
1094
  if (typeof pToast.width === 'number' && pToast.width !== prev.toast?.width) {
1038
1095
  for (const rec of activeToasts.values()) {
1039
- sendCmd({ cmd: 'size', key: rec.key, width: next.toast.width })
1096
+ sendCmd({
1097
+ cmd: 'size',
1098
+ key: rec.key,
1099
+ width: next.toast.width
1100
+ })
1040
1101
  }
1041
1102
  }
1042
1103
  reflowToasts()
@@ -1072,13 +1133,26 @@ export function apply(ctx) {
1072
1133
  title: EDIT_TITLES[lang],
1073
1134
  width: cfg.toast?.width ?? 340,
1074
1135
  position: cfg.toast?.position ?? null,
1136
+ // 新增:类型色卡数组,样板窗内做预览
1137
+ chips: KIND_KEYS.map((k) => ({
1138
+ label: (KIND_BADGES[lang] ?? KIND_BADGES.en)[k],
1139
+ accent: cfg.colors[k].accent,
1140
+ bg: cfg.colors[k].bg,
1141
+ fg: cfg.colors[k].fg,
1142
+ })),
1075
1143
  })
1076
- return { ok: true }
1144
+ return {
1145
+ ok: true
1146
+ }
1077
1147
  },
1078
1148
  /** 退出编辑模式:样板窗淡出消失。 */
1079
1149
  endEditMode() {
1080
- sendCmd({ cmd: 'edit-end' })
1081
- return { ok: true }
1150
+ sendCmd({
1151
+ cmd: 'edit-end'
1152
+ })
1153
+ return {
1154
+ ok: true
1155
+ }
1082
1156
  },
1083
1157
  }
1084
1158
 
@@ -1095,4 +1169,4 @@ export function apply(ctx) {
1095
1169
 
1096
1170
  ctx.provide('notify', service)
1097
1171
  console.log('[dsh-notify-yimit] Loaded (disabled by default; please enable in Settings -> Notifications)')
1098
- }
1172
+ }
@@ -1,5 +1,19 @@
1
1
  <# dsh-notify-yimit 常驻浮窗宿主(Windows PowerShell + WPF)。
2
- 优化点:JSON序列化深度增加、Tag闭包变量传递优化、BeginInvoke资源回收、接收外部Label参数实现多语言。 #>
2
+
3
+ 倒计时即进度(本版核心变化):
4
+ - 非粘性通知整卡抽象为倒计时:强调色 @12% "液面层"锚定左缘,ScaleX 1→0,
5
+ 右缘向左退去 —— 时间从右向左流走,剩余时间 = 剩余液面面积;
6
+ - 独立 2.5px 强调色"进度指针"亮线贴液面右缘同步左移(不被 ScaleX 压扁,
7
+ 在 bg≈accent 相近配色下仍 100% 饱和可辨,是可见性兜底信号);
8
+ - 动画严格线性(无缓动),保证视觉时长与真实剩余时间一致;
9
+ - 粘性通知(运行中/待审批/待回答)无倒计时,左侧强调色条改为呼吸脉动,
10
+ 与"会消失的卡"一眼区分。
11
+
12
+ 其余特性:
13
+ - 强调色(accent)体系:左侧类型色条 + 强调色描边 + 强调色主按钮 + 辉光阴影;
14
+ - 编辑模式样板窗:5 类型实时色卡预览 + 边缘圆点缩放把手 + 多语言提示;
15
+ - JSON 序列化深度增加、Tag 闭包变量传递、接收外部 Label 参数实现多语言。 #>
16
+
3
17
  Add-Type -AssemblyName PresentationFramework
4
18
  Add-Type -AssemblyName PresentationCore
5
19
  Add-Type -AssemblyName WindowsBase
@@ -14,9 +28,34 @@ $script:stdout = New-Object System.IO.StreamWriter([System.Console]::OpenStandar
14
28
  $script:stdout.AutoFlush = $true
15
29
 
16
30
  function Send-Report([object]$obj) {
17
- try {
18
- $script:stdout.WriteLine(($obj | ConvertTo-Json -Compress -Depth 10))
19
- } catch { }
31
+ try { $script:stdout.WriteLine(($obj | ConvertTo-Json -Compress -Depth 10)) } catch { }
32
+ }
33
+
34
+ # ───────────────────────── 颜色 / 效果工具 ─────────────────────────
35
+
36
+ function Convert-Hsl([System.Windows.Media.Color]$c, [double]$lightFactor) {
37
+ # 简易亮度调整:lightFactor > 1 提亮, < 1 压暗(用于主按钮 hover/按压变体)。
38
+ $f = [Math]::Max(0.0, $lightFactor)
39
+ return [System.Windows.Media.Color]::FromRgb(
40
+ [byte][Math]::Min(255, [int]($c.R * $f)),
41
+ [byte][Math]::Min(255, [int]($c.G * $f)),
42
+ [byte]([Math]::Min(255, [int]($c.B * $f))))
43
+ }
44
+
45
+ function New-GlowEffect([System.Windows.Media.Color]$c, [double]$opacity) {
46
+ # 强调色辉光阴影:无位移、大模糊,营造"类型色悬浮"感。
47
+ $effect = New-Object System.Windows.Media.Effects.DropShadowEffect
48
+ $effect.Color = $c
49
+ $effect.BlurRadius = 24
50
+ $effect.ShadowDepth = 0
51
+ $effect.Opacity = $opacity
52
+ return $effect
53
+ }
54
+
55
+ function Add-GridRow($grid) {
56
+ $row = New-Object System.Windows.Controls.RowDefinition
57
+ $row.Height = [System.Windows.GridLength]::Auto
58
+ $grid.RowDefinitions.Add($row) | Out-Null
20
59
  }
21
60
 
22
61
  # 编辑模式几何回传:样板窗当前 left/top/width/height(工作区相对坐标)。
@@ -24,22 +63,26 @@ function Send-Geometry($w) {
24
63
  try {
25
64
  $work = [System.Windows.SystemParameters]::WorkArea
26
65
  Send-Report @{
27
- type = 'geometry'
28
- left = [Math]::Round($w.Left - $work.Left)
29
- top = [Math]::Round($w.Top - $work.Top)
30
- width = [Math]::Round($w.ActualWidth)
66
+ type = 'geometry'
67
+ left = [Math]::Round($w.Left - $work.Left)
68
+ top = [Math]::Round($w.Top - $work.Top)
69
+ width = [Math]::Round($w.ActualWidth)
31
70
  height = [Math]::Round($w.ActualHeight)
32
71
  }
33
72
  } catch { }
34
73
  }
35
74
 
36
- # 编辑模式样板窗:拖拽移动 + 右下角手柄缩放;拖拽/缩放结束后回传 geometry。
37
- # 位置与宽度写回配置后,所有后续浮窗按新几何弹出。
75
+ # ───────────────────────── 编辑模式样板窗 ─────────────────────────
76
+
77
+ # 编辑模式样板窗:拖拽移动 + 左右边缘圆点把手缩放;拖拽/缩放结束后回传 geometry。
78
+ # 强调色描边 + 辉光;内嵌 5 类型实时色卡预览(chips);多语言提示。
38
79
  function New-EditSample($cmd) {
39
80
  $bg = Parse-Color ([string]$cmd.bg) "#203a5c"
40
81
  $fg = Parse-Color ([string]$cmd.fg) "#e8f0fb"
82
+ $accent = Parse-Color ([string]$cmd.accent) "#60a5fa"
41
83
  $bgBrush = New-Object System.Windows.Media.SolidColorBrush($bg)
42
84
  $fgBrush = New-Object System.Windows.Media.SolidColorBrush($fg)
85
+ $accentBrush = New-Object System.Windows.Media.SolidColorBrush($accent)
43
86
 
44
87
  $win = New-Object System.Windows.Window
45
88
  $win.WindowStyle = [System.Windows.WindowStyle]::None
@@ -49,55 +92,121 @@ function New-EditSample($cmd) {
49
92
  $win.ShowInTaskbar = $false
50
93
  if ($null -ne $cmd.width) { $win.Width = [double]$cmd.width } else { $win.Width = 340 }
51
94
  $win.SizeToContent = [System.Windows.SizeToContent]::Height
95
+ $win.Effect = New-GlowEffect $accent 0.25
52
96
 
53
97
  $border = New-Object System.Windows.Controls.Border
54
98
  $border.Background = $bgBrush
55
- $border.BorderBrush = $fgBrush
99
+ $border.BorderBrush = $accentBrush
56
100
  $border.BorderThickness = New-Object System.Windows.Thickness(1)
57
- $border.CornerRadius = New-Object System.Windows.CornerRadius(12)
101
+ $border.CornerRadius = New-Object System.Windows.CornerRadius(14)
58
102
  $border.Margin = New-Object System.Windows.Thickness(12)
59
103
 
60
- $root = New-Object System.Windows.Controls.Grid
61
- $root.Margin = New-Object System.Windows.Thickness(14,12,14,12)
104
+ $root = New-Object System.Windows.Controls.StackPanel
105
+ $root.Margin = New-Object System.Windows.Thickness(16, 14, 16, 14)
62
106
  $border.Child = $root
63
107
 
64
- $stack = New-Object System.Windows.Controls.StackPanel
108
+ # 标题(带强调色小色块前缀)
109
+ $titleRow = New-Object System.Windows.Controls.StackPanel
110
+ $titleRow.Orientation = [System.Windows.Controls.Orientation]::Horizontal
111
+ $titleRow.Margin = New-Object System.Windows.Thickness(0, 0, 0, 8)
112
+ $titleDot = New-Object System.Windows.Controls.Border
113
+ $titleDot.Width = 4
114
+ $titleDot.Height = 14
115
+ $titleDot.CornerRadius = New-Object System.Windows.CornerRadius(2)
116
+ $titleDot.Background = $accentBrush
117
+ $titleDot.Margin = New-Object System.Windows.Thickness(0, 0, 8, 0)
118
+ $titleDot.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
119
+ $titleRow.Children.Add($titleDot) | Out-Null
65
120
  $titleText = New-Object System.Windows.Controls.TextBlock
66
121
  $titleText.Text = if ([string]::IsNullOrEmpty([string]$cmd.title)) { "Notification preview" } else { [string]$cmd.title }
67
122
  $titleText.Foreground = $fgBrush
68
123
  $titleText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
69
124
  $titleText.FontSize = 13
70
125
  $titleText.FontWeight = [System.Windows.FontWeights]::SemiBold
71
- $titleText.Margin = New-Object System.Windows.Thickness(0,0,0,6)
72
- $stack.Children.Add($titleText) | Out-Null
126
+ $titleText.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
127
+ $titleRow.Children.Add($titleText) | Out-Null
128
+ $root.Children.Add($titleRow) | Out-Null
129
+
130
+ # 5 类型实时色卡预览:浮窗色条/描边/主按钮所见即所得
131
+ $chips = @($cmd.chips)
132
+ if ($chips.Count -gt 0) {
133
+ $chipRow = New-Object System.Windows.Controls.WrapPanel
134
+ $chipRow.Margin = New-Object System.Windows.Thickness(0, 0, 0, 8)
135
+ foreach ($c in $chips) {
136
+ if ($c -eq $null) { continue }
137
+ $chipAccent = Parse-Color ([string]$c.accent) "#60a5fa"
138
+ $chipBg = Parse-Color ([string]$c.bg) "#20272f"
139
+ $chip = New-Object System.Windows.Controls.Border
140
+ $chip.CornerRadius = New-Object System.Windows.CornerRadius(8)
141
+ $chip.Padding = New-Object System.Windows.Thickness(8, 3, 8, 4)
142
+ $chip.Margin = New-Object System.Windows.Thickness(0, 0, 6, 4)
143
+ $chip.Background = New-Object System.Windows.Media.SolidColorBrush($chipBg)
144
+ $chip.BorderBrush = New-Object System.Windows.Media.SolidColorBrush($chipAccent)
145
+ $chip.BorderThickness = New-Object System.Windows.Thickness(1)
146
+ $chipText = New-Object System.Windows.Controls.TextBlock
147
+ $chipText.Text = [string]$c.label
148
+ $chipText.Foreground = New-Object System.Windows.Media.SolidColorBrush($chipAccent)
149
+ $chipText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
150
+ $chipText.FontSize = 11
151
+ $chipText.FontWeight = [System.Windows.FontWeights]::SemiBold
152
+ $chip.Child = $chipText
153
+ $chipRow.Children.Add($chip) | Out-Null
154
+ }
155
+ $root.Children.Add($chipRow) | Out-Null
156
+ }
157
+
158
+ # 提示文案(多语言,由 startEditMode 传入 hint)
73
159
  $bodyText = New-Object System.Windows.Controls.TextBlock
74
- $bodyText.Text = "拖动移动 · 拖拽窗口左右边缘调整宽度 · 完成后在设置页点「完成编辑」"
160
+ $bodyText.Text = if ([string]::IsNullOrEmpty([string]$cmd.hint)) {
161
+ "Drag to move · Drag window edges to resize width · Click 'Finish editing' when done"
162
+ } else { [string]$cmd.hint }
75
163
  $bodyText.Foreground = $fgBrush
76
164
  $bodyText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
77
165
  $bodyText.FontSize = 12
78
166
  $bodyText.TextWrapping = [System.Windows.TextWrapping]::Wrap
79
- $stack.Children.Add($bodyText) | Out-Null
80
- $root.Children.Add($stack) | Out-Null
167
+ $root.Children.Add($bodyText) | Out-Null
81
168
 
82
- # 左右边缘缩放条(像 Windows 窗口一样拖拽边缘调宽度):8px 宽、通高,
83
- # 半透明文字色填充提示可拖拽区域,光标为左右双向箭头。
84
- $edgeBrush = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromArgb(38, $fg.R, $fg.G, $fg.B))
169
+ # 左右边缘缩放把手:8px 通高,强调色半透明填充 + 三圆点把手,
170
+ # 悬停时完全不透明提示可拖拽区域,光标为左右双向箭头。
171
+ $edgeBrush = New-Object System.Windows.Media.SolidColorBrush(
172
+ [System.Windows.Media.Color]::FromArgb(115, $accent.R, $accent.G, $accent.B))
85
173
  $leftStrip = New-Object System.Windows.Controls.Border
86
174
  $leftStrip.Width = 8
87
175
  $leftStrip.Cursor = [System.Windows.Input.Cursors]::SizeWE
88
176
  $leftStrip.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Left
89
177
  $leftStrip.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
90
178
  $leftStrip.Background = $edgeBrush
91
- $leftStrip.CornerRadius = New-Object System.Windows.CornerRadius(12,0,0,12)
179
+ $leftStrip.CornerRadius = New-Object System.Windows.CornerRadius(14, 0, 0, 14)
92
180
  $rightStrip = New-Object System.Windows.Controls.Border
93
181
  $rightStrip.Width = 8
94
182
  $rightStrip.Cursor = [System.Windows.Input.Cursors]::SizeWE
95
183
  $rightStrip.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Right
96
184
  $rightStrip.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
97
185
  $rightStrip.Background = $edgeBrush
98
- $rightStrip.CornerRadius = New-Object System.Windows.CornerRadius(0,12,12,0)
186
+ $rightStrip.CornerRadius = New-Object System.Windows.CornerRadius(0, 14, 14, 0)
187
+
188
+ function Add-GripDots($strip, $dotColor) {
189
+ $dots = New-Object System.Windows.Controls.StackPanel
190
+ $dots.Orientation = [System.Windows.Controls.Orientation]::Horizontal
191
+ $dots.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
192
+ $dots.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Center
193
+ foreach ($i in 1..3) {
194
+ $dot = New-Object System.Windows.Shapes.Ellipse
195
+ $dot.Width = 3; $dot.Height = 3
196
+ $dot.Margin = New-Object System.Windows.Thickness(1, 0, 1, 0)
197
+ $dot.Fill = New-Object System.Windows.Media.SolidColorBrush(
198
+ [System.Windows.Media.Color]::FromArgb(170, $dotColor.R, $dotColor.G, $dotColor.B))
199
+ $dots.Children.Add($dot) | Out-Null
200
+ }
201
+ $strip.Child = $dots
202
+ $strip.Opacity = 0.75
203
+ $strip.Add_MouseEnter({ $this.Opacity = 1.0 }) | Out-Null
204
+ $strip.Add_MouseLeave({ $this.Opacity = 0.75 }) | Out-Null
205
+ }
206
+ Add-GripDots $leftStrip $fg
207
+ Add-GripDots $rightStrip $fg
99
208
 
100
- # 外层 Grid:边框 + 左右缩放条叠放(缩放条覆盖窗口左右边缘)
209
+ # 外层 Grid:边框 + 左右缩放把手叠放(把手覆盖窗口左右边缘)
101
210
  $outer = New-Object System.Windows.Controls.Grid
102
211
  $outer.Children.Add($border) | Out-Null
103
212
  $outer.Children.Add($leftStrip) | Out-Null
@@ -119,12 +228,10 @@ function New-EditSample($cmd) {
119
228
  Send-Geometry $win
120
229
  }.GetNewClosure()) | Out-Null
121
230
 
122
- # 拖拽移动(缩放条区域除外):DragMove 阻塞至松开,结束后收敛到工作区内并回传几何。
231
+ # 拖拽移动(缩放把手区域除外):DragMove 阻塞至松开,结束后收敛到工作区内并回传几何。
123
232
  $win.Add_MouseLeftButtonDown({
124
233
  if ($win.Tag.resizing) { $win.Tag.resizing = $false; return }
125
- try {
126
- $win.DragMove()
127
- } catch { }
234
+ try { $win.DragMove() } catch { }
128
235
  try {
129
236
  $work = [System.Windows.SystemParameters]::WorkArea
130
237
  $win.Left = [Math]::Max($work.Left, [Math]::Min($win.Left, $work.Right - $win.ActualWidth))
@@ -189,39 +296,40 @@ function Parse-Color([string]$hex, [string]$fallback) {
189
296
  if ($h.Length -eq 3) { $h = ($h.ToCharArray() | ForEach-Object { "$_$_" }) -join '' }
190
297
  if ($h.Length -eq 6) { $h = "FF$h" }
191
298
  if ($h.Length -ne 8 -or $h -notmatch '^[0-9a-fA-F]{8}$') {
192
- # 非法/缺失输入:改用回退色并同样做 3/6 位展开 + 补 FF(此前 FF 步骤在回退赋值之前,
193
- # 回退为 6 位时未补前缀,Substring 越界抛异常)。
299
+ # 非法/缺失输入:改用回退色并同样做 3/6 位展开 + 补 FF。
194
300
  $h = $fallback.TrimStart('#')
195
301
  if ($h.Length -eq 3) { $h = ($h.ToCharArray() | ForEach-Object { "$_$_" }) -join '' }
196
302
  if ($h.Length -eq 6) { $h = "FF$h" }
197
303
  }
198
304
  return [System.Windows.Media.Color]::FromArgb(
199
- [Convert]::ToInt32($h.Substring(0,2), 16),
200
- [Convert]::ToInt32($h.Substring(2,2), 16),
201
- [Convert]::ToInt32($h.Substring(4,2), 16),
202
- [Convert]::ToInt32($h.Substring(6,2), 16))
305
+ [Convert]::ToInt32($h.Substring(0, 2), 16),
306
+ [Convert]::ToInt32($h.Substring(2, 2), 16),
307
+ [Convert]::ToInt32($h.Substring(4, 2), 16),
308
+ [Convert]::ToInt32($h.Substring(6, 2), 16))
203
309
  }
204
310
 
205
- $downP = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(158,164,170))
206
- $downI = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(30,36,44))
207
- $hoverP = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(196,202,208))
208
- $hoverI = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(44,52,62))
311
+ $downP = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(158, 164, 170))
312
+ $downI = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(30, 36, 44))
313
+ $hoverP = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(196, 202, 208))
314
+ $hoverI = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Color]::FromRgb(44, 52, 62))
209
315
 
316
+ # 浮窗按钮:primary 时传入强调色画刷,hover/按压基于该色自动提亮/压暗;
317
+ # secondary 保持描边透明底样式。
210
318
  function New-ToastButton([string]$label, [scriptblock]$onClick, [bool]$primary, [System.Windows.Media.SolidColorBrush]$fg, [System.Windows.Media.SolidColorBrush]$bg) {
211
319
  $bd = New-Object System.Windows.Controls.Border
212
320
  $bd.CornerRadius = New-Object System.Windows.CornerRadius(10)
213
- $bd.Padding = New-Object System.Windows.Thickness(18,7,18,7)
214
- $bd.Margin = New-Object System.Windows.Thickness(0,0,8,0)
321
+ $bd.Padding = New-Object System.Windows.Thickness(18, 7, 18, 7)
322
+ $bd.Margin = New-Object System.Windows.Thickness(0, 0, 8, 0)
215
323
  $bd.Cursor = [System.Windows.Input.Cursors]::Hand
216
324
  $bd.Focusable = $false
217
-
325
+
218
326
  $txt = New-Object System.Windows.Controls.TextBlock
219
327
  $txt.Text = $label
220
328
  $txt.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
221
329
  $txt.FontSize = 12
222
330
  $txt.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
223
331
  $txt.IsHitTestVisible = $false
224
-
332
+
225
333
  if ($primary) {
226
334
  $bd.Background = $fg
227
335
  $txt.Foreground = $bg
@@ -233,31 +341,32 @@ function New-ToastButton([string]$label, [scriptblock]$onClick, [bool]$primary,
233
341
  $bd.BorderBrush = $fg
234
342
  $bd.BorderThickness = New-Object System.Windows.Thickness(1)
235
343
  }
344
+
236
345
  $bd.Child = $txt
237
-
238
- $bd.Tag = @{ primary = $primary; onClick = $onClick; hoverP = $hoverP; hoverI = $hoverI; downP = $downP; downI = $downI; fg = $fg; bg = $bg }
239
-
346
+ # hover/down 变体:primary 基于传入的强调色提亮/压暗;secondary 用全局灰蓝变体。
347
+ $bd.Tag = @{
348
+ primary = $primary; onClick = $onClick
349
+ hoverP = (Convert-Hsl $fg.Color 1.15); downP = (Convert-Hsl $fg.Color 0.85)
350
+ hoverI = $hoverI; downI = $downI
351
+ fg = $fg; bg = $bg
352
+ }
240
353
  $bd.Add_MouseEnter({
241
354
  $info = $this.Tag
242
355
  $this.Background = if ($info.primary) { $info.hoverP } else { $info.hoverI }
243
356
  }) | Out-Null
244
-
245
357
  $bd.Add_MouseLeave({
246
358
  $info = $this.Tag
247
359
  $this.Background = if ($info.primary) { $info.fg } else { [System.Windows.Media.Brushes]::Transparent }
248
360
  }) | Out-Null
249
-
250
361
  $bd.Add_MouseLeftButtonDown({
251
362
  $info = $this.Tag
252
363
  $this.Background = if ($info.primary) { $info.downP } else { $info.downI }
253
364
  }) | Out-Null
254
-
255
365
  $bd.Add_MouseLeftButtonUp({
256
366
  $info = $this.Tag
257
367
  try { if ($info.onClick -ne $null) { & $info.onClick } } catch { }
258
368
  $this.Background = if ($info.primary) { $info.fg } else { [System.Windows.Media.Brushes]::Transparent }
259
369
  }) | Out-Null
260
-
261
370
  return $bd
262
371
  }
263
372
 
@@ -268,23 +377,41 @@ $easeExit.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseIn
268
377
 
269
378
  function Close-WithFade($w) {
270
379
  try {
271
- if ($w.Tag -eq 'closing') { return }
272
- $w.Tag = 'closing'
273
- $fadeOut = New-Object System.Windows.Media.Animation.DoubleAnimation(1, 0, [TimeSpan]::FromMilliseconds(160))
274
- $fadeOut.EasingFunction = $easeExit
275
- $fadeOut.Add_Completed({ try { $w.Close() } catch { } }.GetNewClosure()) | Out-Null
276
- $w.BeginAnimation([System.Windows.Window]::OpacityProperty, $fadeOut)
277
-
278
- $dropAn = New-Object System.Windows.Media.Animation.DoubleAnimation($w.Top, ($w.Top + 20), [TimeSpan]::FromMilliseconds(160))
279
- $dropAn.EasingFunction = $easeExit
280
- $w.BeginAnimation([System.Windows.Window]::TopProperty, $dropAn)
281
-
380
+ # closing 标记改为 Tag 哈希表(同时携带卡片缩放变换供退场动画用)。
381
+ if ($w.Tag -is [hashtable]) {
382
+ if ($w.Tag.closing) { return }
383
+ $w.Tag.closing =$true
384
+ } else {
385
+ if ($w.Tag -eq 'closing') { return }
386
+ $w.Tag = @{ closing =$true; scale = $null }
387
+ }
388
+ # 淡出 + 下坠 + 轻微收缩(0.98):三者同步 200ms,"沉落"退场。
389
+ $fadeOut = New-Object System.Windows.Media.Animation.DoubleAnimation(1, 0, [TimeSpan]::FromMilliseconds(200))
390
+ $fadeOut.EasingFunction =$easeExit
391
+ $fadeOut.Add_Completed({
392
+ try { $w.Close() } catch { }
393
+ }.GetNewClosure()) | Out-Null
394
+ $w.BeginAnimation([System.Windows.Window]::OpacityProperty,$fadeOut)
395
+ $dropAn = New-Object System.Windows.Media.Animation.DoubleAnimation($w.Top, ($w.Top + 16), [TimeSpan]::FromMilliseconds(200))
396
+ $dropAn.EasingFunction =$easeExit
397
+ $w.BeginAnimation([System.Windows.Window]::TopProperty,$dropAn)
398
+ try {
399
+ $sc =$w.Tag.scale
400
+ if ($sc -ne$null) {
401
+ $sx = New-Object System.Windows.Media.Animation.DoubleAnimation(1, 0.98, [TimeSpan]::FromMilliseconds(200))
402
+ $sx.EasingFunction =$easeExit
403
+ $sy = New-Object System.Windows.Media.Animation.DoubleAnimation(1, 0.98, [TimeSpan]::FromMilliseconds(200))
404
+ $sy.EasingFunction =$easeExit
405
+ $sc.BeginAnimation([System.Windows.Media.ScaleTransform]::ScaleXProperty,$sx)
406
+ $sc.BeginAnimation([System.Windows.Media.ScaleTransform]::ScaleYProperty,$sy)
407
+ }
408
+ } catch { }
282
409
  $backstop = New-Object System.Windows.Threading.DispatcherTimer
283
410
  $backstop.Interval = [TimeSpan]::FromMilliseconds(600)
284
- $backstop.Tag = $w
411
+ $backstop.Tag =$w
285
412
  $backstop.Add_Tick({
286
413
  $this.Stop()
287
- try { if ($this.Tag.IsVisible) { $this.Tag.Close() } } catch { }
414
+ try { if ($this.Tag.IsVisible) {$this.Tag.Close() } } catch { }
288
415
  }) | Out-Null
289
416
  $backstop.Start()
290
417
  } catch {
@@ -292,64 +419,87 @@ function Close-WithFade($w) {
292
419
  }
293
420
  }
294
421
 
422
+ # ───────────────────────── 通知浮窗 ─────────────────────────
423
+
295
424
  function New-ToastWindow($cmd) {
296
425
  $bg = Parse-Color ([string]$cmd.bg) "#20272f"
297
426
  $fg = Parse-Color ([string]$cmd.fg) "#e6edf3"
427
+ $accent = Parse-Color ([string]$cmd.accent) "#60a5fa"
298
428
  $bgBrush = New-Object System.Windows.Media.SolidColorBrush($bg)
299
429
  $fgBrush = New-Object System.Windows.Media.SolidColorBrush($fg)
300
-
430
+ $accentBrush = New-Object System.Windows.Media.SolidColorBrush($accent)
431
+
432
+ # 提前计算:粘性判定与倒计时时长(Loaded 闭包与计时器共用)。
433
+ $isSticky = ($cmd.sticky -eq $true)
434
+ $dur = [Math]::Max(1, [int]$cmd.durationSec)
435
+
301
436
  $win = New-Object System.Windows.Window
302
437
  $win.WindowStyle = [System.Windows.WindowStyle]::None
303
- $win.AllowsTransparency = $true
438
+ $win.AllowsTransparency =$true
304
439
  $win.Background = [System.Windows.Media.Brushes]::Transparent
305
- $win.Topmost = $true
306
- $win.ShowInTaskbar = $false
307
- # 通知窗口样式:宽度来自配置(默认 340);高度自适应内容。
308
- if ($null -ne $cmd.width) { $win.Width = [double]$cmd.width } else { $win.Width = 340 }
440
+ $win.Topmost =$true
441
+ $win.ShowInTaskbar =$false
442
+ if ($null -ne$cmd.width) { $win.Width = [double]$cmd.width } else { $win.Width = 340 }
309
443
  $win.SizeToContent = [System.Windows.SizeToContent]::Height
310
- if (-not [string]::IsNullOrEmpty([string]$cmd.winTitle)) { $win.Title = [string]$cmd.winTitle }
311
-
444
+ if (-not [string]::IsNullOrEmpty([string]$cmd.winTitle)) {$win.Title = [string]$cmd.winTitle }
445
+ $win.Effect = New-GlowEffect $accent 0.28
446
+
312
447
  $border = New-Object System.Windows.Controls.Border
313
- $border.Background = $bgBrush
314
- $border.BorderBrush = $fgBrush
448
+ $border.Background =$bgBrush
449
+ $border.BorderBrush =$accentBrush
315
450
  $border.BorderThickness = New-Object System.Windows.Thickness(1)
316
- $border.CornerRadius = New-Object System.Windows.CornerRadius(12)
451
+ $border.CornerRadius = New-Object System.Windows.CornerRadius(14)
317
452
  $border.Margin = New-Object System.Windows.Thickness(12)
318
-
319
- $root = New-Object System.Windows.Controls.StackPanel
320
- $root.Margin = New-Object System.Windows.Thickness(14,12,14,12)
321
- $border.Child = $root
322
-
323
- # 优化:支持多语言,接收传入的 label,如果没有则使用后备英文
453
+
454
+ # 卡片缩放变换:入场 0.96→1(底部锚点,"浮现"),退场 1→0.98("沉落")。
455
+ # 经 $win.Tag 传给 Close-WithFade,同时 Tag.closing 作防重入标记。
456
+ $cardT = New-Object System.Windows.Media.ScaleTransform(1, 1)
457
+ $border.RenderTransformOrigin = New-Object System.Windows.Point(0.5, 1)
458
+ $border.RenderTransform =$cardT
459
+
460
+ # 内容 Grid:3 行 = 头部(标题) / 正文 / 按钮行
461
+ $root = New-Object System.Windows.Controls.Grid
462
+ $root.Margin = New-Object System.Windows.Thickness(26, 12, 14, 12)
463
+ Add-GridRow $root
464
+ Add-GridRow $root
465
+ Add-GridRow $root
466
+
467
+ # ── 头部:仅标题 ──
468
+ $head = New-Object System.Windows.Controls.StackPanel
469
+ $head.Orientation = [System.Windows.Controls.Orientation]::Horizontal
470
+ $head.Margin = New-Object System.Windows.Thickness(0, 0, 0, 6)
324
471
  $unnamedLabel = if ([string]::IsNullOrEmpty([string]$cmd.unnamedLabel)) { "(Unnamed session)" } else { [string]$cmd.unnamedLabel }
325
-
326
472
  $titleText = New-Object System.Windows.Controls.TextBlock
327
473
  $titleText.Text = if ([string]::IsNullOrEmpty([string]$cmd.title)) { $unnamedLabel } else { [string]$cmd.title }
328
- $titleText.Foreground = $fgBrush
474
+ $titleText.Foreground =$fgBrush
329
475
  $titleText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
330
476
  $titleText.FontSize = 13
331
477
  $titleText.FontWeight = [System.Windows.FontWeights]::SemiBold
332
478
  $titleText.TextTrimming = [System.Windows.TextTrimming]::CharacterEllipsis
333
- $titleText.Margin = New-Object System.Windows.Thickness(0,0,0,6)
334
- $root.Children.Add($titleText) | Out-Null
335
-
479
+ $titleText.VerticalAlignment = [System.Windows.VerticalAlignment]::Center
480
+ $head.Children.Add($titleText) | Out-Null
481
+ [System.Windows.Controls.Grid]::SetRow($head, 0)
482
+ $root.Children.Add($head) | Out-Null
483
+
484
+ # ── 正文 ──
336
485
  $bodyText = New-Object System.Windows.Controls.TextBlock
337
486
  $bodyText.Text = [string]$cmd.text
338
- $bodyText.Foreground = $fgBrush
487
+ $bodyText.Foreground =$fgBrush
339
488
  $bodyText.FontFamily = New-Object System.Windows.Media.FontFamily("Segoe UI, Microsoft YaHei UI")
340
489
  $bodyText.FontSize = 13
341
490
  $bodyText.TextWrapping = [System.Windows.TextWrapping]::Wrap
342
- $bodyText.Margin = New-Object System.Windows.Thickness(0,0,0,10)
491
+ $bodyText.Margin = New-Object System.Windows.Thickness(0, 0, 0, 10)
492
+ [System.Windows.Controls.Grid]::SetRow($bodyText, 1)
343
493
  $root.Children.Add($bodyText) | Out-Null
344
-
494
+
495
+ # ── 按钮行 ──
345
496
  $btnRow = New-Object System.Windows.Controls.StackPanel
346
497
  $btnRow.Orientation = [System.Windows.Controls.Orientation]::Horizontal
347
498
  $btnRow.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Right
348
-
499
+ $btnRow.Margin = New-Object System.Windows.Thickness(0, 0, 0, 2)
349
500
  $ignoreLabel = if ([string]::IsNullOrEmpty([string]$cmd.ignoreLabel)) { "Ignore" } else { [string]$cmd.ignoreLabel }
350
501
  $ignoreBtn = New-ToastButton $ignoreLabel ({ Close-WithFade $win }.GetNewClosure()) $false $fgBrush $bgBrush
351
502
  $btnRow.Children.Add($ignoreBtn) | Out-Null
352
-
353
503
  if (-not [string]::IsNullOrEmpty([string]$cmd.sessionId)) {
354
504
  $jumpLabel = if ([string]::IsNullOrEmpty([string]$cmd.jumpLabel)) { "Open" } else { [string]$cmd.jumpLabel }
355
505
  $jumpBtn = New-ToastButton $jumpLabel ({
@@ -360,93 +510,155 @@ function New-ToastWindow($cmd) {
360
510
  } else { Start-Process $url }
361
511
  } catch { }
362
512
  Close-WithFade $win
363
- }.GetNewClosure()) $true $fgBrush $bgBrush
513
+ }.GetNewClosure()) $true $accentBrush $bgBrush
364
514
  $btnRow.Children.Add($jumpBtn) | Out-Null
365
515
  }
516
+ [System.Windows.Controls.Grid]::SetRow($btnRow, 2)
366
517
  $root.Children.Add($btnRow) | Out-Null
367
- $win.Content = $border
368
-
518
+
519
+ # ── 倒计时液面层(唯一的倒计时视觉,无任何线条元素) ──
520
+ $inner = New-Object System.Windows.Controls.Grid
521
+ $tintLayer = New-Object System.Windows.Controls.Border
522
+ $tintLayer.CornerRadius = New-Object System.Windows.CornerRadius(13, 0, 0, 13)
523
+ $tintLayer.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
524
+ $tintLayer.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Stretch
525
+ $tintBrush = New-Object System.Windows.Media.SolidColorBrush($accent)
526
+ $tintBrush.Opacity = 0.12
527
+ $tintLayer.Background =$tintBrush
528
+ $tintT = New-Object System.Windows.Media.ScaleTransform(1, 1)
529
+ $tintLayer.RenderTransformOrigin = New-Object System.Windows.Point(0, 0.5)
530
+ $tintLayer.RenderTransform =$tintT
531
+ $inner.Children.Add($tintLayer) | Out-Null
532
+ $inner.Children.Add($root) | Out-Null
533
+ $border.Child =$inner
534
+
535
+ # ── 左侧强调色条(粘性通知做呼吸脉动) ──
536
+ $accentStrip = New-Object System.Windows.Controls.Border
537
+ $accentStrip.Width = 4
538
+ $accentStrip.CornerRadius = New-Object System.Windows.CornerRadius(2)
539
+ $accentStrip.VerticalAlignment = [System.Windows.VerticalAlignment]::Stretch
540
+ $accentStrip.HorizontalAlignment = [System.Windows.HorizontalAlignment]::Left
541
+ $accentStrip.Margin = New-Object System.Windows.Thickness(22, 26, 0, 26)
542
+ $accentStrip.Background =$accentBrush
543
+
544
+ # 外层只有:卡片 + 色条。进度指针已彻底移除 —— 任何通知都不再出现那根线。
545
+ $outer = New-Object System.Windows.Controls.Grid
546
+ $outer.Children.Add($border) | Out-Null
547
+ $outer.Children.Add($accentStrip) | Out-Null
548
+ $win.Content =$outer
549
+
550
+ $win.Tag = @{ closing =$false; scale = $cardT }
551
+
369
552
  $win.Add_Loaded({
370
553
  $work = [System.Windows.SystemParameters]::WorkArea
371
- # 通知窗口位置:配置了自定义位置(position)则按其坐标(工作区相对)+ 堆叠偏移;
372
- # 否则默认右下贴边(右/下留边 20)。
373
- $pos = $cmd.position
374
- if ($null -ne $pos -and $null -ne $pos.left -and $null -ne $pos.top) {
554
+ $pos =$cmd.position
555
+ if ($null -ne$pos -and $null -ne$pos.left -and $null -ne$pos.top) {
375
556
  $targetLeft = [double]$pos.left + $work.Left
376
557
  $targetTop = [double]$pos.top + $work.Top - [int]$cmd.offsetY
377
558
  } else {
378
- $targetLeft = $work.Right - $win.ActualWidth - 20
379
- $targetTop = $work.Bottom - $win.ActualHeight - 20 - [int]$cmd.offsetY
559
+ $targetLeft =$work.Right - $win.ActualWidth - 20
560
+ $targetTop =$work.Bottom - $win.ActualHeight - 20 - [int]$cmd.offsetY
380
561
  }
381
- $win.Left = $targetLeft
382
- $win.Top = $targetTop + 32
562
+ $win.Left =$targetLeft
563
+ $win.Top =$targetTop + 36
383
564
  $win.Opacity = 0
384
565
  try {
385
566
  $easingIn = New-Object System.Windows.Media.Animation.QuadraticEase
386
567
  $easingIn.EasingMode = [System.Windows.Media.Animation.EasingMode]::EaseOut
387
- $slideIn = New-Object System.Windows.Media.Animation.DoubleAnimation($targetTop + 32, $targetTop, [TimeSpan]::FromMilliseconds(260))
388
- $slideIn.EasingFunction = $easingIn
389
- $win.BeginAnimation([System.Windows.Window]::TopProperty, $slideIn)
390
- $fadeIn = New-Object System.Windows.Media.Animation.DoubleAnimation(0, 1, [TimeSpan]::FromMilliseconds(180))
391
- $fadeIn.EasingFunction = $easingIn
392
- $win.BeginAnimation([System.Windows.Window]::OpacityProperty, $fadeIn)
393
-
568
+ # 入场三连:上滑 36px + 淡入 + 卡片 0.96→1 缩放(底部锚点),260ms 同步完成。
569
+ $slideIn = New-Object System.Windows.Media.Animation.DoubleAnimation($targetTop + 36, $targetTop, [TimeSpan]::FromMilliseconds(260))
570
+ $slideIn.EasingFunction =$easingIn
571
+ $win.BeginAnimation([System.Windows.Window]::TopProperty,$slideIn)
572
+ $fadeIn = New-Object System.Windows.Media.Animation.DoubleAnimation(0, 1, [TimeSpan]::FromMilliseconds(200))
573
+ $fadeIn.EasingFunction =$easingIn
574
+ $win.BeginAnimation([System.Windows.Window]::OpacityProperty,$fadeIn)
575
+ $scInX = New-Object System.Windows.Media.Animation.DoubleAnimation(0.96, 1, [TimeSpan]::FromMilliseconds(260))
576
+ $scInX.EasingFunction =$easingIn
577
+ $scInY = New-Object System.Windows.Media.Animation.DoubleAnimation(0.96, 1, [TimeSpan]::FromMilliseconds(260))
578
+ $scInY.EasingFunction =$easingIn
579
+ $cardT.BeginAnimation([System.Windows.Media.ScaleTransform]::ScaleXProperty,$scInX)
580
+ $cardT.BeginAnimation([System.Windows.Media.ScaleTransform]::ScaleYProperty,$scInY)
394
581
  $guard = New-Object System.Windows.Threading.DispatcherTimer
395
582
  $guard.Interval = [TimeSpan]::FromMilliseconds(400)
396
- $guard.Tag = @{ w = $win; top = $targetTop }
583
+ $guard.Tag = @{ w =$win; top = $targetTop }
397
584
  $guard.Add_Tick({
398
585
  $this.Stop()
399
- $g = $this.Tag
586
+ $g =$this.Tag
400
587
  try {
401
- if ($g.w.Opacity -lt 0.9) { $g.w.Opacity = 1 }
402
- if ($g.w.Top -ne $g.top) { $g.w.Top = $g.top }
588
+ if ($g.w.Opacity -lt 0.9) {$g.w.Opacity = 1 }
589
+ if ($g.w.Top -ne$g.top) { $g.w.Top =$g.top }
403
590
  } catch { }
404
591
  }) | Out-Null
405
592
  $guard.Start()
406
593
  } catch {
407
594
  $win.Opacity = 1
408
- $win.Top = $targetTop
595
+ $win.Top =$targetTop
409
596
  }
410
597
  try {
411
598
  $hwnd = [System.Windows.Interop.WindowInteropHelper]::new($win).Handle
412
- Send-Report @{ type = 'pos'; key = [string]$cmd.key; instance = [int]$cmd.instance
413
- left = [Math]::Round($targetLeft); top = [Math]::Round($targetTop)
414
- height = [Math]::Round($win.ActualHeight); hwnd = $hwnd.ToInt64()
415
- workBottom = [Math]::Round($work.Bottom) }
599
+ Send-Report @{
600
+ type = 'pos'; key = [string]$cmd.key; instance = [int]$cmd.instance
601
+ left = [Math]::Round($targetLeft); top = [Math]::Round($targetTop)
602
+ height = [Math]::Round($win.ActualHeight); hwnd =$hwnd.ToInt64()
603
+ workBottom = [Math]::Round($work.Bottom)
604
+ }
416
605
  } catch { }
606
+
607
+ # ── 停留阶段:非粘性 = 液面匀速右→左;粘性 = 色条呼吸 ──
608
+ if (-not $isSticky) {
609
+ try {
610
+ $shrink = New-Object System.Windows.Media.Animation.DoubleAnimation(
611
+ 1, 0, [TimeSpan]::FromSeconds($dur))
612
+ # 无缓动:视觉时长 = 真实剩余时间。
613
+ $tintT.BeginAnimation([System.Windows.Media.ScaleTransform]::ScaleXProperty,$shrink)
614
+ } catch { }
615
+ } else {
616
+ try {
617
+ $pulse = New-Object System.Windows.Media.Animation.DoubleAnimation(0.55, 1.0,
618
+ [TimeSpan]::FromMilliseconds(1100))
619
+ $pulse.AutoReverse =$true
620
+ $pulse.RepeatBehavior = [System.Windows.Media.Animation.RepeatBehavior]::Forever
621
+ $pulse.EasingFunction = New-Object System.Windows.Media.Animation.QuadraticEase
622
+ $accentStrip.BeginAnimation([System.Windows.UIElement]::OpacityProperty,$pulse)
623
+ } catch { }
624
+ }
417
625
  }.GetNewClosure()) | Out-Null
418
-
419
- $toastsRef = $script:toasts
626
+
627
+ $toastsRef =$script:toasts
420
628
  $win.Add_Closed({
421
629
  if ($toastsRef.ContainsKey([string]$cmd.key)) {
422
- $rec = $toastsRef[[string]$cmd.key]
423
- if ($rec.Timer -ne $null) { try { $rec.Timer.Stop() } catch { } }
630
+ $rec =$toastsRef[[string]$cmd.key]
631
+ if ($rec.Timer -ne$null) {
632
+ try { $rec.Timer.Stop() } catch { }
633
+ }
424
634
  $toastsRef.Remove([string]$cmd.key) | Out-Null
425
635
  }
426
636
  Send-Report @{ type = 'exit'; key = [string]$cmd.key; instance = [int]$cmd.instance }
427
637
  }.GetNewClosure()) | Out-Null
428
-
429
- $rec = @{ Win = $win; Body = $bodyText; Title = $titleText; Timer = $null; Instance = [int]$cmd.instance }
638
+
639
+ $rec = @{ Win =$win; Body = $bodyText; Title =$titleText; Timer = $null; Instance = [int]$cmd.instance }
430
640
  $script:toasts[[string]$cmd.key] = $rec
431
-
432
- if (-not ($cmd.sticky -eq $true)) {
433
- $dur = [Math]::Max(1, [int]$cmd.durationSec)
641
+
642
+ if (-not $isSticky) {
434
643
  $timer = New-Object System.Windows.Threading.DispatcherTimer
435
644
  $timer.Interval = [TimeSpan]::FromSeconds($dur)
436
- $timer.Add_Tick({ $this.Stop(); Close-WithFade $win }.GetNewClosure()) | Out-Null
645
+ $timer.Add_Tick({$this.Stop(); Close-WithFade $win }.GetNewClosure()) | Out-Null
437
646
  $timer.Start()
438
- $rec.Timer = $timer
647
+ $rec.Timer =$timer
439
648
  }
649
+
440
650
  return $win
441
651
  }
442
652
 
653
+ # ───────────────────────── 命令处理 ─────────────────────────
654
+
443
655
  function Invoke-Command([string]$line) {
444
656
  $line = $line.Trim()
445
657
  if ([string]::IsNullOrEmpty($line)) { return }
446
658
  $cmd = $null
447
659
  try { $cmd = $line | ConvertFrom-Json } catch { return }
448
660
  if ($cmd -eq $null -or [string]::IsNullOrEmpty([string]$cmd.cmd)) { return }
449
-
661
+
450
662
  switch ([string]$cmd.cmd) {
451
663
  'show' {
452
664
  if ($script:toasts.ContainsKey([string]$cmd.key)) {
@@ -497,19 +709,28 @@ function Invoke-Command([string]$line) {
497
709
  if ($rec -ne $null -and $null -ne $cmd.width) { $rec.Win.Width = [double]$cmd.width }
498
710
  }
499
711
  'edit' {
500
- # 编辑模式:关闭旧样板(如有),弹新样板窗(可拖拽/缩放)。
501
- if ($script:editSample -ne $null) { try { $script:editSample.Close() } catch { } ; $script:editSample = $null }
712
+ # 编辑模式:关闭旧样板(如有),弹新样板窗(可拖拽/缩放,内嵌类型色卡预览)。
713
+ if ($script:editSample -ne $null) {
714
+ try { $script:editSample.Close() } catch { }
715
+ $script:editSample = $null
716
+ }
502
717
  $w = New-EditSample $cmd
503
718
  if ($w -ne $null) { $w.Show(); $script:editSample = $w }
504
719
  }
505
720
  'edit-end' {
506
- if ($script:editSample -ne $null) { try { $script:editSample.Close() } catch { } ; $script:editSample = $null }
721
+ if ($script:editSample -ne $null) {
722
+ try { $script:editSample.Close() } catch { }
723
+ $script:editSample = $null
724
+ }
507
725
  }
508
726
  'shutdown' { $script:exitRequested = $true }
509
727
  }
510
728
  }
511
729
 
730
+ # ───────────────────────── 主循环 ─────────────────────────
731
+
512
732
  $script:cmdQueue = New-Object 'System.Collections.Concurrent.ConcurrentQueue[string]'
733
+
513
734
  $readerScript = {
514
735
  param($q)
515
736
  try {
@@ -553,6 +774,7 @@ $cmdTimer.Add_Tick({
553
774
  } catch { }
554
775
  }) | Out-Null
555
776
  $cmdTimer.Start()
777
+
556
778
  [System.Windows.Threading.Dispatcher]::Run()
557
779
 
558
780
  try { if ($readerAsyncResult -ne $null) { $readerPs.EndInvoke($readerAsyncResult) } } catch { }
@@ -10,6 +10,7 @@ import {
10
10
  const kindColorSchema = z.object({
11
11
  bg: z.string(),
12
12
  fg: z.string(),
13
+ accent: z.string().optional(),
13
14
  enabled: z.boolean(),
14
15
  })
15
16
 
@@ -224,4 +225,4 @@ export const TYPERT = {
224
225
  },
225
226
  }
226
227
 
227
- export default TYPERT
228
+ export default TYPERT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-notify-yimit",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "DeepSeek Harness 通知插件:任务完成 / 任务出错 / 运行中(实时活动)/ 等待审批 / 等待回答 时,通过 Windows 系统通知或插件自定义样式通知提醒用户,支持点击跳转到对应会话;设置页集成在 DSH 设置界面。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",