pinokiod 3.85.0 → 3.86.0

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.
Files changed (55) hide show
  1. package/kernel/api/index.js +7 -0
  2. package/kernel/bin/caddy.js +10 -4
  3. package/kernel/peer.js +0 -3
  4. package/kernel/prototype.js +1 -0
  5. package/kernel/shell.js +43 -2
  6. package/kernel/util.js +2 -0
  7. package/package.json +1 -1
  8. package/pipe/views/login.ejs +1 -1
  9. package/server/index.js +133 -83
  10. package/server/public/common.js +534 -0
  11. package/server/public/opener.js +12 -11
  12. package/server/public/serve/style.css +1 -1
  13. package/server/public/style.css +25 -24
  14. package/server/public/urldropdown.css +473 -4
  15. package/server/public/urldropdown.js +202 -8
  16. package/server/views/404.ejs +1 -1
  17. package/server/views/500.ejs +1 -1
  18. package/server/views/app.ejs +29 -33
  19. package/server/views/bookmarklet.ejs +197 -0
  20. package/server/views/connect/x.ejs +4 -4
  21. package/server/views/connect.ejs +10 -10
  22. package/server/views/container.ejs +2 -2
  23. package/server/views/d.ejs +3 -3
  24. package/server/views/download.ejs +1 -1
  25. package/server/views/editor.ejs +1 -1
  26. package/server/views/env_editor.ejs +3 -3
  27. package/server/views/explore.ejs +2 -2
  28. package/server/views/file_explorer.ejs +2 -2
  29. package/server/views/git.ejs +7 -7
  30. package/server/views/github.ejs +3 -3
  31. package/server/views/help.ejs +2 -2
  32. package/server/views/index.ejs +5 -5
  33. package/server/views/index2.ejs +3 -3
  34. package/server/views/init/index.ejs +11 -74
  35. package/server/views/install.ejs +3 -3
  36. package/server/views/keys.ejs +2 -2
  37. package/server/views/mini.ejs +2 -2
  38. package/server/views/net.ejs +6 -6
  39. package/server/views/network.ejs +21 -21
  40. package/server/views/network2.ejs +10 -10
  41. package/server/views/old_network.ejs +8 -8
  42. package/server/views/pro.ejs +369 -0
  43. package/server/views/prototype/index.ejs +2 -2
  44. package/server/views/required_env_editor.ejs +2 -2
  45. package/server/views/review.ejs +6 -6
  46. package/server/views/screenshots.ejs +5 -4
  47. package/server/views/settings.ejs +3 -3
  48. package/server/views/setup.ejs +2 -2
  49. package/server/views/setup_home.ejs +2 -2
  50. package/server/views/share_editor.ejs +4 -4
  51. package/server/views/shell.ejs +3 -3
  52. package/server/views/start.ejs +2 -2
  53. package/server/views/task.ejs +2 -2
  54. package/server/views/terminal.ejs +5 -4
  55. package/server/views/tools.ejs +13 -13
@@ -24,6 +24,9 @@ function initUrlDropdown(config = {}) {
24
24
  let isDropdownVisible = false;
25
25
  let allProcesses = []; // Store all processes for filtering
26
26
  let filteredProcesses = []; // Store currently filtered processes
27
+ let createLauncherModal = null;
28
+ let pendingCreateDetail = null;
29
+ const EMPTY_STATE_DESCRIPTION = 'enter a prompt to create a launcher';
27
30
 
28
31
  // Initialize input field state based on clear behavior
29
32
  initializeInputValue();
@@ -272,11 +275,7 @@ function initUrlDropdown(config = {}) {
272
275
 
273
276
  function populateDropdown(processes) {
274
277
  if (processes.length === 0) {
275
- const query = urlInput.value.toLowerCase().trim();
276
- const message = query
277
- ? `No processes match "${query}"`
278
- : 'No running processes found';
279
- dropdown.innerHTML = `<div class="url-dropdown-empty">${message}</div>`;
278
+ showEmptyState(dropdown, urlInput);
280
279
  return;
281
280
  }
282
281
 
@@ -514,9 +513,7 @@ function initUrlDropdown(config = {}) {
514
513
  const modalInput = modalDropdown.parentElement.querySelector('.url-modal-input');
515
514
 
516
515
  if (processes.length === 0) {
517
- const query = modalInput.value.toLowerCase().trim();
518
- const message = query ? `No processes match "${query}"` : 'No running processes found';
519
- modalDropdown.innerHTML = `<div class="url-dropdown-empty">${message}</div>`;
516
+ showEmptyState(modalDropdown, modalInput);
520
517
  return;
521
518
  }
522
519
 
@@ -633,6 +630,203 @@ function initUrlDropdown(config = {}) {
633
630
  filteredProcesses = [];
634
631
  }
635
632
  };
633
+ function showEmptyState(container, inputElement) {
634
+ container.innerHTML = createEmptyStateHtml(getEmptyStateMessage(inputElement));
635
+ attachCreateButtonHandler(container, inputElement);
636
+ }
637
+
638
+ function getEmptyStateMessage(inputElement) {
639
+ const rawValue = inputElement.value.trim();
640
+ return rawValue ? `No processes match "${rawValue}"` : 'No running processes found';
641
+ }
642
+
643
+ function createEmptyStateHtml(message) {
644
+ return `
645
+ <div class="url-dropdown-empty">
646
+ <div class="url-dropdown-empty-message">${escapeHtml(message)}</div>
647
+ <div class="url-dropdown-empty-actions">
648
+ <button type="button" class="url-dropdown-create-button">Create</button>
649
+ <div class="url-dropdown-empty-description">${escapeHtml(EMPTY_STATE_DESCRIPTION)}</div>
650
+ </div>
651
+ </div>
652
+ `;
653
+ }
654
+
655
+ function attachCreateButtonHandler(container, inputElement) {
656
+ const createButton = container.querySelector('.url-dropdown-create-button');
657
+ if (!createButton) return;
658
+
659
+ createButton.addEventListener('click', function(event) {
660
+ event.preventDefault();
661
+ event.stopPropagation();
662
+ const prompt = inputElement.value.trim();
663
+ const detail = {
664
+ query: prompt,
665
+ prompt,
666
+ input: inputElement,
667
+ dropdown: container,
668
+ context: inputElement === urlInput ? 'dropdown' : 'modal'
669
+ };
670
+
671
+ if (detail.context === 'dropdown') {
672
+ hideDropdown();
673
+ } else {
674
+ closeMobileModal();
675
+ }
676
+
677
+ showCreateLauncherModal(detail);
678
+
679
+ if (typeof options.onCreate === 'function') {
680
+ options.onCreate(detail);
681
+ }
682
+
683
+ if (typeof CustomEvent === 'function') {
684
+ container.dispatchEvent(new CustomEvent('urlDropdownCreate', { detail }));
685
+ }
686
+ });
687
+ }
688
+
689
+ function showCreateLauncherModal(detail) {
690
+ const modal = getCreateLauncherModal();
691
+ pendingCreateDetail = detail;
692
+
693
+ modal.error.textContent = '';
694
+ modal.overlay.style.display = 'flex';
695
+
696
+ // const defaultName = generateFolderName(detail.prompt);
697
+ modal.input.value = "";
698
+ modal.description.textContent = detail.prompt
699
+ ? `Prompt: ${detail.prompt}`
700
+ : 'Enter a prompt in the search bar to describe your launcher.';
701
+ modal.input.focus();
702
+ modal.input.select();
703
+ document.addEventListener('keydown', handleCreateModalEscape, true);
704
+ }
705
+
706
+ function hideCreateLauncherModal() {
707
+ const modal = createLauncherModal;
708
+ if (!modal) return;
709
+ modal.overlay.style.display = 'none';
710
+ pendingCreateDetail = null;
711
+ document.removeEventListener('keydown', handleCreateModalEscape, true);
712
+ }
713
+
714
+ function confirmCreateLauncherModal() {
715
+ if (!createLauncherModal || !pendingCreateDetail) return;
716
+ const folderName = createLauncherModal.input.value.trim();
717
+ if (!folderName) {
718
+ createLauncherModal.error.textContent = 'Please enter a folder name.';
719
+ createLauncherModal.input.focus();
720
+ return;
721
+ }
722
+
723
+ const prompt = pendingCreateDetail.prompt || '';
724
+ const redirectUrl = `/pro?name=${encodeURIComponent(folderName)}&message=${encodeURIComponent(prompt)}`;
725
+ hideCreateLauncherModal();
726
+ window.location.href = redirectUrl;
727
+ }
728
+
729
+ function handleCreateModalKeydown(event) {
730
+ if (event.key === 'Enter') {
731
+ event.preventDefault();
732
+ confirmCreateLauncherModal();
733
+ }
734
+ }
735
+
736
+ function handleCreateModalEscape(event) {
737
+ if (event.key === 'Escape' && pendingCreateDetail) {
738
+ event.preventDefault();
739
+ hideCreateLauncherModal();
740
+ }
741
+ }
742
+
743
+ function getCreateLauncherModal() {
744
+ if (createLauncherModal) {
745
+ return createLauncherModal;
746
+ }
747
+
748
+ const overlay = document.createElement('div');
749
+ overlay.className = 'create-launcher-modal-overlay';
750
+ overlay.style.display = 'none';
751
+
752
+ const modalContent = document.createElement('div');
753
+ modalContent.className = 'create-launcher-modal';
754
+
755
+ const title = document.createElement('h3');
756
+ title.textContent = 'Create';
757
+
758
+ const description = document.createElement('p');
759
+ description.className = 'create-launcher-modal-description';
760
+ description.textContent = 'Enter a prompt in the search bar to describe your launcher.';
761
+
762
+ const label = document.createElement('label');
763
+ label.className = 'create-launcher-modal-label';
764
+ label.textContent = 'Folder name';
765
+
766
+ const input = document.createElement('input');
767
+ input.type = 'text';
768
+ input.className = 'create-launcher-modal-input';
769
+ input.placeholder = 'example: my-launcher';
770
+
771
+ const error = document.createElement('div');
772
+ error.className = 'create-launcher-modal-error';
773
+
774
+ const actions = document.createElement('div');
775
+ actions.className = 'create-launcher-modal-actions';
776
+
777
+ const cancelButton = document.createElement('button');
778
+ cancelButton.type = 'button';
779
+ cancelButton.className = 'create-launcher-modal-button cancel';
780
+ cancelButton.textContent = 'Cancel';
781
+
782
+ const confirmButton = document.createElement('button');
783
+ confirmButton.type = 'button';
784
+ confirmButton.className = 'create-launcher-modal-button confirm';
785
+ confirmButton.textContent = 'OK';
786
+
787
+ actions.appendChild(cancelButton);
788
+ actions.appendChild(confirmButton);
789
+
790
+ label.appendChild(input);
791
+ modalContent.appendChild(title);
792
+ modalContent.appendChild(description);
793
+ modalContent.appendChild(label);
794
+ modalContent.appendChild(error);
795
+ modalContent.appendChild(actions);
796
+ overlay.appendChild(modalContent);
797
+ document.body.appendChild(overlay);
798
+
799
+ overlay.addEventListener('click', function(event) {
800
+ if (event.target === overlay) {
801
+ hideCreateLauncherModal();
802
+ }
803
+ });
804
+
805
+ cancelButton.addEventListener('click', hideCreateLauncherModal);
806
+ confirmButton.addEventListener('click', confirmCreateLauncherModal);
807
+ input.addEventListener('keydown', handleCreateModalKeydown);
808
+
809
+ createLauncherModal = {
810
+ overlay,
811
+ modal: modalContent,
812
+ input,
813
+ cancelButton,
814
+ confirmButton,
815
+ error,
816
+ description
817
+ };
818
+
819
+ return createLauncherModal;
820
+ }
821
+
822
+ function generateFolderName(prompt) {
823
+ if (!prompt) return '';
824
+ const normalized = prompt
825
+ .toLowerCase()
826
+ .replace(/[^a-z0-9\-\s_]/g, '')
827
+ .replace(/[\s_]+/g, '-');
828
+ return normalized.replace(/^-+|-+$/g, '').slice(0, 50);
829
+ }
636
830
  }
637
831
 
638
832
  // Auto-initialize if DOM is already loaded, otherwise wait for it
@@ -69,7 +69,7 @@ body {
69
69
  }
70
70
  .item .title {
71
71
  text-decoration: none;
72
- color: royalblue;
72
+ color: rgba(127, 91, 243, 0.9);
73
73
  }
74
74
  .item .col {
75
75
  padding: 10px;
@@ -6,7 +6,7 @@
6
6
  body.error-body {
7
7
  width: 100%;
8
8
  margin: 0;
9
- background: royalblue;
9
+ background: rgba(127, 91, 243, 0.9);
10
10
  color: white;
11
11
  padding: 50px;
12
12
  box-sizing: border-box;
@@ -422,7 +422,7 @@ body .frame-link.selected {
422
422
  /*
423
423
  background: rgba(0,0,0,0.06) !important;
424
424
  */
425
- background: royalblue !important;
425
+ background: rgba(127, 91, 243, 0.9) !important;
426
426
  color: white !important;
427
427
  }
428
428
  .frame-link.selected .del {
@@ -442,8 +442,8 @@ body.dark .frame-link.selected {
442
442
  flex-shrink: 0;
443
443
  }
444
444
  .loader .btn:hover {
445
- color: royalblue;
446
- border-color: royalblue;
445
+ color: rgba(127, 91, 243, 0.9);
446
+ border-color: rgba(127, 91, 243, 0.9);
447
447
  }
448
448
  .loader .btn {
449
449
  padding: 4px 8px;
@@ -648,7 +648,7 @@ nav .logo {
648
648
  .error-message {
649
649
  width: 100%;
650
650
  /*
651
- background: royalblue !important;
651
+ background: rgba(127, 91, 243, 0.9) !important;
652
652
  */
653
653
  color: white;
654
654
  display: flex;
@@ -792,15 +792,11 @@ body.dark .submenu {
792
792
  overflow: auto;
793
793
  display: flex;
794
794
  flex-grow: 1;
795
- /*
796
795
  border-top: 1px solid rgba(0, 0,0 ,0.04);
797
- */
798
796
  }
799
- /*
800
797
  body.dark .appcanvas {
801
798
  border-top: 1px solid rgba(255,255,255,0.04);
802
799
  }
803
- */
804
800
  .filler {
805
801
  display: none;
806
802
  }
@@ -841,10 +837,12 @@ body.dark .top-menu .btn2.selected {
841
837
  }
842
838
 
843
839
  body.dark #fs-status {
844
- background: rgba(255,255,255,0.04);
840
+ border-bottom: 1px solid rgba(255,255,255,0.04);
845
841
  }
846
842
  #fs-status {
843
+ /*
847
844
  background: rgba(0,0,0,0.04);
845
+ */
848
846
  gap: 5px;
849
847
  box-sizing: border-box;
850
848
  /*
@@ -856,7 +854,8 @@ body.dark #fs-status {
856
854
  /*
857
855
  border-radius: 6px;
858
856
  */
859
- justify-content: flex-end;
857
+ justify-content: flex-start;
858
+ border-bottom: 1px solid rgba(0,0,0,0.04);
860
859
  }
861
860
 
862
861
  .fs-status-btn {
@@ -1185,7 +1184,7 @@ body.dark .pinokio-commit-message-input:focus {
1185
1184
  }
1186
1185
 
1187
1186
  .pinokio-save-version-btn {
1188
- background: #1f883d !important;
1187
+ background: rgba(127, 91, 243, 0.9) !important;
1189
1188
  color: white !important;
1190
1189
  border: 1px solid rgba(31, 35, 40, 0.15) !important;
1191
1190
  border-radius: 6px !important;
@@ -1865,7 +1864,10 @@ body.minimized #fs-status {
1865
1864
  </div>
1866
1865
  <div class='menu-container'>
1867
1866
  <div class='m n system' data-type="n">
1867
+ <a target="<%=src%>" href="<%=src%>" class='btn header-item frame-link' data-index="0" data-mode="refresh">
1868
+ <!--
1868
1869
  <a id='file-browse' target="file-browse" href="<%=asset%>" class='btn header-item frame-link' data-index="0">
1870
+ -->
1869
1871
  <div class='tab'>
1870
1872
  <i class="fa-regular fa-folder-open"></i> Files
1871
1873
  </div>
@@ -1903,20 +1905,6 @@ body.minimized #fs-status {
1903
1905
  <%- include('./partials/dynamic', { dynamic: plugin_menu, }) %>
1904
1906
  <% } %>
1905
1907
  </div>
1906
- <% if (false) { %>
1907
- <% if (type !== "run") { %>
1908
- <% if (plugin_menu) { %>
1909
- <%- include('./partials/dynamic', { dynamic: plugin_menu, }) %>
1910
- <% } else { %>
1911
- <div class="nested-menu">
1912
- <div class="btn header-item frame-link selected">
1913
- <div class="tab"><i class="fa-solid fa-code"></i> Dev</div>
1914
- <div class="loader"><i class="fa-solid fa-circle-notch fa-spin"></i></div>
1915
- </div>
1916
- </div>
1917
- <% } %>
1918
- <% } %>
1919
- <% } %>
1920
1908
  </div>
1921
1909
  <% if (type === "browse") { %>
1922
1910
  <div class='nested-menu selected git blue'>
@@ -3050,7 +3038,7 @@ body.minimized #fs-status {
3050
3038
  const try_dynamic = async () => {
3051
3039
  let rendered
3052
3040
  let status
3053
- const dynamic = await fetch("<%=dynamic%>").then((res) => {
3041
+ const dynamic = await fetch("<%-dynamic%>").then((res) => {
3054
3042
  status = res.status
3055
3043
  return res.text()
3056
3044
  })
@@ -3065,10 +3053,16 @@ body.minimized #fs-status {
3065
3053
  })
3066
3054
  return
3067
3055
  }
3056
+ console.log("DYNAMIC", dynamic)
3068
3057
  if (dynamic && dynamic.length > 0) {
3069
3058
  if (document.querySelector(".dynamic .submenu")) {
3070
3059
  document.querySelector(".dynamic .submenu").innerHTML = dynamic
3071
3060
  }
3061
+ let default_selection = document.querySelector(".dynamic .submenu [data-default]")
3062
+ if (default_selection) {
3063
+ console.log("CLICK default")
3064
+ default_selection.click()
3065
+ }
3072
3066
  rendered = true
3073
3067
  } else {
3074
3068
  rendered = false
@@ -3235,8 +3229,9 @@ body.minimized #fs-status {
3235
3229
  if (selection_url) {
3236
3230
  selection = document.querySelector(`[href='${selection_url}']`)
3237
3231
  } else {
3238
- selection = document.querySelector("#devtab")
3232
+ // selection = document.querySelector("#devtab")
3239
3233
  }
3234
+ console.log("Selection", selection)
3240
3235
  if (selection) {
3241
3236
  // setTimeout(() => {
3242
3237
  selection.click()
@@ -4062,15 +4057,16 @@ body.minimized #fs-status {
4062
4057
  let selected_tab = document.querySelector("aside .frame-link.selected")
4063
4058
  let needs_selection
4064
4059
  if (selected_tab) {
4065
- if (selected_tab.target === foreground_frame.name) {
4066
- needs_selection = false
4060
+ if (foreground_frame) {
4061
+ if (selected_tab.target === foreground_frame.name) {
4062
+ // do nothing
4063
+ } else {
4064
+ selected_tab.click()
4065
+ }
4067
4066
  } else {
4068
- needs_selection = true
4067
+ selected_tab.click()
4069
4068
  }
4070
4069
  } else {
4071
- needs_selection = true
4072
- }
4073
- if (needs_selection) {
4074
4070
  let selection_tab = document.querySelector(`aside .frame-link[target="${foreground_frame.name}"]`)
4075
4071
  selection_tab.click()
4076
4072
  }
@@ -0,0 +1,197 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Pinokio Create Bookmarklet</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light dark;
10
+ font-family: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
11
+ line-height: 1.55;
12
+ }
13
+ body {
14
+ margin: 0;
15
+ padding: 24px 18px 48px;
16
+ background: rgba(248, 250, 255, 0.92);
17
+ color: #0f172a;
18
+ }
19
+ body.dark {
20
+ background: #0f172a;
21
+ color: rgba(226, 232, 240, 0.96);
22
+ }
23
+ .page {
24
+ max-width: 720px;
25
+ margin: 0 auto;
26
+ display: flex;
27
+ flex-direction: column;
28
+ gap: 20px;
29
+ background: rgba(255, 255, 255, 0.88);
30
+ border-radius: 20px;
31
+ padding: 32px 36px;
32
+ box-shadow: 0 40px 100px rgba(15, 23, 42, 0.25);
33
+ }
34
+ body.dark .page {
35
+ background: rgba(17, 24, 39, 0.85);
36
+ box-shadow: 0 50px 120px rgba(2, 6, 20, 0.65);
37
+ }
38
+ h1 {
39
+ margin: 0;
40
+ font-size: 28px;
41
+ letter-spacing: -0.01em;
42
+ }
43
+ p {
44
+ margin: 0;
45
+ }
46
+ .bookmarklet-button {
47
+ display: inline-flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ padding: 12px 18px;
51
+ border-radius: 999px;
52
+ font-weight: 600;
53
+ font-size: 15px;
54
+ text-decoration: none;
55
+ color: #fff;
56
+ background: linear-gradient(135deg, rgba(127, 91, 243, 0.95), rgba(84, 63, 196, 0.95));
57
+ box-shadow: 0 18px 40px rgba(111, 76, 242, 0.32);
58
+ cursor: grab;
59
+ }
60
+ .bookmarklet-button:active {
61
+ cursor: grabbing;
62
+ }
63
+ .steps {
64
+ list-style: decimal;
65
+ margin: 0 0 0 20px;
66
+ padding: 0;
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: 10px;
70
+ font-size: 15px;
71
+ }
72
+ .code-block {
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: 8px;
76
+ }
77
+ textarea {
78
+ width: 100%;
79
+ min-height: 120px;
80
+ border-radius: 14px;
81
+ border: 1px solid rgba(148, 163, 184, 0.3);
82
+ background: rgba(248, 250, 255, 0.7);
83
+ padding: 14px 16px;
84
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
85
+ font-size: 14px;
86
+ color: inherit;
87
+ resize: vertical;
88
+ }
89
+ body.dark textarea {
90
+ background: rgba(30, 41, 59, 0.6);
91
+ border-color: rgba(148, 163, 184, 0.35);
92
+ }
93
+ button.copy-btn {
94
+ align-self: flex-start;
95
+ padding: 10px 16px;
96
+ border-radius: 999px;
97
+ border: none;
98
+ font-weight: 600;
99
+ font-size: 14px;
100
+ cursor: pointer;
101
+ color: #fff;
102
+ background: rgba(15, 23, 42, 0.85);
103
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.25);
104
+ }
105
+ body.dark button.copy-btn {
106
+ background: rgba(148, 163, 184, 0.85);
107
+ color: #0f172a;
108
+ box-shadow: 0 12px 28px rgba(15, 23, 42, 0.45);
109
+ }
110
+ small {
111
+ opacity: 0.75;
112
+ font-size: 13px;
113
+ }
114
+ @media (max-width: 640px) {
115
+ body {
116
+ padding: 18px 12px 36px;
117
+ }
118
+ .page {
119
+ padding: 24px;
120
+ border-radius: 16px;
121
+ }
122
+ h1 {
123
+ font-size: 24px;
124
+ }
125
+ }
126
+ </style>
127
+ </head>
128
+ <body class="<%= theme %>">
129
+ <main class="page">
130
+ <header>
131
+ <h1>Pinokio “Create” Bookmarklet</h1>
132
+ <p>Drop this bookmarklet onto your bookmarks bar. Clicking it on any site opens Pinokio’s Create modal with that page’s URL pre-filled in the prompt.</p>
133
+ </header>
134
+
135
+ <section>
136
+ <a class="bookmarklet-button" href="<%- bookmarkletHref %>">Pinokio Create</a>
137
+ <small>Tip: Drag the button to your bookmarks bar, or right-click → “Add to Favorites/Bookmarks”.</small>
138
+ </section>
139
+
140
+ <section>
141
+ <ol class="steps">
142
+ <li>Make sure your bookmarks bar is visible (View → Toolbars → Bookmarks Toolbar, or press <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> + <kbd>Shift</kbd> + <kbd>B</kbd>).</li>
143
+ <li>Drag the “Pinokio Create” button above to the bar.</li>
144
+ <li>When you are on a page you want to automate, click the bookmark. Pinokio will open at <code><%= baseUrl %>/?create=1</code> with the current page URL in the prompt field.</li>
145
+ </ol>
146
+ </section>
147
+
148
+ <section class="code-block">
149
+ <strong>Manual option</strong>
150
+ <p>If dragging doesn’t work, create a new bookmark manually and paste the code below as the URL.</p>
151
+ <textarea id="bookmarklet-code" readonly><%- bookmarkletHref %></textarea>
152
+ <button type="button" class="copy-btn" data-copy>Copy bookmarklet code</button>
153
+ <small class="copy-status" hidden>Copied!</small>
154
+ </section>
155
+ </main>
156
+
157
+ <script>
158
+ (function() {
159
+ const copyBtn = document.querySelector('[data-copy]');
160
+ const textarea = document.getElementById('bookmarklet-code');
161
+ const status = document.querySelector('.copy-status');
162
+
163
+ if (!copyBtn || !textarea) {
164
+ return;
165
+ }
166
+
167
+ copyBtn.addEventListener('click', async () => {
168
+ const text = textarea.value;
169
+ try {
170
+ if (navigator.clipboard && navigator.clipboard.writeText) {
171
+ await navigator.clipboard.writeText(text);
172
+ } else {
173
+ textarea.select();
174
+ document.execCommand('copy');
175
+ }
176
+ if (status) {
177
+ status.hidden = false;
178
+ status.textContent = 'Copied!';
179
+ setTimeout(() => {
180
+ status.hidden = true;
181
+ }, 1800);
182
+ }
183
+ } catch (error) {
184
+ if (status) {
185
+ status.hidden = false;
186
+ status.textContent = 'Copy failed. Please copy manually.';
187
+ setTimeout(() => {
188
+ status.hidden = true;
189
+ status.textContent = 'Copied!';
190
+ }, 2600);
191
+ }
192
+ }
193
+ });
194
+ })();
195
+ </script>
196
+ </body>
197
+ </html>
@@ -81,7 +81,7 @@ body {
81
81
  }
82
82
  .item .title {
83
83
  text-decoration: none;
84
- color: royalblue;
84
+ color: rgba(127, 91, 243, 0.9);
85
85
  }
86
86
  .item .col {
87
87
  padding: 10px;
@@ -111,7 +111,7 @@ main {
111
111
  width: 100%;
112
112
  }
113
113
  a {
114
- color: royalblue;
114
+ color: rgba(127, 91, 243, 0.9);
115
115
  }
116
116
  body.dark hr {
117
117
  background: rgba(255,255,255,0.05);
@@ -135,7 +135,7 @@ td {
135
135
  }
136
136
  td.key {
137
137
  width: 150px;
138
- color: royalblue;
138
+ color: rgba(127, 91, 243, 0.9);
139
139
  }
140
140
  .head {
141
141
  padding: 30px;
@@ -167,7 +167,7 @@ pre {
167
167
  margin: 20px;
168
168
  }
169
169
  .card:hover {
170
- color: royalblue !important;
170
+ color: rgba(127, 91, 243, 0.9) !important;
171
171
  }
172
172
  .card .desc {
173
173
  opacity: 0.7;