Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e4312f3
feat: add illustrated avatar asset for editorial home redesign
danieljmanningdev Sep 7, 2026
2e5222e
fix: remove homepage avatar and add responsive featured case-study image
danieljmanningdev Sep 7, 2026
ddd68cc
feat: redesign homepage with minimal editorial layout
danieljmanningdev Sep 7, 2026
261f36a
fix: join homepage contact and footer into one dark ending
danieljmanningdev Sep 7, 2026
e1fe5ab
style: build responsive editorial homepage system
danieljmanningdev Sep 7, 2026
ca1d954
fix: match header and footer tokens and anchor footer at end of dark …
danieljmanningdev Sep 7, 2026
0c46b44
feat: simplify global navigation around the DJM mark
danieljmanningdev Sep 7, 2026
df20809
test: cover editorial images, readable homepage and full-height dark …
danieljmanningdev Sep 7, 2026
e58f1c6
style: align global shell and navigation with minimal redesign
danieljmanningdev Sep 7, 2026
14f9dc0
fix: use readable availability text without invalid generic ARIA label
danieljmanningdev Sep 7, 2026
00a6ba5
fix: remove CTA from footer
danieljmanningdev Sep 7, 2026
a316e83
fix: prevent decorative preview aspect ratios from widening mobile pr…
danieljmanningdev Sep 7, 2026
87887f3
chore: add air config
danieljmanningdev Sep 7, 2026
e890028
fix: retain availability text for screen readers on compact navigation
danieljmanningdev Sep 7, 2026
afe1782
fix: add scoped light theme and shared shell colour tokens
danieljmanningdev Sep 7, 2026
8ffbcd0
test: decode lazy homepage images before capturing full-page previews
danieljmanningdev Sep 7, 2026
d2f864a
fix: use theme tokens for homepage contrast and add compact featured …
danieljmanningdev Sep 7, 2026
792dc30
test: align homepage content assertions with approved editorial layout
danieljmanningdev Sep 7, 2026
7300211
chore: remove profile img
danieljmanningdev Sep 7, 2026
d24e583
test: wait for native lazy image requests before decoding in Firefox
danieljmanningdev Sep 7, 2026
a08c409
chore: add more margin
danieljmanningdev Sep 7, 2026
d30cbde
Merge homepage refinements while retaining local edits
danieljmanningdev Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .air.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#:schema https://json.schemastore.org/any.json

env_files = []
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
args_bin = []
bin = "./tmp/main"

# Your Go entrypoint lives here
cmd = "go build -o ./tmp/main ./cmd/server"

# Faster feedback for UI work
delay = 200

entrypoint = ["./tmp/main"]

exclude_dir = [
".git",
"tmp",
"vendor",
"testdata",
"node_modules"
]

exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = true
follow_symlink = false
full_bin = ""

include_dir = []

# Watch your actual frontend files too
include_ext = [
"go",
"tpl",
"tmpl",
"html",
"css",
"js",
"svg"
]

include_file = []

kill_delay = "500ms"
log = "build-errors.log"

poll = false
poll_interval = 0

post_cmd = []
pre_cmd = []

rerun = false
rerun_delay = 500

send_interrupt = true

# Don't silently continue using an old binary if the new build fails
stop_on_error = true

[color]
app = ""
build = "yellow"
main = "magenta"
mode = ""
runner = "green"
watcher = "cyan"

[log]
main_only = false
silent = false
time = false

[misc]
clean_on_exit = true

# Air has built-in browser live reload
[proxy]
enabled = true

# Your Go server
app_port = 8080

# Open THIS in Chrome
proxy_port = 8090

# Give your app a moment to restart
app_start_timeout = 10000

[screen]
clear_on_rebuild = false
keep_scroll = true
30 changes: 23 additions & 7 deletions internal/http/home_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,21 +245,37 @@ func TestHomeIncludesSelectedWorkAndTooling(t *testing.T) {
t.Fatalf("status = %d", rec.Code)
}
body := rec.Body.String()
// Verify the current editorial content, not IDs and secondary repository
// links belonging to the retired full-length tooling section.
for _, expected := range []string{
`href="/work/salon-rebuild/"`, `href="/work/portfolio"`,
`id="workspace-project-title"`, `id="tooling-title"`,
`id="selected-work-title"`, `id="capabilities-title"`,
`Portfolio & Client Workspace`,
`go-jsonld-schema`, `djm-cli`,
`https://github.com/danieljmanningdev/djm-cli`,
`https://github.com/danieljmanningdev/go-jsonld-schema`,
`https://github.com/danieljmanningdev/go-web-core`,
`https://github.com/danieljmanningdev/go-web-security`,
`https://github.com/danieljmanningdev/go-web-auth`,
`https://github.com/danieljmanningdev/go-starter-auth-app`,
`data-theme="light"`,
`class="editorial-feature__media"`,
`salon-rebuild-home-480.avif`, `salon-rebuild-home-480.webp`,
`site-ending--home`,
`aria-labelledby="contact-title"`,
`href="mailto:daniel@danieljmanningdev.com"`,
} {
if !strings.Contains(body, expected) {
t.Errorf("missing %q", expected)
}
}
if strings.Contains(body, `marquee-track`) {
t.Error("homepage must not autoplay an unpausable capability list")
for _, removed := range []string{
`marquee-track`,
`daniel-avatar.svg`,
`class="footer-cta"`,
`Make the complex feel`,
} {
if strings.Contains(body, removed) {
t.Errorf("homepage must not restore removed content %q", removed)
}
}
if count := strings.Count(body, `id="contact"`); count != 1 {
t.Errorf("expected one contact section, got %d", count)
}
}
81 changes: 66 additions & 15 deletions scripts/check-browser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ async function check(name, callback) {
catch (error) { failures.push({ name, error: String(error.stack || error) }); results.push({ name, passed: false }); }
}

async function decodeVisibleImage(page, image) {
await image.scrollIntoViewIfNeeded();
const element = await image.elementHandle();
try {
// Scrolling schedules native lazy loading; Firefox may not have a valid
// image request yet. Wait for loaded pixels instead of racing decode().
await page.waitForFunction(el => el.complete && el.naturalWidth > 0, element);
await image.evaluate(el => el.decode());
} finally {
await element.dispose();
}
}

function overflowInPage() {
const width = document.documentElement.clientWidth;
const bad = [];
Expand Down Expand Up @@ -84,6 +97,14 @@ function overflowInPage() {
const response = await page.goto(base + route, { waitUntil: 'load' });
assert.equal(response.status(), status, 'Unexpected document status');
await page.evaluate(() => document.fonts.ready);
if (route === '/') {
// A full-page screenshot alone does not trigger every lazy image.
// Scroll and decode all homepage assets, then restore the top.
for (const image of await page.locator('img').all()) {
await decodeVisibleImage(page, image);
}
await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' }));
}
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
const overflow = await page.evaluate(overflowInPage);
assert.ok(overflow.scrollWidth <= width + 1, JSON.stringify(overflow));
Expand All @@ -99,10 +120,11 @@ function overflowInPage() {
const scan = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa']).analyze();
const name = `${engine}-${route.replace(/[^a-z0-9]/gi, '_') || 'home'}-${width}`;
fs.writeFileSync(path.join(reportDirectory, `${name}.axe.json`), JSON.stringify({ url: route, violations: scan.violations, incomplete: scan.incomplete }, null, 2));
assert.deepEqual(scan.violations.map(v => ({ id: v.id, impact: v.impact, nodes: v.nodes.map(n => n.target) })), [], 'Automated accessibility findings');
// Keep screenshots even when an accessibility assertion fails.
if (route === '/' || (route === '/work/portfolio' && width === 390)) {
await page.screenshot({ path: path.join(reportDirectory, `${name}.png`), fullPage: true });
}
assert.deepEqual(scan.violations.map(v => ({ id: v.id, impact: v.impact, nodes: v.nodes.map(n => n.target) })), [], 'Automated accessibility findings');
}
});
}
Expand All @@ -127,28 +149,57 @@ function overflowInPage() {
await page.keyboard.press('Enter');
assert.equal(await menu.getAttribute('open'), null);
});
await check(`${engine} images decode and use responsive source`, async () => {
await check(`${engine} images decode and use responsive sources`, async () => {
await page.goto(base + '/');
const image = page.locator('.home-work-preview__image');
await image.scrollIntoViewIfNeeded();
await page.waitForFunction(() => {
const el = document.querySelector('.home-work-preview__image');
return el && el.complete && el.naturalWidth > 0;
});
await image.evaluate(el => el.decode());
const info = await image.evaluate(el => ({ source: el.currentSrc, width: el.naturalWidth, height: el.naturalHeight }));
assert.ok(info.width > 0 && info.height > 0);
assert.match(info.source, /salon-rebuild-home-(480|960|1600)\.(avif|webp)$/);
results.push({ name: `${engine} responsive image selected`, ...info, passed: true });
for (const selector of ['.editorial-feature__media img', '.editorial-project--lead .editorial-project__media img']) {
const image = page.locator(selector);
await decodeVisibleImage(page, image);
const info = await image.evaluate(el => ({ source: el.currentSrc, width: el.naturalWidth, height: el.naturalHeight }));
assert.ok(info.width > 0 && info.height > 0);
assert.match(info.source, /salon-rebuild-home-(480|960|1600)\.(avif|webp)$/);
results.push({ name: `${engine} responsive image selected: ${selector}`, ...info, passed: true });
}
});
for (const width of widths) {
await check(`${engine} dark homepage ending ${width}px`, async () => {
await page.setViewportSize({ width, height: 900 });
await page.goto(base + '/', { waitUntil: 'load' });
assert.equal(await page.locator('main[data-theme="light"]').count(), 1);
assert.equal(await page.locator('.editorial-hero__avatar').count(), 0, 'Removed avatar must not return');
assert.equal(await page.locator('.footer-cta').count(), 0, 'Removed duplicate CTA must not return');
assert.equal(await page.locator('#contact').count(), 1);
await page.evaluate(() => window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'instant' }));
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
const ending = await page.evaluate(() => {
const header = document.querySelector('.site-header');
const panel = document.querySelector('.site-ending--home');
const footer = document.querySelector('.site-footer');
return {
headerBottom: header.getBoundingClientRect().bottom,
panelTop: panel.getBoundingClientRect().top,
footerTop: footer.getBoundingClientRect().top,
footerBottom: footer.getBoundingClientRect().bottom,
viewport: window.innerHeight,
colours: [header, panel, document.querySelector('#contact'), footer].map(el => getComputedStyle(el).backgroundColor),
};
});
assert.ok(ending.panelTop <= ending.headerBottom + 1, 'Light content must not show between header and ending');
assert.ok(Math.abs(ending.footerBottom - ending.viewport) <= 1, 'Footer must reach the viewport bottom');
assert.ok(ending.footerTop > ending.viewport / 2, 'Footer belongs at the end, below the contact content');
assert.equal(new Set(ending.colours).size, 1, 'Header, contact and footer must share the shell colour token');
if (width === 390 || width === 1440) {
await page.screenshot({ path: path.join(reportDirectory, `${engine}-home-ending-${width}.png`) });
}
});
}
await check(`${engine} no JavaScript content and navigation`, async () => {
const noJS = await browser.newContext({ javaScriptEnabled: false, viewport: { width: 390, height: 900 }, reducedMotion: 'reduce' });
try {
const noJSPage = await noJS.newPage();
await noJSPage.goto(base + '/', { waitUntil: 'domcontentloaded' });
// DOM readiness can precede stylesheet layout, especially in Firefox.
await noJSPage.locator('#workspace-project-title').waitFor({ state: 'visible' });
await noJSPage.locator('#tooling-title').waitFor({ state: 'visible' });
await noJSPage.getByRole('heading', { name: 'Portfolio & Client Workspace', exact: true }).waitFor({ state: 'visible' });
await noJSPage.getByRole('heading', { name: 'go-jsonld-schema', exact: true }).waitFor({ state: 'visible' });
await noJSPage.locator('.mobile-nav summary').click();
await noJSPage.locator('.mobile-nav-link').first().waitFor({ state: 'visible' });
} finally {
Expand Down
Loading
Loading