@digital-ai/dot-illustrations 2.0.33 → 2.0.36

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);
150
- });
151
- });
152
- floatingTabs.forEach(tab => {
153
- tab.addEventListener('click', function() {
154
- setTab(this.dataset.tab);
161
+ function wireTabs() {
162
+ document.querySelectorAll('.tab-btn').forEach(tab => {
163
+ tab.addEventListener('click', function() { setTab(this.dataset.tab); });
155
164
  });
156
- });
165
+ }
166
+ wireTabs();
157
167
 
158
168
  // --- ILLUSTRATION DATA ---
159
169
  const globalList = [
@@ -191,6 +201,7 @@ document.addEventListener('DOMContentLoaded', () => {
191
201
  "nothing-defined",
192
202
  "password-token",
193
203
  "protection-in-progress",
204
+ "remote-cloud",
194
205
  "reports","scan-document",
195
206
  "releases",
196
207
  "survey",
@@ -228,7 +239,28 @@ document.addEventListener('DOMContentLoaded', () => {
228
239
  "data-extraction",
229
240
  "workflow"
230
241
  ];
231
- 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
+ ];
232
264
 
233
265
  // --- FAVOURITES LOGIC ---
234
266
  function getFavourites() {
@@ -270,43 +302,44 @@ document.addEventListener('DOMContentLoaded', () => {
270
302
  const modalClose = document.getElementById('modal-close');
271
303
  const modalCloseBottom = document.getElementById('modal-close-bottom');
272
304
 
273
- function openModal(illustration) {
305
+ function openModal(item, type) {
274
306
  if (modalDescription) modalDescription.innerHTML = '';
275
307
  if (!descriptionsLoaded) return;
308
+
276
309
  const themeClass = root.classList.contains('dark') ? 'dark' : 'light';
277
- let categoryFolder = "";
278
- if (globalList.includes(illustration)) {
279
- categoryFolder = "global";
280
- } else if (dashboardsList.includes(illustration)) {
281
- 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>`;
282
323
  }
283
- const svgPath = `illustrations/${themeClass}/${categoryFolder}/${illustration}.svg`;
284
- modalImage.innerHTML = `<span class=\"dot-illustration\"><img src=\"${svgPath}\" class=\"${illustration} ${themeClass}\" style=\"width:286px;height:286px;object-fit:contain;\"/></span>`;
285
- // Set the illustration name
324
+
325
+ modalImage.innerHTML = imgHtml;
286
326
  const modalIllustrationName = document.getElementById('modal-illustration-name');
287
- if (modalIllustrationName) modalIllustrationName.textContent = illustration;
288
- // Set the Dot Code textarea value
289
- const dotCodeString = `<DotIllustration illustrationId=\"${illustration}\" />`;
327
+ if (modalIllustrationName) modalIllustrationName.textContent = item;
290
328
  const modalDotCode = document.getElementById('modal-dot-code');
291
329
  if (modalDotCode) modalDotCode.value = dotCodeString;
292
- // Show the HTML code snippet in the modal
293
- const codeString = `<span class=\"dot-illustration\"><img src=\"${svgPath}\" class=\"${illustration} ${themeClass}\"/></span>`;
294
330
  modalCode.value = codeString;
331
+
295
332
  modal.classList.add('active');
296
333
  modal.classList.remove('hidden');
297
- // Attach Copy Dot Code button event listener each time modal opens
334
+
298
335
  const modalCopyDotCode = document.getElementById('modal-copy-dot-code');
299
336
  if (modalCopyDotCode) {
300
- modalCopyDotCode.onclick = function() {
337
+ modalCopyDotCode.onclick = () => {
301
338
  if (modalDotCode) {
302
- navigator.clipboard.writeText(modalDotCode.value).then(function() {
339
+ navigator.clipboard.writeText(modalDotCode.value).then(() => {
303
340
  modalCopyDotCode.textContent = 'Copied!';
304
341
  showToast('Copied Dot Code: ' + modalDotCode.value);
305
- setTimeout(() => {
306
- modalCopyDotCode.textContent = 'Copy Dot Code';
307
- }, 2000);
308
- }, function(err) {
309
- console.error('Failed to copy dot code: ', err);
342
+ setTimeout(() => { modalCopyDotCode.textContent = 'Copy Dot Code'; }, 2000);
310
343
  });
311
344
  }
312
345
  };
@@ -325,75 +358,139 @@ document.addEventListener('DOMContentLoaded', () => {
325
358
  showToast('Copied code: ' + modalCode.value);
326
359
  });
327
360
 
328
- // --- 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 ---
329
404
  function renderIllustrations() {
330
405
  const list = document.getElementById("illustration-list");
331
406
  list.innerHTML = "";
332
- let toShow = [];
333
- if (currentTab === 'all') {
334
- toShow = illustrations;
335
- } else if (currentTab === 'global') {
336
- toShow = globalList;
337
- } else if (currentTab === 'dashboards') {
338
- toShow = dashboardsList;
339
- } else if (currentTab === 'favourites') {
340
- const favs = getFavourites();
341
- toShow = illustrations.filter(id => favs.includes(id));
342
- }
343
- const filter = (document.getElementById('myInput')?.value || '').toUpperCase();
344
- 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
+
345
413
  if (toShow.length === 0) {
346
- 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>`;
347
417
  return;
348
418
  }
419
+
420
+ const isIntegrations = currentTab === 'integrations' ||
421
+ (currentTab === 'favourites' && toShow.every(id => integrationsList.includes(id)));
349
422
  const favs = getFavourites();
350
423
  const themeClass = root.classList.contains('dark') ? 'dark' : 'light';
351
- toShow.forEach(illustration => {
352
- let categoryFolder = "";
353
- if (globalList.includes(illustration)) {
354
- categoryFolder = "global";
355
- } else if (dashboardsList.includes(illustration)) {
356
- categoryFolder = "dashboards";
357
- }
358
- const svgPath = `illustrations/${themeClass}/${categoryFolder}/${illustration}.svg`;
359
- const isFav = favs.includes(illustration);
360
- const div = document.createElement("div");
361
- 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");
362
- // Show the HTML code snippet in the card
363
- const codeString = `<span class=\"dot-illustration\"><img src=\"${svgPath}\" class=\"${illustration} ${themeClass}\"/></span>`;
364
- div.innerHTML = `
365
- <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}')">
366
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="${isFav ? 'gold' : 'none'}" viewBox="0 0 24 24" stroke="gold">
367
- <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"/>
368
- </svg>
369
- </button>
370
- <div class="flex-1 flex flex-col justify-center items-center">
371
- <span class="dot-illustration">
372
- <img src="${svgPath}" class="${illustration} ${themeClass}"/>
373
- </span>
374
- <span class="iconID block text-center text-base font-medium text-gray-800 dark:text-gray-200 my-2">${illustration}</span>
375
- </div>
376
- <div class="button-group flex flex-row items-center justify-center gap-2 mt-2">
377
- <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)">
378
- Copy Dot Code
379
- <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>
380
439
  </button>
381
- <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)">
382
- Copy Code
383
- <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>
384
467
  </button>
385
- </div>
386
- <pre class="codeBlock"><code class="codeBlock">${codeString}</code></pre>
387
- `;
388
- 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
+ }
389
484
  });
390
- // Attach modal click handler to each card
485
+
486
+ // Modal click on each card
391
487
  document.querySelectorAll('.copy-container').forEach(card => {
392
488
  card.addEventListener('click', function(e) {
393
489
  if (e.target.closest('button')) return;
394
- if (!descriptionsLoaded) return; // Wait for descriptions
395
- const id = this.querySelector('.iconID')?.textContent;
396
- 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);
397
494
  });
398
495
  });
399
496
  }
@@ -409,6 +506,178 @@ document.addEventListener('DOMContentLoaded', () => {
409
506
  document.getElementById('myInput').value = '';
410
507
  document.getElementById('floatingInput').value = '';
411
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
+ } else {
520
+ btn.classList.remove('hidden');
521
+ badge.style.display = 'none';
522
+ }
523
+ }
524
+
525
+ function updateUploadAuthUI() {
526
+ const token = window.GitHubUpload?.getToken?.();
527
+ ['upload-illus-auth-gate','upload-integ-auth-gate'].forEach(id => {
528
+ document.getElementById(id)?.classList.toggle('hidden', !!token);
529
+ });
530
+ ['upload-illus-form','upload-integ-form'].forEach(id => {
531
+ document.getElementById(id)?.classList.toggle('hidden', !token);
532
+ });
533
+ }
534
+
535
+ async function tryDeviceFlow() {
536
+ const modal = document.getElementById('gh-device-modal');
537
+ modal.classList.remove('hidden');
538
+ document.getElementById('gh-device-step-code').classList.remove('hidden');
539
+ document.getElementById('gh-device-step-pat').classList.add('hidden');
540
+ document.getElementById('gh-device-error').classList.add('hidden');
541
+ try {
542
+ const { user_code, verification_uri, device_code, expires_in, interval } =
543
+ await window.GitHubUpload.startDeviceFlow();
544
+ document.getElementById('gh-device-uri').href = verification_uri;
545
+ document.getElementById('gh-device-uri').textContent = verification_uri;
546
+ document.getElementById('gh-device-code').textContent = user_code;
547
+ document.getElementById('gh-copy-code-btn').onclick = () =>
548
+ navigator.clipboard.writeText(user_code).then(() => showToast('Code copied!'));
549
+ const token = await window.GitHubUpload.pollForToken(device_code, interval || 5);
550
+ window.GitHubUpload.setToken(token);
551
+ const user = await window.GitHubUpload.getUser();
552
+ updateAuthBadge(user);
553
+ modal.classList.add('hidden');
554
+ updateUploadAuthUI();
555
+ showToast(`Connected as @${user.login}`);
556
+ } catch (err) {
557
+ if (err.message === 'NO_CLIENT_ID' || err.name === 'TypeError') {
558
+ document.getElementById('gh-device-step-code').classList.add('hidden');
559
+ document.getElementById('gh-device-step-pat').classList.remove('hidden');
560
+ } else {
561
+ document.getElementById('gh-device-error').textContent = err.message;
562
+ document.getElementById('gh-device-error').classList.remove('hidden');
563
+ }
564
+ }
565
+ }
566
+
567
+ document.getElementById('gh-connect-btn')?.addEventListener('click', tryDeviceFlow);
568
+ document.getElementById('gh-logout-btn')?.addEventListener('click', () => {
569
+ window.GitHubUpload?.clearToken?.();
570
+ updateAuthBadge(null);
571
+ updateUploadAuthUI();
572
+ showToast('Disconnected from GitHub');
573
+ });
574
+ document.getElementById('gh-device-close')?.addEventListener('click', () => {
575
+ document.getElementById('gh-device-modal').classList.add('hidden');
576
+ });
577
+ document.getElementById('gh-pat-submit')?.addEventListener('click', async () => {
578
+ const pat = document.getElementById('gh-pat-input').value.trim();
579
+ if (!pat) return;
580
+ window.GitHubUpload.setToken(pat);
581
+ try {
582
+ const user = await window.GitHubUpload.getUser();
583
+ updateAuthBadge(user);
584
+ document.getElementById('gh-device-modal').classList.add('hidden');
585
+ updateUploadAuthUI();
586
+ showToast(`Connected as @${user.login}`);
587
+ } catch {
588
+ document.getElementById('gh-device-error').textContent = 'Invalid token.';
589
+ document.getElementById('gh-device-error').classList.remove('hidden');
590
+ }
591
+ });
592
+
593
+ // No session restore — token is in-memory only for security
594
+
595
+ // ── Upload panel ─────────────────────────────────────────────────────────
596
+ const uploadBufs = { illusLight: null, illusDark: null, integSvg: null };
597
+
598
+ function bindFileDrop(dropId, inputId, nameId, bufKey) {
599
+ const drop = document.getElementById(dropId);
600
+ const input = document.getElementById(inputId);
601
+ const nameEl = document.getElementById(nameId);
602
+ if (!drop || !input) return;
603
+ drop.addEventListener('click', () => input.click());
604
+ drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('border-blue-400'); });
605
+ drop.addEventListener('dragleave', () => drop.classList.remove('border-blue-400'));
606
+ drop.addEventListener('drop', e => { e.preventDefault(); drop.classList.remove('border-blue-400'); handleFile(e.dataTransfer.files[0], bufKey, nameEl, drop); });
607
+ input.addEventListener('change', () => handleFile(input.files[0], bufKey, nameEl, drop));
608
+ }
609
+
610
+ function handleFile(file, bufKey, nameEl, drop) {
611
+ if (!file || !file.name.endsWith('.svg')) { showToast('Please select an SVG file'); return; }
612
+ const reader = new FileReader();
613
+ reader.onload = e => {
614
+ uploadBufs[bufKey] = e.target.result;
615
+ if (nameEl) { nameEl.textContent = `✓ ${file.name}`; nameEl.classList.remove('hidden'); }
616
+ drop.classList.add('border-green-400', 'dark:border-green-600');
617
+ drop.classList.remove('border-gray-300', 'dark:border-gray-600');
618
+ };
619
+ reader.readAsArrayBuffer(file);
620
+ }
621
+
622
+ bindFileDrop('illus-light-drop', 'illus-light-input', 'illus-light-name', 'illusLight');
623
+ bindFileDrop('illus-dark-drop', 'illus-dark-input', 'illus-dark-name', 'illusDark');
624
+ bindFileDrop('integ-svg-drop', 'integ-svg-input', 'integ-svg-name', 'integSvg');
625
+
626
+ async function handleIllusUpload() {
627
+ const id = document.getElementById('upload-illus-id')?.value.trim();
628
+ const cat = document.getElementById('upload-illus-category')?.value;
629
+ const btn = document.getElementById('upload-illus-btn');
630
+ const status = document.getElementById('upload-illus-status');
631
+ const prLink = document.getElementById('upload-illus-pr-link');
632
+ if (!id) { showToast('Enter illustration ID'); return; }
633
+ if (!uploadBufs.illusLight) { showToast('Select the light SVG'); return; }
634
+ if (!uploadBufs.illusDark) { showToast('Select the dark SVG'); return; }
635
+ if (!window.GitHubUpload?.getToken?.()) { showToast('Connect GitHub first'); return; }
636
+ btn.disabled = true; btn.textContent = 'Creating PR…';
637
+ status.classList.add('hidden'); prLink.classList.add('hidden');
638
+ try {
639
+ const url = await window.GitHubUpload.createIllustrationPR(id, cat, uploadBufs.illusLight, uploadBufs.illusDark);
640
+ status.textContent = 'PR created!';
641
+ status.className = 'text-sm text-center text-green-600 dark:text-green-400';
642
+ status.classList.remove('hidden');
643
+ document.getElementById('upload-illus-pr-anchor').href = url;
644
+ prLink.classList.remove('hidden');
645
+ showToast('PR created!');
646
+ } catch (err) {
647
+ status.textContent = `Error: ${err.message}`;
648
+ status.className = 'text-sm text-center text-red-600 dark:text-red-400';
649
+ status.classList.remove('hidden');
650
+ } finally { btn.disabled = false; btn.textContent = 'Create PR'; }
651
+ }
652
+
653
+ async function handleIntegUpload() {
654
+ const id = document.getElementById('upload-integ-id')?.value.trim();
655
+ const btn = document.getElementById('upload-integ-btn');
656
+ const status = document.getElementById('upload-integ-status');
657
+ const prLink = document.getElementById('upload-integ-pr-link');
658
+ if (!id) { showToast('Enter integration ID'); return; }
659
+ if (!uploadBufs.integSvg) { showToast('Select an SVG file'); return; }
660
+ if (!window.GitHubUpload?.getToken?.()) { showToast('Connect GitHub first'); return; }
661
+ btn.disabled = true; btn.textContent = 'Creating PR…';
662
+ status.classList.add('hidden'); prLink.classList.add('hidden');
663
+ try {
664
+ const url = await window.GitHubUpload.createIntegrationPR(id, uploadBufs.integSvg);
665
+ status.textContent = 'PR created!';
666
+ status.className = 'text-sm text-center text-green-600 dark:text-green-400';
667
+ status.classList.remove('hidden');
668
+ document.getElementById('upload-integ-pr-anchor').href = url;
669
+ prLink.classList.remove('hidden');
670
+ showToast('PR created!');
671
+ } catch (err) {
672
+ status.textContent = `Error: ${err.message}`;
673
+ status.className = 'text-sm text-center text-red-600 dark:text-red-400';
674
+ status.classList.remove('hidden');
675
+ } finally { btn.disabled = false; btn.textContent = 'Create PR'; }
676
+ }
677
+
678
+ document.getElementById('upload-illus-btn')?.addEventListener('click', handleIllusUpload);
679
+ document.getElementById('upload-integ-btn')?.addEventListener('click', handleIntegUpload);
680
+
412
681
  // Set initial tab state to 'all' and update UI (this will also render illustrations)
413
682
  setTab('all');
414
683
 
@@ -445,6 +714,7 @@ function getSuggestions(query) {
445
714
  if (currentTab === 'all') pool = illustrations;
446
715
  else if (currentTab === 'global') pool = globalList;
447
716
  else if (currentTab === 'dashboards') pool = dashboardsList;
717
+ else if (currentTab === 'integrations') pool = integrationsList;
448
718
  else if (currentTab === 'favourites') pool = getFavourites();
449
719
  // Fuzzy match, sort by closeness (shorter index of first match, then length)
450
720
  let scored = pool