Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0e8d2b4
chore(agent): baseline — the task worktree started here
ryzizub Sep 10, 2026
b95d29a
refactor(scss): ink alpha tokens
ryzizub Sep 10, 2026
6df2323
Merge remote-tracking branch 'origin/2026' into agent/astro-engineer/…
ryzizub Sep 10, 2026
64db205
fix(styles): drop unused --ink-faint token
ryzizub Sep 10, 2026
f5e958f
merge: origin/2026 into devf-55 branch
ryzizub Sep 10, 2026
74e4993
refactor(scss): mono label mixin
ryzizub Sep 10, 2026
73c6d16
Merge remote-tracking branch 'origin/2026' into agent/astro-engineer/…
ryzizub Sep 10, 2026
cd3e432
refactor(scss): warm-wash, film-grade, on-accent, monogram/avatar/pul…
ryzizub Sep 10, 2026
9f21dbd
Merge remote-tracking branch 'origin/2026' into agent/devf-55-rest
ryzizub Sep 11, 2026
194b811
refactor(scss): pulse-opacity mixin, film S-D9, lede/link-rule primit…
ryzizub Sep 11, 2026
d0fb283
revert(scss): keep .allLink and .inline-link raw, not link-rule mixin
ryzizub Sep 11, 2026
720de8c
chore: dead code sweep (PR 4)
ryzizub Sep 11, 2026
d5af5b2
fix(invoice): stop stranding a paid invoice in processing (PR 5, O-F2)
ryzizub Sep 11, 2026
05ec7d8
ci: harden the deploy workflows (PR 6, without C-O7)
ryzizub Sep 11, 2026
03acafe
refactor(functions): one helper for a non-OK HTTP response (PR 14, F-D1)
ryzizub Sep 11, 2026
15d50c4
refactor(functions): one ti.to client for both domains (PR 15, F-D2/F…
ryzizub Sep 11, 2026
bb9518f
fix(functions): coalesce concurrent /api/* cache misses (PR 16, O-F3)
ryzizub Sep 11, 2026
99942ee
refactor(sessionize): deduplicate the All-view guards and validators …
ryzizub Sep 11, 2026
06ee7b9
fix(invoice): type the iDoklad envelope; hash the IČO in the rate-lim…
ryzizub Sep 11, 2026
49a0611
refactor(scripts): one fixture server, one route list (PR 22, C-D1/C-…
ryzizub Sep 11, 2026
6c6e1d1
chore: drop dead config (PR 25, C-O19/C-O20/C-O22/C-A10)
ryzizub Sep 11, 2026
63450b3
refactor(ci): composite build action (PR 27, C-D3)
ryzizub Sep 11, 2026
3215564
refactor(react): useRemoteData (PR 9, R-D1)
ryzizub Sep 11, 2026
b41c826
refactor(react): useDialog + <Sheet> (PR 10, R-D2, O-R16, R-D5, R-D6 …
ryzizub Sep 11, 2026
18f70c5
fix(forms): validation and a11y consistency (PR 19, O-R8/9/10/15/18/1…
ryzizub Sep 11, 2026
f6d1d79
refactor(react): time & shared helpers (PR 24, R-D3/4/6/7, O-R13/20/21)
ryzizub Sep 11, 2026
a7ec4f5
fix(react): stable onClose so useDialog's autofocus effect doesn't re…
ryzizub Sep 11, 2026
b0d5552
fix(invoice): send the field name in details, not just message (O-R19)
ryzizub Sep 11, 2026
49c4392
refactor(astro): Closer slot + /partners de-duplication (PR 8)
ryzizub Sep 11, 2026
77d3b43
refactor(astro): SectionHead + press SCSS deletion (PR 12)
ryzizub Sep 11, 2026
d9a7d37
refactor(astro): NextSteps + EmptyState + NoScriptNote (PR 13)
ryzizub Sep 11, 2026
49e365a
refactor(scss): .sr-only global primitive (PR 20 remainder)
ryzizub Sep 11, 2026
2f9e7b2
docs: refresh DESIGN.md / CLAUDE.md for the five review batches (PR 29)
ryzizub Sep 11, 2026
da27ebe
fix(astro): satisfy astro check on PartnerLogo's Image spread
ryzizub Sep 11, 2026
345d850
fix(react): stabilize useReturnFocus setter with useCallback
ryzizub Sep 11, 2026
4dd0a83
docs(design): list only mixins that actually exist in _type.scss
ryzizub Sep 11, 2026
532f684
chore(functions): delete orphaned docblocks left behind by a rename
ryzizub Sep 11, 2026
f6dae3a
Merge remote-tracking branch 'origin/2026' into agent/astro-engineer/…
ryzizub Sep 12, 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
25 changes: 25 additions & 0 deletions .github/actions/build-site/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Build site
description: >
Install and build the Astro site. Shared by firebase-hosting-merge.yml and
firebase-hosting-pull-request.yml, which were identical through the build
step and differed only in the deploy args after it. Callers must check out
the repo themselves first — a local composite action can't do it for them
(its own definition has to already be on disk to be loaded at all).
inputs:
invite-discount-url:
description: Value for PUBLIC_INVITE_DISCOUNT_URL during the build.
required: false
default: ""
runs:
using: composite
steps:
- uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
- run: npm ci
shell: bash
- run: npm run build
shell: bash
env:
PUBLIC_INVITE_DISCOUNT_URL: ${{ inputs.invite-discount-url }}
8 changes: 7 additions & 1 deletion .github/workflows/accessibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ on:
- 'astro.config.mjs'
- 'package.json'
- 'package-lock.json'
- 'scripts/a11y.mjs'
- 'scripts/**'
- '.github/workflows/accessibility.yml'
push:
branches: [2026]

permissions:
contents: read

# A re-push while an audit is still running must not stack a second one for
# the same PR — cancel the stale run, the new push wins.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
axe:
runs-on: ubuntu-latest
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/correctness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ on:
permissions:
contents: read

# A re-push while a check is still running must not stack a second one for
# the same PR — cancel the stale run, the new push wins.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
astro-check:
runs-on: ubuntu-latest
Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/firebase-functions-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ on:
# run this workflow from the Actions tab after adding the new version.
workflow_dispatch:

permissions:
contents: read

# Two merges to 2026 in quick succession must not race two overlapping
# deploys — queue them instead of cancelling either.
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
deploy_functions:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -49,13 +58,17 @@ jobs:
TITO_ACCOUNT_SLUG: ${{ vars.TITO_ACCOUNT_SLUG }}
TITO_EVENT_SLUG: ${{ vars.TITO_EVENT_SLUG }}
run: |
if [ -z "$TITO_ACCOUNT_SLUG" ] || [ -z "$TITO_EVENT_SLUG" ]; then
echo "::error::TITO_ACCOUNT_SLUG or TITO_EVENT_SLUG repo variable is unset — refusing to deploy with an empty ti.to slug" >&2
exit 1
fi
{
printf 'TITO_ACCOUNT_SLUG=%s\n' "$TITO_ACCOUNT_SLUG"
printf 'TITO_EVENT_SLUG=%s\n' "$TITO_EVENT_SLUG"
} > functions/.env
- name: Deploy functions
run: |
npx --yes firebase-tools@latest deploy \
npx --yes firebase-tools@15.30.0 deploy \
--only functions:website \
--project devfest-cz-app \
--non-interactive
24 changes: 16 additions & 8 deletions .github/workflows/firebase-hosting-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,33 @@ on:
push:
branches:
- "2026"

permissions:
contents: read

# Two merges to 2026 in quick succession must not race two overlapping
# deploys, and the older build must never be allowed to land last — queue
# them instead of cancelling either.
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: ./.github/actions/build-site
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
env:
PUBLIC_INVITE_DISCOUNT_URL: ${{ secrets.PUBLIC_INVITE_DISCOUNT_URL }}
invite-discount-url: ${{ secrets.PUBLIC_INVITE_DISCOUNT_URL }}
# Deploy with w9jds/firebase-action, which runs firebase-tools inside a
# Node 21 container. This avoids the intermittent "Premature close"
# keep-alive failure that breaks FirebaseExtended/action-hosting-deploy
# when it runs firebase-tools on the runner's security-patched Node.
# Pinned to a commit SHA (v15.22.3) rather than the tag: a git tag can
# be re-pointed by its owner, and this action receives GCP_SA_KEY.
- name: Deploy to Firebase Hosting (live)
uses: w9jds/firebase-action@v15.22.3
uses: w9jds/firebase-action@221518149891c89541cd93a1edc14a03b3e8e53d # v15.22.3
with:
args: deploy --only hosting --project devfest-cz-app
env:
Expand Down
20 changes: 12 additions & 8 deletions .github/workflows/firebase-hosting-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ name: Deploy to Firebase Hosting on PR
on: pull_request
permissions:
contents: read

# A re-push while a preview deploy is still building must not stack a second
# one for the same PR — cancel the stale build, the new push wins.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
build_and_preview:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
- uses: ./.github/actions/build-site
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
env:
PUBLIC_INVITE_DISCOUNT_URL: ${{ secrets.PUBLIC_INVITE_DISCOUNT_URL }}
invite-discount-url: ${{ secrets.PUBLIC_INVITE_DISCOUNT_URL }}
# Deploy to a per-PR preview channel with w9jds/firebase-action, which
# runs firebase-tools inside a Node 21 container. This avoids the
# intermittent "Premature close" keep-alive failure that breaks
Expand All @@ -33,8 +35,10 @@ jobs:
# deployed PRODUCTION functions; before their first prod deploy they 404 and
# the islands show their "unavailable" state. Functions deploy only via
# firebase-functions-merge.yml. To verify SSR-function CHANGES, run locally.
# Pinned to a commit SHA (v15.22.3) rather than the tag: a git tag can
# be re-pointed by its owner, and this action receives GCP_SA_KEY.
- name: Deploy to Firebase Hosting (preview channel)
uses: w9jds/firebase-action@v15.22.3
uses: w9jds/firebase-action@221518149891c89541cd93a1edc14a03b3e8e53d # v15.22.3
with:
args: hosting:channel:deploy pr-${{ github.event.pull_request.number }} --project devfest-cz-app --expires 7d
env:
Expand Down
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ function bundle.

File-based Astro routing under `src/pages/` — full route list in [README.md](README.md#key-pages). React islands (`.tsx`, `client:load`) live in `src/components/` alongside their static Astro counterparts.

**Shared building blocks, not copied blocks.** A section repeated on more than one page is a component; a fetch-then-render island is `useRemoteData`; a modal is `useDialog` + `<Sheet>`. Reach for these before writing a new copy:

- `useRemoteData<T>(load)` (`src/lib/useRemoteData.ts`) — the fetch/loading/error/data shape every data-backed island (`Speakers`, `Sessions`, `Agenda`, `SpeakersTeaser`, `Tickets`, `InvoiceForm`) shares.
- `useDialog` (`src/lib/useDialog.ts`) + `<Sheet>` (`src/components/Sheet.tsx`) — focus trap, Esc-close, autofocus and a refcounted `inert` on `#main-content` (a stacked dialog holds it until every layer closes). `SessionDetail`/`SpeakerDetail` both mount through it.
- `useMediaQuery` / `usePrefersReducedMotion` (`src/lib/useMediaQuery.ts`) — the two `matchMedia` subscriptions the islands need.
- Astro components: `Closer` (closing band, now with a default `<slot />` for a page's own contact block), `SectionHead` (the `.head-stack` primitive), `NextSteps` (the "what now?" band + field of `NextStep` rows), `NoScriptNote` (the `<noscript>` fallback under a data-backed island), `EmptyState` (the "nothing here yet" block), `PartnerLogo` (a partner mark's link-wrap + `is-plated` class).

### Firebase Integration (`src/lib/firebase.ts`)

Firebase Analytics (GA4) runs in **Google Consent Mode** for every visitor, not just those who accept. `initAnalytics()` pushes a gtag `consent: 'default'` onto the dataLayer *before* `getAnalytics()`. For an undecided visitor everything is denied, so GA4 boots cookieless — no `_ga` / `client_id`, no storage, only aggregated identifier-free pings. That yields basic traffic numbers from visitors who decline or never decide, which is the ePrivacy-exempt part. On accept, `grantAnalyticsConsent()` sends `consent: 'update'` with `analytics_storage: 'granted'` and GA4 switches to full measurement. `ad_*` stay denied permanently — we never collect for advertising.
Expand Down
54 changes: 25 additions & 29 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ ships in; if one has drifted, the token name is the durable reference.
| `src/components/*.module.scss` | co-located CSS Modules for React islands (`import s from './X.module.scss'`) |
| `src/components/*.astro` + `*.scss` | static components with a sibling stylesheet |
| `src/pages/*.scss` | page-scoped styles, one file per page |
| `src/styles/*.scss` | `@use`d mixins, not classes — `_type.scss` (`mono`, `monogram`, `lede`), `_film.scss` (`film`), `_avatar.scss` (`avatar-ring`), `_link.scss` (`link-rule`), `_motion.scss` (`pulse-opacity`). Each parameterizes the per-site drift rather than snapping it to one value — see "Component conventions" |

**[MUST] No Tailwind, no CSS-in-JS, no utility framework.** SCSS + CSS custom
properties only. There is no `tailwind.config`, no theme object, and no runtime
Expand Down Expand Up @@ -168,7 +169,6 @@ Text scale, `BaseLayout.scss:100–110`:
| `--fs-body-lg` | `1.15rem` | 106 | footnotes, short ledes |
| `--fs-lede` | `clamp(1.2rem, 1.6vw, 1.5rem)` | 107 | section ledes, status prose |
| `--fs-title-sm` | `1.45rem` | 108 | small titles, mobile record titles |
| `--fs-figure` | `1.9rem` | 109 | row figures — **no call sites today**, see Open points |
| `--fs-monogram` | `3.4rem` | 110 | initials in an empty photo well |

**[MUST]** Display type is set through `--lh-display` (`0.84`, line 114) and
Expand Down Expand Up @@ -810,10 +810,10 @@ each needs a decision, none is fixed by this PR.
single `aria-live` region; individual invalid fields are not marked, and there
is no per-field inline error. Acceptable for a two-form site, but it should be
a stated decision rather than an omission.
6. **`--fs-figure` (`BaseLayout.scss:109`) has zero call sites.** Either it is
dead and should be deleted, or something is using a literal `1.9rem` where it
should use the token. `--panel-2` and `--panel-hover` are each used in exactly
one file, which is close to the same question.
6. **Resolved: `--fs-figure` is gone.** It had zero call sites; the ramp now
runs `--fs-label-xs` → `--fs-monogram` (`BaseLayout.scss:73–83`) with no gap
where it used to sit. `--panel-2` and `--panel-hover` are each used in
exactly one file, which is close to the same question the token was.
7. **`min-width` is no longer an outlier.** `SpeakersTeaser.module.scss:60` was
the only mobile-first query when this document was written; six more
(`press/downloads.scss:109,113,117,131,135,139`) landed via merged PRs since,
Expand All @@ -839,30 +839,26 @@ each needs a decision, none is fixed by this PR.
Currently harmless — every `Closer` call site passes `tone="accent"`
explicitly — but the default itself is dead and would silently render
unstyled if a future page omitted `tone`.
12. **The font-size ramp is stated as [MUST] but broken at 23 call sites: 18**
**absolute literals** (`Countdown.module.scss:33,47,72,84`,
`Menu.scss:348,471`, `Speakers.module.scss:201,277`,
`faq.scss:33`, `press.scss:162`, `team.scss:121,141`,
`index.scss:73,148,598,643,666,756`; the `index.scss:135`
site this counted no longer exists — see below), **2 relative**
(`Footer.scss:153`, `0.85em`; `Ticker.scss:78`, `0.5em`),
**2 inherited** (`Footer.scss:321,325`), and **1 on its own token**
(`Ticker.scss:57`, `--ticker-size`). The same "every
`font-size` goes through one of these steps" claim is repeated in a source
comment at `BaseLayout.scss:98` and is equally untrue there — worth fixing
next time that file is touched, not on its own. Either these get folded
into the ramp as named steps, or downgraded to `[CURRENT]` literals with a
reason each.
**[UNRESOLVED — flagged, not guessed]** Two of the cited call sites no
longer back this count after the rebase, rather than having simply moved:
`LandingNotice.scss` (component + stylesheet) was deleted in `d38c5187`
and replaced by `NextStep.astro`, which uses `var(--fs-row)` /
`var(--fs-body)` — no literal, so that count-of-14 entry has no current
home. `index.scss:135` is now `font-size: var(--fs-label);` (not a
literal); the file does have four literal `font-size` declarations today
(`index.scss:598,643,666,756`, all `.hero-statement` / `.meta-value`
breakpoint overrides), any/all of which may be what this site meant to
count, but which one(s) requires a decision, not a relocation.
12. **The font-size ramp is stated as [MUST] but broken at several call sites**
— live literals in breakpoint overrides or one-off display type:
`index.scss`, `Countdown.module.scss`, `Menu.scss`, `Speakers.module.scss`,
`faq.scss`, `press.scss`, `team.scss`, `Ticker.scss`, `Footer.scss`.
`Footer.scss`'s two `font-size: inherit` on `.copyright`/`.opensource` are
**not** bypasses, though — both elements are `<small>`, so `inherit` resets
the UA's `0.8em` rather than picking a value outside the ramp; corrected
off this list. Separately, `privacy-policy.scss` sets the page `<h1>` to
`--fs-hero`, documented above as "home hero statement" — `--fs-display` is
the subpage h1 token. The same "every `font-size` goes through one of
these steps" claim is repeated in a source comment at
`BaseLayout.scss:98` and is equally untrue there — worth fixing next time
that file is touched, not on its own. Either these get folded into the
ramp as named steps, or downgraded to `[CURRENT]` literals with a reason
each.
**Line numbers throughout this document drift as the codebase moves; the
token name, not the cited line, is the durable reference (see intro).**
An exact call-site count is intentionally not restated here — it has
drifted at least twice already as unrelated PRs touched these files, and
the count itself is not the decision that matters.
13. **`/` has no `.fallback-note`.** `agenda`, `sessions` and `speakers` each
ship one for the no-JS / endpoint-down case; the home page's `Tickets`
island does not. Either add one, or state the exception in the MUST
Expand Down
2 changes: 1 addition & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const PRIORITY = {
'https://devfest.cz': 1.0,
'https://devfest.cz/speakers': 0.9,
'https://devfest.cz/sessions': 0.9,
'https://devfest.cz/agenda': 0.9,
'https://devfest.cz/partners': 0.8,
'https://devfest.cz/faq': 0.7,
'https://devfest.cz/team': 0.7,
Expand Down Expand Up @@ -102,7 +103,6 @@ export default defineConfig({
responsiveStyles: true,
},
prefetch: {
prefetchAll: true,
defaultStrategy: 'hover',
},
integrations: [
Expand Down
3 changes: 2 additions & 1 deletion firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Strict-Transport-Security", "value": "max-age=31536000" },
{ "key": "Content-Security-Policy", "value": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; img-src 'self' data: https:; font-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.google.com https://www.gstatic.com https://www.googletagmanager.com https://*.google-analytics.com; connect-src 'self' https://*.googleapis.com https://*.firebasedatabase.app wss://*.firebasedatabase.app https://*.cloudfunctions.net https://*.google-analytics.com https://*.analytics.google.com https://www.google.com https://www.gstatic.com; frame-src https://www.google.com https://www.youtube.com; form-action 'self' https://app.smartemailing.cz" }
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=(), interest-cohort=()" },
{ "key": "Content-Security-Policy", "value": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; img-src 'self' data: https:; font-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.google.com https://www.gstatic.com https://www.googletagmanager.com https://*.google-analytics.com; connect-src 'self' https://*.googleapis.com https://*.cloudfunctions.net https://*.google-analytics.com https://*.analytics.google.com https://www.google.com https://www.gstatic.com; frame-src https://www.google.com; form-action 'self' https://app.smartemailing.cz" }
]
},
{
Expand Down
6 changes: 2 additions & 4 deletions functions/src/invoice/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* Docs: https://resend.com/docs/api-reference/emails/send-email
*/

import { errorBody, fetchWithRetry } from '../lib/http.js';
import { assertOk, fetchWithRetry } from '../lib/http.js';
import {
BRAND,
button,
Expand Down Expand Up @@ -82,9 +82,7 @@ export async function sendEmail(cfg: EmailConfig, msg: EmailMessage): Promise<Se
{ label: 'Resend send' },
);

if (!res.ok) {
throw new Error(`Resend ${res.status} ${res.statusText}: ${await errorBody(res)}`);
}
await assertOk('Resend send', res);
return { sent: true };
}

Expand Down
Loading
Loading