Commit graph

1,087 commits

Author SHA1 Message Date
443c8b6a37 feat(activities): brand-kit logo + app name in the events page header
Replace the bare "Events" h1 with the brand-kit logo paired with the
standalone's localized name. Deployers get per-standalone logo
control via branding/<dep>/icons/events/logo.{svg,png}; the
component itself stays brand-agnostic.

Brand-kit plumbing:

- `resolveAppLogo(app?)` in vite-branding.ts mirrors the resolution
  chain pwa-assets.config.ts already uses for PWA icons
  (per-standalone svg → png → global svg → png).
- `brandAppLogoAliasEntry(app)` returns a vite alias array entry. A
  regex matches `@brand-app-logo` with or without a `?url` query so
  the file resolves cleanly under either form.
- vite.events.config.ts switches its resolve.alias to the array form
  so the per-standalone regex doesn't clash with the bare `@brand`
  string alias.

Component side: a single `import brandAppLogoUrl from '@brand-app-logo?url'`
gives EventsPage the best-resolved logo without any fallback chain
in the component.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 18:29:39 +02:00
5e3d77efec feat(activities): seat Map right after Home in the bottom nav
Map is a primary discovery surface for events, so it earns the second
slot next to Home instead of sitting at the tail of the auth-gated
tabs (My tickets, Hosting). Unchanged auth gating on the other tabs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 17:56:18 +02:00
60369ce1b1 feat(activities): drop RSVP buttons from EventDetailPage
The Going/Maybe/Not going row was redundant: the bookmark heart count
already signals casual interest and ticket sales answer "who's
actually going". Cuts an affordance whose value-add was unclear and
whose visual weight competed with the buy-ticket CTA right below it.

Removes the RSVPButton component, the useRSVP composable, and the
i18n strings that only fed those buttons. Keeps NIP52_KINDS.RSVP and
the CalendarRSVP type as protocol documentation in case we revisit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 17:54:40 +02:00
6d4a9f8c22 fix(activities): keep CreateEvent form actions inside narrow dialogs
Two narrow-viewport overflow points:

- Pricing row was a hard grid-cols-3 even at 320–360px viewports, so
  the Currency select was getting pinched. Drop to grid-cols-2 with
  Currency spanning both cols on small screens, lift to grid-cols-3
  at sm+.

- Action row was justify-end with no wrap and no width fallback, so a
  wide localized "Submit Event" label could push Cancel out of the
  dialog. Stack full-width on mobile (flex-col-reverse so Submit is
  under the thumb), back to inline at sm+.

Also belt-and-suspenders: overflow-x-hidden on the dialog content so
any future runaway child can't induce a horizontal scroll inside the
dialog body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 17:44:23 +02:00
c6455b3235 feat(activities): move Past pill to end of the temporal strip
Past used to live in the Filters collapsible — a dropdown row of its
own for a single boolean toggle. Hoist it onto the temporal strip
right after This month so it's discoverable alongside the time-window
pills without claiming any extra vertical space. Composes orthogonally
with the temporal pills the same way as before.

Drop the now-redundant past-count contribution to the Filters badge —
the pill carries its own pressed state on the strip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 17:42:11 +02:00
7af0f364cb fix(sidebar): full-width identity values with corner-offset legend badge
Restyle the Lightning / npub rows so the value gets the entire row
and the field-name label sits as a small badge straddling the top
border (fieldset-legend pattern). Long bech32 / username@domain
strings now have room to render without truncation crowding.

The bolt icon picks up a yellow tint so the Lightning row reads at
a glance alongside the neutral npub row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 17:41:58 +02:00
00f6a99f92 refactor(sidebar): show identity inline, move edit form into gear popup
Read-only identity (avatar, display name, @username, copyable
Lightning address, copyable npub) stays visible in the sheet so the
user can grab their address without opening any form. The full edit
form moves behind a gear-icon dialog. Log out stays in the sheet
footer so signing out doesn't require opening the popup.

Lightning Address and NIP-05 share the same `username@domain` in this
stack — the @username row above the card already signals NIP-05, so
the copyable address row is labeled just "Lightning".

The picture-upload row now stacks the preview above the upload widget
on narrow viewports so the Gallery/Camera buttons stay inside the
sheet/dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 17:26:25 +02:00
2c7597c25f fix(sidebar): clip horizontal overflow on profile sheets
Defensive guard against content (long addresses, code spans, etc.)
forcing a horizontal scroll inside the hamburger / profile sheet.
The root cause for individual cards is addressed separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 17:12:01 +02:00
4f7a5dcd88 fix(avatar): pair bg-secondary with text-secondary-foreground
The Avatar primitive paired bg-secondary with text-foreground, which
isn't a Shadcn-designed pair. In themes whose secondary is a bright
color (Starry Night dark: bright yellow) the global foreground
(near-white) lands on top and the initial becomes unreadable.

Switch to text-secondary-foreground so contrast is whatever the theme
author guaranteed for that pair. Pre-emptively protects any future
theme since every theme must keep secondary/secondary-foreground
contrast valid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 15:05:24 +00:00
54efff1cc6 fix(activities): relabel "Get invoice" → "Proceed" on PurchaseTicket
The lightning rail's CTA in PurchaseTicketDialog now reads
"Proceed" (or "Proceed (N tickets)" for multi-quantity) instead
of "Get invoice". Matches the language used on the fiat rails
("Continue to Stripe checkout" etc.) and reads as a generic
forward action regardless of which payment path the user picks.
2026-06-10 15:05:24 +00:00
cf9fc2db9d fix(activities): drop duplicate empty-state line on ActivityList
The empty state rendered "No activities found" twice — once as the
heading and once as the description below, because
`activities.noActivities` and `activities.search.noResults`
translate to the same string in every locale. Drop the description
paragraph (and its mb-1 spacer on the heading).
2026-06-10 15:05:24 +00:00
b7fd8c99e5 feat(activities): fuzzy search on the Tickets roster
Adds a search box above the roster list that fuzzy-matches the
holder name and ticket id via the shared useFuzzySearch (Fuse.js)
composable. Empty query keeps the unregistered-first sort intact;
typing reorders by relevance. The empty-state message now
distinguishes "no tickets sold yet" from "no rows matched the
current query" so a busy roster + a typo doesn't look like
backend trouble.
2026-06-10 15:05:24 +00:00
31e48c8f1d feat(activities): manual ticket registration from the roster tab
The "Scanned" tab becomes "Tickets" and now lists the full event
roster (sold tickets), not just the registered subset. Unregistered
rows lead the list with a Register button so the host can manually
mark someone present without a QR scan — e.g. lost phone, known in
person, or alternate proof of identity.

useTicketScanner gains registerManually(ticketId), which calls the
same PUT /tickets/register/{id} the scanner uses (so it inherits
the event-ownership gate and the unpaid/already-registered backend
checks), then refreshes stats. It skips the scanner pause + full-
screen banner since the operator initiated the action from the
list, and mirrors the session-local dedup so a subsequent QR scan
on the same ticket reports "Already scanned" instead of a duplicate
register round-trip.

The header now reads "registered / total · N to go" so the host
sees roster progress at a glance; failures from the manual register
surface as a sonner toast and the row reverts.
2026-06-10 15:05:24 +00:00
ba7c1e4cdc revert: move scan counts back above the tabs + fix tab centering
Reverts 1aeea23 and folds in the actual fix the relocation was
chasing: the Scanner / Scanned tab labels were rendering with
their icons and text mis-aligned because TabsTrigger wraps its
slot in an inline `<span class="truncate">`. A `gap-1.5` on
TabsTrigger never reached the icon/label children. Wrap each
trigger's content in an `inline-flex items-center gap-1.5` span
so the icon and label share a real flex container.
2026-06-10 15:05:24 +00:00
4de88f92f5 feat(activities): move scan counts below the camera
The Scanned / Sold / Remaining strip moves out of the page header
to below the Tabs block. The camera (or scanned list, depending on
the active tab) stays prominent at the top; the counts read as a
summary footer instead of competing with the title for attention.
The stats-error notice follows the counts strip so the warning
stays adjacent to the values it affects.
2026-06-10 15:05:24 +00:00
1dfb025df3 feat(activities): refine activity card for pending/rejected + compact
- Wash out pending/rejected events with opacity-50 + grayscale on a
  wrapper div so the operator sees at a glance the event isn't live,
  not just the small badge.
- Pull the status badge OUT of the wash-out wrapper and absolute-
  position it on Card root (bottom-2 left-2, z-10) so it stays in
  full color above the dim card. Both pending and rejected use the
  destructive token — the label text differentiates the two states.
  Bottom-left so it doesn't collide with the category chip on full
  cards or the thumbnail on compact ones.
- Compact rows in the Hosting view now show a small left-aligned
  thumbnail (w-20 h-20, self-center, ml-3, rounded-md) when the
  event carries an image — host can still recognize each event at a
  glance without paying the visual weight of a full hero.
- Card root becomes `relative overflow-hidden`; the wrapper div
  owns the conditional flex-row (compact) / flex-col (default)
  layout and the opacity/grayscale toggling.
2026-06-10 15:05:24 +00:00
e86be3229d feat(activities): My tickets toggle on the calendar view
Adds a small filter chip above the month grid that, when on, limits
the calendar to events the signed-in user holds at least one paid
ticket for (intersecting ownedActivityIds from useOwnedTickets).
Hidden when logged out — nothing to own. Left-aligned so it
doesn't collide with the fixed top-right hamburger menu.

State is local to the page on purpose: narrowing the calendar
shouldn't also narrow the feed when the user navigates back.
2026-06-10 15:05:24 +00:00
e174048052 feat(activities): tailor Hosting tab + host detail view for operators
Hosting feed (ActivitiesPage when onlyHosting):
- Hide the date picker strip + calendar shortcut and the entire
  Filters/temporal-pills row; an operator managing their roster
  doesn't need calendar navigation or temporal narrowing.
- Keep the search bar — finding a specific event in a long roster
  still matters.
- Render compact cards via a new `compact` prop on ActivityList +
  ActivityCard: no hero image, single-line title, no summary, no
  bookmark, no "Yours" badge (every card is the operator's own),
  tighter p-3 padding, single-column flex layout.

Host detail view (ActivityDetailPage when ownedLnbitsEvent):
- Drop the top-bar Scan and Edit buttons. Edit moves into the title
  row as a prominent filled-primary icon button right of the title;
  Scan moves into the tickets section.
- Render a full-width "Scan tickets" CTA in place of Buy ticket, and
  hoist it outside the ticketInfo gate so it appears even on hosted
  events that were published without AIO ticket tags.
- Hide BookmarkButton and RSVPButton for the host (favoriting /
  RSVPing your own event are noise affordances).

Detail-page badge row: "Yours" leads the row in the highlighted
secondary variant; category and tags drop to outline so the
ownership signal stands out.
2026-06-10 15:05:24 +00:00
85419ea5fa fix(activities): share filter refs across useActivities consumers
useActivityFilters allocated a fresh set of refs on every call, so
when activities-app/App.vue (Hosting bottom-nav tab) and
ActivitiesPage.vue each invoked useActivities(), they got
independent onlyHosting/temporal/etc state. Tapping Hosting toggled
the App.vue ref; the page never saw the change. Hoist the filter
refs to module scope so every consumer shares the same instance.
2026-06-10 15:05:24 +00:00
5f6eb5b142 feat(activities): restructure bottom nav around Home/MyTickets/Hosting
The bottom-nav tabs become Home, My tickets, Hosting, Map, Favorites.
- Feed is relabeled "Home" (en/fr; es was already "Inicio").
- My tickets and Hosting move out of the sidebar menu back into the
  bottom nav. Hosting is a synthetic tab — no path of its own; it
  toggles the existing onlyHosting feed filter and lands on
  /activities, with Home as the inverse (clears the filter on tap).
- Calendar leaves the bottom nav. The week strip now ends with a
  small calendar icon button that routes to /activities/calendar,
  so the entry point sits adjacent to the date UI instead of
  competing for a tab slot.
- Create activity leaves the bottom nav too. A full-width "+ Create
  activity" CTA appears at the top of the feed only when the Hosting
  tab is active, so the Create entry point lives inside the section
  it belongs to.

BottomTab gains an optional `isActive()` predicate so tabs whose
active condition doesn't reduce to "current path starts with x"
(e.g. Hosting) can compute their own state.
2026-06-10 15:05:24 +00:00
37c2b78836 feat(activities): drop image placeholder when an event has no image
Cards without an image no longer render the solid-color 16:9
placeholder + calendar glyph. They go straight to the content area
with the badges (category, price, Yours, status, Past) shown
inline in a small row at the top, so the title and details aren't
pushed below a meaningless filler block.

The placeholderBg computed (hash → HSL) is removed; it was only
feeding the deleted no-image branch.
2026-06-10 15:05:24 +00:00
d92690d3f1 feat(activities): stationary Filters column next to scrolling pills
Filters icon + Clear-all sit in a stationary left-aligned column;
only the All/Today/Tomorrow/etc temporal pills scroll horizontally.
Clear-all is tucked tightly under the Filters icon (h-5, 10px text,
gap-0.5) and shows only when a filter is active. The badge no
longer lives inside the overflow-x scroll container, so the count
chip isn't clipped at the corner anymore.
2026-06-10 15:05:24 +00:00
fb0f687c07 feat(activities): reclaim vertical space above the feed
Past events no longer gets its own row — it folds into the existing
collapsible (renamed "Filters") alongside Categories, so the feed
gains that row by default. The Filters trigger badge counts past-
events being on plus any selected categories, so users still see at
a glance when hidden toggles are active.

The standalone "Filters active / Clear all" notice is gone too;
Clear all sits inline beside the trigger only when something's
active. Header is tightened (text-xl) and inter-row margins drop
from mb-4 to mb-3 across the date strip + temporal pills.
2026-06-10 15:05:24 +00:00
d871093168 feat(webapp): replace HubPill with hamburger sidebar menu
The top-right "Back to hub" pill in each standalone is replaced by a
hamburger button that opens a right-side sheet reusing the existing
ProfileSheetContent (identity card, back-to-hub link, theme/lang/
currency prefs, profile settings or log-in CTA). The redundant
Profile entry is removed from BottomNav and its loggedOutOpensSheet
plumbing (BottomNav → AppShell) is dropped — Hub.vue still mounts
ProfileSheetTrigger directly so it's unaffected.

ProfileSheetContent gains an `app-nav` slot so standalones can inject
app-specific nav items above the cross-app section. AppShell exposes
a new optional `sidebarNav` prop that forwards items to the menu;
unset on non-activities standalones, those still get the hamburger
menu showing just the shared profile/preferences content.

Activities passes "My tickets" (routes to /my-tickets) and "Hosting"
(toggles the onlyHosting feed filter and lands on /activities), so
those entries leave the inline filter chip row on ActivitiesPage and
live in the sidebar instead. The "Past events" chip stays inline —
it doesn't require auth and pairs visually with the temporal filters.
2026-06-10 15:05:24 +00:00
247b0a6d5b feat(activities): restructure event detail page layout
- Move bookmark heart from top bar to the right of the title.
- Replace the When/Where info cards with caption-style lines directly
  under the title (calendar + map-pin icons + muted text).
- Move description above the organizer so it sits right under the
  title/info separator; push the organizer card to the bottom.
- Promote the "you own N tickets" CTA (filled primary "View" button)
  and demote "Buy another ticket" to outline when the user already
  owns tickets, so the My-Tickets path is what jumps out.
- Tighten ticket availability against the buy button: standalone strip
  removed, count rendered as an xs muted caption directly under the
  buy CTA.
2026-06-10 15:05:24 +00:00
de03fac69f Merge pull request 'feat(nix): lib.mkWebapp accepts extraEnv attr' (#100) from feat/lib-mkwebapp-extraenv into dev
Reviewed-on: #100
2026-06-10 15:05:12 +00:00
f2967616d3 feat(nix): lib.mkWebapp accepts extraEnv for build-time VITE_* vars
NixOS consumers (webapp-module's hub build, server-deploy's
standalones module) bake VITE_* env vars into the bundle at build
time: VITE_BASE_PATH for path-mounted standalones,
VITE_LNBITS_BASE_URL / VITE_NOSTR_RELAYS / VITE_LIGHTNING_DOMAIN /
etc. for per-deployment service URLs.

extraEnv is merged on top of the brand kit + sandbox defaults
(BRAND_DIR, BRAND_APP, CI). Brand-controlled vars (VITE_APP_NAME via
brand.name) stay strict — the vite configs internally set
process.env.VITE_APP_NAME from brand.json after the derivation
starts, so an extraEnv attempt at VITE_APP_NAME is a no-op (by
design, per #99 strict-from-the-start). Per-host name customization
flows through per-host brandDir, not env vars.

Verified VITE_BASE_PATH=/events/ via extraEnv emits hrefs prefixed
with /events/ in the built events.html.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 16:42:57 +02:00
4fc026d14d Merge pull request 'fix(nix): pin pnpm bits to flake's own nixpkgs' (#99) from fix/flake-pnpm-from-flake-nixpkgs into dev
Reviewed-on: #99
2026-06-10 14:20:12 +00:00
378a16d621 fix(nix): pin pnpm bits to flake's own nixpkgs, not consumer's
mkWebapp was passing the consumer's `pkgs.pnpm_10` into fetchPnpmDeps,
which means the pnpmDeps snapshot is byte-for-byte different across
consumers using different nixpkgs minor versions (flake's
nixos-unstable has pnpm_10@10.34.0, server-deploy's nixpkgs may have
a different 10.x). The pinned hash matches one snapshot exactly, so
the wrong consumer gets:

  ERR_PNPM_NO_OFFLINE_TARBALL @vite-pwa/assets-generator-1.0.2.tgz

at deploy time.

Fix: derive a `flakePkgs` from THIS flake's pinned nixpkgs (via
`flakePkgsFor`) and source pnpm, pnpmConfigHook, fetchPnpmDeps,
nodejs, autoPatchelfHook, stdenv, and stdc++ from it. The consumer's
`pkgs` argument is now used only for its system attribute.

Net effect: the pnpmDeps snapshot is now reproducible regardless of
who's calling mkWebapp. The pinned hash
sha256-FUN2lMHsaBTkk1tljDysYZAoQD+5MIBIEvGnRUWiF4s= remains valid (it
was computed against the flake's own nixpkgs originally).

Verified:
- `nix build .#main` — produces same dist/ as before (uses flake pkgs
  internally either way)
- `nix build --impure --expr '...lib.mkWebapp { pkgs = <system>; ... }'`
  — now succeeds with the system's nixpkgs, where it would fail
  before with NO_OFFLINE_TARBALL on @vite-pwa/assets-generator

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 16:19:04 +02:00
b0ee932e77 Merge pull request 'feat(nix): flake.nix exposing lib.mkWebapp' (#98) from feat/flake-mkwebapp into dev
Reviewed-on: #98
2026-06-10 13:52:29 +00:00
0ede6f70db docs(nix): document lib.mkWebapp in branding/README + CLAUDE.md
branding/README's "Integration with NixOS deployment" section now
describes the actual lib.mkWebapp API + the per-host call site, with
a ready-to-paste server-deploy snippet. Also documents the pnpm_10
pin, sharp/autoPatchelfHook handling, and CI=true bypass — anchors
that surface in error logs and benefit from being grep-able.

CLAUDE.md's NixOS deployment paragraph stops calling lib.mkWebapp a
future TODO and points at the API directly.

Adds a `nix build` recipe (default + impure brand override) for local
sanity-checking.

Part of aiolabs/webapp#97.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 10:46:17 +02:00
14283f62e0 fix(nix): pin pnpm_10 and set CI=true for downstream consumers
Two issues found when calling lib.mkWebapp from an external nixpkgs
(server-deploy's scenario):

- pnpm 10 in the sandbox aborts with
  ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY when it sees a
  modules-purge prompt without a TTY. CI=true is pnpm's documented
  bypass; harmless on builds that don't need it.

- Pinning pkgs.pnpm leaves it floating with the consumer's nixpkgs
  (flake's nixos-unstable has pnpm 11.5.1, system nixpkgs has 10.33,
  etc.). pnpmDeps hash is per-pnpm-version so a floating pnpm means
  consumers hit hash mismatches. Pinning pkgs.pnpm_10 locks to the
  same major series that produced the lockfile (package.json's
  packageManager: pnpm@10.33.0) while still allowing minor drift
  inside major-10.

New pnpmDeps hash reflects pnpm_10's snapshot format.

Verified end-to-end: `nix build --impure --expr '...lib.mkWebapp {
brandDir = /tmp/fixture; app = "events"; }'` with an external pkgs
produces a Sortir-branded dist-events (manifest name "Sortir", theme
#dc2626, bg #fff5f5, HTML title "Sortir").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 10:43:54 +02:00
08568fc0c0 feat(nix): add flake.nix exposing lib.mkWebapp
Establishes the nix build path so deploy/server-deploy can call
inputs.webapp.lib.mkWebapp { brandDir, app } per-host instead of
running its own derivation.

lib.mkWebapp { pkgs, brandDir ? ./branding/default, app ? "main" }
returns a stdenv.mkDerivation that:
- Uses pnpmConfigHook + fetchPnpmDeps (fetcherVersion = 3) to install
  node_modules from a hash-pinned snapshot, offline.
- Wires brandDir into the BRAND_DIR env var so vite-branding.ts and
  pwa-assets.config.ts resolve the right brand.
- Sets BRAND_APP from `app` so per-standalone overrides
  (branding/<dep>/icons/<app>/logo.*) work.
- autoPatchelfHook + stdenv.cc.cc.lib patch the prebuilt
  @img/sharp-libvips-linux-x64 binaries to run under the nix sandbox.
- Runs `pnpm run build` for the hub or `pnpm run build:<app>` for a
  standalone, then copies the resulting dist/ or dist-<app>/ into $out.

Per-system exposure:
- packages.<app> for each of main/events/wallet/chat/market/forum/
  tasks/restaurant/libra — exercises the builder under CI.
- packages.default = packages.main.

Closes aiolabs/webapp#97. Server-deploy hosts can now migrate via
aiolabs/server-deploy#8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 10:21:40 +02:00
fadf5407a5 Merge pull request 'feat(branding): brand kit architecture (Phase 1)' (#96) from feat/brand-kit into dev
Reviewed-on: #96
2026-06-10 08:17:55 +00:00
be427f1821 feat(branding): swap default brand logo for proper AIO mark
Replaces the seed copy of src/assets/logo.png (8-bit colormap) with
the official AIO logo from ~/Pictures/AIO/aio.png (8-bit/color RGBA,
1024x1024, with alpha — better for maskable icons).

Generator output verified: all 6 icons regenerate cleanly from the
new source.

Part of aiolabs/webapp#95 / #96.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 09:51:28 +02:00
3dfed23b43 docs(branding): brand kit contract + CLAUDE.md section
branding/README.md is the deployer contract:
- Directory layout, source format constraints (SVG > PNG ≥ 1024),
  brand.json schema, per-standalone override resolution order
- BRAND_DIR / BRAND_APP usage, generator pipeline walkthrough
- Pointer to issue #95 + the NixOS Phase 2 integration

webapp CLAUDE.md gains a Brand Kit section describing the moving
parts (vite-branding.ts, @brand alias, brandAssetsPlugin,
public/icons/ gitignore, per-app override path) so future sessions
on this repo know the convention without grepping.

Adds BRAND_DIR / BRAND_APP to the Environment Variables example.

Workspace ~/dev/CLAUDE.md note about "brand changes don't need
flake.lock bump" deferred to Phase 2 (server-deploy migration) —
that's when the workflow becomes reality.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 00:02:23 +02:00
3efae30e84 feat(branding): auto-generate icons on vite build/dev start
vite-branding.ts now exports brandAssetsPlugin() — a Vite plugin
whose buildStart hook runs scripts/generate-pwa-assets.mjs once per
build / dev-server start. All 9 vite configs register it first in
plugins[], so PWA icons under public/icons/ are guaranteed to exist
before VitePWA's includeAssets / manifest.icons get processed and
before the public/ → dist/ copy.

Removes the "did you remember to pnpm generate-pwa-assets?" failure
mode. Dev mode now also auto-populates icons (no more dev 404s on
/icons/favicon.ico).

Verified build from clean state (no public/icons/ pre-existing): the
plugin generates, all 6 icons land in dist-wallet/icons/.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 23:43:50 +02:00
faf41cd1c0 refactor(branding): switch to /icons/ paths and remove committed binaries
PWA icons now ship from public/icons/ (generated by
@vite-pwa/assets-generator, gitignored). The seven hand-crafted
binaries at public/ root come out of the tree.

Changes:
- 7 deleted: public/{favicon.ico, apple-touch-icon.png, mask-icon.svg,
  icon-{192,512}.png, icon-maskable-{192,512}.png}
- 9 HTML: <link rel="icon"|"apple-touch-icon"> hrefs prefixed with
  /icons/. mask-icon link dropped (PNG source → no sharp SVG; modern
  browsers prefer favicon.svg anyway, which we can revisit when an
  SVG brand source lands).
- 8 vite configs: includeAssets[] + manifest.icons[].src prefixed
  with icons/. Vite rewrites /icons/foo → <base>/icons/foo when base
  is set (so /events/icons/favicon.ico under /events/ deploys).

Build is now dependent on `pnpm generate-pwa-assets` running first
(or whatever invokes the generator — Phase 2 NixOS builds wire this
into buildNpmPackage). Standalone dev runs the generator on first
boot or whenever BRAND_DIR changes.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 23:37:55 +02:00
4e7502b30c refactor(branding): drop VITE_APP_NAME compat shim
Per the pre-public-launch policy in CLAUDE.md, strict-from-the-start.
brand.json is the sole source for app naming; VITE_APP_NAME no longer
overrides.

Migration responsibility moves to aiolabs/server-deploy#8 (Phase 2):
hosts that currently set VITE_APP_NAME (cfaun → "Bouge") must instead
declare BRAND_DIR pointing at a per-host branding/ directory before
their next webapp flake input bump.

process.env.VITE_APP_NAME is still set internally (from brand.name) to
keep Vite's HTML %VITE_APP_NAME% substitution working — but it's
internal wiring now, not a user-facing knob.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 23:15:58 +02:00
ce5a1a6a56 feat(branding): drive PWA manifest from brand.json
vite-branding.ts now loads brand.json into a typed `brand` object and
exports a `brandManifestName()` helper. Schema:

  { name (required), shortName?, themeColor?, backgroundColor? }

Default brand.json drops themeColor/backgroundColor — they're optional
overrides; per-app accents (wallet yellow, chat green, …) keep working
via `?? '<existing>'` fallbacks in each standalone's vite config.

events: manifest.name/short_name driven by brand. VITE_APP_NAME env
override stays (Phase 2 server-deploy migration still in flight) and,
when set, overrides both name and short_name to preserve pre-#95
behavior. cfaun's VITE_APP_NAME=Bouge keeps working unchanged.

hub (vite.config.ts): brand.name flows into %VITE_APP_NAME% Hub title.

7 other standalones (wallet, chat, market, forum, tasks, restaurant,
libra): only theme_color/background_color get brand overrides. Their
manifest.name/short_name stay hardcoded so multi-PWA home-screen
labels remain differentiated ("Wallet", "Chat", …) rather than all
collapsing to the brand short_name.

Verified default build: events manifest name=AIO; wallet keeps
"Wallet — Lightning" + #eab308 accent.
Verified VITE_APP_NAME=Sortir override: events name+short_name=Sortir.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 23:11:30 +02:00
88ab432629 fix(branding): wrap generator to clean up staged brand source
pwa-assets.config.ts stages the brand logo in public/icons/.brand-source.*
so the CLI (which emits next to its source) writes alongside it. Without
cleanup, the full-resolution 1024+ source ends up in dist/icons/ on every
build and is publicly served at /icons/.brand-source.png.

scripts/generate-pwa-assets.mjs runs the CLI, then removes the staged
source. Wire it through the `generate-pwa-assets` pnpm script.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 22:58:07 +02:00
eebb566323 feat(branding): add @brand vite alias + migrate in-app img consumers
vite-branding.ts is the shared resolver. Exports BRAND_DIR (absolute,
defaults to ./branding/default) and brandAlias for spreading into each
vite config's resolve.alias map.

All 9 vite configs now spread brandAlias so `@brand/<file>` resolves
to the active brand dir at build time.

Migrates the four <img src="@/assets/logo.png"> consumers
(Login.vue, LoginDemo.vue, AppSidebar.vue, MobileDrawer.vue) to
@brand/logo.png. Unused Navbar.old.vue left as-is.

Build verified: dist/assets/logo-<hash>.png emits from the aliased
import. Future deployers point BRAND_DIR at their brand kit and the
in-app logo follows automatically.

Part of aiolabs/webapp#95.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 22:55:42 +02:00
50a345ce4e feat(branding): install @vite-pwa/assets-generator + config
Adds pwa-assets.config.ts that reads $BRAND_DIR (default
./branding/default) and $BRAND_APP (optional per-standalone
override), resolves logo.svg/logo.png with documented fallback
order, and emits the existing icon set (favicon.ico,
icon-{192,512}.png, icon-maskable-{192,512}.png,
apple-touch-icon.png) into public/icons/.

Generator outputs alongside its source, so the config stages the
brand source into public/icons/.brand-source.{svg,png}; gitignoring
public/icons/ covers both staged source and generated icons in one
line.

Adds pnpm script `generate-pwa-assets`. Vite configs / HTML <link>
href updates come in follow-up commits; this commit alone produces
the icon set under public/icons/ but doesn't yet replace the
committed public/*.png binaries.

Part of aiolabs/webapp#95 (brand kit architecture).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 22:38:26 +02:00
a8c997ca8d feat(branding): scaffold default brand kit
Introduces branding/default/ as the unparameterized aiolabs brand:
- logo.png (1024x1024, seeded from src/assets/logo.png)
- brand.json with name, shortName, themeColor, backgroundColor

First step toward white-label PWA branding (aiolabs/webapp#95). No
consumer wiring yet — that's the next commits. Future deployers
(NixOS hosts in server-deploy, third-party white-labelers) point
BRAND_DIR at their own variant of this layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 21:59:13 +02:00
5541d2bc7a refactor(events): rename activities module to events + wire VITE_APP_NAME for per-deployment branding (#94)
## Why

The module was named `activities` originally to avoid colliding with Nostr's `Event` type. In practice that defense added friction without preventing confusion — the backend extension is named `events`, NIP-52 calls them "Calendar Events", and the UI already displayed "Events". The collision with `nostr-tools` `Event` is handled cleanly by the existing `import { Event as NostrEvent }` alias pattern (already in 5 files inside the module). Renaming collapses the 4-way mismatch into one consistent term.

Separately, deployments needed per-instance app names. `VITE_APP_NAME` was already plumbed per-standalone via NixOS `services.webapp-standalones.<app>.displayName` (e.g. cfaun shipped `"Sortir"`, now rebranded to `"Bouge"`), but nothing in the standalone consumed it — PWA manifest, HTML title, and runtime branding were all hardcoded. This PR wires the env through every app-name display point so any deploy can flip `displayName = "Bouge"` (or anything) and pick up the brand end-to-end.

## What

Nine commits on the branch:

1. **`refactor(events): rename activities module to events`** — 70-file rename. `src/modules/activities/` → `src/modules/events/`, `src/activities-app/` → `src/events-app/`, types/services/composables/views/components/store renamed (`Activity`→`Event`, `Activities`→`Events`). Routes `/activities/*` → `/events/*`; the legacy `/events` (ticketing management) moves to `/my-events` so `/events` belongs to the canonical feed. DI tokens `ACTIVITIES_*` → `EVENTS_*`. i18n namespace renamed; English domain strings updated, French/Spanish title key realigned. npm scripts `:activities` → `:events`. Build output `dist-activities/` → `dist-events/`.

2. **`feat(events): wire VITE_APP_NAME through PWA manifest, HTML, runtime`** — PWA manifest `name`/`short_name` template from `process.env.VITE_APP_NAME` with fallback `'Events'`. `events.html` uses Vite's `%VITE_APP_NAME%` substitution. `src/events-app/app.ts` + `main.ts` runtime brand string drives console logs, offline notification, and `acceptTokenFromUrl()`. `events.title` route meta sources from VITE_APP_NAME. `.env.example` updated with per-standalone scoping notes.

3. **`docs(events): update activities→events references`** — `docs/nostr-patterns/*.md` "Canonical: src/modules/activities/composables/useRSVP.ts" anchors point at renamed paths. CLAUDE.md Payment Rails Pattern section updated.

4. **`fix(events): drop lowercase from PWA description brand name`** — minor casing fix caught during verification.

5. **`fix(events): use domain noun in description, not brand name`** — manifest + HTML description switched from `"Discover ${BRAND} near you"` to static `"Discover events near you"`. Description is about *what* the app does, not the brand.

6. **`chore(events): scrub leftover sortir/activities references`** — nginx.conf.example, package.json (concurrently process label + `build:demo` BASE_PATH), router-helpers comment, useMarket comment, and vite.events.config.ts doc-comment.

7. **`refactor(events): conditional brand in console label, tighten docs`** — adds `APP_LABEL` next to `APP_NAME` in `events-app/app.ts`. Reads as `Events` on unbranded builds and `Events (Bouge)` (etc.) when `VITE_APP_NAME` is set to anything other than "Events" (case-insensitive). Used by all four console messages; `acceptTokenFromUrl` keeps the raw `APP_NAME` (it's a token-namespace identifier, not display copy). Also collapses the redundant "cfaun sets X; future deployments can override (e.g. X)" doc-comment.

8. **`i18n(events): finish activité/actividad → événement/evento sweep`** — completes the i18n rename in fr.ts and es.ts (search placeholders, favorites prompts, settings prompt) and fixes five gender-agreement errors that came along with the masculine `événement`/`evento` switch (French: `Aucune ... trouvée`→`Aucun ... trouvé`, `préférées`→`préférés`, `d'une ... la sauvegarder`→`d'un ... le sauvegarder`; Spanish: `favoritas`→`favoritos`, `guardarla`→`guardarlo`).

9. **`chore(events): finish sortir → bouge sweep in .env.example`** — four remaining doc-comment refs in `.env.example` (cfaun branding, ticket-scanner comment, section header, subdomain-mode URL example).

## Cross-repo coordination

This PR has matching changes already pushed to two other repos. They have to land in a coordinated bump because the names are tightly coupled.

- **`aiolabs/webapp-module` main** — commit `9d82016`. Renames `hubActivitiesUrl` → `hubEventsUrl` and `VITE_HUB_ACTIVITIES_URL` → `VITE_HUB_EVENTS_URL`. No backwards-compat shim.
- **`aiolabs/server-deploy` main** — commits `f15e1eb`, `bf4698b`, `d46a520`:
  - `standalones.nix` `apps.events` uses `build:events` / `dist-events` / `events.html`; `hubUrlOption` maps `events → "hubEventsUrl"`; comment + cfaun-as-example doc switched from sortir/Sortir to bouge/Bouge.
  - `hosts/cfaun/default.nix` rebranded: `subdomain = "bouge"` + `displayName = "Bouge"`. (Native-French feedback retired the "Sortir" branding as awkward.)
  - `hosts/atio/default.nix` stale "activities app" comment dropped (atio's `displayName = "Eventos"` config is unchanged).

server-deploy's `flake.lock` still pins the OLD webapp + OLD webapp-module, so nix builds from server-deploy main will fail until the bumps. Suggested order after this PR merges to `dev`: one server-deploy commit that does `nix flake lock --update-input webapp-module` + `--update-input webapp-demo` together. DNS for `bouge.ariege.io` needs to point at the cfaun host before the deploy lands.

## Verification

- `pnpm typecheck` — clean ✓
- `pnpm build:events` (default) — `dist-events/manifest.webmanifest` has `name: "Events"`, description "Discover events near you" ✓
- `VITE_APP_NAME=Bouge pnpm build:events` — `name: "Bouge"`, HTML title "Bouge", console label resolves to `Events (Bouge)` ✓
- Cross-repo grep sweep: no `Sortir`/`sortir`/`activities`/`Activities` references in events-module scope. (Domain false positives — "Market Activity" in market dashboard, "time-based activities" in nostr-feed content filters, "Activity Lifecycle Kills" in CLAUDE.md mobile-browser docs — are unrelated and intentionally left alone.)

Not verified in-browser (no GUI in this session) — needs a manual smoke at `app.ariege.io/bouge/` once the coordinated flake bump lands on cfaun. Watch for: feed loads, `/events/calendar`, `/events/map`, `/events/favorites`, `/events/:id` routes work; "My Events" appears in the user dropdown and `/my-events` shows the management page; PWA install prompt shows "Bouge".

## Out of scope (deferred)

- Backend extension rename — `aiolabs/events` is already named correctly.
- Nix standalone attribute name — `services.webapp-standalones.events` is already correct.
- Overriding domain-noun strings ("Your event was created", RSVP labels) — those stay locale-driven; `VITE_APP_NAME` covers app-name only.
- Backwards-compat redirects from `/activities/*` to `/events/*` — pre-launch, public URL was `/sortir/` (path-mode) or `sortir.ariege.io` (subdomain-mode), never `/activities/`. No external bookmarks to preserve. (`sortir.ariege.io` → `bouge.ariege.io` is a separate decision; up to the deploy operator whether to keep a 301 for word-of-mouth referrers.)

Reviewed-on: #94
2026-06-09 18:18:26 +00:00
1f20d5f00c Merge pull request 'refactor(libra): redesign transactions list status + type encoding' (#93) from feat/libra-tx-status-encoding into dev
Reviewed-on: #93
2026-06-06 21:16:45 +00:00
75dfd8a541 refactor(libra): redesign transactions list status + type encoding
Rework how the standalone transactions list communicates entry status
and type so each visual channel does one job and the filter UI matches
the underlying axes.

Encoding:
- Type lives in the signed/colored amount (+green income, -red expense)
  and a matching Income/Expense badge in the badge row.
- Status lives in badges only: red Voided (leftmost) and yellow Pending
  (after the type badge). Cleared entries carry no status badge — the
  quiet default.
- Voided rows additionally strike-through and mute the amount.
- Drop the title-row status icons and the colored left border that
  previously fought with the amount color for the same meaning.

Filter UI:
- Replace the type radio + voided switch with three category chips —
  Income, Expenses, Voided — that independently toggle inclusion of one
  bucket of rows. Each row belongs to exactly one bucket (voided wins
  over type). Defaults: Income + Expenses on, Voided off.
- Empty-selection state nudges the user to enable a category.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 23:12:57 +02:00
4af220adda Merge pull request 'feat(libra): show voided transactions in standalone' (#92) from feat/libra-show-voided into dev
Reviewed-on: #92
2026-06-06 20:31:58 +00:00
1fbf7b3d26 fix(libra): exclude voided txs from balance Pending section
BalancePage filtered tx.flag === '!' to compute pending count/sum/list.
After the libra backend stops hiding voided transactions, those will
arrive with flag='!' plus a 'voided' tag and would otherwise leak into
the Pending section. Add the tag-aware exclusion to keep Pending
showing only genuinely pending entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 20:45:55 +02:00
e9195978c1 feat(libra): surface voided transactions in standalone history
Voided entries keep their '!' flag and gain a 'voided' tag per the libra
reject convention, so detecting them needs a tag check rather than a new
flag char. Render them inline with the existing 'x'-flag voided styling
(grey XCircle icon, strike-through title/amount, red-tinted Voided badge)
so users like Nancy can see their rejected entries instead of having them
silently disappear from the list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-06 20:45:25 +02:00