@kitkat-vn/kitcode 0.2.0-staging.8 → 0.3.0-staging.15

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.
@@ -5,16 +5,35 @@ import {
5
5
  setupPlatformTheme,
6
6
  } from './onboarding-platform.mjs';
7
7
  import {contentSecurityPolicy, fontFaces} from './window-page.mjs';
8
+ import {WELCOME_ENGLISH, WELCOME_TESTER_WORDS, WELCOME_WORDS} from './welcome-words.mjs';
9
+
10
+ // A word into the page's markup, where it is text: the words are this package's own, but a word is never markup.
11
+ function text(value) {
12
+ return String(value).replace(/[&<>"']/g, (character) => `&#${character.charCodeAt(0)};`);
13
+ }
14
+
15
+ // A word into the page's script: JSON, with every "<" escaped, so that no word can end the script element.
16
+ function scriptValue(value) {
17
+ return JSON.stringify(value).replace(/</g, '\\u003c');
18
+ }
8
19
 
9
20
  // `diagnostics`: whether this is a tester build (src/diagnostics.mjs), unless a test says. Then, and only then, Welcome says
10
- // that the build sends diagnostic logs to KitCode's staging server, and what they never hold, and asks, optionally, for the
11
- // email the tester was invited with, so that staff can find their logs by it. Without it the page is as it was, byte for
12
- // byte.
21
+ // that the build sends diagnostic logs to KitCode's server, and what they never hold, and asks, optionally, for the email the
22
+ // tester was invited with, so that staff can find their logs by it (rows 78-80 of the words file, since 2026-10-22 in
23
+ // Vietnamese: src/welcome-words.mjs). Without it the page holds none of those words.
13
24
  export function renderOnboardingWindow(platform = resolveSetupPlatform(), {diagnostics = diagnosticsEnabled()} = {}) {
14
25
  const theme = setupPlatformTheme(platform);
26
+ // The words the page's script writes, and nothing else: a tester build's words are in its markup alone, so any other
27
+ // build's page holds none of them.
28
+ const scriptWords = {
29
+ messages: WELCOME_WORDS.messages,
30
+ buttons: WELCOME_WORDS.buttons,
31
+ projects: {empty: WELCOME_WORDS.projects.empty, removeTitle: WELCOME_WORDS.projects.removeTitle, removeName: WELCOME_WORDS.projects.removeName},
32
+ english: {pending: WELCOME_ENGLISH.pending, tracked: WELCOME_ENGLISH.tracked, added: WELCOME_ENGLISH.added},
33
+ };
15
34
  // The page's one stylesheet and one script, kept apart so that its policy allows exactly them.
16
35
  const style = `
17
- ${fontFaces(['fira-code'])}
36
+ ${fontFaces(['jetbrains-mono'])}
18
37
 
19
38
  :root {
20
39
  color-scheme: dark;
@@ -24,7 +43,7 @@ export function renderOnboardingWindow(platform = resolveSetupPlatform(), {diagn
24
43
  --grey-text: #88888e;
25
44
  --white-text: #e1e1e6;
26
45
  --error: #ff9d9d;
27
- --font-mono: 'Fira Code', 'Courier New', Courier, ui-monospace, monospace;
46
+ --font-mono: 'JetBrains Mono', 'Courier New', Courier, ui-monospace, monospace;
28
47
  }
29
48
 
30
49
  * { box-sizing: border-box; margin: 0; padding: 0; }
@@ -227,9 +246,8 @@ export function renderOnboardingWindow(platform = resolveSetupPlatform(), {diagn
227
246
 
228
247
  .option {
229
248
  position: relative;
230
- width: 72px;
231
249
  height: 38px;
232
- flex: 0 0 72px;
250
+ flex: 0 0 auto;
233
251
  }
234
252
 
235
253
  .option input {
@@ -243,14 +261,17 @@ export function renderOnboardingWindow(platform = resolveSetupPlatform(), {diagn
243
261
  white-space: nowrap;
244
262
  }
245
263
 
264
+ /* At least 72 px, as the English answers were, and as wide as its words with 12 px either side: "[ KHÔNG ]" filled a
265
+ 72 px card, while "[ CÓ ]" keeps the 72 px it had, with about 13 px either side. */
246
266
  .option-card {
247
267
  display: inline-flex;
248
268
  align-items: center;
249
269
  justify-content: center;
250
- width: 72px;
270
+ min-width: 72px;
251
271
  height: 38px;
252
272
  background: transparent;
253
- padding: 0;
273
+ padding: 0 12px;
274
+ white-space: nowrap;
254
275
  font-size: 11px;
255
276
  font-weight: 700;
256
277
  letter-spacing: 0.5px;
@@ -423,13 +444,14 @@ export function renderOnboardingWindow(platform = resolveSetupPlatform(), {diagn
423
444
  margin: 12px 0;
424
445
  }
425
446
 
447
+ /* The message line takes two lines when a message needs them, as the deck writes rows 66 and 76 (the plan's question
448
+ 5c): never cut with an ellipsis. A message's own line break is the deck's. */
426
449
  .message.comment-text {
427
450
  min-width: 0;
428
451
  color: var(--white-text);
429
452
  font-size: 11px;
430
- overflow: hidden;
431
- text-overflow: ellipsis;
432
- white-space: nowrap;
453
+ white-space: pre-line;
454
+ overflow-wrap: break-word;
433
455
  }
434
456
 
435
457
  .message[data-state="error"] { color: var(--error); }
@@ -498,9 +520,14 @@ ${diagnostics ? `
498
520
  const setupContent = document.getElementById('setupContent');
499
521
  const railItems = [...document.querySelectorAll('.rail-item')];
500
522
  const stepSections = [...document.querySelectorAll('[data-setup-step]')];
501
- const SAVE_LABEL = '+ SAVE AND OPEN KITCODE ->';${diagnostics ? `
523
+ const WORDS = ${scriptValue(scriptWords)};${diagnostics ? `
502
524
  const invitedEmail = document.getElementById('invitedEmail');` : ''}
503
525
  const FOLDER_ICON = '<svg class="dropzone-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>';
526
+ // A word with its count or name in place, by a function, so that a "$&" in a folder's name stays the name's own.
527
+ function fill(template, values) {
528
+ return template.replace(/\\{(N|name)\\}/g, (_match, key) => String(values[key]));
529
+ }
530
+
504
531
  let persistedProjects = [];
505
532
  let pendingProjects = [];
506
533
  let saving = false;
@@ -572,7 +599,8 @@ ${diagnostics ? `
572
599
  path.className = 'project-path';
573
600
  path.textContent = project.repoRoot;
574
601
  meta.className = 'project-meta';
575
- meta.textContent = (project.sourceType || 'vibe').toUpperCase() + ' · ' + (kind === 'pending' ? 'PENDING' : 'TRACKED');
602
+ meta.lang = 'en';
603
+ meta.textContent = (project.sourceType || 'vibe').toUpperCase() + ' · ' + (kind === 'pending' ? WORDS.english.pending : WORDS.english.tracked);
576
604
  copy.append(name, path);
577
605
  row.append(copy, meta);
578
606
 
@@ -580,20 +608,20 @@ ${diagnostics ? `
580
608
  remove.type = 'button';
581
609
  remove.dataset.testid = 'remove-project';
582
610
  remove.textContent = 'x';
583
- remove.title = 'Remove project';
584
- remove.setAttribute('aria-label', 'Remove ' + projectName(project));
611
+ remove.title = WORDS.projects.removeTitle;
612
+ remove.setAttribute('aria-label', fill(WORDS.projects.removeName, {name: projectName(project)}));
585
613
  remove.addEventListener('click', async () => {
586
614
  if (kind === 'pending') {
587
615
  pendingProjects = pendingProjects.filter((candidate) => candidate.id !== project.id);
588
616
  renderProjects();
589
- setFeedback('Project removed.', 'normal', 'projects:removed');
617
+ setFeedback(WORDS.messages.removed, 'normal', 'projects:removed');
590
618
  return;
591
619
  }
592
620
 
593
621
  if (!bridge?.removeProject) {
594
622
  persistedProjects = persistedProjects.filter((candidate) => candidate.id !== project.id);
595
623
  renderProjects();
596
- setFeedback('Project removed from this setup.', 'normal', 'projects:removed');
624
+ setFeedback(WORDS.messages.removedFromSetup, 'normal', 'projects:removed');
597
625
  return;
598
626
  }
599
627
 
@@ -602,17 +630,17 @@ ${diagnostics ? `
602
630
  const result = await bridge.removeProject(project.id);
603
631
  if (!result.ok) {
604
632
  remove.disabled = false;
605
- setFeedback(result.error || 'Project could not be removed.', 'error', 'error:remove-project');
633
+ setFeedback(result.error || WORDS.messages.removeFailed, 'error', 'error:remove-project');
606
634
  return;
607
635
  }
608
636
  persistedProjects = Array.isArray(result.projects)
609
637
  ? result.projects
610
638
  : persistedProjects.filter((candidate) => candidate.id !== project.id);
611
639
  renderProjects();
612
- setFeedback('Project removed.', 'normal', 'projects:removed');
640
+ setFeedback(WORDS.messages.removed, 'normal', 'projects:removed');
613
641
  } catch {
614
642
  remove.disabled = false;
615
- setFeedback('Project could not be removed. Try again.', 'error', 'error:remove-project');
643
+ setFeedback(WORDS.messages.removeFailedRetry, 'error', 'error:remove-project');
616
644
  }
617
645
  });
618
646
  row.append(remove);
@@ -623,14 +651,17 @@ ${diagnostics ? `
623
651
  function renderProjects() {
624
652
  projectList.textContent = '';
625
653
  const projects = persistedProjects.concat(pendingProjects);
626
- projectCount.textContent = projects.length + ' ADDED';
654
+ projectCount.textContent = fill(WORDS.english.added, {N: projects.length});
627
655
  projectList.classList.toggle('is-empty', !projects.length);
628
656
  projectList.classList.toggle('tech-box-white', !projects.length);
629
657
 
630
658
  if (!projects.length) {
631
659
  const empty = document.createElement('div');
632
660
  empty.className = 'empty-state';
633
- empty.innerHTML = FOLDER_ICON + '<span>No projects added</span>';
661
+ empty.innerHTML = FOLDER_ICON;
662
+ const label = document.createElement('span');
663
+ label.textContent = WORDS.projects.empty;
664
+ empty.append(label);
634
665
  projectList.append(empty);
635
666
  return;
636
667
  }
@@ -692,11 +723,11 @@ ${diagnostics ? `
692
723
  renderProjects();
693
724
 
694
725
  pickFolders.addEventListener('click', async () => {
695
- setFeedback('Opening the native multi-folder picker...', 'normal', 'projects:select');
726
+ setFeedback(WORDS.messages.picking, 'normal', 'projects:select');
696
727
  try {
697
728
  const result = await bridge.selectFolders();
698
729
  if (result.canceled) {
699
- setFeedback('Project selection canceled.', 'normal', 'projects:unchanged');
730
+ setFeedback(WORDS.messages.pickCanceled, 'normal', 'projects:unchanged');
700
731
  return;
701
732
  }
702
733
 
@@ -713,26 +744,26 @@ ${diagnostics ? `
713
744
  renderProjects();
714
745
  const added = (result.projects || []).length - duplicates;
715
746
  setFeedback(
716
- added ? added + ' project' + (added === 1 ? '' : 's') + ' ready to add.' : 'Those projects are already added.',
747
+ added ? fill(WORDS.messages.readyToAdd, {N: added}) : WORDS.messages.alreadyAdded,
717
748
  'normal',
718
749
  duplicates ? 'projects:deduplicated' : 'projects:pending'
719
750
  );
720
751
  } catch {
721
- setFeedback('Folder picker could not open. Restart setup and try again.', 'error', 'error:folder-picker');
752
+ setFeedback(WORDS.messages.pickerFailed, 'error', 'error:folder-picker');
722
753
  }
723
754
  });
724
755
 
725
756
  saveButton.addEventListener('click', async () => {
726
757
  if (saving) return;
727
758
  if (!persistedProjects.length && !pendingProjects.length) {
728
- setFeedback('Add at least one readable project folder.', 'error', 'error:no-projects');
759
+ setFeedback(WORDS.messages.noProjects, 'error', 'error:no-projects');
729
760
  pickFolders.focus();
730
761
  return;
731
762
  }
732
763
 
733
764
  setBusy(true);
734
- saveButton.textContent = 'Saving...';
735
- setFeedback('Saving projects and preferences...', 'normal', 'setup:save');
765
+ saveButton.textContent = WORDS.buttons.saving;
766
+ setFeedback(WORDS.messages.saving, 'normal', 'setup:save');
736
767
  const autoTrack = document.querySelector('input[name="autoTrack"]:checked').value === 'yes';
737
768
  const companionView = 'mini';
738
769
 
@@ -753,17 +784,17 @@ ${diagnostics ? `
753
784
 
754
785
  if (!result.ok) {
755
786
  setBusy(false);
756
- saveButton.textContent = 'Retry Save';
757
- setFeedback(result.error || 'Setup could not be saved.', 'error', 'error:retry-available');
787
+ saveButton.textContent = WORDS.buttons.retry;
788
+ setFeedback(result.error || WORDS.messages.saveFailed, 'error', 'error:retry-available');
758
789
  return;
759
790
  }
760
791
 
761
- saveButton.textContent = 'Setup Complete';
762
- setFeedback('Setup complete. Opening KitCode companion...', 'success', 'setup:complete');
792
+ saveButton.textContent = WORDS.buttons.saved;
793
+ setFeedback(WORDS.messages.saved, 'success', 'setup:complete');
763
794
  } catch {
764
795
  setBusy(false);
765
- saveButton.textContent = 'Retry Save';
766
- setFeedback('Setup request failed. Your selections are still here.', 'error', 'error:retry-available');
796
+ saveButton.textContent = WORDS.buttons.retry;
797
+ setFeedback(WORDS.messages.requestFailed, 'error', 'error:retry-available');
767
798
  }
768
799
  });
769
800
 
@@ -789,7 +820,7 @@ ${diagnostics ? `
789
820
 
790
821
  if (!bridge) {
791
822
  setBusy(true);
792
- setFeedback('KitCode setup bridge is unavailable. Close this window and run kitcode setup again.', 'error', 'error:preload');
823
+ setFeedback(WORDS.messages.noBridge, 'error', 'error:preload');
793
824
  } else {
794
825
  bridge.initialState().then((state) => {
795
826
  persistedProjects = Array.isArray(state.projects) ? state.projects : [];
@@ -812,99 +843,99 @@ ${diagnostics ? `
812
843
 
813
844
  if (suggestedCount) {
814
845
  setFeedback(
815
- suggestedCount + ' project folder' + (suggestedCount === 1 ? '' : 's') + ' added from your chat. Review and save to continue.',
846
+ fill(WORDS.messages.suggested, {N: suggestedCount}),
816
847
  'normal',
817
848
  'projects:chat-suggested'
818
849
  );
819
850
  } else {
820
851
  setFeedback(
821
- persistedProjects.length ? persistedProjects.length + ' tracked project' + (persistedProjects.length === 1 ? '' : 's') + ' loaded.' : 'Add one or more projects to continue.',
852
+ persistedProjects.length ? fill(WORDS.messages.loaded, {N: persistedProjects.length}) : WORDS.messages.ready,
822
853
  'normal',
823
854
  persistedProjects.length ? 'projects:loaded' : 'setup:ready'
824
855
  );
825
856
  }
826
857
  }).catch(() => {
827
858
  renderProjects();
828
- setFeedback('Local setup state could not be loaded. Restart setup and try again.', 'error', 'error:initial-state');
859
+ setFeedback(WORDS.messages.stateFailed, 'error', 'error:initial-state');
829
860
  });
830
861
  }
831
862
  `;
832
863
 
833
864
  return `<!doctype html>
834
- <html lang="en">
865
+ <html lang="vi">
835
866
  <head>
836
867
  <meta charset="utf-8">
837
868
  <meta http-equiv="Content-Security-Policy" content="${contentSecurityPolicy({script, style})}">
838
869
  <meta name="viewport" content="width=device-width, initial-scale=1">
839
- <title>KitCode Setup</title>
870
+ <title>${text(WELCOME_WORDS.title)}</title>
840
871
  <style>${style}</style>
841
872
  </head>
842
873
  <body data-platform="${theme.id}" data-theme-marker="${theme.marker}">
843
874
  <main class="shell window" data-testid="setup-shell">
844
875
  ${renderPlatformChrome(theme)}
845
876
  <div class="container">
846
- <div class="top-tag">local-first setup</div>
877
+ <div class="top-tag" lang="en">${text(WELCOME_ENGLISH.topTag)}</div>
847
878
  <div class="dashed-divider section-divider" aria-hidden="true">==================================================================================================================================================================================================</div>
848
879
  <div class="workspace layout steps-panel" id="setupContent" data-testid="setup-step-rail">
849
880
  <aside class="sidebar">
850
881
  <div class="rail-item is-active" data-step="tracking" data-testid="setup-rail-tracking">
851
882
  <span class="rail-badge tech-box-red">01</span>
852
- <span class="rail-label">START TRACKING<br>AFTER SETUP</span>
883
+ <span class="rail-label">${text(WELCOME_WORDS.rail.tracking.toUpperCase())}</span>
853
884
  </div>
854
885
  <div class="step-connector" aria-hidden="true"></div>
855
886
  <div class="rail-item" data-step="projects" data-testid="setup-rail-projects">
856
887
  <span class="rail-badge tech-box-red">02</span>
857
- <span class="rail-label">PROJECTS</span>
888
+ <span class="rail-label">${text(WELCOME_WORDS.rail.projects.toUpperCase())}</span>
858
889
  </div>${diagnostics ? `
859
890
  <div class="step-connector" aria-hidden="true"></div>
860
891
  <div class="rail-item" data-step="diagnostics" data-testid="setup-rail-diagnostics">
861
892
  <span class="rail-badge tech-box-red">03</span>
862
- <span class="rail-label">DIAGNOSTICS</span>
893
+ <span class="rail-label">${text(WELCOME_TESTER_WORDS.rail.toUpperCase())}</span>
863
894
  </div>` : ''}
864
895
  </aside>
865
896
  <div class="content">
866
897
  <fieldset class="step" id="trackingStep" data-setup-step="tracking">
867
- <legend class="section-title">Start tracking after setup?</legend>
868
- <div class="option-group" id="autoTrackGroup" role="radiogroup" aria-label="Start tracking after setup" data-next-focus="pickFolders">
898
+ <legend class="section-title">${text(WELCOME_WORDS.tracking.question)}</legend>
899
+ <div class="option-group" id="autoTrackGroup" role="radiogroup" aria-label="${text(WELCOME_WORDS.tracking.groupName)}" data-next-focus="pickFolders">
869
900
  <label class="option">
870
901
  <input type="radio" name="autoTrack" value="yes" checked data-testid="auto-track-yes">
871
- <span class="option-card btn btn-red tech-box-red option-mark">[ YES ]</span>
902
+ <span class="option-card btn btn-red tech-box-red option-mark">[ ${text(WELCOME_WORDS.tracking.yes)} ]</span>
872
903
  </label>
873
904
  <label class="option">
874
905
  <input type="radio" name="autoTrack" value="no" data-testid="auto-track-no">
875
- <span class="option-card btn btn-grey tech-box-grey option-mark">[ NO ]</span>
906
+ <span class="option-card btn btn-grey tech-box-grey option-mark">[ ${text(WELCOME_WORDS.tracking.no)} ]</span>
876
907
  </label>
877
908
  </div>
878
909
  </fieldset>
879
910
  <section class="step projects-step" aria-labelledby="projectsTitle" data-setup-step="projects">
880
911
  <div class="step-head">
881
- <h2 class="section-title" id="projectsTitle">Select one or more local folders</h2>
882
- <span class="project-count" id="projectCount">0 ADDED</span>
912
+ <h2 class="section-title" id="projectsTitle">${text(WELCOME_WORDS.projects.heading)}</h2>
913
+ <span class="project-count" id="projectCount" lang="en">${text(WELCOME_ENGLISH.added.replace('{N}', '0'))}</span>
883
914
  </div>
884
- <div class="project-list is-empty tech-box-white" id="projectList" role="list" aria-label="KitCode projects" data-testid="project-list"></div>
915
+ <div class="project-list is-empty tech-box-white" id="projectList" role="list" aria-label="${text(WELCOME_WORDS.projects.listName)}" data-testid="project-list"></div>
885
916
  <div class="project-actions">
886
- <button class="terminal-button tech-box-red" id="pickFolders" type="button" data-testid="add-projects">+ ADD PROJECTS</button>
887
- <p class="hint">Existing files become the baseline.<br>Source and diffs stay local.</p>
917
+ <button class="terminal-button tech-box-red" id="pickFolders" type="button" data-testid="add-projects">+ ${text(WELCOME_WORDS.projects.add.toUpperCase())}</button>
918
+ <p class="hint">${WELCOME_WORDS.projects.hint.split('\n').map(text).join('<br>')}</p>
888
919
  </div>
889
920
  </section>${diagnostics ? `
890
921
  <section class="step diagnostics-step" aria-labelledby="diagnosticsTitle" data-setup-step="diagnostics" data-testid="setup-diagnostics">
891
- <h2 class="section-title" id="diagnosticsTitle">This test build sends diagnostic logs</h2>
892
- <p>This test build of KitCode sends diagnostic logs to KitCode’s staging server: KitCode’s own events and errors, such as the tracker starting or a hook failing, so that a problem you meet can be traced without asking you for it. They never hold your code, your files, your prompts or your project names.</p>
893
- <label for="invitedEmail">The email address you were invited with, optional: staff find your logs by it.</label>
922
+ <h2 class="section-title" id="diagnosticsTitle">${text(WELCOME_TESTER_WORDS.heading)}</h2>
923
+ ${WELCOME_TESTER_WORDS.text.split('\n\n').map((paragraph) => `<p>${text(paragraph)}</p>`).join('\n ')}
924
+ <label for="invitedEmail">${text(WELCOME_TESTER_WORDS.emailLabel)}</label>
894
925
  <input id="invitedEmail" name="invitedEmail" type="email" autocomplete="email" maxlength="254" spellcheck="false" data-testid="invited-email">
895
926
  </section>` : ''}
896
927
  </div>
897
928
  </div>
898
929
  <div class="dashed-divider section-divider" aria-hidden="true">==================================================================================================================================================================================================</div>
899
930
  <footer class="content-footer footer-action">
900
- <div class="message comment-text" id="message" aria-live="polite">/* Add one or more projects to continue.</div>
901
- <button class="terminal-button primary-button tech-box-red" id="saveButton" type="button" data-testid="save-setup">+ SAVE AND OPEN KITCODE -&gt;</button>
931
+ <div class="message comment-text" id="message" aria-live="polite">/* ${text(WELCOME_WORDS.messages.ready)}</div>
932
+ <button class="terminal-button primary-button tech-box-red" id="saveButton" type="button" data-testid="save-setup">+ ${text(WELCOME_WORDS.buttons.save)} -&gt;</button>
902
933
  </footer>
903
934
  <div class="dashed-divider section-divider" aria-hidden="true">==================================================================================================================================================================================================</div>
904
935
  <footer class="statusline status-bar">
905
- <span class="normal-mode status-mode tech-box-red">NORMAL</span>
906
- <span class="status-text" id="statusText" aria-live="polite">+++ setup: ready +++</span>
907
- <span class="keyboard-hint status-shortcuts">&#8593;&#8595; move # space select # enter continue</span>
936
+ <span class="normal-mode status-mode tech-box-red" lang="en">${text(WELCOME_ENGLISH.mode)}</span>
937
+ <span class="status-text" id="statusText" aria-live="polite" lang="en">+++ setup: ready +++</span>
938
+ <span class="keyboard-hint status-shortcuts" lang="en">${text(WELCOME_ENGLISH.shortcuts)}</span>
908
939
  </footer>
909
940
  </div>
910
941
  </main>