@promovaweb/specsfy 0.7.0 → 0.8.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/dist/tui.js CHANGED
@@ -14,7 +14,7 @@ import { resolvePath } from "./filesystem.js";
14
14
  import { AUXILIARY_SKILLS, DOCUMENTATION_SKILLS, FRAMEWORK_SKILLS, SkillInstaller, } from "./installer.js";
15
15
  import { scanSpecs, specsFingerprint, summarizeSpecs, } from "./progress.js";
16
16
  import { runProjectTests } from "./project-testing.js";
17
- import { ensureSkillsLock, skillsLockFingerprint, } from "./skill-lock.js";
17
+ import { ensureSkillsLock, skillsLockFingerprint } from "./skill-lock.js";
18
18
  import { VERSION } from "./version.js";
19
19
  marked.use(markedTerminal({
20
20
  reflowText: true,
@@ -87,6 +87,30 @@ export const TUI_TABS = [
87
87
  { id: "skills", label: "Skills", key: "C-k" },
88
88
  { id: "about", label: "Sobre", key: "C-o" },
89
89
  ];
90
+ /**
91
+ * Paleta semântica da TUI, derivada dos tokens oficiais do dark mode.
92
+ *
93
+ * Os pares de texto, seleção e foco mantêm contraste mensurável sem depender
94
+ * das cores configuráveis de 16 posições do terminal da pessoa usuária.
95
+ */
96
+ export const TUI_THEME = {
97
+ background: "#000A0E",
98
+ surface: "#001117",
99
+ surfaceRaised: "#03212A",
100
+ text: "#F2F8F9",
101
+ textMuted: "#B2C6CE",
102
+ border: "#5F7D8C",
103
+ accent: "#C4B5FD",
104
+ activeBackground: "#5EEDE1",
105
+ activeText: "#001117",
106
+ selectedBackground: "#6D28D9",
107
+ selectedText: "#F2F8F9",
108
+ primaryBackground: "#15626A",
109
+ primaryText: "#F2F8F9",
110
+ focusBackground: "#5EEDE1",
111
+ focusText: "#001117",
112
+ warning: "#FCD34D",
113
+ };
90
114
  /** Dashboard terminal principal. */
91
115
  export class SpecsfyTui {
92
116
  project;
@@ -181,7 +205,11 @@ export class SpecsfyTui {
181
205
  height: 1,
182
206
  align: "center",
183
207
  content: "Specsfy — Dashboard de specs e skills",
184
- style: { fg: "white", bg: "black", bold: true },
208
+ style: {
209
+ fg: TUI_THEME.text,
210
+ bg: TUI_THEME.selectedBackground,
211
+ bold: true,
212
+ },
185
213
  });
186
214
  this.#projectInput = blessed.textbox({
187
215
  parent: screen,
@@ -195,10 +223,10 @@ export class SpecsfyTui {
195
223
  keys: true,
196
224
  mouse: true,
197
225
  style: {
198
- fg: "white",
199
- bg: "black",
200
- border: { fg: "gray" },
201
- focus: { border: { fg: "cyan" } },
226
+ fg: TUI_THEME.text,
227
+ bg: TUI_THEME.surface,
228
+ border: { fg: TUI_THEME.border },
229
+ focus: { border: { fg: TUI_THEME.focusBackground } },
202
230
  },
203
231
  });
204
232
  this.#projectInput.on("submit", (value) => {
@@ -238,10 +266,14 @@ export class SpecsfyTui {
238
266
  mouse: true,
239
267
  keys: true,
240
268
  style: {
241
- fg: "gray",
242
- bg: "black",
243
- focus: { fg: "white", bg: "blue", bold: true },
244
- hover: { fg: "white", bg: "blue" },
269
+ fg: TUI_THEME.textMuted,
270
+ bg: TUI_THEME.background,
271
+ focus: {
272
+ fg: TUI_THEME.focusText,
273
+ bg: TUI_THEME.focusBackground,
274
+ bold: true,
275
+ },
276
+ hover: { fg: TUI_THEME.focusText, bg: TUI_THEME.focusBackground },
245
277
  },
246
278
  });
247
279
  button.on("press", () => this.showTab(id));
@@ -254,7 +286,7 @@ export class SpecsfyTui {
254
286
  left: 0,
255
287
  width: "100%",
256
288
  orientation: "horizontal",
257
- style: { fg: "gray" },
289
+ style: { fg: TUI_THEME.border },
258
290
  });
259
291
  this.#body = blessed.box({
260
292
  parent: screen,
@@ -262,7 +294,7 @@ export class SpecsfyTui {
262
294
  left: 1,
263
295
  right: 1,
264
296
  bottom: 4,
265
- style: { fg: "white", bg: "black" },
297
+ style: { fg: TUI_THEME.text, bg: TUI_THEME.background },
266
298
  });
267
299
  this.#status = blessed.box({
268
300
  parent: screen,
@@ -271,7 +303,7 @@ export class SpecsfyTui {
271
303
  width: "100%",
272
304
  height: 1,
273
305
  padding: { left: 1 },
274
- style: { fg: "gray", bg: "black" },
306
+ style: { fg: TUI_THEME.textMuted, bg: TUI_THEME.surface },
275
307
  });
276
308
  blessed.box({
277
309
  parent: screen,
@@ -281,7 +313,7 @@ export class SpecsfyTui {
281
313
  height: 1,
282
314
  content: " ^ = Ctrl · Tab/Shift+Tab: foco · Setas: navegar · " +
283
315
  "Espaço: abrir spec/alternar skill · Esc: voltar · Mouse: disponível",
284
- style: { fg: "cyan", bg: "black" },
316
+ style: { fg: TUI_THEME.accent, bg: TUI_THEME.background },
285
317
  });
286
318
  blessed.box({
287
319
  parent: screen,
@@ -290,7 +322,7 @@ export class SpecsfyTui {
290
322
  width: "100%",
291
323
  height: 1,
292
324
  content: " ^Q Sair Esc Voltar",
293
- style: { fg: "white", bg: "black", bold: true },
325
+ style: { fg: TUI_THEME.text, bg: TUI_THEME.surface, bold: true },
294
326
  });
295
327
  }
296
328
  bindKeys(screen) {
@@ -339,8 +371,9 @@ export class SpecsfyTui {
339
371
  if (!body || !this.#screen)
340
372
  return;
341
373
  for (const [id, button] of this.#tabButtons) {
342
- button.style.bg = id === tab ? "cyan" : "black";
343
- button.style.fg = id === tab ? "black" : "gray";
374
+ button.style.bg =
375
+ id === tab ? TUI_THEME.activeBackground : TUI_THEME.background;
376
+ button.style.fg = id === tab ? TUI_THEME.activeText : TUI_THEME.textMuted;
344
377
  button.style.bold = id === tab;
345
378
  }
346
379
  for (const child of [...body.children])
@@ -387,24 +420,24 @@ export class SpecsfyTui {
387
420
  renderHome(body) {
388
421
  const summary = summarizeSpecs(this.#specs);
389
422
  const cards = [
390
- ["Specs", String(summary.total_specs), "#173e67", "#2f81f7"],
423
+ ["Specs", String(summary.total_specs), "#021C26", "#5EEDE1"],
391
424
  [
392
425
  `Tarefas · ${summary.pending_tasks} pendentes`,
393
426
  `${summary.completed_tasks}/${summary.total_tasks}`,
394
- "#3f2d63",
395
- "#a371f7",
427
+ "#2E1065",
428
+ "#C4B5FD",
396
429
  ],
397
430
  [
398
431
  `Itens · ${summary.pending_items} pendentes`,
399
432
  `${summary.completed_items}/${summary.total_items}`,
400
- "#174b43",
401
- "#2ea043",
433
+ "#03212A",
434
+ "#37E1D0",
402
435
  ],
403
436
  [
404
437
  "",
405
438
  `${summary.percent}%\n${progressBar(summary.percent, 16)}`,
406
- "#4a381c",
407
- "#d29922",
439
+ "#001117",
440
+ TUI_THEME.warning,
408
441
  ],
409
442
  ];
410
443
  cards.forEach(([label, value, background, border], index) => {
@@ -419,7 +452,7 @@ export class SpecsfyTui {
419
452
  valign: "middle",
420
453
  content: `{bold}${value}{/bold}${label ? `\n${label}` : ""}`,
421
454
  tags: true,
422
- style: { fg: "white", bg: background, border: { fg: border } },
455
+ style: { fg: TUI_THEME.text, bg: background, border: { fg: border } },
423
456
  });
424
457
  });
425
458
  blessed.box({
@@ -433,7 +466,11 @@ export class SpecsfyTui {
433
466
  valign: "middle",
434
467
  content: "Visão consolidada do andamento das especificações. " +
435
468
  "Os números são recalculados quando os arquivos mudam.",
436
- style: { fg: "white", border: { fg: "gray" } },
469
+ style: {
470
+ fg: TUI_THEME.text,
471
+ bg: TUI_THEME.surface,
472
+ border: { fg: TUI_THEME.border },
473
+ },
437
474
  });
438
475
  }
439
476
  renderBacklogs(body) {
@@ -451,8 +488,8 @@ export class SpecsfyTui {
451
488
  keys: true,
452
489
  vi: true,
453
490
  mouse: true,
454
- scrollbar: { ch: " ", track: { bg: "gray" }, style: { bg: "cyan" } },
455
- style: { border: { fg: "gray" }, focus: { border: { fg: "yellow" } } },
491
+ scrollbar: terminalScrollbar(),
492
+ style: focusablePanelStyle(),
456
493
  });
457
494
  const list = blessed.list({
458
495
  parent: body,
@@ -468,17 +505,21 @@ export class SpecsfyTui {
468
505
  mouse: true,
469
506
  tags: false,
470
507
  style: {
471
- selected: { fg: "white", bg: "blue", bold: true },
472
- item: { fg: "white" },
473
- border: { fg: "gray" },
474
- focus: { border: { fg: "yellow" } },
508
+ ...focusablePanelStyle(),
509
+ selected: {
510
+ fg: TUI_THEME.selectedText,
511
+ bg: TUI_THEME.selectedBackground,
512
+ bold: true,
513
+ },
514
+ item: { fg: TUI_THEME.text, bg: TUI_THEME.background },
475
515
  },
476
516
  });
477
517
  const updatePreview = () => {
478
518
  const item = this.#backlogs[selectedIndex(list)];
479
519
  preview.setLabel(item ? ` ${item.identifier} · ${item.status} ` : " Nenhum backlog ");
480
- preview.setContent(item ? renderMarkdown(formatBacklogPreview(item.content)) :
481
- "Crie arquivos em specs/backlog/<NNNN>-<slug>.md para visualizá-los aqui.");
520
+ preview.setContent(item
521
+ ? renderMarkdown(formatBacklogPreview(item.content))
522
+ : "Crie arquivos em specs/backlog/<NNNN>-<slug>.md para visualizá-los aqui.");
482
523
  this.#screen?.render();
483
524
  };
484
525
  list.on("select item", updatePreview);
@@ -495,7 +536,7 @@ export class SpecsfyTui {
495
536
  width: "100%",
496
537
  height: "100%",
497
538
  border: "line",
498
- style: { border: { fg: "gray" } },
539
+ style: { border: { fg: TUI_THEME.border } },
499
540
  });
500
541
  blessed.box({
501
542
  parent: panel,
@@ -504,7 +545,11 @@ export class SpecsfyTui {
504
545
  width: "100%-2",
505
546
  height: 1,
506
547
  content: heading,
507
- style: { fg: "white", bg: "cyan", bold: true },
548
+ style: {
549
+ fg: TUI_THEME.activeText,
550
+ bg: TUI_THEME.activeBackground,
551
+ bold: true,
552
+ },
508
553
  });
509
554
  const list = blessed.list({
510
555
  parent: panel,
@@ -522,8 +567,12 @@ export class SpecsfyTui {
522
567
  vi: true,
523
568
  mouse: true,
524
569
  style: {
525
- selected: { fg: "white", bg: "blue", bold: true },
526
- item: { fg: "white" },
570
+ selected: {
571
+ fg: TUI_THEME.selectedText,
572
+ bg: TUI_THEME.selectedBackground,
573
+ bold: true,
574
+ },
575
+ item: { fg: TUI_THEME.text, bg: TUI_THEME.background },
527
576
  },
528
577
  });
529
578
  const open = () => {
@@ -558,8 +607,12 @@ export class SpecsfyTui {
558
607
  vi: true,
559
608
  mouse: true,
560
609
  padding: { left: 1, right: 1 },
561
- scrollbar: { ch: " ", track: { bg: "gray" }, style: { bg: "cyan" } },
562
- style: { fg: "white", bg: "black", border: { fg: "cyan" } },
610
+ scrollbar: terminalScrollbar(),
611
+ style: {
612
+ fg: TUI_THEME.text,
613
+ bg: TUI_THEME.background,
614
+ border: { fg: TUI_THEME.focusBackground },
615
+ },
563
616
  });
564
617
  this.#modal = modal;
565
618
  modal.focus();
@@ -576,12 +629,7 @@ export class SpecsfyTui {
576
629
  align: "center",
577
630
  mouse: true,
578
631
  keys: true,
579
- style: {
580
- fg: "white",
581
- bg: "blue",
582
- focus: { bg: "cyan", fg: "black" },
583
- hover: { bg: "cyan", fg: "black" },
584
- },
632
+ style: buttonStyle(true),
585
633
  });
586
634
  button.on("press", () => void this.runTests());
587
635
  blessed.box({
@@ -643,7 +691,7 @@ export class SpecsfyTui {
643
691
  vi: true,
644
692
  mouse: true,
645
693
  scrollable: true,
646
- style: { border: { fg: "gray" }, focus: { border: { fg: "yellow" } } },
694
+ style: focusablePanelStyle(),
647
695
  });
648
696
  }
649
697
  else {
@@ -666,7 +714,7 @@ export class SpecsfyTui {
666
714
  alwaysScroll: true,
667
715
  tags: false,
668
716
  content: this.#testOutput.join("\n"),
669
- style: { border: { fg: "gray" }, focus: { border: { fg: "yellow" } } },
717
+ style: focusablePanelStyle(),
670
718
  });
671
719
  }
672
720
  button.focus();
@@ -716,8 +764,10 @@ export class SpecsfyTui {
716
764
  height: 1,
717
765
  content: `${this.project}/skills-lock.json · ` +
718
766
  `${this.#installed.size} skill(s) Specsfy instalada(s)` +
719
- (this.#catalogError ? ` · catálogo indisponível: ${this.#catalogError}` : ""),
720
- style: { fg: "gray" },
767
+ (this.#catalogError
768
+ ? ` · catálogo indisponível: ${this.#catalogError}`
769
+ : ""),
770
+ style: { fg: TUI_THEME.textMuted },
721
771
  });
722
772
  const search = blessed.textbox({
723
773
  parent: body,
@@ -731,7 +781,7 @@ export class SpecsfyTui {
731
781
  inputOnFocus: false,
732
782
  keys: true,
733
783
  mouse: true,
734
- style: { border: { fg: "gray" }, focus: { border: { fg: "yellow" } } },
784
+ style: focusablePanelStyle(),
735
785
  });
736
786
  search.on("submit", (value) => {
737
787
  this.#skillQuery = String(value ?? "");
@@ -787,7 +837,7 @@ export class SpecsfyTui {
787
837
  height: 1,
788
838
  content: `${this.#selected.size} selecionada(s) · ${visible.length} visível(is) · ` +
789
839
  `${toInstall} para instalar · ${toRemove} para remover`,
790
- style: { fg: "white", bold: true },
840
+ style: { fg: TUI_THEME.text, bold: true },
791
841
  });
792
842
  const catalog = blessed.box({
793
843
  parent: body,
@@ -811,10 +861,13 @@ export class SpecsfyTui {
811
861
  vi: true,
812
862
  mouse: true,
813
863
  style: {
814
- selected: { fg: "white", bg: "blue", bold: true },
815
- item: { fg: "white" },
816
- border: { fg: "gray" },
817
- focus: { border: { fg: "yellow" } },
864
+ ...focusablePanelStyle(),
865
+ selected: {
866
+ fg: TUI_THEME.selectedText,
867
+ bg: TUI_THEME.selectedBackground,
868
+ bold: true,
869
+ },
870
+ item: { fg: TUI_THEME.text, bg: TUI_THEME.background },
818
871
  },
819
872
  });
820
873
  const detail = blessed.box({
@@ -830,7 +883,7 @@ export class SpecsfyTui {
830
883
  keys: true,
831
884
  vi: true,
832
885
  mouse: true,
833
- style: { border: { fg: "gray" }, focus: { border: { fg: "yellow" } } },
886
+ style: focusablePanelStyle(),
834
887
  });
835
888
  blessed.box({
836
889
  parent: catalog,
@@ -839,7 +892,11 @@ export class SpecsfyTui {
839
892
  width: "64%",
840
893
  height: 1,
841
894
  content: " PLANO SKILL CATEGORIA ESTADO ",
842
- style: { fg: "black", bg: "white", bold: true },
895
+ style: {
896
+ fg: TUI_THEME.activeText,
897
+ bg: TUI_THEME.activeBackground,
898
+ bold: true,
899
+ },
843
900
  });
844
901
  const updateDetail = () => {
845
902
  const option = visible[selectedIndex(list)];
@@ -929,7 +986,9 @@ export class SpecsfyTui {
929
986
  const summary = summarizeSpecs(specs);
930
987
  this.showStatus(`${this.project} · ${summary.completed_specs}/${summary.total_specs} specs completas · ` +
931
988
  "atualização automática ativa" +
932
- (this.#catalogError ? ` · catálogo indisponível: ${this.#catalogError}` : ""));
989
+ (this.#catalogError
990
+ ? ` · catálogo indisponível: ${this.#catalogError}`
991
+ : ""));
933
992
  this.showTab(this.#activeTab);
934
993
  }
935
994
  catch (error) {
@@ -1066,7 +1125,8 @@ export function buildSkillOptions(catalogEntries, installed) {
1066
1125
  const options = FRAMEWORK_SKILLS.map((name) => ({
1067
1126
  name,
1068
1127
  description: BASE_DESCRIPTIONS[name] ?? "Skill do framework Specsfy.",
1069
- kind: AUXILIARY_SKILLS.includes(name) || name === "specsfy-setup"
1128
+ kind: AUXILIARY_SKILLS.includes(name) ||
1129
+ name === "specsfy-setup"
1070
1130
  ? "auxiliary"
1071
1131
  : DOCUMENTATION_SKILLS.includes(name)
1072
1132
  ? "documentation"
@@ -1152,9 +1212,7 @@ function friendlySkillName(name) {
1152
1212
  };
1153
1213
  if (baseNames[name])
1154
1214
  return baseNames[name];
1155
- return titleCase(name
1156
- .replace(/^specsfy-(?:aux|specialist)-/u, "")
1157
- .replaceAll("-", " "));
1215
+ return titleCase(name.replace(/^specsfy-(?:aux|specialist)-/u, "").replaceAll("-", " "));
1158
1216
  }
1159
1217
  function isSpecsfySkill(name) {
1160
1218
  return (FRAMEWORK_SKILLS.includes(name) ||
@@ -1200,20 +1258,45 @@ function renderMarkdown(content) {
1200
1258
  }
1201
1259
  function buttonStyle(primary = false) {
1202
1260
  return {
1203
- fg: "white",
1204
- bg: primary ? "blue" : "black",
1205
- border: { fg: primary ? "cyan" : "gray" },
1206
- focus: { fg: "white", bg: "blue", border: { fg: "cyan" } },
1207
- hover: { fg: "white", bg: "blue", border: { fg: "cyan" } },
1261
+ fg: primary ? TUI_THEME.primaryText : TUI_THEME.text,
1262
+ bg: primary ? TUI_THEME.primaryBackground : TUI_THEME.surface,
1263
+ border: { fg: primary ? TUI_THEME.focusBackground : TUI_THEME.border },
1264
+ focus: {
1265
+ fg: TUI_THEME.focusText,
1266
+ bg: TUI_THEME.focusBackground,
1267
+ border: { fg: TUI_THEME.focusBackground },
1268
+ },
1269
+ hover: {
1270
+ fg: TUI_THEME.focusText,
1271
+ bg: TUI_THEME.focusBackground,
1272
+ border: { fg: TUI_THEME.focusBackground },
1273
+ },
1208
1274
  };
1209
1275
  }
1210
1276
  function tabStyle(active) {
1211
1277
  return {
1212
- fg: active ? "black" : "gray",
1213
- bg: active ? "cyan" : "black",
1278
+ fg: active ? TUI_THEME.activeText : TUI_THEME.textMuted,
1279
+ bg: active ? TUI_THEME.activeBackground : TUI_THEME.background,
1214
1280
  bold: active,
1215
- focus: { fg: "white", bg: "blue" },
1216
- hover: { fg: "white", bg: "blue" },
1281
+ focus: { fg: TUI_THEME.focusText, bg: TUI_THEME.focusBackground },
1282
+ hover: { fg: TUI_THEME.focusText, bg: TUI_THEME.focusBackground },
1283
+ };
1284
+ }
1285
+ /** Mantém borda e foco legíveis em painéis, inputs e regiões roláveis. */
1286
+ function focusablePanelStyle() {
1287
+ return {
1288
+ fg: TUI_THEME.text,
1289
+ bg: TUI_THEME.background,
1290
+ border: { fg: TUI_THEME.border },
1291
+ focus: { border: { fg: TUI_THEME.focusBackground } },
1292
+ };
1293
+ }
1294
+ /** Usa trilho discreto e indicador turquesa na rolagem dos painéis. */
1295
+ function terminalScrollbar() {
1296
+ return {
1297
+ ch: " ",
1298
+ track: { bg: TUI_THEME.surfaceRaised },
1299
+ style: { bg: TUI_THEME.focusBackground },
1217
1300
  };
1218
1301
  }
1219
1302
  //# sourceMappingURL=tui.js.map