@digital-ai/dot-illustrations 2.0.32 → 2.0.35

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/demo/script.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Make currentTab and root globally accessible
2
2
  const root = document.documentElement;
3
- let currentTab = 'all';
3
+ let currentTab = 'all';
4
+ let alphabetFilter = ''; // '' = show all letters
4
5
 
5
6
  function myFunction() {
6
7
  var input, filter, divs, i, div, txtValue;
@@ -69,20 +70,18 @@ window.copyCode = function(button) {
69
70
  const origCopyText = window.copyText;
70
71
  window.copyText = function(button) {
71
72
  var parentDiv = button.closest('.copy-container');
72
- var copyText = parentDiv.querySelector('.iconID');
73
- var illustrationId = copyText.textContent;
74
- var dotCode = `<DotIllustration illustrationId=\"${illustrationId}\" />`;
73
+ var idEl = parentDiv.querySelector('.iconID');
74
+ var itemId = idEl.textContent;
75
+ var isInteg = integrationsList && integrationsList.includes(itemId) && !globalList.concat(dashboardsList).includes(itemId);
76
+ var dotCode = isInteg
77
+ ? `<span class="dot-integration"><img class="${itemId}"/></span>`
78
+ : `<DotIllustration illustrationId="${itemId}" />`;
75
79
  navigator.clipboard.writeText(dotCode).then(function() {
76
80
  var tooltip = button.querySelector('.tooltiptext');
77
81
  tooltip.innerHTML = "Copied Dot Code";
78
82
  tooltip.style.visibility = 'visible';
79
- setTimeout(function() {
80
- tooltip.innerHTML = "Copy Dot Code";
81
- tooltip.style.visibility = 'hidden';
82
- }, 2000);
83
+ setTimeout(function() { tooltip.innerHTML = "Copy Dot Code"; tooltip.style.visibility = 'hidden'; }, 2000);
83
84
  showToast('Copied Dot Code: ' + dotCode);
84
- }, function(err) {
85
- console.error('Failed to copy dot code: ', err);
86
85
  });
87
86
  }
88
87
 
@@ -117,43 +116,54 @@ document.addEventListener('DOMContentLoaded', () => {
117
116
  const mainTabs = document.querySelectorAll('.tab-btn');
118
117
  const floatingTabs = document.querySelectorAll('.floating-tab');
119
118
 
119
+ function updateTabUI(tab) {
120
+ [...mainTabs, ...floatingTabs].forEach(t => {
121
+ const isActive = t.dataset.tab === tab;
122
+ t.classList.toggle('text-blue-600', isActive);
123
+ t.classList.toggle('dark:text-blue-400', isActive);
124
+ t.classList.toggle('bg-blue-100', isActive);
125
+ t.classList.toggle('dark:bg-blue-900', isActive);
126
+ t.classList.toggle('text-gray-700', !isActive && t.dataset.tab !== 'upload');
127
+ t.classList.toggle('dark:text-gray-300', !isActive && t.dataset.tab !== 'upload');
128
+ t.setAttribute('aria-selected', isActive ? 'true' : 'false');
129
+ });
130
+ }
131
+
120
132
  function setTab(tab) {
121
133
  currentTab = tab;
122
- // Update main tabs
123
- mainTabs.forEach(t => {
124
- t.classList.remove('text-blue-600', 'dark:text-blue-400', 'bg-blue-100', 'dark:bg-blue-900');
125
- t.classList.add('text-gray-700', 'dark:text-gray-300');
126
- t.setAttribute('aria-selected', 'false');
127
- if (t.dataset.tab === tab) {
128
- t.classList.add('text-blue-600', 'dark:text-blue-400', 'bg-blue-100', 'dark:bg-blue-900');
129
- t.classList.remove('text-gray-700', 'dark:text-gray-300');
130
- t.setAttribute('aria-selected', 'true');
131
- }
132
- });
133
- // Update floating tabs
134
- floatingTabs.forEach(t => {
135
- t.classList.remove('text-blue-600', 'dark:text-blue-400', 'bg-blue-100', 'dark:bg-blue-900');
136
- t.classList.add('text-gray-700', 'dark:text-gray-300');
137
- t.setAttribute('aria-selected', 'false');
138
- if (t.dataset.tab === tab) {
139
- t.classList.add('text-blue-600', 'dark:text-blue-400', 'bg-blue-100', 'dark:bg-blue-900');
140
- t.classList.remove('text-gray-700', 'dark:text-gray-300');
141
- t.setAttribute('aria-selected', 'true');
142
- }
143
- });
144
- renderIllustrations();
134
+ alphabetFilter = '';
135
+ updateTabUI(tab);
136
+
137
+ const uploadPanel = document.getElementById('upload-panel');
138
+ const grid = document.getElementById('illustration-list');
139
+ const alphaStrip = document.getElementById('alphabet-strip');
140
+ const integNote = document.getElementById('integrations-note');
141
+
142
+ if (tab === 'upload') {
143
+ uploadPanel.classList.remove('hidden');
144
+ grid.classList.add('hidden');
145
+ alphaStrip.classList.add('hidden');
146
+ integNote.classList.add('hidden');
147
+ document.getElementById('result-count').textContent = '';
148
+ updateUploadAuthUI();
149
+ } else {
150
+ uploadPanel.classList.add('hidden');
151
+ grid.classList.remove('hidden');
152
+ // Show alphabet strip only for tabs that have many items
153
+ const showStrip = ['all','global','dashboards','integrations','favourites'].includes(tab);
154
+ alphaStrip.classList.toggle('hidden', !showStrip);
155
+ integNote.classList.toggle('hidden', tab !== 'integrations');
156
+ renderAlphabetStrip();
157
+ renderIllustrations();
158
+ }
145
159
  }
146
160
 
147
- mainTabs.forEach(tab => {
148
- tab.addEventListener('click', function() {
149
- setTab(this.dataset.tab);
161
+ function wireTabs() {
162
+ document.querySelectorAll('.tab-btn').forEach(tab => {
163
+ tab.addEventListener('click', function() { setTab(this.dataset.tab); });
150
164
  });
151
- });
152
- floatingTabs.forEach(tab => {
153
- tab.addEventListener('click', function() {
154
- setTab(this.dataset.tab);
155
- });
156
- });
165
+ }
166
+ wireTabs();
157
167
 
158
168
  // --- ILLUSTRATION DATA ---
159
169
  const globalList = [
@@ -168,6 +178,7 @@ document.addEventListener('DOMContentLoaded', () => {
168
178
  "commitrepo",
169
179
  "chart",
170
180
  "custom-dashboards",
181
+ "delete",
171
182
  "dependency-down",
172
183
  "dependency-up",
173
184
  "disconnected",
@@ -190,6 +201,7 @@ document.addEventListener('DOMContentLoaded', () => {
190
201
  "nothing-defined",
191
202
  "password-token",
192
203
  "protection-in-progress",
204
+ "remote-cloud",
193
205
  "reports","scan-document",
194
206
  "releases",
195
207
  "survey",
@@ -227,7 +239,28 @@ document.addEventListener('DOMContentLoaded', () => {
227
239
  "data-extraction",
228
240
  "workflow"
229
241
  ];
230
- const illustrations = [...globalList, ...dashboardsList, "my-new-illustration"];
242
+ const illustrations = [...globalList, ...dashboardsList];
243
+
244
+ const integrationsList = [
245
+ "agility","amazon-eks","ansible","apache-tomcat","app-aware","app-dynamics",
246
+ "app-management","application-protection","aras","arc-sight","argocd","atlassian",
247
+ "avaya","aws","azure","azure-aks-2","azure-devops-tfs","azure-functions",
248
+ "bamboo","bit-bucket","biztalk","black-duck","blazemeter","bmc-helix",
249
+ "checkmarx","chef","chrome","cisco","citrix","clarity-ppm","cloud-foundry",
250
+ "continuostesting","cyberark","dai-logo","delphix","deploy","docker","docker-compose",
251
+ "dynatrace","edge","essential-app-protection","explorer","f5","firefox",
252
+ "flux-cd","fortify","fortify-on-demand","free-marker","git-ops","github",
253
+ "github-actions","gitlab","google","google-cloud","google-cloud-functions",
254
+ "hashi-corp-vault","helm","ibm","ingress","inteligence","jboss","jenkins",
255
+ "jfrog","jira","key","kubernetes","linux","mailhog","microsoft-iis",
256
+ "microsoft-teams","mysql","netscaler","new-relic","octopus-deploy",
257
+ "open-policy-agent","openid","openshift","opsgenie","oracle","parasoft",
258
+ "pendo","planview","podman","powershell","progress-chef","puppet","python",
259
+ "rabbitmq","release","safari","salesforce","servicenow","skytab","slack",
260
+ "sonarcloud","sonarqube","sonatype","svn","team-forge","teamcity","terraform",
261
+ "testlink","topaz","travis-ci","urbancode-deploy","vmware","webhook",
262
+ "windows","xray","zendesk",
263
+ ];
231
264
 
232
265
  // --- FAVOURITES LOGIC ---
233
266
  function getFavourites() {
@@ -269,43 +302,44 @@ document.addEventListener('DOMContentLoaded', () => {
269
302
  const modalClose = document.getElementById('modal-close');
270
303
  const modalCloseBottom = document.getElementById('modal-close-bottom');
271
304
 
272
- function openModal(illustration) {
305
+ function openModal(item, type) {
273
306
  if (modalDescription) modalDescription.innerHTML = '';
274
307
  if (!descriptionsLoaded) return;
308
+
275
309
  const themeClass = root.classList.contains('dark') ? 'dark' : 'light';
276
- let categoryFolder = "";
277
- if (globalList.includes(illustration)) {
278
- categoryFolder = "global";
279
- } else if (dashboardsList.includes(illustration)) {
280
- categoryFolder = "dashboards";
310
+ const isInteg = type === 'integration' || integrationsList.includes(item) && !illustrations.includes(item);
311
+
312
+ let imgHtml, dotCodeString, codeString;
313
+ if (isInteg) {
314
+ imgHtml = `<span class="dot-integration" style="display:inline-flex;align-items:center;justify-content:center;width:200px;height:200px;"><img class="${item}" style="width:100%;height:100%;object-fit:contain;"/></span>`;
315
+ dotCodeString = `<span class="dot-integration"><img class="${item}"/></span>`;
316
+ codeString = dotCodeString;
317
+ } else {
318
+ const cat = globalList.includes(item) ? 'global' : 'dashboards';
319
+ const svgPath = `illustrations/${themeClass}/${cat}/${item}.svg`;
320
+ imgHtml = `<span class="dot-illustration"><img src="${svgPath}" class="${item} ${themeClass}" style="width:286px;height:286px;object-fit:contain;"/></span>`;
321
+ dotCodeString = `<DotIllustration illustrationId="${item}" />`;
322
+ codeString = `<span class="dot-illustration"><img src="${svgPath}" class="${item} ${themeClass}"/></span>`;
281
323
  }
282
- const svgPath = `illustrations/${themeClass}/${categoryFolder}/${illustration}.svg`;
283
- modalImage.innerHTML = `<span class=\"dot-illustration\"><img src=\"${svgPath}\" class=\"${illustration} ${themeClass}\" style=\"width:286px;height:286px;object-fit:contain;\"/></span>`;
284
- // Set the illustration name
324
+
325
+ modalImage.innerHTML = imgHtml;
285
326
  const modalIllustrationName = document.getElementById('modal-illustration-name');
286
- if (modalIllustrationName) modalIllustrationName.textContent = illustration;
287
- // Set the Dot Code textarea value
288
- const dotCodeString = `<DotIllustration illustrationId=\"${illustration}\" />`;
327
+ if (modalIllustrationName) modalIllustrationName.textContent = item;
289
328
  const modalDotCode = document.getElementById('modal-dot-code');
290
329
  if (modalDotCode) modalDotCode.value = dotCodeString;
291
- // Show the HTML code snippet in the modal
292
- const codeString = `<span class=\"dot-illustration\"><img src=\"${svgPath}\" class=\"${illustration} ${themeClass}\"/></span>`;
293
330
  modalCode.value = codeString;
331
+
294
332
  modal.classList.add('active');
295
333
  modal.classList.remove('hidden');
296
- // Attach Copy Dot Code button event listener each time modal opens
334
+
297
335
  const modalCopyDotCode = document.getElementById('modal-copy-dot-code');
298
336
  if (modalCopyDotCode) {
299
- modalCopyDotCode.onclick = function() {
337
+ modalCopyDotCode.onclick = () => {
300
338
  if (modalDotCode) {
301
- navigator.clipboard.writeText(modalDotCode.value).then(function() {
339
+ navigator.clipboard.writeText(modalDotCode.value).then(() => {
302
340
  modalCopyDotCode.textContent = 'Copied!';
303
341
  showToast('Copied Dot Code: ' + modalDotCode.value);
304
- setTimeout(() => {
305
- modalCopyDotCode.textContent = 'Copy Dot Code';
306
- }, 2000);
307
- }, function(err) {
308
- console.error('Failed to copy dot code: ', err);
342
+ setTimeout(() => { modalCopyDotCode.textContent = 'Copy Dot Code'; }, 2000);
309
343
  });
310
344
  }
311
345
  };
@@ -324,75 +358,139 @@ document.addEventListener('DOMContentLoaded', () => {
324
358
  showToast('Copied code: ' + modalCode.value);
325
359
  });
326
360
 
327
- // --- ILLUSTRATION RENDERING ---
361
+ // --- ALPHABET STRIP ---
362
+ function renderAlphabetStrip() {
363
+ const strip = document.getElementById('alphabet-strip');
364
+ if (!strip) return;
365
+
366
+ const pool = getCurrentPool('');
367
+ const available = new Set(pool.map(id => id[0].toUpperCase()).filter(c => /[A-Z]/.test(c)));
368
+ const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
369
+
370
+ strip.innerHTML = `
371
+ <button class="alpha-btn px-2 py-0.5 text-xs rounded font-mono ${alphabetFilter === '' ? 'bg-blue-600 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'} transition" data-letter="">All</button>
372
+ ${letters.map(l => `
373
+ <button class="alpha-btn px-2 py-0.5 text-xs rounded font-mono ${
374
+ available.has(l)
375
+ ? alphabetFilter === l
376
+ ? 'bg-blue-600 text-white'
377
+ : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-blue-100 dark:hover:bg-blue-800 transition'
378
+ : 'opacity-30 cursor-default bg-gray-100 dark:bg-gray-800 text-gray-400'
379
+ }" data-letter="${l}" ${available.has(l) ? '' : 'disabled'}>${l}</button>
380
+ `).join('')}
381
+ `;
382
+ strip.querySelectorAll('.alpha-btn:not([disabled])').forEach(btn => {
383
+ btn.addEventListener('click', () => {
384
+ alphabetFilter = btn.dataset.letter;
385
+ renderAlphabetStrip();
386
+ renderIllustrations();
387
+ });
388
+ });
389
+ }
390
+
391
+ function getCurrentPool(filterOverride) {
392
+ const f = filterOverride !== undefined ? filterOverride : alphabetFilter;
393
+ let base = [];
394
+ if (currentTab === 'all') base = illustrations;
395
+ else if (currentTab === 'global') base = globalList;
396
+ else if (currentTab === 'dashboards') base = dashboardsList;
397
+ else if (currentTab === 'integrations') base = integrationsList;
398
+ else if (currentTab === 'favourites') base = [...illustrations, ...integrationsList].filter(id => getFavourites().includes(id));
399
+ if (f) base = base.filter(id => id[0].toUpperCase() === f);
400
+ return base;
401
+ }
402
+
403
+ // --- ILLUSTRATION + INTEGRATION RENDERING ---
328
404
  function renderIllustrations() {
329
405
  const list = document.getElementById("illustration-list");
330
406
  list.innerHTML = "";
331
- let toShow = [];
332
- if (currentTab === 'all') {
333
- toShow = illustrations;
334
- } else if (currentTab === 'global') {
335
- toShow = globalList;
336
- } else if (currentTab === 'dashboards') {
337
- toShow = dashboardsList;
338
- } else if (currentTab === 'favourites') {
339
- const favs = getFavourites();
340
- toShow = illustrations.filter(id => favs.includes(id));
341
- }
342
- const filter = (document.getElementById('myInput')?.value || '').toUpperCase();
343
- toShow = toShow.filter(id => id.toUpperCase().includes(filter));
407
+ const searchFilter = (document.getElementById('myInput')?.value || '').toUpperCase();
408
+ let toShow = getCurrentPool().filter(id => id.toUpperCase().includes(searchFilter));
409
+
410
+ const countEl = document.getElementById('result-count');
411
+ if (countEl) countEl.textContent = toShow.length > 0 ? `${toShow.length} item${toShow.length === 1 ? '' : 's'}` : '';
412
+
344
413
  if (toShow.length === 0) {
345
- list.innerHTML = '<div class="col-span-full text-center text-gray-500 dark:text-gray-400">No illustrations found.</div>';
414
+ list.innerHTML = `<div class="col-span-full text-center text-gray-500 dark:text-gray-400 py-8">${
415
+ currentTab === 'favourites' ? 'No favourites yet.' : 'No items found.'
416
+ }</div>`;
346
417
  return;
347
418
  }
419
+
420
+ const isIntegrations = currentTab === 'integrations' ||
421
+ (currentTab === 'favourites' && toShow.every(id => integrationsList.includes(id)));
348
422
  const favs = getFavourites();
349
423
  const themeClass = root.classList.contains('dark') ? 'dark' : 'light';
350
- toShow.forEach(illustration => {
351
- let categoryFolder = "";
352
- if (globalList.includes(illustration)) {
353
- categoryFolder = "global";
354
- } else if (dashboardsList.includes(illustration)) {
355
- categoryFolder = "dashboards";
356
- }
357
- const svgPath = `illustrations/${themeClass}/${categoryFolder}/${illustration}.svg`;
358
- const isFav = favs.includes(illustration);
359
- const div = document.createElement("div");
360
- div.setAttribute("class", "copy-container relative group flex flex-col items-center justify-between bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-2 md:p-3 transition-transform duration-200 hover:scale-105 hover:shadow-2xl h-[302px] w-full justify-center");
361
- // Show the HTML code snippet in the card
362
- const codeString = `<span class=\"dot-illustration\"><img src=\"${svgPath}\" class=\"${illustration} ${themeClass}\"/></span>`;
363
- div.innerHTML = `
364
- <button class="absolute top-2 right-2 z-10 p-1 rounded-full bg-white/80 dark:bg-gray-800/80 hover:bg-yellow-100 dark:hover:bg-yellow-300 transition" title="Toggle favourite" onclick="event.stopPropagation(); window.toggleFavourite && window.toggleFavourite('${illustration}')">
365
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="${isFav ? 'gold' : 'none'}" viewBox="0 0 24 24" stroke="gold">
366
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l2.036 6.29a1 1 0 00.95.69h6.631c.969 0 1.371 1.24.588 1.81l-5.37 3.905a1 1 0 00-.364 1.118l2.036 6.29c.3.921-.755 1.688-1.54 1.118l-5.37-3.905a1 1 0 00-1.176 0l-5.37 3.905c-.784.57-1.838-.197-1.54-1.118l2.036-6.29a1 1 0 00-.364-1.118L2.342 11.717c-.783-.57-.38-1.81.588-1.81h6.631a1 1 0 00.95-.69l2.036-6.29z"/>
367
- </svg>
368
- </button>
369
- <div class="flex-1 flex flex-col justify-center items-center">
370
- <span class="dot-illustration">
371
- <img src="${svgPath}" class="${illustration} ${themeClass}"/>
372
- </span>
373
- <span class="iconID block text-center text-base font-medium text-gray-800 dark:text-gray-200 my-2">${illustration}</span>
374
- </div>
375
- <div class="button-group flex flex-row items-center justify-center gap-2 mt-2">
376
- <button id="CopyID" class="button flex items-center justify-center min-w-[80px] px-1.5 py-0.5 text-[11px] text-gray-400 border border-gray-300 dark:border-gray-700 rounded bg-white dark:bg-gray-800 font-normal hover:bg-blue-100 dark:hover:bg-blue-900 transition relative text-center" onmouseover="showTooltip(this)" onmouseout="hideTooltip(this)" onclick="copyText(this)">
377
- Copy Dot Code
378
- <span class="tooltiptext left-1/2 -translate-x-1/2 bottom-10 shadow-md">Copy Dot Code</span>
424
+
425
+ toShow.forEach(item => {
426
+ const isInteg = integrationsList.includes(item) && !illustrations.includes(item);
427
+ const isFav = favs.includes(item);
428
+
429
+ if (isInteg) {
430
+ // Integration card (single SVG, dot-integration wrapper)
431
+ const codeString = `<span class="dot-integration"><img class="${item}"/></span>`;
432
+ const div = document.createElement('div');
433
+ div.setAttribute('class', 'copy-container relative group flex flex-col items-center justify-between bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-2 md:p-3 transition-transform duration-200 hover:scale-105 hover:shadow-2xl h-[302px] w-full');
434
+ div.dataset.itemId = item;
435
+ div.dataset.itemType = 'integration';
436
+ div.innerHTML = `
437
+ <button class="absolute top-2 right-2 z-10 p-1 rounded-full bg-white/80 dark:bg-gray-800/80 hover:bg-yellow-100 dark:hover:bg-yellow-300 transition" title="Toggle favourite" onclick="event.stopPropagation();window.toggleFavourite('${item}')">
438
+ <svg class="h-5 w-5" fill="${isFav ? 'gold' : 'none'}" viewBox="0 0 24 24" stroke="gold"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l2.036 6.29a1 1 0 00.95.69h6.631c.969 0 1.371 1.24.588 1.81l-5.37 3.905a1 1 0 00-.364 1.118l2.036 6.29c.3.921-.755 1.688-1.54 1.118l-5.37-3.905a1 1 0 00-1.176 0l-5.37 3.905c-.784.57-1.838-.197-1.54-1.118l2.036-6.29a1 1 0 00-.364-1.118L2.342 11.717c-.783-.57-.38-1.81.588-1.81h6.631a1 1 0 00.95-.69l2.036-6.29z"/></svg>
379
439
  </button>
380
- <button class="button flex items-center justify-center min-w-[80px] px-1.5 py-0.5 text-[11px] text-gray-400 border border-gray-300 dark:border-gray-700 rounded bg-white dark:bg-gray-800 font-normal hover:bg-blue-100 dark:hover:bg-blue-900 transition relative text-center" onmouseover="showTooltip(this)" onmouseout="hideTooltip(this)" onclick="copyCode(this)">
381
- Copy Code
382
- <span class="tooltiptext left-1/2 -translate-x-1/2 bottom-10 shadow-md">Copy Code</span>
440
+ <div class="flex-1 flex flex-col justify-center items-center">
441
+ <span class="dot-integration flex items-center justify-center" style="width:160px;height:160px;"><img class="${item}" style="width:100%;height:100%;object-fit:contain;"/></span>
442
+ <span class="iconID block text-center text-base font-medium text-gray-800 dark:text-gray-200 my-2">${item}</span>
443
+ </div>
444
+ <div class="button-group flex flex-row items-center justify-center gap-2 mt-2">
445
+ <button class="button flex items-center justify-center min-w-[80px] px-1.5 py-0.5 text-[11px] text-gray-400 border border-gray-300 dark:border-gray-700 rounded bg-white dark:bg-gray-800 font-normal hover:bg-blue-100 dark:hover:bg-blue-900 transition relative text-center" onmouseover="showTooltip(this)" onmouseout="hideTooltip(this)" onclick="copyText(this)">
446
+ Copy Dot Code<span class="tooltiptext left-1/2 -translate-x-1/2 bottom-10 shadow-md">Copy Dot Code</span>
447
+ </button>
448
+ <button class="button flex items-center justify-center min-w-[80px] px-1.5 py-0.5 text-[11px] text-gray-400 border border-gray-300 dark:border-gray-700 rounded bg-white dark:bg-gray-800 font-normal hover:bg-blue-100 dark:hover:bg-blue-900 transition relative text-center" onmouseover="showTooltip(this)" onmouseout="hideTooltip(this)" onclick="copyCode(this)">
449
+ Copy Code<span class="tooltiptext left-1/2 -translate-x-1/2 bottom-10 shadow-md">Copy Code</span>
450
+ </button>
451
+ </div>
452
+ <pre class="codeBlock"><code class="codeBlock">${codeString}</code></pre>
453
+ `;
454
+ list.appendChild(div);
455
+ } else {
456
+ // Illustration card (light/dark, dot-illustration wrapper)
457
+ let categoryFolder = globalList.includes(item) ? 'global' : 'dashboards';
458
+ const svgPath = `illustrations/${themeClass}/${categoryFolder}/${item}.svg`;
459
+ const codeString = `<span class="dot-illustration"><img src="${svgPath}" class="${item} ${themeClass}"/></span>`;
460
+ const div = document.createElement('div');
461
+ div.setAttribute('class', 'copy-container relative group flex flex-col items-center justify-between bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-2 md:p-3 transition-transform duration-200 hover:scale-105 hover:shadow-2xl h-[302px] w-full');
462
+ div.dataset.itemId = item;
463
+ div.dataset.itemType = 'illustration';
464
+ div.innerHTML = `
465
+ <button class="absolute top-2 right-2 z-10 p-1 rounded-full bg-white/80 dark:bg-gray-800/80 hover:bg-yellow-100 dark:hover:bg-yellow-300 transition" title="Toggle favourite" onclick="event.stopPropagation();window.toggleFavourite('${item}')">
466
+ <svg class="h-5 w-5" fill="${isFav ? 'gold' : 'none'}" viewBox="0 0 24 24" stroke="gold"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l2.036 6.29a1 1 0 00.95.69h6.631c.969 0 1.371 1.24.588 1.81l-5.37 3.905a1 1 0 00-.364 1.118l2.036 6.29c.3.921-.755 1.688-1.54 1.118l-5.37-3.905a1 1 0 00-1.176 0l-5.37 3.905c-.784.57-1.838-.197-1.54-1.118l2.036-6.29a1 1 0 00-.364-1.118L2.342 11.717c-.783-.57-.38-1.81.588-1.81h6.631a1 1 0 00.95-.69l2.036-6.29z"/></svg>
383
467
  </button>
384
- </div>
385
- <pre class="codeBlock"><code class="codeBlock">${codeString}</code></pre>
386
- `;
387
- list.appendChild(div);
468
+ <div class="flex-1 flex flex-col justify-center items-center">
469
+ <span class="dot-illustration"><img src="${svgPath}" class="${item} ${themeClass}"/></span>
470
+ <span class="iconID block text-center text-base font-medium text-gray-800 dark:text-gray-200 my-2">${item}</span>
471
+ </div>
472
+ <div class="button-group flex flex-row items-center justify-center gap-2 mt-2">
473
+ <button id="CopyID" class="button flex items-center justify-center min-w-[80px] px-1.5 py-0.5 text-[11px] text-gray-400 border border-gray-300 dark:border-gray-700 rounded bg-white dark:bg-gray-800 font-normal hover:bg-blue-100 dark:hover:bg-blue-900 transition relative text-center" onmouseover="showTooltip(this)" onmouseout="hideTooltip(this)" onclick="copyText(this)">
474
+ Copy Dot Code<span class="tooltiptext left-1/2 -translate-x-1/2 bottom-10 shadow-md">Copy Dot Code</span>
475
+ </button>
476
+ <button class="button flex items-center justify-center min-w-[80px] px-1.5 py-0.5 text-[11px] text-gray-400 border border-gray-300 dark:border-gray-700 rounded bg-white dark:bg-gray-800 font-normal hover:bg-blue-100 dark:hover:bg-blue-900 transition relative text-center" onmouseover="showTooltip(this)" onmouseout="hideTooltip(this)" onclick="copyCode(this)">
477
+ Copy Code<span class="tooltiptext left-1/2 -translate-x-1/2 bottom-10 shadow-md">Copy Code</span>
478
+ </button>
479
+ </div>
480
+ <pre class="codeBlock"><code class="codeBlock">${codeString}</code></pre>
481
+ `;
482
+ list.appendChild(div);
483
+ }
388
484
  });
389
- // Attach modal click handler to each card
485
+
486
+ // Modal click on each card
390
487
  document.querySelectorAll('.copy-container').forEach(card => {
391
488
  card.addEventListener('click', function(e) {
392
489
  if (e.target.closest('button')) return;
393
- if (!descriptionsLoaded) return; // Wait for descriptions
394
- const id = this.querySelector('.iconID')?.textContent;
395
- if (id) openModal(id);
490
+ if (!descriptionsLoaded) return;
491
+ const id = this.dataset.itemId || this.querySelector('.iconID')?.textContent;
492
+ const type = this.dataset.itemType;
493
+ if (id) openModal(id, type);
396
494
  });
397
495
  });
398
496
  }
@@ -408,6 +506,183 @@ document.addEventListener('DOMContentLoaded', () => {
408
506
  document.getElementById('myInput').value = '';
409
507
  document.getElementById('floatingInput').value = '';
410
508
 
509
+ // ── GitHub auth ──────────────────────────────────────────────────────────
510
+ function updateAuthBadge(user) {
511
+ const btn = document.getElementById('gh-connect-btn');
512
+ const badge = document.getElementById('gh-user-badge');
513
+ const name = document.getElementById('gh-user-name');
514
+ if (user) {
515
+ btn.classList.add('hidden');
516
+ badge.classList.remove('hidden');
517
+ badge.style.display = 'flex';
518
+ name.textContent = `@${user.login}`;
519
+ localStorage.setItem('gh_illus_user', JSON.stringify(user));
520
+ } else {
521
+ btn.classList.remove('hidden');
522
+ badge.style.display = 'none';
523
+ }
524
+ }
525
+
526
+ function updateUploadAuthUI() {
527
+ const token = window.GitHubUpload?.getToken?.();
528
+ ['upload-illus-auth-gate','upload-integ-auth-gate'].forEach(id => {
529
+ document.getElementById(id)?.classList.toggle('hidden', !!token);
530
+ });
531
+ ['upload-illus-form','upload-integ-form'].forEach(id => {
532
+ document.getElementById(id)?.classList.toggle('hidden', !token);
533
+ });
534
+ }
535
+
536
+ async function tryDeviceFlow() {
537
+ const modal = document.getElementById('gh-device-modal');
538
+ modal.classList.remove('hidden');
539
+ document.getElementById('gh-device-step-code').classList.remove('hidden');
540
+ document.getElementById('gh-device-step-pat').classList.add('hidden');
541
+ document.getElementById('gh-device-error').classList.add('hidden');
542
+ try {
543
+ const { user_code, verification_uri, device_code, expires_in, interval } =
544
+ await window.GitHubUpload.startDeviceFlow();
545
+ document.getElementById('gh-device-uri').href = verification_uri;
546
+ document.getElementById('gh-device-uri').textContent = verification_uri;
547
+ document.getElementById('gh-device-code').textContent = user_code;
548
+ document.getElementById('gh-copy-code-btn').onclick = () =>
549
+ navigator.clipboard.writeText(user_code).then(() => showToast('Code copied!'));
550
+ const token = await window.GitHubUpload.pollForToken(device_code, interval || 5);
551
+ window.GitHubUpload.setToken(token);
552
+ const user = await window.GitHubUpload.getUser();
553
+ updateAuthBadge(user);
554
+ modal.classList.add('hidden');
555
+ updateUploadAuthUI();
556
+ showToast(`Connected as @${user.login}`);
557
+ } catch (err) {
558
+ if (err.message === 'NO_CLIENT_ID' || err.name === 'TypeError') {
559
+ document.getElementById('gh-device-step-code').classList.add('hidden');
560
+ document.getElementById('gh-device-step-pat').classList.remove('hidden');
561
+ } else {
562
+ document.getElementById('gh-device-error').textContent = err.message;
563
+ document.getElementById('gh-device-error').classList.remove('hidden');
564
+ }
565
+ }
566
+ }
567
+
568
+ document.getElementById('gh-connect-btn')?.addEventListener('click', tryDeviceFlow);
569
+ document.getElementById('gh-logout-btn')?.addEventListener('click', () => {
570
+ window.GitHubUpload?.clearToken?.();
571
+ updateAuthBadge(null);
572
+ updateUploadAuthUI();
573
+ showToast('Disconnected from GitHub');
574
+ });
575
+ document.getElementById('gh-device-close')?.addEventListener('click', () => {
576
+ document.getElementById('gh-device-modal').classList.add('hidden');
577
+ });
578
+ document.getElementById('gh-pat-submit')?.addEventListener('click', async () => {
579
+ const pat = document.getElementById('gh-pat-input').value.trim();
580
+ if (!pat) return;
581
+ window.GitHubUpload.setToken(pat);
582
+ try {
583
+ const user = await window.GitHubUpload.getUser();
584
+ updateAuthBadge(user);
585
+ document.getElementById('gh-device-modal').classList.add('hidden');
586
+ updateUploadAuthUI();
587
+ showToast(`Connected as @${user.login}`);
588
+ } catch {
589
+ document.getElementById('gh-device-error').textContent = 'Invalid token.';
590
+ document.getElementById('gh-device-error').classList.remove('hidden');
591
+ }
592
+ });
593
+
594
+ // Restore session
595
+ const savedUser = localStorage.getItem('gh_illus_user');
596
+ if (window.GitHubUpload?.getToken?.() && savedUser) {
597
+ try { updateAuthBadge(JSON.parse(savedUser)); } catch {}
598
+ }
599
+
600
+ // ── Upload panel ─────────────────────────────────────────────────────────
601
+ const uploadBufs = { illusLight: null, illusDark: null, integSvg: null };
602
+
603
+ function bindFileDrop(dropId, inputId, nameId, bufKey) {
604
+ const drop = document.getElementById(dropId);
605
+ const input = document.getElementById(inputId);
606
+ const nameEl = document.getElementById(nameId);
607
+ if (!drop || !input) return;
608
+ drop.addEventListener('click', () => input.click());
609
+ drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('border-blue-400'); });
610
+ drop.addEventListener('dragleave', () => drop.classList.remove('border-blue-400'));
611
+ drop.addEventListener('drop', e => { e.preventDefault(); drop.classList.remove('border-blue-400'); handleFile(e.dataTransfer.files[0], bufKey, nameEl, drop); });
612
+ input.addEventListener('change', () => handleFile(input.files[0], bufKey, nameEl, drop));
613
+ }
614
+
615
+ function handleFile(file, bufKey, nameEl, drop) {
616
+ if (!file || !file.name.endsWith('.svg')) { showToast('Please select an SVG file'); return; }
617
+ const reader = new FileReader();
618
+ reader.onload = e => {
619
+ uploadBufs[bufKey] = e.target.result;
620
+ if (nameEl) { nameEl.textContent = `✓ ${file.name}`; nameEl.classList.remove('hidden'); }
621
+ drop.classList.add('border-green-400', 'dark:border-green-600');
622
+ drop.classList.remove('border-gray-300', 'dark:border-gray-600');
623
+ };
624
+ reader.readAsArrayBuffer(file);
625
+ }
626
+
627
+ bindFileDrop('illus-light-drop', 'illus-light-input', 'illus-light-name', 'illusLight');
628
+ bindFileDrop('illus-dark-drop', 'illus-dark-input', 'illus-dark-name', 'illusDark');
629
+ bindFileDrop('integ-svg-drop', 'integ-svg-input', 'integ-svg-name', 'integSvg');
630
+
631
+ async function handleIllusUpload() {
632
+ const id = document.getElementById('upload-illus-id')?.value.trim();
633
+ const cat = document.getElementById('upload-illus-category')?.value;
634
+ const btn = document.getElementById('upload-illus-btn');
635
+ const status = document.getElementById('upload-illus-status');
636
+ const prLink = document.getElementById('upload-illus-pr-link');
637
+ if (!id) { showToast('Enter illustration ID'); return; }
638
+ if (!uploadBufs.illusLight) { showToast('Select the light SVG'); return; }
639
+ if (!uploadBufs.illusDark) { showToast('Select the dark SVG'); return; }
640
+ if (!window.GitHubUpload?.getToken?.()) { showToast('Connect GitHub first'); return; }
641
+ btn.disabled = true; btn.textContent = 'Creating PR…';
642
+ status.classList.add('hidden'); prLink.classList.add('hidden');
643
+ try {
644
+ const url = await window.GitHubUpload.createIllustrationPR(id, cat, uploadBufs.illusLight, uploadBufs.illusDark);
645
+ status.textContent = 'PR created!';
646
+ status.className = 'text-sm text-center text-green-600 dark:text-green-400';
647
+ status.classList.remove('hidden');
648
+ document.getElementById('upload-illus-pr-anchor').href = url;
649
+ prLink.classList.remove('hidden');
650
+ showToast('PR created!');
651
+ } catch (err) {
652
+ status.textContent = `Error: ${err.message}`;
653
+ status.className = 'text-sm text-center text-red-600 dark:text-red-400';
654
+ status.classList.remove('hidden');
655
+ } finally { btn.disabled = false; btn.textContent = 'Create PR'; }
656
+ }
657
+
658
+ async function handleIntegUpload() {
659
+ const id = document.getElementById('upload-integ-id')?.value.trim();
660
+ const btn = document.getElementById('upload-integ-btn');
661
+ const status = document.getElementById('upload-integ-status');
662
+ const prLink = document.getElementById('upload-integ-pr-link');
663
+ if (!id) { showToast('Enter integration ID'); return; }
664
+ if (!uploadBufs.integSvg) { showToast('Select an SVG file'); return; }
665
+ if (!window.GitHubUpload?.getToken?.()) { showToast('Connect GitHub first'); return; }
666
+ btn.disabled = true; btn.textContent = 'Creating PR…';
667
+ status.classList.add('hidden'); prLink.classList.add('hidden');
668
+ try {
669
+ const url = await window.GitHubUpload.createIntegrationPR(id, uploadBufs.integSvg);
670
+ status.textContent = 'PR created!';
671
+ status.className = 'text-sm text-center text-green-600 dark:text-green-400';
672
+ status.classList.remove('hidden');
673
+ document.getElementById('upload-integ-pr-anchor').href = url;
674
+ prLink.classList.remove('hidden');
675
+ showToast('PR created!');
676
+ } catch (err) {
677
+ status.textContent = `Error: ${err.message}`;
678
+ status.className = 'text-sm text-center text-red-600 dark:text-red-400';
679
+ status.classList.remove('hidden');
680
+ } finally { btn.disabled = false; btn.textContent = 'Create PR'; }
681
+ }
682
+
683
+ document.getElementById('upload-illus-btn')?.addEventListener('click', handleIllusUpload);
684
+ document.getElementById('upload-integ-btn')?.addEventListener('click', handleIntegUpload);
685
+
411
686
  // Set initial tab state to 'all' and update UI (this will also render illustrations)
412
687
  setTab('all');
413
688
 
@@ -444,6 +719,7 @@ function getSuggestions(query) {
444
719
  if (currentTab === 'all') pool = illustrations;
445
720
  else if (currentTab === 'global') pool = globalList;
446
721
  else if (currentTab === 'dashboards') pool = dashboardsList;
722
+ else if (currentTab === 'integrations') pool = integrationsList;
447
723
  else if (currentTab === 'favourites') pool = getFavourites();
448
724
  // Fuzzy match, sort by closeness (shorter index of first match, then length)
449
725
  let scored = pool