@digital-ai/dot-illustrations 2.0.33 → 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.
@@ -6,6 +6,12 @@ on:
6
6
  branches:
7
7
  - main
8
8
 
9
+ # OIDC token for npm Trusted Publishers (no NPM_TOKEN secret needed).
10
+ # contents: write is required for the version-bump commit and release creation.
11
+ permissions:
12
+ id-token: write # Required for npm OIDC Trusted Publishing
13
+ contents: write # Required for version bump commit + GitHub release
14
+
9
15
  jobs:
10
16
  # https://github.com/marketplace/actions/automated-version-bump
11
17
  bump-version:
@@ -19,7 +25,7 @@ jobs:
19
25
  uses: actions/checkout@v4
20
26
 
21
27
  - name: Setup Node.js
22
- uses: actions/setup-node@v3
28
+ uses: actions/setup-node@v4
23
29
  with:
24
30
  node-version: 20
25
31
 
@@ -64,7 +70,7 @@ jobs:
64
70
 
65
71
  # https://github.com/marketplace/actions/npm-publish
66
72
  npm-publish:
67
- name: Publish Latest to NPM
73
+ name: Publish to npm (OIDC Trusted Publisher)
68
74
  runs-on: ubuntu-latest
69
75
  needs: [bump-version, create-release]
70
76
  if: needs.bump-version.outputs.new-version != ''
@@ -73,14 +79,19 @@ jobs:
73
79
  - name: Checkout code
74
80
  uses: actions/checkout@v4
75
81
  with:
76
- fetch-depth: 0 # necessary to get newly created tag above
82
+ fetch-depth: 0
77
83
  ref: 'main'
78
84
 
79
- # https://github.com/JS-DevTools/npm-publish
80
- - name: Publish to NPM
81
- uses: JS-DevTools/npm-publish@v1
85
+ # registry-url wires the OIDC token into .npmrc automatically —
86
+ # no NPM_TOKEN secret required.
87
+ - name: Setup Node.js
88
+ uses: actions/setup-node@v4
82
89
  with:
83
- access: public
84
- check-version: true
85
- greater-version-only: true
86
- token: ${{ secrets.NPM_TOKEN }}
90
+ node-version: 20
91
+ registry-url: 'https://registry.npmjs.org'
92
+
93
+ - name: Update npm (≥ 11.5.1 required for OIDC)
94
+ run: npm install -g npm@latest
95
+
96
+ - name: Publish to npm
97
+ run: npm publish --access public
package/demo/demo.css CHANGED
@@ -179,3 +179,30 @@ body.dark-mode .dot-illustration img.dark {
179
179
  height: 140px;
180
180
  }
181
181
  }
182
+
183
+ /* ── Alphabet strip ─────────────────────────────────────────────────────── */
184
+ #alphabet-strip {
185
+ display: flex;
186
+ flex-wrap: wrap;
187
+ justify-content: center;
188
+ gap: 0.25rem;
189
+ }
190
+
191
+ /* ── Tab nav horizontal scroll ──────────────────────────────────────────── */
192
+ #main-tabs {
193
+ overflow-x: auto;
194
+ scrollbar-width: none;
195
+ }
196
+ #main-tabs::-webkit-scrollbar { display: none; }
197
+ #floating-tabs { scrollbar-width: none; }
198
+ #floating-tabs::-webkit-scrollbar { display: none; }
199
+
200
+ /* ── GitHub user badge flex ─────────────────────────────────────────────── */
201
+ #gh-user-badge[style*="flex"] { display: flex !important; }
202
+
203
+ /* ── Integration card image sizing ─────────────────────────────────────── */
204
+ .dot-integration img {
205
+ width: 100%;
206
+ height: 100%;
207
+ object-fit: contain;
208
+ }
@@ -0,0 +1,289 @@
1
+ /**
2
+ * GitHub Upload Helper — dot-illustrations
3
+ * OAuth Device Flow + REST API for illustrations and integrations.
4
+ *
5
+ * Setup:
6
+ * 1. Register a GitHub OAuth App at https://github.com/settings/developers
7
+ * - Homepage: https://digital-ai.github.io/dot-illustrations/demo/
8
+ * - Check "Enable Device Flow" — no callback URL needed
9
+ * 2. Set GITHUB_OAUTH_CLIENT_ID to your Client ID
10
+ */
11
+
12
+ const GITHUB_UPLOAD_CONFIG = {
13
+ CLIENT_ID: 'YOUR_GITHUB_OAUTH_CLIENT_ID',
14
+ REPO_OWNER: 'digital-ai',
15
+ REPO_NAME: 'dot-illustrations',
16
+ BASE_BRANCH: 'main',
17
+ SCOPES: 'public_repo',
18
+ };
19
+
20
+ const STORAGE_TOKEN_KEY = 'gh_illus_token';
21
+ const STORAGE_USER_KEY = 'gh_illus_user';
22
+
23
+ // ─── Token helpers ──────────────────────────────────────────────────────────
24
+
25
+ function ghGetToken() { return localStorage.getItem(STORAGE_TOKEN_KEY); }
26
+ function ghSetToken(t) { localStorage.setItem(STORAGE_TOKEN_KEY, t); }
27
+ function ghClearToken() {
28
+ localStorage.removeItem(STORAGE_TOKEN_KEY);
29
+ localStorage.removeItem(STORAGE_USER_KEY);
30
+ }
31
+
32
+ // ─── GitHub API helpers ──────────────────────────────────────────────────────
33
+
34
+ async function ghApi(path, opts = {}) {
35
+ const token = ghGetToken();
36
+ const resp = await fetch(`https://api.github.com${path}`, {
37
+ ...opts,
38
+ headers: {
39
+ 'Authorization': `Bearer ${token}`,
40
+ 'Accept': 'application/vnd.github.v3+json',
41
+ 'Content-Type': 'application/json',
42
+ ...(opts.headers || {}),
43
+ },
44
+ });
45
+ const data = await resp.json();
46
+ if (!resp.ok) throw new Error(data.message || `GitHub API error ${resp.status}`);
47
+ return data;
48
+ }
49
+
50
+ async function ghGetUser() { return ghApi('/user'); }
51
+
52
+ async function ghGetMainSha() {
53
+ const d = await ghApi(
54
+ `/repos/${GITHUB_UPLOAD_CONFIG.REPO_OWNER}/${GITHUB_UPLOAD_CONFIG.REPO_NAME}/git/ref/heads/${GITHUB_UPLOAD_CONFIG.BASE_BRANCH}`
55
+ );
56
+ return d.object.sha;
57
+ }
58
+
59
+ async function ghCreateBranch(name, sha) {
60
+ return ghApi(
61
+ `/repos/${GITHUB_UPLOAD_CONFIG.REPO_OWNER}/${GITHUB_UPLOAD_CONFIG.REPO_NAME}/git/refs`,
62
+ { method: 'POST', body: JSON.stringify({ ref: `refs/heads/${name}`, sha }) }
63
+ );
64
+ }
65
+
66
+ async function ghGetFile(path, branch) {
67
+ try {
68
+ const d = await ghApi(
69
+ `/repos/${GITHUB_UPLOAD_CONFIG.REPO_OWNER}/${GITHUB_UPLOAD_CONFIG.REPO_NAME}/contents/${path}?ref=${branch}`
70
+ );
71
+ return { content: atob(d.content.replace(/\n/g, '')), sha: d.sha };
72
+ } catch { return null; }
73
+ }
74
+
75
+ async function ghPutFile(path, textContent, message, branch, existingSha) {
76
+ const body = { message, branch, content: btoa(unescape(encodeURIComponent(textContent))) };
77
+ if (existingSha) body.sha = existingSha;
78
+ return ghApi(
79
+ `/repos/${GITHUB_UPLOAD_CONFIG.REPO_OWNER}/${GITHUB_UPLOAD_CONFIG.REPO_NAME}/contents/${path}`,
80
+ { method: 'PUT', body: JSON.stringify(body) }
81
+ );
82
+ }
83
+
84
+ async function ghPutBinaryFile(path, arrayBuffer, message, branch, existingSha) {
85
+ const bytes = new Uint8Array(arrayBuffer);
86
+ let bin = '';
87
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
88
+ const body = { message, branch, content: btoa(bin) };
89
+ if (existingSha) body.sha = existingSha;
90
+ return ghApi(
91
+ `/repos/${GITHUB_UPLOAD_CONFIG.REPO_OWNER}/${GITHUB_UPLOAD_CONFIG.REPO_NAME}/contents/${path}`,
92
+ { method: 'PUT', body: JSON.stringify(body) }
93
+ );
94
+ }
95
+
96
+ async function ghCreatePR(head, title, body) {
97
+ return ghApi(
98
+ `/repos/${GITHUB_UPLOAD_CONFIG.REPO_OWNER}/${GITHUB_UPLOAD_CONFIG.REPO_NAME}/pulls`,
99
+ { method: 'POST', body: JSON.stringify({ title, body, head, base: GITHUB_UPLOAD_CONFIG.BASE_BRANCH }) }
100
+ );
101
+ }
102
+
103
+ // ─── Device Flow ─────────────────────────────────────────────────────────────
104
+
105
+ async function ghStartDeviceFlow() {
106
+ if (!GITHUB_UPLOAD_CONFIG.CLIENT_ID || GITHUB_UPLOAD_CONFIG.CLIENT_ID === 'YOUR_GITHUB_OAUTH_CLIENT_ID') {
107
+ throw new Error('NO_CLIENT_ID');
108
+ }
109
+ const resp = await fetch('https://github.com/login/device/code', {
110
+ method: 'POST',
111
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
112
+ body: JSON.stringify({ client_id: GITHUB_UPLOAD_CONFIG.CLIENT_ID, scope: GITHUB_UPLOAD_CONFIG.SCOPES }),
113
+ });
114
+ if (!resp.ok) throw new Error('Device flow initiation failed');
115
+ return resp.json();
116
+ }
117
+
118
+ function ghPollForToken(deviceCode, intervalSec) {
119
+ return new Promise((resolve, reject) => {
120
+ const timer = setInterval(async () => {
121
+ try {
122
+ const resp = await fetch('https://github.com/login/oauth/access_token', {
123
+ method: 'POST',
124
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
125
+ body: JSON.stringify({
126
+ client_id: GITHUB_UPLOAD_CONFIG.CLIENT_ID,
127
+ device_code: deviceCode,
128
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
129
+ }),
130
+ });
131
+ const d = await resp.json();
132
+ if (d.access_token) { clearInterval(timer); resolve(d.access_token); }
133
+ else if (d.error === 'expired_token' || d.error === 'access_denied') {
134
+ clearInterval(timer); reject(new Error(d.error_description || d.error));
135
+ }
136
+ } catch (e) { clearInterval(timer); reject(e); }
137
+ }, Math.max(intervalSec, 5) * 1000);
138
+ });
139
+ }
140
+
141
+ // ─── High-level: create PR for a new illustration ────────────────────────────
142
+
143
+ /**
144
+ * @param {string} id - illustration ID (no .svg)
145
+ * @param {string} category - 'global' | 'dashboards'
146
+ * @param {ArrayBuffer} lightBuf - light SVG
147
+ * @param {ArrayBuffer} darkBuf - dark SVG
148
+ * @returns {string} PR URL
149
+ */
150
+ async function ghCreateIllustrationPR(id, category, lightBuf, darkBuf) {
151
+ const slug = id.toLowerCase().replace(/[^a-z0-9-]/g, '-');
152
+ const branch = `add-illustration-${slug}-${Date.now()}`;
153
+
154
+ const mainSha = await ghGetMainSha();
155
+ await ghCreateBranch(branch, mainSha);
156
+
157
+ // Upload light + dark SVGs
158
+ const lightPath = `illustrations/light/${category}/${id}.svg`;
159
+ const darkPath = `illustrations/dark/${category}/${id}.svg`;
160
+
161
+ const [existingLight, existingDark] = await Promise.all([
162
+ ghGetFile(lightPath, branch),
163
+ ghGetFile(darkPath, branch),
164
+ ]);
165
+ await Promise.all([
166
+ ghPutBinaryFile(lightPath, lightBuf, `feat(illustrations): add ${id} (light)`, branch, existingLight?.sha),
167
+ ghPutBinaryFile(darkPath, darkBuf, `feat(illustrations): add ${id} (dark)`, branch, existingDark?.sha),
168
+ ]);
169
+
170
+ // Update index.css — add CSS rules
171
+ const cssFile = await ghGetFile('index.css', branch);
172
+ if (cssFile) {
173
+ const newRules = `\n.dot-illustration img.${id}.light {\n content: url('./illustrations/light/${category}/${id}.svg');\n}\n.dot-illustration img.${id}.dark {\n content: url('./illustrations/dark/${category}/${id}.svg');\n}\n`;
174
+ const updatedCss = cssFile.content + newRules;
175
+ await ghPutFile('index.css', updatedCss, `feat(css): add ${id} illustration rules`, branch, cssFile.sha);
176
+ }
177
+
178
+ // Update demo/script.js — append to the correct array
179
+ const scriptFile = await ghGetFile('demo/script.js', branch);
180
+ if (scriptFile) {
181
+ const arrayName = category === 'dashboards' ? 'dashboardsList' : 'globalList';
182
+ const updated = appendToJsArray(scriptFile.content, arrayName, id);
183
+ if (updated !== scriptFile.content) {
184
+ await ghPutFile('demo/script.js', updated, `feat(demo): add ${id} to ${arrayName}`, branch, scriptFile.sha);
185
+ }
186
+ }
187
+
188
+ const pr = await ghCreatePR(
189
+ branch,
190
+ `feat(illustrations): add ${id} (${category})`,
191
+ `## New illustration: \`${id}\`
192
+
193
+ **Category:** ${category}
194
+
195
+ ### Files added
196
+ - \`illustrations/light/${category}/${id}.svg\`
197
+ - \`illustrations/dark/${category}/${id}.svg\`
198
+
199
+ ### Checklist for reviewer
200
+ - [ ] Light and dark variants are visually correct
201
+ - [ ] SVGs are optimised (no raster images, no external references)
202
+ - [ ] CSS rules added to \`index.css\`
203
+ - [ ] Demo list updated in \`demo/script.js\`
204
+
205
+ > This PR was created automatically via the dot-illustrations demo upload UI.`
206
+ );
207
+ return pr.html_url;
208
+ }
209
+
210
+ // ─── High-level: create PR for a new integration ─────────────────────────────
211
+
212
+ /**
213
+ * @param {string} id - integration ID (no .svg)
214
+ * @param {ArrayBuffer} svgBuf - SVG file
215
+ * @returns {string} PR URL
216
+ */
217
+ async function ghCreateIntegrationPR(id, svgBuf) {
218
+ const slug = id.toLowerCase().replace(/[^a-z0-9-]/g, '-');
219
+ const branch = `add-integration-${slug}-${Date.now()}`;
220
+
221
+ const mainSha = await ghGetMainSha();
222
+ await ghCreateBranch(branch, mainSha);
223
+
224
+ // Upload SVG
225
+ const svgPath = `integrations/${id}.svg`;
226
+ const existingSvg = await ghGetFile(svgPath, branch);
227
+ await ghPutBinaryFile(svgPath, svgBuf, `feat(integrations): add ${id}`, branch, existingSvg?.sha);
228
+
229
+ // Update index.css
230
+ const cssFile = await ghGetFile('index.css', branch);
231
+ if (cssFile) {
232
+ const newRule = `\n.dot-integration img.${id} {\n content: url('./integrations/${id}.svg');\n}\n`;
233
+ await ghPutFile('index.css', cssFile.content + newRule, `feat(css): add ${id} integration rule`, branch, cssFile.sha);
234
+ }
235
+
236
+ // Update demo/script.js — append to integrationsList
237
+ const scriptFile = await ghGetFile('demo/script.js', branch);
238
+ if (scriptFile) {
239
+ const updated = appendToJsArray(scriptFile.content, 'integrationsList', id);
240
+ if (updated !== scriptFile.content) {
241
+ await ghPutFile('demo/script.js', updated, `feat(demo): add ${id} to integrationsList`, branch, scriptFile.sha);
242
+ }
243
+ }
244
+
245
+ const pr = await ghCreatePR(
246
+ branch,
247
+ `feat(integrations): add ${id} logo`,
248
+ `## New integration: \`${id}\`
249
+
250
+ **File:** \`integrations/${id}.svg\`
251
+
252
+ ### Checklist for reviewer
253
+ - [ ] SVG is optimised and legible at 48×48px
254
+ - [ ] Logo follows brand guidelines
255
+ - [ ] No raster images or external references
256
+ - [ ] CSS rule added to \`index.css\`
257
+ - [ ] Demo list updated in \`demo/script.js\`
258
+
259
+ > This PR was created automatically via the dot-illustrations demo upload UI.`
260
+ );
261
+ return pr.html_url;
262
+ }
263
+
264
+ // ─── Utility: append an ID to a JS array in source text ──────────────────────
265
+
266
+ function appendToJsArray(source, arrayName, id) {
267
+ // Find the last entry in the named array and insert after it
268
+ const re = new RegExp(`(const ${arrayName} = \\[)([\\s\\S]*?)(\\s*\\];)`, 'm');
269
+ const match = source.match(re);
270
+ if (!match) return source;
271
+ const body = match[2];
272
+ const indent = (body.match(/\n(\s+)"/) || body.match(/\n(\s+)/) || ['', ' '])[1];
273
+ const newEntry = `\n${indent}"${id}",`;
274
+ return source.replace(re, `$1$2${newEntry}$3`);
275
+ }
276
+
277
+ // ─── Export ──────────────────────────────────────────────────────────────────
278
+
279
+ window.GitHubUpload = {
280
+ config: GITHUB_UPLOAD_CONFIG,
281
+ getToken: ghGetToken,
282
+ setToken: ghSetToken,
283
+ clearToken: ghClearToken,
284
+ getUser: ghGetUser,
285
+ startDeviceFlow: ghStartDeviceFlow,
286
+ pollForToken: ghPollForToken,
287
+ createIllustrationPR: ghCreateIllustrationPR,
288
+ createIntegrationPR: ghCreateIntegrationPR,
289
+ };
package/demo/index.html CHANGED
@@ -9,6 +9,7 @@
9
9
  <script src="https://unpkg.com/@heroicons/vue@2.0.18/dist/heroicons.min.js"></script>
10
10
  <link rel="stylesheet" href="../index.css" />
11
11
  <link rel="stylesheet" href="./demo.css" />
12
+ <script src="./github-upload.js" defer></script>
12
13
  <script src="script.js" defer></script>
13
14
  <script>
14
15
  // Tailwind dark mode config
@@ -19,23 +20,38 @@
19
20
  <div id="floating-bar" class="fixed top-0 left-0 w-full z-50 flex items-center justify-center bg-white/90 dark:bg-gray-900/90 shadow-md px-2 py-1 transition-transform duration-300 -translate-y-full" style="backdrop-filter: blur(6px);">
20
21
  <div class="flex flex-row items-center gap-2 w-full max-w-5xl">
21
22
  <input class="floating-search flex-1 px-2 py-1 border border-gray-300 dark:border-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm" type="text" id="floatingInput" autocomplete="off" placeholder="Search...">
22
- <nav class="inline-flex rounded shadow overflow-hidden border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800" role="tablist">
23
- <button class="floating-tab tab-btn px-3 py-1 text-xs font-medium focus:outline-none transition-colors duration-200 text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900" id="floating-tab-all" data-tab="all" aria-selected="true" role="tab">All</button>
24
- <button class="floating-tab tab-btn px-3 py-1 text-xs font-medium focus:outline-none transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" id="floating-tab-global" data-tab="global" aria-selected="false" role="tab">Global</button>
25
- <button class="floating-tab tab-btn px-3 py-1 text-xs font-medium focus:outline-none transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" id="floating-tab-dashboards" data-tab="dashboards" aria-selected="false" role="tab">Dashboards</button>
26
- <button class="floating-tab tab-btn px-3 py-1 text-xs font-medium focus:outline-none transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" id="floating-tab-favourites" data-tab="favourites" aria-selected="false" role="tab">Favourites</button>
23
+ <nav class="inline-flex rounded shadow overflow-hidden border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-x-auto" id="floating-tabs" role="tablist">
24
+ <button class="floating-tab tab-btn whitespace-nowrap px-3 py-1 text-xs font-medium focus:outline-none transition-colors duration-200 text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900" id="floating-tab-all" data-tab="all" aria-selected="true" role="tab">All</button>
25
+ <button class="floating-tab tab-btn whitespace-nowrap px-3 py-1 text-xs font-medium focus:outline-none transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" id="floating-tab-global" data-tab="global" aria-selected="false" role="tab">Global</button>
26
+ <button class="floating-tab tab-btn whitespace-nowrap px-3 py-1 text-xs font-medium focus:outline-none transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" id="floating-tab-dashboards" data-tab="dashboards" aria-selected="false" role="tab">Dashboards</button>
27
+ <button class="floating-tab tab-btn whitespace-nowrap px-3 py-1 text-xs font-medium focus:outline-none transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" id="floating-tab-integrations" data-tab="integrations" aria-selected="false" role="tab">Integrations</button>
28
+ <button class="floating-tab tab-btn whitespace-nowrap px-3 py-1 text-xs font-medium focus:outline-none transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" id="floating-tab-favourites" data-tab="favourites" aria-selected="false" role="tab">Favourites</button>
29
+ <button class="floating-tab tab-btn whitespace-nowrap px-3 py-1 text-xs font-medium focus:outline-none transition-colors duration-200 text-green-700 dark:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/40" id="floating-tab-upload" data-tab="upload" aria-selected="false" role="tab">+ Upload</button>
27
30
  </nav>
28
31
  </div>
29
32
  </div>
30
33
  <div class="container mx-auto px-4 py-8">
31
34
  <div class="flex flex-col md:flex-row items-center justify-between mb-6 gap-4">
32
35
  <h1 class="text-3xl md:text-5xl font-bold text-center md:text-left text-gray-900 dark:text-white">Dot Illustrations</h1>
33
- <button id="theme-toggle" aria-label="Toggle dark mode" class="flex items-center gap-2 px-4 py-2 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-full shadow hover:bg-gray-300 dark:hover:bg-gray-700 transition focus:outline-none focus:ring-2 focus:ring-blue-500">
34
- <span id="theme-icon" class="transition-transform duration-300">
35
- <!-- Sun/Moon icon will be injected by JS -->
36
- </span>
37
- <span class="hidden sm:inline">Switch theme</span>
38
- </button>
36
+ <div class="flex items-center gap-3">
37
+ <!-- GitHub connect -->
38
+ <div id="gh-auth-area">
39
+ <button id="gh-connect-btn" class="flex items-center gap-2 px-3 py-2 bg-gray-900 dark:bg-gray-700 text-white rounded-full shadow hover:bg-gray-700 dark:hover:bg-gray-600 transition text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
40
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222 0 1.606-.015 2.898-.015 3.293 0 .322.216.694.825.576C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
41
+ Connect GitHub
42
+ </button>
43
+ <span id="gh-user-badge" class="hidden items-center gap-2 px-3 py-2 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded-full text-sm">
44
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>
45
+ <span id="gh-user-name"></span>
46
+ <button id="gh-logout-btn" class="ml-1 text-xs underline opacity-70 hover:opacity-100">Log out</button>
47
+ </span>
48
+ </div>
49
+ <!-- Dark mode toggle -->
50
+ <button id="theme-toggle" aria-label="Toggle dark mode" class="flex items-center gap-2 px-4 py-2 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-full shadow hover:bg-gray-300 dark:hover:bg-gray-700 transition focus:outline-none focus:ring-2 focus:ring-blue-500">
51
+ <span id="theme-icon" class="transition-transform duration-300"></span>
52
+ <span class="hidden sm:inline">Switch theme</span>
53
+ </button>
54
+ </div>
39
55
  </div>
40
56
  <p class="text-center text-gray-700 dark:text-gray-300 mb-2">
41
57
  For more details please see
@@ -47,16 +63,111 @@
47
63
  <div id="search-suggestions" class="absolute top-full left-1/2 -translate-x-1/2 w-full md:w-1/2 z-50 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-b-lg shadow-lg hidden max-h-72 overflow-y-auto"></div>
48
64
  </div>
49
65
  <!-- Tabs -->
50
- <div class="mb-8 flex justify-center">
51
- <nav class="inline-flex rounded-lg shadow overflow-hidden border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800" role="tablist">
52
- <button class="tab-btn px-6 py-2 text-sm font-medium focus:outline-none transition-colors duration-200 text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900" id="tab-all" data-tab="all" aria-selected="true" role="tab">All Illustrations</button>
53
- <button class="tab-btn px-6 py-2 text-sm font-medium focus:outline-none transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" id="tab-global" data-tab="global" aria-selected="false" role="tab">Global</button>
54
- <button class="tab-btn px-6 py-2 text-sm font-medium focus:outline-none transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" id="tab-dashboards" data-tab="dashboards" aria-selected="false" role="tab">Dashboards</button>
55
- <button class="tab-btn px-6 py-2 text-sm font-medium focus:outline-none transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" id="tab-favourites" data-tab="favourites" aria-selected="false" role="tab">Favourites</button>
66
+ <div class="mb-4 flex justify-center overflow-x-auto">
67
+ <nav class="inline-flex rounded-lg shadow overflow-hidden border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shrink-0" role="tablist" id="main-tabs">
68
+ <button class="tab-btn whitespace-nowrap px-5 py-2 text-sm font-medium focus:outline-none transition-colors duration-200 text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900" id="tab-all" data-tab="all" aria-selected="true" role="tab">All Illustrations</button>
69
+ <button class="tab-btn whitespace-nowrap px-5 py-2 text-sm font-medium focus:outline-none transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" id="tab-global" data-tab="global" aria-selected="false" role="tab">Global</button>
70
+ <button class="tab-btn whitespace-nowrap px-5 py-2 text-sm font-medium focus:outline-none transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" id="tab-dashboards" data-tab="dashboards" aria-selected="false" role="tab">Dashboards</button>
71
+ <button class="tab-btn whitespace-nowrap px-5 py-2 text-sm font-medium focus:outline-none transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" id="tab-integrations" data-tab="integrations" aria-selected="false" role="tab">Integrations</button>
72
+ <button class="tab-btn whitespace-nowrap px-5 py-2 text-sm font-medium focus:outline-none transition-colors duration-200 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" id="tab-favourites" data-tab="favourites" aria-selected="false" role="tab">Favourites</button>
73
+ <button class="tab-btn whitespace-nowrap px-5 py-2 text-sm font-medium focus:outline-none transition-colors duration-200 text-green-700 dark:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/40" id="tab-upload" data-tab="upload" aria-selected="false" role="tab">+ Upload New</button>
56
74
  </nav>
57
75
  </div>
58
- <div id="illustration-list" class="demo-wrapper dot-illustration grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 gap-6 max-h-[calc(100vh-300px)] overflow-y-auto px-2">
59
- <!-- Illustrations will be injected here -->
76
+
77
+ <!-- Alphabet filter strip -->
78
+ <div id="alphabet-strip" class="hidden mb-4 flex flex-wrap justify-center gap-1 px-2">
79
+ <button class="alpha-btn px-2 py-0.5 text-xs rounded font-mono bg-blue-600 text-white" data-letter="all">All</button>
80
+ </div>
81
+
82
+ <!-- Result count + integration note -->
83
+ <p id="result-count" class="text-center text-sm text-gray-500 dark:text-gray-400 mb-4"></p>
84
+ <p id="integrations-note" class="hidden text-center text-xs text-gray-500 dark:text-gray-400 mb-4">
85
+ Integration logos are single-theme (no dark variant). Use <code class="font-mono bg-gray-200 dark:bg-gray-800 px-1 rounded">.dot-integration</code> wrapper.
86
+ </p>
87
+
88
+ <!-- Grid -->
89
+ <div id="illustration-list" class="demo-wrapper dot-illustration grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 gap-6 max-h-[calc(100vh-320px)] overflow-y-auto px-2">
90
+ <!-- injected by JS -->
91
+ </div>
92
+
93
+ <!-- Upload panel -->
94
+ <div id="upload-panel" class="hidden mt-8">
95
+ <div class="max-w-2xl mx-auto grid md:grid-cols-2 gap-6">
96
+
97
+ <!-- Illustration upload -->
98
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 border border-gray-200 dark:border-gray-700 flex flex-col gap-4">
99
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Upload Illustration → PR</h2>
100
+ <div id="upload-illus-auth-gate" class="p-3 bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-700 rounded-lg text-sm text-yellow-800 dark:text-yellow-200">
101
+ Connect GitHub (header) to enable.
102
+ </div>
103
+ <div id="upload-illus-form" class="hidden flex flex-col gap-3">
104
+ <div>
105
+ <label class="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 block">Illustration ID <span class="text-red-500">*</span></label>
106
+ <input id="upload-illus-id" type="text" placeholder="e.g. my-illustration" class="w-full px-3 py-1.5 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
107
+ </div>
108
+ <div>
109
+ <label class="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 block">Category</label>
110
+ <select id="upload-illus-category" class="w-full px-3 py-1.5 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
111
+ <option value="global" selected>Global</option>
112
+ <option value="dashboards">Dashboards</option>
113
+ </select>
114
+ </div>
115
+ <div class="grid grid-cols-2 gap-3">
116
+ <div>
117
+ <label class="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 block">☀️ Light SVG <span class="text-red-500">*</span></label>
118
+ <div id="illus-light-drop" class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-3 text-center cursor-pointer hover:border-blue-400 transition text-xs text-gray-500 dark:text-gray-400">
119
+ Drop or click
120
+ <input id="illus-light-input" type="file" accept=".svg" class="hidden" />
121
+ </div>
122
+ <p id="illus-light-name" class="text-xs text-green-600 dark:text-green-400 mt-1 truncate hidden"></p>
123
+ </div>
124
+ <div>
125
+ <label class="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 block">🌙 Dark SVG <span class="text-red-500">*</span></label>
126
+ <div id="illus-dark-drop" class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-3 text-center cursor-pointer hover:border-blue-400 transition text-xs text-gray-500 dark:text-gray-400">
127
+ Drop or click
128
+ <input id="illus-dark-input" type="file" accept=".svg" class="hidden" />
129
+ </div>
130
+ <p id="illus-dark-name" class="text-xs text-green-600 dark:text-green-400 mt-1 truncate hidden"></p>
131
+ </div>
132
+ </div>
133
+ <button id="upload-illus-btn" class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium text-sm transition focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50">Create PR</button>
134
+ <div id="upload-illus-status" class="hidden text-sm text-center"></div>
135
+ <div id="upload-illus-pr-link" class="hidden text-center">
136
+ <a id="upload-illus-pr-anchor" href="#" target="_blank" class="inline-flex items-center gap-2 px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white rounded-lg font-medium text-sm transition">View Pull Request ↗</a>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ <!-- Integration upload -->
142
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 border border-gray-200 dark:border-gray-700 flex flex-col gap-4">
143
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Upload Integration → PR</h2>
144
+ <div id="upload-integ-auth-gate" class="p-3 bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-700 rounded-lg text-sm text-yellow-800 dark:text-yellow-200">
145
+ Connect GitHub (header) to enable.
146
+ </div>
147
+ <div id="upload-integ-form" class="hidden flex flex-col gap-3">
148
+ <div>
149
+ <label class="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 block">Integration ID <span class="text-red-500">*</span></label>
150
+ <input id="upload-integ-id" type="text" placeholder="e.g. my-tool" class="w-full px-3 py-1.5 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
151
+ </div>
152
+ <div>
153
+ <label class="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 block">SVG file (single, no dark variant) <span class="text-red-500">*</span></label>
154
+ <div id="integ-svg-drop" class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-4 text-center cursor-pointer hover:border-blue-400 transition text-sm text-gray-500 dark:text-gray-400">
155
+ Drop SVG here or click to browse
156
+ <input id="integ-svg-input" type="file" accept=".svg" class="hidden" />
157
+ </div>
158
+ <p id="integ-svg-name" class="text-xs text-green-600 dark:text-green-400 mt-1 truncate hidden"></p>
159
+ </div>
160
+ <div class="p-3 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700 rounded-lg text-xs text-blue-800 dark:text-blue-200">
161
+ Integration logos have no dark variant. Use a square, brand-accurate SVG.
162
+ </div>
163
+ <button id="upload-integ-btn" class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium text-sm transition focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50">Create PR</button>
164
+ <div id="upload-integ-status" class="hidden text-sm text-center"></div>
165
+ <div id="upload-integ-pr-link" class="hidden text-center">
166
+ <a id="upload-integ-pr-anchor" href="#" target="_blank" class="inline-flex items-center gap-2 px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white rounded-lg font-medium text-sm transition">View Pull Request ↗</a>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
60
171
  </div>
61
172
  </div>
62
173
  <!-- Modal for illustration details -->
@@ -85,5 +196,33 @@
85
196
  <button id="modal-close-bottom" class="mt-4 px-4 py-2 rounded bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 font-medium hover:bg-gray-300 dark:hover:bg-gray-600 transition">Close</button>
86
197
  </div>
87
198
  </div>
199
+ <!-- GitHub Device Flow modal -->
200
+ <div id="gh-device-modal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 dark:bg-black/70 hidden">
201
+ <div class="bg-white dark:bg-gray-900 rounded-xl shadow-2xl max-w-sm w-full p-6 relative flex flex-col items-center mx-4">
202
+ <button id="gh-device-close" class="absolute top-3 right-3 text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 text-2xl font-bold focus:outline-none">&times;</button>
203
+ <svg class="w-10 h-10 mb-4 text-gray-800 dark:text-gray-200" fill="currentColor" viewBox="0 0 24 24"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222 0 1.606-.015 2.898-.015 3.293 0 .322.216.694.825.576C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
204
+ <h2 class="text-xl font-bold text-gray-900 dark:text-white mb-2">Connect GitHub</h2>
205
+ <div id="gh-device-step-code" class="w-full flex flex-col items-center gap-3">
206
+ <p class="text-sm text-gray-600 dark:text-gray-400 text-center">Open the URL below and enter the code to authorise.</p>
207
+ <a id="gh-device-uri" href="#" target="_blank" class="text-blue-600 dark:text-blue-400 underline text-sm font-medium"></a>
208
+ <div class="flex items-center gap-2">
209
+ <code id="gh-device-code" class="text-2xl font-mono tracking-widest bg-gray-100 dark:bg-gray-800 px-4 py-2 rounded-lg text-gray-900 dark:text-white"></code>
210
+ <button id="gh-copy-code-btn" class="text-xs px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600 transition">Copy</button>
211
+ </div>
212
+ <div class="flex items-center gap-2 text-gray-500 dark:text-gray-400 text-sm">
213
+ <svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path></svg>
214
+ Waiting for authorisation…
215
+ </div>
216
+ </div>
217
+ <div id="gh-device-step-pat" class="w-full flex flex-col gap-3 hidden">
218
+ <p class="text-sm text-gray-600 dark:text-gray-400 text-center">Enter a GitHub Personal Access Token with <code class="font-mono text-xs">public_repo</code> scope.</p>
219
+ <input id="gh-pat-input" type="password" placeholder="ghp_…" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
220
+ <a href="https://github.com/settings/tokens/new?scopes=public_repo&description=dot-illustrations+demo+upload" target="_blank" class="text-xs text-blue-600 dark:text-blue-400 underline text-center">Create a token on GitHub ↗</a>
221
+ <button id="gh-pat-submit" class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition">Connect with token</button>
222
+ </div>
223
+ <div id="gh-device-error" class="hidden mt-2 text-sm text-red-600 dark:text-red-400 text-center"></div>
224
+ </div>
225
+ </div>
226
+
88
227
  </body>
89
228
  </html>