@goodandready/dsh-goal 0.1.4 → 0.1.6

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
@@ -3,7 +3,7 @@ window.__ModuleLoader__.load({
3
3
  factory: (require) => {
4
4
  const module = { exports: {} };
5
5
  const React = require('react');
6
- const { useState, useEffect, useRef, useCallback } = React;
6
+ const { useState, useEffect, useRef, useCallback, useMemo } = React;
7
7
 
8
8
  const NS = 'dsh-goal';
9
9
 
@@ -108,7 +108,6 @@ window.__ModuleLoader__.load({
108
108
  strokeWidth: 2.5,
109
109
  strokeLinecap: 'round',
110
110
  strokeLinejoin: 'round',
111
- style: { color: 'var(--dsw-alias-status-success)' },
112
111
  className,
113
112
  },
114
113
  React.createElement('polyline', { points: '20 6 9 17 4 12' }),
@@ -173,7 +172,7 @@ window.__ModuleLoader__.load({
173
172
  );
174
173
  }
175
174
 
176
- // --- СТИЛИ И CSS ПРАВИЛА (ЧИСТЫЕ DSH ТОКЕНЫ БЕЗ ХАРДКОД-ФОЛЛБЕКОВ) ---
175
+ // --- СТИЛИ И CSS ПРАВИЛА (СТАНДАРТ DSH-CLINEBOT & ДИЗАЙН-СИСТЕМА DSH) ---
177
176
  const CSS_STYLES = `
178
177
  .dsh-goal-dock {
179
178
  box-sizing: border-box;
@@ -189,16 +188,23 @@ window.__ModuleLoader__.load({
189
188
  display: flex;
190
189
  align-items: center;
191
190
  justify-content: space-between;
192
- background: var(--dsw-alias-bg-layer-2);
191
+ background: var(--dsw-alias-bg-layer-3);
193
192
  border: 1px solid var(--dsw-alias-border-l2);
194
193
  border-radius: 12px;
195
- padding: 6px 14px;
194
+ padding: 7px 14px;
196
195
  font-family: inherit;
197
196
  font-size: 13px;
198
197
  color: var(--dsw-alias-label-primary);
199
198
  user-select: none;
200
199
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
201
- transition: all 0.2s ease;
200
+ transition: all 0.15s ease;
201
+ }
202
+ .dsh-goal-banner:hover {
203
+ border-color: var(--dsw-alias-label-tertiary);
204
+ }
205
+ .dsh-goal-banner.completed {
206
+ border-color: var(--dsw-alias-state-success-primary);
207
+ background: var(--dsw-alias-bg-layer-3);
202
208
  }
203
209
  .dsh-goal-left {
204
210
  display: flex;
@@ -213,13 +219,36 @@ window.__ModuleLoader__.load({
213
219
  display: flex;
214
220
  align-items: center;
215
221
  }
222
+ .dsh-goal-badge {
223
+ font-size: 11px;
224
+ padding: 2px 8px;
225
+ border-radius: 999px;
226
+ border: 1px solid var(--dsw-alias-border-l2);
227
+ display: inline-flex;
228
+ align-items: center;
229
+ gap: 4px;
230
+ font-weight: 600;
231
+ white-space: nowrap;
232
+ background: var(--dsw-alias-bg-layer-2);
233
+ color: var(--dsw-alias-label-primary);
234
+ }
235
+ .dsh-goal-badge-ok {
236
+ border-color: var(--dsw-alias-state-success-primary);
237
+ color: var(--dsw-alias-state-success-primary);
238
+ background: rgba(16, 185, 129, 0.08);
239
+ }
240
+ .dsh-goal-badge-warn {
241
+ border-color: var(--dsw-alias-state-warning-primary);
242
+ color: var(--dsw-alias-state-warning-primary);
243
+ background: rgba(245, 158, 11, 0.08);
244
+ }
216
245
  .dsh-goal-label {
217
246
  font-weight: 600;
218
247
  color: var(--dsw-alias-label-primary);
219
248
  white-space: nowrap;
220
249
  }
221
250
  .dsh-goal-label.completed {
222
- color: var(--dsw-alias-status-success);
251
+ color: var(--dsw-alias-state-success-primary);
223
252
  }
224
253
  .dsh-goal-title {
225
254
  color: var(--dsw-alias-label-secondary);
@@ -249,21 +278,38 @@ window.__ModuleLoader__.load({
249
278
  flex-shrink: 0;
250
279
  }
251
280
  .dsh-goal-btn {
252
- background: transparent;
253
- border: 0;
254
- padding: 4px;
281
+ appearance: none;
282
+ font: inherit;
283
+ background: var(--dsw-alias-bg-layer-2);
284
+ border: 1px solid var(--dsw-alias-border-l2);
285
+ padding: 5px 8px;
255
286
  margin: 0;
256
- color: var(--dsw-alias-label-tertiary);
287
+ color: var(--dsw-alias-label-primary);
257
288
  cursor: pointer;
258
- display: flex;
289
+ display: inline-flex;
259
290
  align-items: center;
260
291
  justify-content: center;
261
- border-radius: 6px;
292
+ gap: 5px;
293
+ border-radius: 8px;
294
+ font-size: 12px;
295
+ font-weight: 500;
262
296
  transition: all 0.15s ease;
263
297
  }
264
- .dsh-goal-btn:hover {
298
+ .dsh-goal-btn:hover:not(:disabled) {
299
+ background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));
300
+ border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2));
301
+ }
302
+ .dsh-goal-btn.icon-only {
303
+ padding: 4px 6px;
304
+ border-radius: 6px;
305
+ border-color: transparent;
306
+ background: transparent;
307
+ color: var(--dsw-alias-label-tertiary);
308
+ }
309
+ .dsh-goal-btn.icon-only:hover {
265
310
  color: var(--dsw-alias-label-primary);
266
- background: var(--dsw-alias-bg-layer-3);
311
+ background: var(--dsw-alias-bg-layer-2);
312
+ border-color: var(--dsw-alias-border-l2);
267
313
  }
268
314
  .dsh-goal-btn.close {
269
315
  font-size: 13px;
@@ -271,10 +317,23 @@ window.__ModuleLoader__.load({
271
317
  padding: 3px 6px;
272
318
  }
273
319
  .dsh-goal-btn.close:hover {
274
- color: var(--dsw-alias-status-danger);
320
+ color: var(--dsw-alias-state-error-primary);
321
+ }
322
+ .dsh-goal-btn-primary {
323
+ background: var(--dsw-alias-label-primary);
324
+ color: var(--dsw-alias-bg-layer-3);
325
+ border-color: transparent;
326
+ }
327
+ .dsh-goal-btn-primary:hover:not(:disabled) {
328
+ opacity: 0.88;
329
+ }
330
+ .dsh-goal-btn-danger {
331
+ color: var(--dsw-alias-state-error-primary);
332
+ border-color: rgba(239, 68, 68, 0.3);
275
333
  }
276
- .dsh-goal-btn.danger:hover {
277
- color: var(--dsw-alias-status-danger);
334
+ .dsh-goal-btn-danger:hover:not(:disabled) {
335
+ background: rgba(239, 68, 68, 0.12) !important;
336
+ border-color: rgba(239, 68, 68, 0.5);
278
337
  }
279
338
 
280
339
  /* Modal Details */
@@ -292,7 +351,7 @@ window.__ModuleLoader__.load({
292
351
  background: var(--dsw-alias-bg-layer-3);
293
352
  border: 1px solid var(--dsw-alias-border-l2);
294
353
  border-radius: 12px;
295
- width: 520px;
354
+ width: 560px;
296
355
  max-width: 90vw;
297
356
  max-height: 80vh;
298
357
  display: flex;
@@ -300,48 +359,81 @@ window.__ModuleLoader__.load({
300
359
  box-shadow: 0 16px 36px rgba(0, 0, 0, 0.4);
301
360
  }
302
361
  .dsh-goal-modal-head {
303
- padding: 14px 18px;
362
+ padding: 16px 20px;
304
363
  border-bottom: 1px solid var(--dsw-alias-border-l2);
305
364
  display: flex;
306
365
  align-items: center;
307
366
  justify-content: space-between;
308
367
  }
309
368
  .dsh-goal-modal-title {
310
- font-size: 15px;
369
+ font-size: 16px;
311
370
  font-weight: 600;
312
371
  color: var(--dsw-alias-label-primary);
372
+ display: flex;
373
+ align-items: center;
374
+ gap: 8px;
313
375
  }
314
376
  .dsh-goal-modal-body {
315
- padding: 16px 18px;
377
+ padding: 18px 20px;
316
378
  overflow-y: auto;
317
379
  display: flex;
318
380
  flex-direction: column;
319
- gap: 12px;
381
+ gap: 14px;
382
+ }
383
+ .dsh-goal-stat-grid {
384
+ display: grid;
385
+ grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
386
+ gap: 10px;
387
+ }
388
+ .dsh-goal-stat-box {
389
+ padding: 10px 12px;
390
+ border: 1px solid var(--dsw-alias-border-l2);
391
+ border-radius: 8px;
392
+ background: var(--dsw-alias-bg-layer-2);
393
+ display: flex;
394
+ flex-direction: column;
395
+ gap: 2px;
396
+ }
397
+ .dsh-goal-stat-val {
398
+ font-size: 16px;
399
+ font-weight: 700;
400
+ color: var(--dsw-alias-label-primary);
401
+ }
402
+ .dsh-goal-stat-lbl {
403
+ font-size: 11px;
404
+ color: var(--dsw-alias-label-secondary);
320
405
  }
321
406
  .dsh-goal-milestone-item {
322
407
  display: flex;
323
408
  align-items: flex-start;
324
409
  gap: 10px;
325
- padding: 8px 10px;
410
+ padding: 10px 12px;
326
411
  border-radius: 8px;
327
412
  background: var(--dsw-alias-bg-layer-2);
413
+ border: 1px solid var(--dsw-alias-border-l2);
328
414
  font-size: 13px;
415
+ transition: all 0.15s ease;
329
416
  }
330
417
  .dsh-goal-modal-foot {
331
- padding: 12px 18px;
418
+ padding: 14px 20px;
332
419
  border-top: 1px solid var(--dsw-alias-border-l2);
333
420
  display: flex;
334
421
  justify-content: flex-end;
335
- gap: 8px;
422
+ align-items: center;
423
+ gap: 10px;
336
424
  }
337
425
 
338
- /* Settings Card */
426
+ /* Settings Card (clinebot pattern) */
339
427
  .dsh-goal-card {
340
428
  border: 1px solid var(--dsw-alias-border-l2);
341
429
  background: var(--dsw-alias-bg-layer-3);
342
430
  border-radius: 12px;
343
431
  list-style: none;
344
- margin-bottom: 10px;
432
+ margin-bottom: 12px;
433
+ transition: border-color 0.15s ease;
434
+ }
435
+ .dsh-goal-card:hover {
436
+ border-color: var(--dsw-alias-label-tertiary);
345
437
  }
346
438
  .dsh-goal-card-head {
347
439
  appearance: none;
@@ -350,13 +442,13 @@ window.__ModuleLoader__.load({
350
442
  color: inherit;
351
443
  text-align: left;
352
444
  cursor: pointer;
353
- background: 0 0;
445
+ background: transparent;
354
446
  border: 0;
355
447
  border-radius: 12px;
356
448
  display: flex;
357
449
  align-items: center;
358
450
  gap: 12px;
359
- padding: 14px 16px;
451
+ padding: 16px 20px;
360
452
  }
361
453
  .dsh-goal-card-title {
362
454
  color: var(--dsw-alias-label-primary);
@@ -370,14 +462,16 @@ window.__ModuleLoader__.load({
370
462
  }
371
463
  .dsh-goal-card-body {
372
464
  border-top: 1px solid var(--dsw-alias-border-l2);
373
- margin: 0 16px;
374
- padding-bottom: 12px;
465
+ margin: 0 20px;
466
+ padding: 16px 0 16px;
467
+ display: flex;
468
+ flex-direction: column;
469
+ gap: 14px;
375
470
  }
376
471
  .dsh-goal-card-field {
377
472
  display: flex;
378
473
  flex-direction: column;
379
474
  gap: 6px;
380
- padding: 12px 0;
381
475
  }
382
476
  .dsh-goal-field-label-row {
383
477
  display: flex;
@@ -385,6 +479,7 @@ window.__ModuleLoader__.load({
385
479
  justify-content: space-between;
386
480
  font-size: 13px;
387
481
  font-weight: 500;
482
+ color: var(--dsw-alias-label-primary);
388
483
  }
389
484
  .dsh-goal-field-meta {
390
485
  display: flex;
@@ -395,28 +490,35 @@ window.__ModuleLoader__.load({
395
490
  }
396
491
  .dsh-goal-override-tag {
397
492
  font-size: 11px;
398
- padding: 1px 6px;
493
+ padding: 2px 6px;
399
494
  border-radius: 4px;
400
495
  background: var(--dsw-alias-bg-layer-2);
401
496
  color: var(--dsw-alias-label-secondary);
497
+ border: 1px solid var(--dsw-alias-border-l2);
402
498
  }
403
499
  .dsh-goal-card-input {
404
- height: 34px;
500
+ height: 36px;
405
501
  border: 1px solid var(--dsw-alias-border-l2);
406
- background: var(--dsw-alias-bg-layer-3);
502
+ background: var(--dsw-alias-bg-layer-2);
407
503
  color: var(--dsw-alias-label-primary);
408
504
  border-radius: 8px;
409
505
  padding: 0 12px;
410
506
  font-size: 13px;
507
+ width: 100%;
508
+ box-sizing: border-box;
509
+ }
510
+ .dsh-goal-card-input:focus {
511
+ outline: none;
512
+ border-color: var(--dsw-alias-state-brand-primary);
411
513
  }
412
514
  .dsh-goal-card-input[aria-invalid="true"] {
413
- border-color: var(--dsw-alias-status-danger);
515
+ border-color: var(--dsw-alias-state-error-primary);
414
516
  }
415
517
  .dsh-goal-chev {
416
518
  margin-left: auto;
417
519
  flex: none;
418
520
  color: var(--dsw-alias-label-tertiary);
419
- transition: transform .16s;
521
+ transition: transform .16s ease;
420
522
  }
421
523
  .dsh-goal-chev-open {
422
524
  transform: rotate(180deg);
@@ -425,7 +527,7 @@ window.__ModuleLoader__.load({
425
527
  display: flex;
426
528
  align-items: center;
427
529
  justify-content: space-between;
428
- padding: 8px 0;
530
+ padding: 6px 0;
429
531
  }
430
532
  .dsh-goal-check {
431
533
  display: flex;
@@ -440,11 +542,11 @@ window.__ModuleLoader__.load({
440
542
  display: flex;
441
543
  justify-content: flex-end;
442
544
  align-items: center;
443
- gap: 8px;
444
- padding: 12px 0 4px;
545
+ gap: 10px;
546
+ padding: 14px 0 4px;
445
547
  }
446
548
  .dsh-goal-foot-note {
447
- color: var(--dsw-alias-status-danger);
549
+ color: var(--dsw-alias-state-error-primary);
448
550
  font-size: 12px;
449
551
  margin-right: auto;
450
552
  }
@@ -452,7 +554,7 @@ window.__ModuleLoader__.load({
452
554
  background: transparent;
453
555
  border: 0;
454
556
  cursor: pointer;
455
- padding: 2px 4px;
557
+ padding: 2px 6px;
456
558
  display: flex;
457
559
  align-items: center;
458
560
  gap: 4px;
@@ -470,19 +572,19 @@ window.__ModuleLoader__.load({
470
572
  cursor: pointer;
471
573
  border: 1px solid var(--dsw-alias-border-l2);
472
574
  border-radius: 8px;
473
- padding: 5px 14px;
575
+ padding: 6px 14px;
474
576
  font-size: 13px;
475
- background: transparent;
476
- color: var(--dsw-alias-label-secondary);
577
+ background: var(--dsw-alias-bg-layer-2);
578
+ color: var(--dsw-alias-label-primary);
477
579
  transition: all 0.15s ease;
478
580
  }
479
581
  .dsh-goal-discard:hover:not(:disabled) {
480
- color: var(--dsw-alias-label-primary);
481
- background: var(--dsw-alias-bg-layer-2);
582
+ background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));
583
+ border-color: var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2));
482
584
  }
483
585
  .dsh-goal-discard:disabled {
484
586
  opacity: 0.4;
485
- cursor: default;
587
+ cursor: not-allowed;
486
588
  }
487
589
  .dsh-goal-save {
488
590
  appearance: none;
@@ -490,16 +592,19 @@ window.__ModuleLoader__.load({
490
592
  cursor: pointer;
491
593
  border: 1px solid transparent;
492
594
  border-radius: 8px;
493
- padding: 5px 14px;
595
+ padding: 6px 16px;
494
596
  font-size: 13px;
495
597
  background: var(--dsw-alias-label-primary);
496
598
  color: var(--dsw-alias-bg-layer-3);
497
599
  font-weight: 500;
498
600
  transition: opacity 0.15s ease;
499
601
  }
602
+ .dsh-goal-save:hover:not(:disabled) {
603
+ opacity: 0.88;
604
+ }
500
605
  .dsh-goal-save:disabled {
501
606
  opacity: 0.4;
502
- cursor: default;
607
+ cursor: not-allowed;
503
608
  }
504
609
  `;
505
610
 
@@ -508,10 +613,51 @@ window.__ModuleLoader__.load({
508
613
  if (document.getElementById('dsh-goal-styles')) return;
509
614
  const style = document.createElement('style');
510
615
  style.id = 'dsh-goal-styles';
616
+ style.dataset.dshPlugin = NS;
511
617
  style.textContent = CSS_STYLES;
512
618
  document.head.appendChild(style);
513
619
  }
514
620
 
621
+
622
+ // Синтез приятных звуковых колокольчиков через Web Audio API (Item 6)
623
+ function playGoalChime(isSuccess = true) {
624
+ if (typeof window === 'undefined') return;
625
+ try {
626
+ const AudioCtx = window.AudioContext || window.webkitAudioContext;
627
+ if (!AudioCtx) return;
628
+ const actx = new AudioCtx();
629
+ if (actx.state === 'suspended') {
630
+ actx.resume().catch(() => {});
631
+ }
632
+ const now = actx.currentTime;
633
+ // Пентатонические аккорды: успех — восходящий C5-E5-G5-C6; сбой/ошибка — нисходящий G4-Eb4-C4
634
+ const notes = isSuccess
635
+ ? [523.25, 659.25, 783.99, 1046.50]
636
+ : [392.00, 311.13, 261.63];
637
+
638
+ notes.forEach((freq, idx) => {
639
+ const osc = actx.createOscillator();
640
+ const gain = actx.createGain();
641
+ osc.type = 'sine';
642
+ osc.frequency.setValueAtTime(freq, now + idx * 0.12);
643
+
644
+ gain.gain.setValueAtTime(0.001, now + idx * 0.12);
645
+ gain.gain.exponentialRampToValueAtTime(0.15, now + idx * 0.12 + 0.02);
646
+ gain.gain.exponentialRampToValueAtTime(0.0001, now + idx * 0.12 + 0.4);
647
+
648
+ osc.connect(gain);
649
+ gain.connect(actx.destination);
650
+
651
+ osc.start(now + idx * 0.12);
652
+ osc.stop(now + idx * 0.12 + 0.45);
653
+ });
654
+
655
+ setTimeout(() => {
656
+ try { actx.close(); } catch (_) {}
657
+ }, 2000);
658
+ } catch (_) {}
659
+ }
660
+
515
661
  // --- МОДАЛЬНОЕ ОКНО ДЕТАЛЕЙ (MODAL DETAILS) ---
516
662
  function GoalDetailsModal({ state, onClose, onAction, t }) {
517
663
  if (!state) return null;
@@ -521,31 +667,31 @@ window.__ModuleLoader__.load({
521
667
 
522
668
  return React.createElement(
523
669
  'div',
524
- {
525
- className: 'dsh-goal-modal-overlay',
526
- onClick: (e) => {
527
- if (e.target === e.currentTarget) onClose();
528
- },
529
- },
670
+ { className: 'dsh-goal-modal-overlay', onClick: onClose },
530
671
  React.createElement(
531
672
  'div',
532
- { className: 'dsh-goal-modal' },
673
+ {
674
+ className: 'dsh-goal-modal',
675
+ onClick: (e) => e.stopPropagation(),
676
+ },
533
677
  React.createElement(
534
678
  'div',
535
679
  { className: 'dsh-goal-modal-head' },
536
680
  React.createElement(
537
681
  'div',
538
- { style: { display: 'flex', alignItems: 'center', gap: 8 } },
539
- isCompleted ? React.createElement(IconCheck, { size: 18 }) : React.createElement(IconTarget, { size: 18 }),
540
- React.createElement(
541
- 'span',
542
- { className: 'dsh-goal-modal-title' },
543
- isCompleted ? (t('modalTitleCompleted') || 'Цель выполнена: план и результаты') : (t('modalTitle') || 'План и статус цели'),
544
- ),
682
+ { className: 'dsh-goal-modal-title' },
683
+ isCompleted
684
+ ? React.createElement(IconCheck, { size: 18, className: 'cb-badge-ok' })
685
+ : React.createElement(IconTarget, { size: 18 }),
686
+ isCompleted ? (t('modalTitleCompleted') || 'Цель выполнена: план и результаты') : (t('modalTitle') || 'План и статус цели'),
545
687
  ),
546
688
  React.createElement(
547
689
  'button',
548
- { className: 'dsh-goal-btn', onClick: onClose, title: t('close') || 'Закрыть' },
690
+ {
691
+ className: 'dsh-goal-btn icon-only close',
692
+ onClick: onClose,
693
+ title: t('close') || 'Закрыть',
694
+ },
549
695
  '✕',
550
696
  ),
551
697
  ),
@@ -555,25 +701,42 @@ window.__ModuleLoader__.load({
555
701
  React.createElement(
556
702
  'div',
557
703
  null,
558
- React.createElement('div', { style: { fontWeight: 600, fontSize: 15, marginBottom: 4 } }, state.title),
559
- state.description ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 13, marginBottom: 4 } }, state.description) : null,
704
+ React.createElement('div', { style: { fontWeight: 600, fontSize: 16, marginBottom: 4, color: 'var(--dsw-alias-label-primary)' } }, state.title),
705
+ state.description ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 13, marginBottom: 6, lineHeight: 1.4 } }, state.description) : null,
560
706
  ),
561
707
  React.createElement(
562
708
  'div',
563
- { style: { display: 'flex', justifyContent: 'flex-end', fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
564
- React.createElement('span', null, `${t('time') || 'Время'}: ${state.formattedElapsed || '0s'}`),
709
+ { className: 'dsh-goal-stat-grid' },
710
+ React.createElement(
711
+ 'div',
712
+ { className: 'dsh-goal-stat-box' },
713
+ React.createElement('span', { className: 'dsh-goal-stat-val' }, state.formattedElapsed || '0s'),
714
+ React.createElement('span', { className: 'dsh-goal-stat-lbl' }, t('time') || 'Время работы'),
715
+ ),
716
+ React.createElement(
717
+ 'div',
718
+ { className: 'dsh-goal-stat-box' },
719
+ React.createElement('span', { className: 'dsh-goal-stat-val' }, `${state.iterationsCount || 0}/${state.maxIterations || 25}`),
720
+ React.createElement('span', { className: 'dsh-goal-stat-lbl' }, 'Итерации'),
721
+ ),
722
+ React.createElement(
723
+ 'div',
724
+ { className: 'dsh-goal-stat-box' },
725
+ React.createElement('span', { className: 'dsh-goal-stat-val' }, `${state.progressPercent || 0}%`),
726
+ React.createElement('span', { className: 'dsh-goal-stat-lbl' }, 'Прогресс'),
727
+ ),
565
728
  ),
566
729
  React.createElement(
567
730
  'div',
568
731
  { style: { display: 'flex', flexDirection: 'column', gap: 8, marginTop: 4 } },
569
- React.createElement('div', { style: { fontWeight: 600, fontSize: 13 } }, t('milestones') || 'План работ:'),
732
+ React.createElement('div', { style: { fontWeight: 600, fontSize: 13, color: 'var(--dsw-alias-label-primary)' } }, t('milestones') || 'План работ:'),
570
733
  milestones.length === 0
571
734
  ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12, padding: '8px 0' } }, t('noMilestones') || 'Агент формирует план работ...')
572
735
  : milestones.map((m, i) =>
573
736
  React.createElement(
574
737
  'div',
575
738
  { key: m.id || i, className: 'dsh-goal-milestone-item' },
576
- React.createElement('span', { style: { fontSize: 14 } }, m.status === 'completed' ? '✅' : m.status === 'in_progress' ? '🔄' : '⏳'),
739
+ React.createElement('span', { style: { fontSize: 15 } }, m.status === 'completed' ? '✅' : m.status === 'in_progress' ? '🔄' : '⏳'),
577
740
  React.createElement(
578
741
  'div',
579
742
  { style: { flex: 1, minWidth: 0 } },
@@ -601,12 +764,11 @@ window.__ModuleLoader__.load({
601
764
  ? React.createElement(
602
765
  'button',
603
766
  {
604
- className: 'dsh-goal-btn danger',
767
+ className: 'dsh-goal-btn dsh-goal-btn-danger',
605
768
  onClick: () => {
606
769
  onAction('clear');
607
770
  onClose();
608
771
  },
609
- style: { padding: '6px 12px', fontSize: 13 },
610
772
  },
611
773
  t('closeBanner') || 'Закрыть плашку цели',
612
774
  )
@@ -614,9 +776,8 @@ window.__ModuleLoader__.load({
614
776
  React.createElement(
615
777
  'button',
616
778
  {
617
- className: 'dsh-goal-save',
779
+ className: 'dsh-goal-btn dsh-goal-btn-primary',
618
780
  onClick: onClose,
619
- style: { padding: '6px 16px' },
620
781
  },
621
782
  t('close') || 'Закрыть',
622
783
  ),
@@ -625,7 +786,7 @@ window.__ModuleLoader__.load({
625
786
  );
626
787
  }
627
788
 
628
- function sessionIdOf(ctx, props) {
789
+ function sessionIdOf(ctx, props) {
629
790
  try {
630
791
  if (props?.session?.id) return String(props.session.id);
631
792
  if (props?.session?.sessionId) return String(props.session.sessionId);
@@ -651,6 +812,7 @@ window.__ModuleLoader__.load({
651
812
  const [now, setNow] = useState(() => Date.now());
652
813
  const stateRef = useRef(null);
653
814
  stateRef.current = state;
815
+ const lastStateRef = useRef(null);
654
816
 
655
817
  const t = (key) => {
656
818
  if (ctx?.locale?.getSnapshot) {
@@ -681,18 +843,37 @@ window.__ModuleLoader__.load({
681
843
  });
682
844
  if (res.ok) {
683
845
  const data = await res.json();
684
- setState(data);
846
+ updateStateWithAudio(data);
847
+ return data;
685
848
  }
686
849
  } catch (_) {}
687
850
  }, [sid]);
688
851
 
689
- // Адаптивный поллинг: 1.5s когда RUNNING и вкладка активна, 8s когда IDLE/PAUSED/COMPLETED или вкладка скрыта
852
+ // Звуковое оповещение при переходе в COMPLETED или FAILED
853
+ const updateStateWithAudio = useCallback((nextState) => {
854
+ if (nextState && lastStateRef.current) {
855
+ const prev = lastStateRef.current;
856
+ if (prev.state !== nextState.state && nextState.enableSound !== false) {
857
+ if (nextState.state === 'COMPLETED') {
858
+ playGoalChime(true);
859
+ } else if (nextState.state === 'FAILED') {
860
+ playGoalChime(false);
861
+ }
862
+ }
863
+ }
864
+ lastStateRef.current = nextState;
865
+ setState(nextState);
866
+ }, []);
867
+
868
+ // Realtime Server-Sent Events (SSE) с автоматическим адаптивным fallback-поллингом
690
869
  useEffect(() => {
870
+ let es = null;
691
871
  let timer = null;
692
872
  let isDisposed = false;
873
+ let sseActive = false;
693
874
 
694
875
  const scheduleNextPoll = () => {
695
- if (isDisposed) return;
876
+ if (isDisposed || sseActive) return;
696
877
  const currentState = stateRef.current;
697
878
  const isDocHidden = typeof document !== 'undefined' && document.hidden;
698
879
  const isRunning = currentState?.hasActiveGoal && currentState?.state === 'RUNNING';
@@ -710,16 +891,58 @@ window.__ModuleLoader__.load({
710
891
  }, delay);
711
892
  };
712
893
 
713
- fetchState().then(() => {
894
+ // Запуск SSE соединения
895
+ if (typeof window !== 'undefined' && typeof window.EventSource !== 'undefined') {
896
+ try {
897
+ const query = sid && sid !== 'default' ? `?sessionId=${encodeURIComponent(sid)}` : '';
898
+ es = new window.EventSource(`/dsh-goal/events${query}`);
899
+
900
+ es.onopen = () => {
901
+ sseActive = true;
902
+ if (timer) {
903
+ clearTimeout(timer);
904
+ timer = null;
905
+ }
906
+ };
907
+
908
+ es.onmessage = (event) => {
909
+ if (isDisposed) return;
910
+ try {
911
+ const data = JSON.parse(event.data);
912
+ if (data && typeof data === 'object') {
913
+ updateStateWithAudio(data);
914
+ }
915
+ } catch (_) {}
916
+ };
917
+
918
+ es.onerror = () => {
919
+ // При сбое соединения SSE переключаемся на адаптивный fallback поллинг
920
+ sseActive = false;
921
+ if (!isDisposed && !timer) {
922
+ scheduleNextPoll();
923
+ }
924
+ };
925
+ } catch (_) {
926
+ sseActive = false;
927
+ scheduleNextPoll();
928
+ }
929
+ } else {
714
930
  scheduleNextPoll();
931
+ }
932
+
933
+ // Первоначальная загрузка состояния
934
+ fetchState().then((data) => {
935
+ if (!sseActive) scheduleNextPoll();
715
936
  });
716
937
 
717
938
  const onVisibilityChange = () => {
718
939
  if (typeof document !== 'undefined' && !document.hidden) {
719
- if (timer) clearTimeout(timer);
720
- fetchState().then(() => {
721
- scheduleNextPoll();
722
- });
940
+ if (!sseActive) {
941
+ if (timer) clearTimeout(timer);
942
+ fetchState().then(() => {
943
+ scheduleNextPoll();
944
+ });
945
+ }
723
946
  }
724
947
  };
725
948
 
@@ -729,12 +952,16 @@ window.__ModuleLoader__.load({
729
952
 
730
953
  return () => {
731
954
  isDisposed = true;
955
+ if (es) {
956
+ es.close();
957
+ es = null;
958
+ }
732
959
  if (timer) clearTimeout(timer);
733
960
  if (typeof document !== 'undefined' && document.removeEventListener) {
734
961
  document.removeEventListener('visibilitychange', onVisibilityChange);
735
962
  }
736
963
  };
737
- }, [fetchState]);
964
+ }, [sid, fetchState, updateStateWithAudio]);
738
965
 
739
966
  const handleAction = async (action, extra = {}) => {
740
967
  try {
@@ -748,7 +975,7 @@ window.__ModuleLoader__.load({
748
975
  });
749
976
  if (res.ok) {
750
977
  const data = await res.json();
751
- if (data.state) setState(data.state);
978
+ if (data.state) updateStateWithAudio(data.state);
752
979
  }
753
980
  } catch (_) {}
754
981
  };
@@ -789,8 +1016,10 @@ window.__ModuleLoader__.load({
789
1016
  ),
790
1017
  React.createElement(
791
1018
  'span',
792
- { className: `dsh-goal-label${isCompleted ? ' completed' : ''}` },
793
- isCompleted ? (t('goalCompleted') || 'Цель выполнена') : (t('goalLabel') || 'Текущая цель'),
1019
+ {
1020
+ className: `dsh-goal-badge ${isCompleted ? 'dsh-goal-badge-ok' : isPaused ? 'dsh-goal-badge-warn' : ''}`,
1021
+ },
1022
+ isCompleted ? (t('goalCompleted') || 'Цель выполнена') : isPaused ? (t('pause') || 'На паузе') : (t('goalLabel') || 'Текущая цель'),
794
1023
  ),
795
1024
  React.createElement('span', { className: 'dsh-goal-title', title: state.title }, state.title),
796
1025
  ),
@@ -806,7 +1035,7 @@ window.__ModuleLoader__.load({
806
1035
  ? React.createElement(
807
1036
  'button',
808
1037
  {
809
- className: 'dsh-goal-btn',
1038
+ className: 'dsh-goal-btn icon-only',
810
1039
  title: isPaused ? (t('resume') || 'Возобновить') : (t('pause') || 'Приостановить'),
811
1040
  onClick: () => handleAction(isPaused ? 'resume' : 'pause'),
812
1041
  },
@@ -816,7 +1045,7 @@ window.__ModuleLoader__.load({
816
1045
  React.createElement(
817
1046
  'button',
818
1047
  {
819
- className: 'dsh-goal-btn',
1048
+ className: 'dsh-goal-btn icon-only',
820
1049
  title: t('details') || 'Развернуть детали цели',
821
1050
  onClick: () => setIsModalOpen(true),
822
1051
  },
@@ -826,7 +1055,7 @@ window.__ModuleLoader__.load({
826
1055
  ? React.createElement(
827
1056
  'button',
828
1057
  {
829
- className: 'dsh-goal-btn close',
1058
+ className: 'dsh-goal-btn icon-only close',
830
1059
  title: t('closeBanner') || 'Закрыть плашку цели',
831
1060
  onClick: () => handleAction('clear'),
832
1061
  },
@@ -848,12 +1077,6 @@ window.__ModuleLoader__.load({
848
1077
  }
849
1078
 
850
1079
  // --- ЛОГИКА ФОРМЫ НАСТРОЕК ---
851
- // Архитектурное решение (Issue #23):
852
- // Чистая логика состояния формы настроек вынесена в `lib/card-form-state.js` для запуска
853
- // быстрых изолированных unit-тестов через `node --test`. В `lib/client.js` эти функции
854
- // продублированы встроенным образом, поскольку клиентский бандл загружается браузерным
855
- // загрузчиком DSH (`window.__ModuleLoader__`) как самодостаточный автономный скрипт
856
- // без этапа сборки (bundler / webpack) и не может динамически импортировать локальные файлы.
857
1080
  const DEFAULT_SETTINGS = {
858
1081
  maxIterations: 25,
859
1082
  autoDrive: true,
@@ -899,7 +1122,7 @@ window.__ModuleLoader__.load({
899
1122
  }
900
1123
  } else {
901
1124
  if (draftValue !== undefined) {
902
- isDirty = draftValue !== (userVal !== undefined ? userVal : baseVal);
1125
+ isDirty = Boolean(draftValue) !== (userVal !== undefined ? Boolean(userVal) : Boolean(baseVal));
903
1126
  }
904
1127
  }
905
1128
 
@@ -916,8 +1139,8 @@ window.__ModuleLoader__.load({
916
1139
  function computeSavePlan(draft, snap) {
917
1140
  if (!draft) return [];
918
1141
 
919
- const writes = [];
920
1142
  const fields = ['maxIterations', 'autoDrive', 'enableSound'];
1143
+ const writes = [];
921
1144
 
922
1145
  for (const f of fields) {
923
1146
  if (draft[f] === undefined) continue;
@@ -1066,124 +1289,124 @@ window.__ModuleLoader__.load({
1066
1289
  React.createElement(
1067
1290
  'div',
1068
1291
  { className: 'dsh-goal-card-field' },
1069
- React.createElement(
1070
- 'div',
1071
- { className: 'dsh-goal-field-label-row' },
1072
- React.createElement('label', { htmlFor: 'dsh-goal-max-iter' }, t('maxIterLabel') || 'Максимум итераций (Safety Limit):'),
1073
- React.createElement(
1074
- 'div',
1075
- { className: 'dsh-goal-field-meta' },
1076
- maxIterStatus.isOverridden
1077
- ? React.createElement('span', { className: 'dsh-goal-override-tag' }, t('overridden') || 'изменено')
1078
- : null,
1079
- maxIterStatus.isOverridden
1080
- ? React.createElement(
1292
+ React.createElement(
1293
+ 'div',
1294
+ { className: 'dsh-goal-field-label-row' },
1295
+ React.createElement('label', { htmlFor: 'dsh-goal-max-iter' }, t('maxIterLabel') || 'Максимум итераций (Safety Limit):'),
1296
+ React.createElement(
1297
+ 'div',
1298
+ { className: 'dsh-goal-field-meta' },
1299
+ maxIterStatus.isOverridden
1300
+ ? React.createElement('span', { className: 'dsh-goal-override-tag' }, t('overridden') || 'изменено')
1301
+ : null,
1302
+ maxIterStatus.isOverridden
1303
+ ? React.createElement(
1304
+ 'button',
1305
+ {
1306
+ type: 'button',
1307
+ className: 'dsh-goal-btn-inline-reset',
1308
+ title: t('resetField') || 'Сбросить к значению по умолчанию',
1309
+ onClick: () => resetFieldToDefault('maxIterations'),
1310
+ },
1311
+ React.createElement(IconRotateCcw, { size: 12 }),
1312
+ t('resetField') || 'По умолчанию',
1313
+ )
1314
+ : null,
1315
+ ),
1316
+ ),
1317
+ React.createElement('input', {
1318
+ id: 'dsh-goal-max-iter',
1319
+ type: 'number',
1320
+ min: 1,
1321
+ placeholder: String(maxIterStatus.baseValue),
1322
+ className: 'dsh-goal-card-input',
1323
+ value: maxIterStatus.value !== undefined ? maxIterStatus.value : '',
1324
+ disabled,
1325
+ 'aria-invalid': invalid,
1326
+ onChange: (e) => edit('maxIterations', e.target.value),
1327
+ }),
1328
+ ),
1329
+ React.createElement(
1330
+ 'div',
1331
+ { className: 'dsh-goal-check-row' },
1332
+ React.createElement(
1333
+ 'label',
1334
+ { className: 'dsh-goal-check' },
1335
+ React.createElement('input', {
1336
+ type: 'checkbox',
1337
+ checked: autoDriveStatus.value === true,
1338
+ disabled,
1339
+ onChange: (e) => edit('autoDrive', e.target.checked),
1340
+ }),
1341
+ t('autoDriveLabel') || 'Авто-драйв: продолжать цикл автоматически',
1342
+ ),
1343
+ autoDriveStatus.isOverridden
1344
+ ? React.createElement(
1345
+ 'button',
1346
+ {
1347
+ type: 'button',
1348
+ className: 'dsh-goal-btn-inline-reset',
1349
+ title: t('resetField') || 'Сбросить к значению по умолчанию',
1350
+ onClick: () => resetFieldToDefault('autoDrive'),
1351
+ },
1352
+ React.createElement(IconRotateCcw, { size: 12 }),
1353
+ )
1354
+ : null,
1355
+ ),
1356
+ React.createElement(
1357
+ 'div',
1358
+ { className: 'dsh-goal-check-row' },
1359
+ React.createElement(
1360
+ 'label',
1361
+ { className: 'dsh-goal-check' },
1362
+ React.createElement('input', {
1363
+ type: 'checkbox',
1364
+ checked: enableSoundStatus.value === true,
1365
+ disabled,
1366
+ onChange: (e) => edit('enableSound', e.target.checked),
1367
+ }),
1368
+ t('soundLabel') || 'Звук по завершении цели',
1369
+ ),
1370
+ enableSoundStatus.isOverridden
1371
+ ? React.createElement(
1372
+ 'button',
1373
+ {
1374
+ type: 'button',
1375
+ className: 'dsh-goal-btn-inline-reset',
1376
+ title: t('resetField') || 'Сбросить к значению по умолчанию',
1377
+ onClick: () => resetFieldToDefault('enableSound'),
1378
+ },
1379
+ React.createElement(IconRotateCcw, { size: 12 }),
1380
+ )
1381
+ : null,
1382
+ ),
1383
+ React.createElement(
1384
+ 'div',
1385
+ { className: 'dsh-goal-foot' },
1386
+ failed
1387
+ ? React.createElement('span', { className: 'dsh-goal-foot-note' }, t('saveFailed') || 'Не сохранено — исправьте и повторите')
1388
+ : null,
1389
+ React.createElement(
1081
1390
  'button',
1082
1391
  {
1083
- type: 'button',
1084
- className: 'dsh-goal-btn-inline-reset',
1085
- title: t('resetField') || 'Сбросить к значению по умолчанию',
1086
- onClick: () => resetFieldToDefault('maxIterations'),
1392
+ className: 'dsh-goal-discard',
1393
+ disabled: !dirty || disabled,
1394
+ onClick: () => {
1395
+ setFailed(false);
1396
+ setDraft(null);
1397
+ },
1087
1398
  },
1088
- React.createElement(IconRotateCcw, { size: 12 }),
1089
- t('resetField') || 'По умолчанию',
1090
- )
1091
- : null,
1092
- ),
1093
- ),
1094
- React.createElement('input', {
1095
- id: 'dsh-goal-max-iter',
1096
- type: 'number',
1097
- min: 1,
1098
- placeholder: String(maxIterStatus.baseValue),
1099
- className: 'dsh-goal-card-input',
1100
- value: maxIterStatus.value !== undefined ? maxIterStatus.value : '',
1101
- disabled,
1102
- 'aria-invalid': invalid,
1103
- onChange: (e) => edit('maxIterations', e.target.value),
1104
- }),
1105
- ),
1106
- React.createElement(
1107
- 'div',
1108
- { className: 'dsh-goal-check-row' },
1109
- React.createElement(
1110
- 'label',
1111
- { className: 'dsh-goal-check' },
1112
- React.createElement('input', {
1113
- type: 'checkbox',
1114
- checked: autoDriveStatus.value === true,
1115
- disabled,
1116
- onChange: (e) => edit('autoDrive', e.target.checked),
1117
- }),
1118
- t('autoDriveLabel') || 'Авто-драйв: продолжать цикл автоматически',
1119
- ),
1120
- autoDriveStatus.isOverridden
1121
- ? React.createElement(
1122
- 'button',
1123
- {
1124
- type: 'button',
1125
- className: 'dsh-goal-btn-inline-reset',
1126
- title: t('resetField') || 'Сбросить к значению по умолчанию',
1127
- onClick: () => resetFieldToDefault('autoDrive'),
1128
- },
1129
- React.createElement(IconRotateCcw, { size: 12 }),
1130
- )
1131
- : null,
1132
- ),
1133
- React.createElement(
1134
- 'div',
1135
- { className: 'dsh-goal-check-row' },
1136
- React.createElement(
1137
- 'label',
1138
- { className: 'dsh-goal-check' },
1139
- React.createElement('input', {
1140
- type: 'checkbox',
1141
- checked: enableSoundStatus.value === true,
1142
- disabled,
1143
- onChange: (e) => edit('enableSound', e.target.checked),
1144
- }),
1145
- t('soundLabel') || 'Звук по завершении цели',
1146
- ),
1147
- enableSoundStatus.isOverridden
1148
- ? React.createElement(
1149
- 'button',
1150
- {
1151
- type: 'button',
1152
- className: 'dsh-goal-btn-inline-reset',
1153
- title: t('resetField') || 'Сбросить к значению по умолчанию',
1154
- onClick: () => resetFieldToDefault('enableSound'),
1155
- },
1156
- React.createElement(IconRotateCcw, { size: 12 }),
1157
- )
1158
- : null,
1159
- ),
1160
- React.createElement(
1161
- 'div',
1162
- { className: 'dsh-goal-foot' },
1163
- failed
1164
- ? React.createElement('span', { className: 'dsh-goal-foot-note' }, t('saveFailed') || 'Не сохранено — исправьте и повторите')
1165
- : null,
1166
- React.createElement(
1167
- 'button',
1168
- {
1169
- className: 'dsh-goal-discard',
1170
- disabled: !dirty || disabled,
1171
- onClick: () => {
1172
- setFailed(false);
1173
- setDraft(null);
1174
- },
1175
- },
1176
- t('discard') || 'Отменить правки',
1177
- ),
1178
- React.createElement(
1179
- 'button',
1180
- { className: 'dsh-goal-save', disabled: !dirty || disabled || invalid, onClick: save },
1181
- saving ? t('saving') || 'Сохранение…' : t('save') || 'Сохранить',
1182
- ),
1183
- ),
1399
+ t('discard') || 'Отменить правки',
1400
+ ),
1401
+ React.createElement(
1402
+ 'button',
1403
+ { className: 'dsh-goal-save', disabled: !dirty || disabled || invalid, onClick: save },
1404
+ saving ? t('saving') || 'Сохранение…' : t('save') || 'Сохранить',
1405
+ ),
1406
+ ),
1407
+ ),
1184
1408
  )
1185
- )
1186
- : null,
1409
+ : null,
1187
1410
  );
1188
1411
  }
1189
1412
 
@@ -1199,7 +1422,7 @@ window.__ModuleLoader__.load({
1199
1422
  details: 'Развернуть детали цели',
1200
1423
  modalTitle: 'План и статус цели',
1201
1424
  modalTitleCompleted: 'Цель выполнена: план и результаты',
1202
- time: 'Время',
1425
+ time: 'Время работы',
1203
1426
  milestones: 'План работ:',
1204
1427
  noMilestones: 'Агент формирует план работ...',
1205
1428
  close: 'Закрыть',
@@ -1227,7 +1450,7 @@ window.__ModuleLoader__.load({
1227
1450
  details: 'Expand Goal Details',
1228
1451
  modalTitle: 'Goal Plan & Status',
1229
1452
  modalTitleCompleted: 'Goal Completed: Plan & Results',
1230
- time: 'Time',
1453
+ time: 'Running time',
1231
1454
  milestones: 'Plan of work:',
1232
1455
  noMilestones: 'Agent is preparing the plan of work...',
1233
1456
  close: 'Close',