From 8ca4e83a970853a10a31c713c06213c01e42c57e Mon Sep 17 00:00:00 2001 From: widal001 Date: Tue, 24 Mar 2026 19:20:50 -0400 Subject: [PATCH 1/9] feat: Adds kv bindings --- wrangler.jsonc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wrangler.jsonc b/wrangler.jsonc index 62e5ea3..f04dcc9 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -9,7 +9,8 @@ "kv_namespaces": [ { "binding": "CACHE", - "id": "" + "id": "333b7b5cf0b14c1986dba3bb17489491", + "preview_id": "394556b512d04b5794ea208739b6d79a" } ] } From 53f77183dfdcf2c8281c049652284d09b585bc09 Mon Sep 17 00:00:00 2001 From: widal001 Date: Tue, 24 Mar 2026 19:44:48 -0400 Subject: [PATCH 2/9] ci: Adds CI/CD for cloudflare --- .github/workflows/deploy.yml | 31 +++++++++++++++ .github/workflows/preview-cleanup.yml | 28 ++++++++++++++ .github/workflows/preview.yml | 54 +++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/preview-cleanup.yml create mode 100644 .github/workflows/preview.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..26a57a8 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,31 @@ +name: Deploy + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v5 + with: + version: 10 + + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm run build + + - name: Deploy to Cloudflare Workers + run: pnpm exec wrangler deploy + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.github/workflows/preview-cleanup.yml b/.github/workflows/preview-cleanup.yml new file mode 100644 index 0000000..6c84822 --- /dev/null +++ b/.github/workflows/preview-cleanup.yml @@ -0,0 +1,28 @@ +name: Preview Cleanup + +on: + pull_request: + types: [closed] + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v5 + with: + version: 10 + + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Delete preview alias + run: pnpm exec wrangler versions delete-alias pr-${{ github.event.pull_request.number }} + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 0000000..65b1820 --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,54 @@ +name: Preview Deploy + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + preview: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v5 + with: + version: 10 + + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm run build + + - name: Deploy preview + run: pnpm exec wrangler versions upload --preview-alias pr-${{ github.event.pull_request.number }} + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + - name: Find existing comment + id: find-comment + uses: peter-evans/find-comment@v3 + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: "github-actions[bot]" + body-includes: Preview Deployed + + - name: Post preview link comment + uses: peter-evans/create-or-update-comment@v4 + with: + comment-id: ${{ steps.find-comment.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body: | + **Preview Deployed!** + + Preview your changes at: https://pr-${{ github.event.pull_request.number }}-github-activity-dashboard.${{ secrets.CLOUDFLARE_WORKERS_SUBDOMAIN }}.workers.dev + + This preview will be automatically deleted when the PR is closed. + edit-mode: replace From 232799cee308e7dfa3af1d8da7ee70ce29a80a28 Mon Sep 17 00:00:00 2001 From: widal001 Date: Tue, 24 Mar 2026 20:04:38 -0400 Subject: [PATCH 3/9] ci: Fix GitHub action formatting --- .github/workflows/preview.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 65b1820..3955d88 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -37,7 +37,7 @@ jobs: uses: peter-evans/find-comment@v3 with: issue-number: ${{ github.event.pull_request.number }} - comment-author: "github-actions[bot]" + comment-author: 'github-actions[bot]' body-includes: Preview Deployed - name: Post preview link comment From f9254ea93355bd8ec7b45e59b5969ddaeca5f76f Mon Sep 17 00:00:00 2001 From: widal001 Date: Tue, 24 Mar 2026 20:07:10 -0400 Subject: [PATCH 4/9] build: Adds wrangler --- package.json | 3 ++- pnpm-lock.yaml | 25 ++++++++++++++----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 54f6f38..1cfb5f5 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "typescript-eslint": "^8.54.0", "vite": "^7.3.1", "vitest": "^4.1.0", - "vitest-browser-svelte": "^2.0.2" + "vitest-browser-svelte": "^2.0.2", + "wrangler": "^4.77.0" }, "dependencies": { "@sveltejs/adapter-cloudflare": "^7.2.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e1c802..3307fc0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@sveltejs/adapter-cloudflare': specifier: ^7.2.8 - version: 7.2.8(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@22.19.15)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)))(wrangler@4.76.0(@cloudflare/workers-types@4.20260317.1)) + version: 7.2.8(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@22.19.15)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)))(wrangler@4.77.0(@cloudflare/workers-types@4.20260317.1)) octokit: specifier: ^5.0.5 version: 5.0.5 @@ -81,6 +81,9 @@ importers: vitest-browser-svelte: specifier: ^2.0.2 version: 2.1.0(svelte@5.54.0)(vitest@4.1.0) + wrangler: + specifier: ^4.77.0 + version: 4.77.0(@cloudflare/workers-types@4.20260317.1) packages: @@ -1476,8 +1479,8 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - miniflare@4.20260317.1: - resolution: {integrity: sha512-A3csI1HXEIfqe3oscgpoRMHdYlkReQKPH/g5JE53vFSjoM6YIAOGAzyDNeYffwd9oQkPWDj9xER8+vpxei8klA==} + miniflare@4.20260317.2: + resolution: {integrity: sha512-qNL+yWAFMX6fr0pWU6Lx1vNpPobpnDSF1V8eunIckWvoIQl8y1oBjL2RJFEGY3un+l3f9gwW9dirDPP26usYJQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -1889,9 +1892,9 @@ packages: resolution: {integrity: sha512-+TvsA6VAVoMC3XDKR5MoC/qlLqDixEfOBysDEKnPIPou/NvoPWCAuXHXMsswwlvmEuvX56lQjvELLyLuzTKvRw==} engines: {node: '>=12'} - wrangler@4.76.0: - resolution: {integrity: sha512-Wan+CU5a0tu4HIxGOrzjNbkmxCT27HUmzrMj6kc7aoAnjSLv50Ggcn2Ant7wNQrD6xW3g31phKupZJgTZ8wZfQ==} - engines: {node: '>=20.0.0'} + wrangler@4.77.0: + resolution: {integrity: sha512-E2Gm69+K++BFd3QvoWjC290RPQj1vDOUotA++sNHmtKPb7EP6C8Qv+1D5Ii73tfZtyNgakpqHlh8lBBbVWTKAQ==} + engines: {node: '>=20.3.0'} hasBin: true peerDependencies: '@cloudflare/workers-types': ^4.20260317.1 @@ -2576,12 +2579,12 @@ snapshots: dependencies: '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@22.19.15)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)) - '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@22.19.15)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)))(wrangler@4.76.0(@cloudflare/workers-types@4.20260317.1))': + '@sveltejs/adapter-cloudflare@7.2.8(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@22.19.15)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)))(wrangler@4.77.0(@cloudflare/workers-types@4.20260317.1))': dependencies: '@cloudflare/workers-types': 4.20260317.1 '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@22.19.15)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15)) worktop: 0.8.0-next.18 - wrangler: 4.76.0(@cloudflare/workers-types@4.20260317.1) + wrangler: 4.77.0(@cloudflare/workers-types@4.20260317.1) '@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.0)(vite@7.3.1(@types/node@22.19.15)))(svelte@5.54.0)(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.15))': dependencies: @@ -3175,7 +3178,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - miniflare@4.20260317.1: + miniflare@4.20260317.2: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.34.5 @@ -3580,13 +3583,13 @@ snapshots: mrmime: 2.0.1 regexparam: 3.0.0 - wrangler@4.76.0(@cloudflare/workers-types@4.20260317.1): + wrangler@4.77.0(@cloudflare/workers-types@4.20260317.1): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1) blake3-wasm: 2.1.5 esbuild: 0.27.3 - miniflare: 4.20260317.1 + miniflare: 4.20260317.2 path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 workerd: 1.20260317.1 From 358a21e993b8fb1d4302b92fc4966897ba1b0f1f Mon Sep 17 00:00:00 2001 From: widal001 Date: Tue, 24 Mar 2026 20:58:41 -0400 Subject: [PATCH 5/9] ci: Fix playwright tests --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5615ef..af98a82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,9 @@ jobs: - run: pnpm install --frozen-lockfile + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps chromium + - name: Lint run: pnpm run lint From de734812eb54024944db44b29d7750eadae6e3f8 Mon Sep 17 00:00:00 2001 From: widal001 Date: Tue, 24 Mar 2026 21:07:49 -0400 Subject: [PATCH 6/9] fix: Repo input combobox a11y --- src/lib/components/RepoInput.svelte | 109 +++++++++++++++++++++++++--- 1 file changed, 99 insertions(+), 10 deletions(-) diff --git a/src/lib/components/RepoInput.svelte b/src/lib/components/RepoInput.svelte index b99591f..03f8957 100644 --- a/src/lib/components/RepoInput.svelte +++ b/src/lib/components/RepoInput.svelte @@ -13,6 +13,7 @@ let loading = $state(false); let fetchedUser = $state(''); let fetchedPat = $state(''); + let highlightIndex = $state(-1); async function loadSuggestions() { if (!username || (username === fetchedUser && (pat || '') === fetchedPat)) return; @@ -39,6 +40,7 @@ repos = [...repos, trimmed]; } inputValue = ''; + highlightIndex = -1; showSuggestions = false; } @@ -47,25 +49,93 @@ } function handleKeydown(e: KeyboardEvent) { - if (e.key === 'Enter' && inputValue.trim()) { + const visible = showSuggestions && filteredSuggestions.length > 0; + + if (e.key === 'ArrowDown') { + e.preventDefault(); + if (!visible) { + showSuggestions = true; + highlightIndex = 0; + } else { + highlightIndex = Math.min(highlightIndex + 1, filteredSuggestions.length - 1); + } + scrollToHighlighted(); + return; + } + + if (e.key === 'ArrowUp') { e.preventDefault(); - addRepo(inputValue); + if (visible) { + highlightIndex = Math.max(highlightIndex - 1, 0); + scrollToHighlighted(); + } + return; } + + if (e.key === 'Enter') { + e.preventDefault(); + if (visible && highlightIndex >= 0 && highlightIndex < filteredSuggestions.length) { + addRepo(filteredSuggestions[highlightIndex]); + } else if (inputValue.trim()) { + addRepo(inputValue); + } + return; + } + + if (e.key === 'Escape') { + showSuggestions = false; + highlightIndex = -1; + return; + } + + if (e.key === 'Tab' && visible && highlightIndex >= 0) { + e.preventDefault(); + addRepo(filteredSuggestions[highlightIndex]); + return; + } + if (e.key === 'Backspace' && !inputValue && repos.length > 0) { repos = repos.slice(0, -1); } } + function scrollToHighlighted() { + requestAnimationFrame(() => { + const el = document.querySelector('.suggestion-item.highlighted'); + el?.scrollIntoView({ block: 'nearest' }); + }); + } + + function handleInput() { + showSuggestions = true; + highlightIndex = -1; + } + function handleFocus() { loadSuggestions(); showSuggestions = true; } + function handleBlur() { + // Delay so mousedown on suggestion fires before dropdown closes + setTimeout(() => { + showSuggestions = false; + highlightIndex = -1; + }, 200); + } + let filteredSuggestions = $derived( suggestions .filter((s) => !repos.includes(s)) .filter((s) => !inputValue || s.toLowerCase().includes(inputValue.toLowerCase())) + .slice(0, 20) ); + + // Reset highlight when filter changes + $effect(() => { + void filteredSuggestions; + highlightIndex = -1; + });
@@ -79,25 +149,39 @@ setTimeout(() => (showSuggestions = false), 200)} + onblur={handleBlur} placeholder={repos.length === 0 ? 'owner/repo' : 'Add another...'} + role="combobox" + aria-expanded={showSuggestions && filteredSuggestions.length > 0} + aria-autocomplete="list" + aria-controls="repo-suggestions" + aria-activedescendant={highlightIndex >= 0 ? `repo-option-${highlightIndex}` : undefined} />
{#if showSuggestions && (filteredSuggestions.length > 0 || loading)} -
+
    {#if loading} -
    Loading repos...
    +
  • Loading repos...
  • {:else} - {#each filteredSuggestions.slice(0, 20) as suggestion (suggestion)} - + {/each} {/if} -
+ {/if} @@ -171,6 +255,8 @@ overflow-y: auto; z-index: 10; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + list-style: none; + padding: 0; } .suggestion-item { @@ -183,9 +269,11 @@ font-family: var(--font-mono); font-size: 13px; color: var(--color-text); + cursor: pointer; } - .suggestion-item:hover { + .suggestion-item:hover, + .suggestion-item.highlighted { background: var(--color-bg-secondary); } @@ -193,5 +281,6 @@ color: var(--color-text-secondary); font-style: italic; font-family: var(--font-sans); + cursor: default; } From 26c5875469608f443dca9b672ca234fcbb758efd Mon Sep 17 00:00:00 2001 From: widal001 Date: Tue, 24 Mar 2026 21:22:39 -0400 Subject: [PATCH 7/9] fix: Heatmap boundaries and tooltips --- src/lib/components/ActivityHeatmap.svelte | 155 +++++++++++----------- src/lib/utils.ts | 10 +- 2 files changed, 83 insertions(+), 82 deletions(-) diff --git a/src/lib/components/ActivityHeatmap.svelte b/src/lib/components/ActivityHeatmap.svelte index 2624fcc..4e6f3d8 100644 --- a/src/lib/components/ActivityHeatmap.svelte +++ b/src/lib/components/ActivityHeatmap.svelte @@ -16,9 +16,7 @@ const dayLabels = ['', 'Mon', '', 'Wed', '', 'Fri', '']; - // Tooltip state let tooltip = $state<{ text: string; x: number; y: number } | null>(null); - let containerRef = $state(null); function getColor(count: number, maxCount: number): string { if (count === 0) return 'var(--heatmap-0)'; @@ -29,72 +27,81 @@ return 'var(--heatmap-4)'; } - interface WeekColumn { - weekIndex: number; - days: { entry: HeatmapEntry; dayOfWeek: number }[]; + // Compute grid position for each cell and month labels from the flat entries list. + // Each entry gets a column (week index) and row (day of week) based on the + // number of days since the start date's week-start (Sunday). + interface Cell { + entry: HeatmapEntry; + col: number; + row: number; } - let weeks = $derived.by(() => { - if (entries.length === 0) return []; - - const cols: WeekColumn[] = []; - let currentWeek: WeekColumn = { weekIndex: 0, days: [] }; + let grid = $derived.by(() => { + if (entries.length === 0) + return { + cells: [] as Cell[], + monthLabels: [] as { text: string; col: number }[], + totalCols: 0 + }; + + // Find the Sunday on or before the first entry + const firstDate = new Date(entries[0].date + 'T00:00:00'); + const startTime = firstDate.getTime() - firstDate.getDay() * 86400000; + const msPerDay = 86400000; + + const cells: Cell[] = []; + const monthLabels: { text: string; col: number }[] = []; + let lastMonth = -1; + // First pass: collect all month boundaries + const rawLabels: { text: string; col: number }[] = []; for (const entry of entries) { const date = new Date(entry.date + 'T00:00:00'); - const dayOfWeek = date.getDay(); // 0=Sun, 6=Sat - - if (dayOfWeek === 0 && currentWeek.days.length > 0) { - cols.push(currentWeek); - currentWeek = { weekIndex: cols.length, days: [] }; + const daysSinceStart = Math.round((date.getTime() - startTime) / msPerDay); + const col = Math.floor(daysSinceStart / 7); + const row = daysSinceStart % 7; + + cells.push({ entry, col, row }); + + const month = date.getMonth(); + if (month !== lastMonth) { + rawLabels.push({ + text: date.toLocaleDateString('en-US', { month: 'short' }), + col + }); + lastMonth = month; } - - currentWeek.days.push({ entry, dayOfWeek }); } - if (currentWeek.days.length > 0) { - cols.push(currentWeek); - } - - return cols; - }); - - let maxCount = $derived(Math.max(1, ...entries.map((e) => e.count))); - - let monthLabels = $derived.by(() => { - const labels: { text: string; weekIndex: number }[] = []; - let lastMonth = -1; - - for (const week of weeks) { - for (const day of week.days) { - const date = new Date(day.entry.date + 'T00:00:00'); - const month = date.getMonth(); - if (month !== lastMonth) { - labels.push({ - text: date.toLocaleDateString('en-US', { month: 'short' }), - weekIndex: week.weekIndex - }); - lastMonth = month; - } - break; // Only check first day of each week + // Second pass: remove labels that are too close, preferring the later one + // (i.e., if Dec col=0 and Jan col=1, drop Dec and keep Jan) + for (let i = 0; i < rawLabels.length - 1; i++) { + if (rawLabels[i + 1].col - rawLabels[i].col < 3) { + // Skip the earlier label + continue; } + monthLabels.push(rawLabels[i]); + } + // Always include the last label + if (rawLabels.length > 0) { + monthLabels.push(rawLabels[rawLabels.length - 1]); } - return labels; + const totalCols = cells.length > 0 ? cells[cells.length - 1].col + 1 : 0; + return { cells, monthLabels, totalCols }; }); - let svgWidth = $derived(dayLabelWidth + weeks.length * cellStep + cellGap); + let maxCount = $derived(Math.max(1, ...entries.map((e) => e.count))); + let svgWidth = $derived(dayLabelWidth + grid.totalCols * cellStep + cellGap); let svgHeight = $derived(monthLabelHeight + 7 * cellStep + cellGap); function showTooltip(entry: HeatmapEntry, event: MouseEvent) { - if (!containerRef) return; - const rect = containerRef.getBoundingClientRect(); const count = entry.count; const label = count === 0 ? 'No activity' : `${count} activit${count === 1 ? 'y' : 'ies'}`; tooltip = { text: `${label} on ${formatDisplayDate(entry.date)}`, - x: event.clientX - rect.left, - y: event.clientY - rect.top - 8 + x: event.clientX, + y: event.clientY - 8 }; } @@ -103,47 +110,36 @@ } -
+
- - {#each monthLabels as label (label.weekIndex)} - + {#each grid.monthLabels as label (label.col)} + {label.text} {/each} - {#each dayLabels as label, i (i)} {label} {/each} - - {#each weeks as week (week.weekIndex)} - {#each week.days as day (day.entry.date)} - showTooltip(day.entry, e)} - onmouseleave={hideTooltip} - role="img" - aria-label={`${day.entry.count} activit${day.entry.count === 1 ? 'y' : 'ies'} on ${formatDisplayDate(day.entry.date)}`} - /> - {/each} + {#each grid.cells as cell (cell.entry.date)} + showTooltip(cell.entry, e)} + onmouseleave={hideTooltip} + role="img" + aria-label={`${cell.entry.count} activit${cell.entry.count === 1 ? 'y' : 'ies'} on ${formatDisplayDate(cell.entry.date)}`} + /> {/each} - {#if tooltip} -
- {tooltip.text} -
- {/if} -
Less @@ -155,10 +151,15 @@
+{#if tooltip} +
+ {tooltip.text} +
+{/if} +