Skip to content

Commit 35a8f39

Browse files
authored
Merge pull request #19 from austin-smith/sidebar-info
Add hidden sidebar info
2 parents f062276 + 8b2a2c3 commit 35a8f39

10 files changed

Lines changed: 146 additions & 69 deletions

File tree

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
{
22
"name": "crapshack.net-astro",
33
"type": "module",
4-
"version": "3.0.2",
4+
"version": "3.1.0",
5+
"repository": {
6+
"type": "git",
7+
"url": "https://github.com/austin-smith/crapshack.net"
8+
},
59
"scripts": {
610
"dev": "astro dev",
711
"build": "astro build",
Lines changed: 1 addition & 0 deletions
Loading

src/components/Sidebar.astro

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
---
22
import WeatherToggle from './WeatherToggle.astro';
33
import { ChevronDown, ChevronUp } from '@lucide/astro';
4+
import packageJson from '../../package.json';
5+
6+
const version = packageJson.version;
7+
const repoUrl = typeof packageJson.repository === 'string'
8+
? packageJson.repository
9+
: packageJson.repository?.url || '';
410
---
511
<div id="sidebar-overlay" class="fixed inset-0 z-[90] hidden bg-transparent"></div>
612

@@ -55,7 +61,23 @@ import { ChevronDown, ChevronUp } from '@lucide/astro';
5561
</details>
5662
</div>
5763
</nav>
58-
<div class="mt-auto px-4 sm:px-5 pb-3 sm:pb-4">
64+
<div class="mt-auto px-4 sm:px-5 pb-3 sm:pb-4 space-y-3">
5965
<WeatherToggle />
66+
<div
67+
id="sidebar-info"
68+
class="hidden items-center justify-between text-xs border-t border-white/10 pt-3"
69+
>
70+
<a
71+
href={repoUrl}
72+
target="_blank"
73+
rel="noopener noreferrer"
74+
class="inline-flex items-center gap-2 text-white/60 hover:text-[var(--color-brand)] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/70 rounded-sm"
75+
aria-label="View source on GitHub"
76+
>
77+
<span aria-hidden="true" class="h-4 w-4 shrink-0 icon-github"></span>
78+
<span>crapshack.net</span>
79+
</a>
80+
<span class="text-white/40">v{version}</span>
81+
</div>
6082
</div>
6183
</aside>

src/lib/ui/toggle-group.ts

Lines changed: 81 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,37 @@
66

77
let initialized = false;
88

9+
/** Avoid rAF loops: if a group has 0-size layout, wait for resize instead. */
10+
const pendingResizeObservers = new WeakMap<HTMLElement, ResizeObserver>();
11+
12+
function getCheckedLabel(group: HTMLElement): HTMLElement | null {
13+
const checkedInput = group.querySelector<HTMLInputElement>('input[type="radio"]:checked');
14+
return checkedInput?.closest<HTMLElement>('.toggle-option')?.querySelector<HTMLElement>('.toggle-label') ?? null;
15+
}
16+
17+
function ensureResizeObserver(group: HTMLElement): void {
18+
if (pendingResizeObservers.has(group)) return;
19+
20+
const observer = new ResizeObserver(() => {
21+
// Once the group has measurable size, update to the currently checked label and stop observing.
22+
const rect = group.getBoundingClientRect();
23+
if (rect.width === 0) return;
24+
25+
const label = getCheckedLabel(group);
26+
if (!label) return;
27+
28+
observer.disconnect();
29+
pendingResizeObservers.delete(group);
30+
updateSliderPosition(group, label);
31+
});
32+
33+
pendingResizeObservers.set(group, observer);
34+
observer.observe(group);
35+
}
36+
937
/**
10-
* Updates the sliding indicator position and size for a toggle group
38+
* Updates the sliding indicator position and size for a toggle group.
39+
* Handles hidden containers and steady-state CSS scale transforms.
1140
*/
1241
function updateSliderPosition(group: HTMLElement, targetLabel: HTMLElement): void {
1342
const slider = group.querySelector<HTMLElement>('.toggle-slider');
@@ -16,10 +45,33 @@ function updateSliderPosition(group: HTMLElement, targetLabel: HTMLElement): voi
1645
const groupRect = group.getBoundingClientRect();
1746
const labelRect = targetLabel.getBoundingClientRect();
1847

19-
// Calculate position relative to the group's padding (0.25rem = 4px at default scale)
20-
// The slider starts at left: 0.25rem, so we need to offset from there
21-
const left = labelRect.left - groupRect.left - 4; // Subtract the initial 0.25rem offset
22-
const width = labelRect.width;
48+
// If completely unmeasurable (e.g. display:none), wait for layout to exist (no rAF loop).
49+
if (labelRect.width === 0 || groupRect.width === 0) {
50+
ensureResizeObserver(group);
51+
return;
52+
}
53+
54+
// Detect X scale from transforms (e.g. dialog open/close scale animation).
55+
// Important: transforms affect getBoundingClientRect() results but NOT computed styles.
56+
// We convert measured (scaled) distances back into the element’s unscaled coordinate space.
57+
const scaleXRaw = group.offsetWidth > 0 ? groupRect.width / group.offsetWidth : 1;
58+
const scaleX = Number.isFinite(scaleXRaw) && scaleXRaw > 0 ? scaleXRaw : 1;
59+
60+
// Calculate position relative to the slider's inline start (respects padding/RTL)
61+
const groupStyles = getComputedStyle(group);
62+
const sliderStyles = getComputedStyle(slider);
63+
const offsetCandidates = [
64+
parseFloat(sliderStyles.insetInlineStart || ''),
65+
parseFloat(sliderStyles.left || ''),
66+
parseFloat(groupStyles.paddingInlineStart || ''),
67+
parseFloat(groupStyles.paddingLeft || ''),
68+
];
69+
const startOffset = offsetCandidates.find((value) => Number.isFinite(value)) ?? 0;
70+
71+
// Convert scaled viewport-space distances back to unscaled local px.
72+
const deltaXScaled = labelRect.left - groupRect.left;
73+
const left = deltaXScaled / scaleX - startOffset;
74+
const width = labelRect.width / scaleX;
2375

2476
slider.style.transform = `translateX(${left}px)`;
2577
slider.style.width = `${width}px`;
@@ -33,20 +85,17 @@ function initToggleGroup(group: HTMLElement): void {
3385
if (!slider) return;
3486

3587
// Position slider on the initially checked option
36-
const checkedInput = group.querySelector<HTMLInputElement>('input[type="radio"]:checked');
37-
if (checkedInput) {
38-
const label = checkedInput.closest<HTMLElement>('.toggle-option')?.querySelector<HTMLElement>('.toggle-label');
39-
if (label) {
40-
// Disable transitions temporarily for initial positioning
41-
slider.style.transition = 'none';
42-
updateSliderPosition(group, label);
43-
// Mark as initialized to trigger opacity transition
44-
slider.setAttribute('data-initialized', 'true');
45-
// Force reflow
46-
void slider.offsetHeight;
47-
// Re-enable transitions
48-
slider.style.transition = '';
49-
}
88+
const label = getCheckedLabel(group);
89+
if (label) {
90+
// Disable transitions temporarily for initial positioning
91+
slider.style.transition = 'none';
92+
updateSliderPosition(group, label);
93+
// Mark as initialized to trigger opacity transition
94+
slider.setAttribute('data-initialized', 'true');
95+
// Force reflow
96+
void slider.offsetHeight;
97+
// Re-enable transitions
98+
slider.style.transition = '';
5099
}
51100
}
52101

@@ -58,6 +107,19 @@ export function initToggleGroups(): void {
58107
const groups = document.querySelectorAll<HTMLElement>('[data-toggle-group]');
59108
groups.forEach(initToggleGroup);
60109

110+
// Listen for programmatic value changes via custom event
111+
// Usage: document.dispatchEvent(new CustomEvent('toggle-set-value', { detail: { name: 'group-name', value: 'option-value' } }))
112+
document.addEventListener('toggle-set-value', ((e: CustomEvent<{ name: string; value: string }>) => {
113+
const { name, value } = e.detail;
114+
const radio = document.querySelector<HTMLInputElement>(
115+
`[data-toggle-group="${name}"] input[value="${value}"]`
116+
);
117+
if (radio) {
118+
radio.checked = true;
119+
radio.dispatchEvent(new Event('change', { bubbles: true }));
120+
}
121+
}) as EventListener);
122+
61123
// Handle toggle changes
62124
document.addEventListener('change', (e) => {
63125
const target = e.target;

src/pages/bitdream.astro

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const screenshotDark = '/images/bitdream/screen-grabs/screen-grab-dark.png';
2929

3030
<div class="flex flex-wrap gap-3">
3131
<a href={repoUrl} target="_blank" rel="noopener noreferrer" class="inline-flex items-center rounded-md bg-[var(--color-brand)] px-4 py-2 font-medium hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/70">
32-
<img src="/images/brands/github.svg" alt="" aria-hidden="true" class="mr-2 h-4 w-4" />
32+
<img src="/images/brands/github-black.svg" alt="" aria-hidden="true" class="mr-2 h-4 w-4" />
3333
<span>github</span>
3434
</a>
3535
<a href={releasesUrl} target="_blank" rel="noopener noreferrer" class="inline-flex items-center rounded-md border border-white/20 px-4 py-2 font-medium text-[var(--color-text)] hover:text-[var(--color-brand)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/70">
@@ -140,26 +140,16 @@ const screenshotDark = '/images/bitdream/screen-grabs/screen-grab-dark.png';
140140
}
141141
}
142142

143-
function setToggleValue(name: string, value: string) {
144-
const radio = document.querySelector<HTMLInputElement>(
145-
`[data-toggle-group="${name}"] input[value="${value}"]`
146-
);
147-
if (radio) {
148-
radio.checked = true;
149-
}
150-
}
151-
152143
screenshotTriggers.forEach((trigger) => {
153144
trigger.addEventListener('click', () => {
154145
const src = trigger.dataset.src;
155146
const alt = trigger.dataset.alt;
156147
if (dialogImg && src) {
157148
dialogImg.src = src;
158149
dialogImg.alt = alt ?? '';
159-
160-
const mode = src.includes('light') ? 'light' : 'dark';
161-
setToggleValue('screenshot-mode', mode);
162-
150+
document.dispatchEvent(new CustomEvent('toggle-set-value', {
151+
detail: { name: 'screenshot-mode', value: src.includes('light') ? 'light' : 'dark' }
152+
}));
163153
openDialog('screenshot-dialog');
164154
}
165155
});

src/pages/crapdash.astro

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const screenshotDark = '/images/crapdash/screen-grabs/screen-grab-dark.png';
2828

2929
<div class="flex flex-wrap gap-3">
3030
<a href={repoUrl} target="_blank" rel="noopener noreferrer" class="inline-flex items-center rounded-md bg-[var(--color-brand)] px-4 py-2 font-medium hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/70">
31-
<img src="/images/brands/github.svg" alt="" aria-hidden="true" class="mr-2 h-4 w-4" />
31+
<img src="/images/brands/github-black.svg" alt="" aria-hidden="true" class="mr-2 h-4 w-4" />
3232
<span>github</span>
3333
</a>
3434
<a href={dockerUrl} target="_blank" rel="noopener noreferrer" class="inline-flex items-center rounded-md border border-white/20 px-4 py-2 font-medium text-[var(--color-text)] hover:text-[var(--color-brand)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/70">
@@ -162,27 +162,16 @@ const screenshotDark = '/images/crapdash/screen-grabs/screen-grab-dark.png';
162162
}
163163
}
164164

165-
function setToggleValue(name: string, value: string) {
166-
const radio = document.querySelector<HTMLInputElement>(
167-
`[data-toggle-group="${name}"] input[value="${value}"]`
168-
);
169-
if (radio) {
170-
radio.checked = true;
171-
}
172-
}
173-
174165
screenshotTriggers.forEach((trigger) => {
175166
trigger.addEventListener('click', () => {
176167
const src = trigger.dataset.src;
177168
const alt = trigger.dataset.alt;
178169
if (dialogImg && src) {
179170
dialogImg.src = src;
180171
dialogImg.alt = alt ?? '';
181-
182-
// Set toggle to match clicked screenshot
183-
const mode = src.includes('light') ? 'light' : 'dark';
184-
setToggleValue('screenshot-mode', mode);
185-
172+
document.dispatchEvent(new CustomEvent('toggle-set-value', {
173+
detail: { name: 'screenshot-mode', value: src.includes('light') ? 'light' : 'dark' }
174+
}));
186175
openDialog('screenshot-dialog');
187176
}
188177
});

src/pages/stuckers-app.astro

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ const appStoreUrl = 'https://apps.apple.com/us/app/stuckers/id1173389437';
99
const iconUrl = '/images/stuckers/stuckers-icon.png';
1010
1111
const screenshots = [
12-
{ id: 'all', src: '/images/stuckers/screen-grabs/screengrab-all.png', alt: 'Stuckers sticker pack overview' },
13-
{ id: 'oat-milk-man', src: '/images/stuckers/screen-grabs/screengrab-oat-milk-man.png', alt: 'Stuckers oat milk man sticker' },
14-
{ id: 'wink', src: '/images/stuckers/screen-grabs/screengrab-wink.png', alt: 'Stuckers wink sticker' },
12+
{ id: 'all', src: '/images/stuckers/screen-grabs/screengrab-all.png', alt: 'stucker overview' },
13+
{ id: 'oat-milk-man', src: '/images/stuckers/screen-grabs/screengrab-oat-milk-man.png', alt: 'stucker oat milk man' },
14+
{ id: 'wink', src: '/images/stuckers/screen-grabs/screengrab-wink.png', alt: 'stucker wink' },
1515
];
1616
---
1717

@@ -31,7 +31,7 @@ const screenshots = [
3131

3232
<div class="flex flex-wrap gap-3">
3333
<a href={repoUrl} target="_blank" rel="noopener noreferrer" class="inline-flex items-center rounded-md bg-[var(--color-brand)] px-4 py-2 font-medium hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/70">
34-
<img src="/images/brands/github.svg" alt="" aria-hidden="true" class="mr-2 h-4 w-4" />
34+
<img src="/images/brands/github-black.svg" alt="" aria-hidden="true" class="mr-2 h-4 w-4" />
3535
<span>github</span>
3636
</a>
3737
<a
@@ -153,8 +153,8 @@ const screenshots = [
153153
// Screenshot sources
154154
const screenshotSources: Record<string, { src: string; alt: string }> = {
155155
'all': { src: '/images/stuckers/screen-grabs/screengrab-all.png', alt: 'stucker overview' },
156-
'oat-milk-man': { src: '/images/stuckers/screen-grabs/screengrab-oat-milk-man.png', alt: 'oat milk man stucker' },
157-
'wink': { src: '/images/stuckers/screen-grabs/screengrab-wink.png', alt: 'wink stucker' },
156+
'oat-milk-man': { src: '/images/stuckers/screen-grabs/screengrab-oat-milk-man.png', alt: 'stucker oat milk man' },
157+
'wink': { src: '/images/stuckers/screen-grabs/screengrab-wink.png', alt: 'stucker wink' },
158158
};
159159

160160
function setScreenshot(mode: string) {
@@ -166,15 +166,6 @@ const screenshots = [
166166
}
167167
}
168168

169-
function setToggleValue(name: string, value: string) {
170-
const radio = document.querySelector<HTMLInputElement>(
171-
`[data-toggle-group="${name}"] input[value="${value}"]`
172-
);
173-
if (radio) {
174-
radio.checked = true;
175-
}
176-
}
177-
178169
screenshotTriggers.forEach((trigger) => {
179170
trigger.addEventListener('click', () => {
180171
const src = trigger.dataset.src;
@@ -183,12 +174,11 @@ const screenshots = [
183174
if (dialogImg && src) {
184175
dialogImg.src = src;
185176
dialogImg.alt = alt ?? '';
186-
187-
// Set toggle to match clicked screenshot
188177
if (id) {
189-
setToggleValue('screenshot-view', id);
178+
document.dispatchEvent(new CustomEvent('toggle-set-value', {
179+
detail: { name: 'screenshot-view', value: id }
180+
}));
190181
}
191-
192182
openDialog('screenshot-dialog');
193183
}
194184
});

src/scripts/sidebar.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,18 @@ class SidebarController {
110110
}
111111

112112
if (!this.isOpen) return;
113+
114+
// "i" key toggles info section (version/github)
115+
if (e.key.toLowerCase() === 'i' && !this.isEditableTarget(e.target as Element | null)) {
116+
e.preventDefault();
117+
const infoEl = document.getElementById('sidebar-info');
118+
if (infoEl) {
119+
infoEl.classList.toggle('hidden');
120+
infoEl.classList.toggle('flex');
121+
}
122+
return;
123+
}
124+
113125
if (e.key === 'Escape') {
114126
e.preventDefault();
115127
this.close();

src/styles/global.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,13 @@
312312
mask: url('/images/brands/apple.svg') center/contain no-repeat;
313313
-webkit-mask: url('/images/brands/apple.svg') center/contain no-repeat;
314314
}
315+
316+
.icon-github {
317+
display: inline-block;
318+
background-color: currentColor;
319+
mask: url('/images/brands/github-white.svg') center/contain no-repeat;
320+
-webkit-mask: url('/images/brands/github-white.svg') center/contain no-repeat;
321+
}
315322
}
316323

317324
@layer utilities {

0 commit comments

Comments
 (0)