Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion .dialyzerignore.exs
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
[{"deps/phoenix/lib/phoenix/router.ex", :pattern_match}]
# Falso positivo do backend gerado por `use Gettext.Backend`: o Dialyzer
# reclama de `call_without_opaque` ao chamar `Gettext.Plural.plural/2` com o
# tipo opaco `%Expo.PluralForms{}` do proprio Gettext/Expo. Nao e codigo nosso
# e some quando o Gettext relaxar a opacidade; ignoramos so este arquivo/aviso.
[
{"lib/taina_web/gettext.ex", :call_without_opaque}
]
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ result

# Ybira dev storage
/priv/storage/

# esbuild output (built from assets/ via `mix assets.build`)
/priv/static/assets/
174 changes: 174 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,177 @@ This is a web application written using the Phoenix web framework.
<!-- phoenix:ecto-end -->

<!-- usage-rules-end -->

## Frontend / UI

Rules for building Tainá's web UI (LiveViews, components, CSS, JS hooks). These
are non-negotiable; when in doubt, read the cited examples in the repo.

### Source of truth

- The aesthetic is **already committed** in Penpot ("Cofre da Comunidade - UI
v1") and mirrored 1:1 in `assets/css/tokens.css`. **Do not invent a look.**
You are executing a design, not freestyling one — precision over novelty.
- `tekoa/` docs win over instinct. For non-trivial UI, run the
`community-ux-reviewer` agent before shipping; it grounds on the docs and
argues the product, not just the pixels.

### Tokens — never raw values

- Two levels: **primitives** (raw palette/scale) → **semantic** (what
components consume). Components consume **semantic only** — never a primitive,
never a raw hex or px in `.heex` or component CSS.
- A new value is born as a **semantic token first** (and must trace back to
Penpot). No magic numbers in markup.
- Typography: use the `type-*` utility classes and the type scale. Fonts are
fixed — Bricolage Grotesque (display) / Schibsted Grotesk (body) / JetBrains
Mono (mono). **Don't add fonts.**
- Color: dark-first night palette via semantic tokens (`--text-*`,
`--surface-*`, `--border-*`, `--brand-*`). Each subsystem has a service accent
(`--service-ybira` forest, `--service-jaci` moon, …) — use it, don't recolor.
- No AI slop: no Inter/Roboto/Arial/system fonts, no purple-on-white, no
cookie-cutter layouts. The character is the dark Tupi-Guarani vault — keep it.

### Functional Core / Imperative Shell (LiveView **and** JS)

This is the single most important rule and it applies to the frontend too.

- **LiveView callbacks are the imperative shell.** `mount`, `handle_params`,
`handle_event` stay thin — they only `assign` / `stream` / `push_patch` /
`put_flash`. Every decision lives in **pure private helpers that take data and
return data**, not the socket. See `gallery_live.ex`: `merge_groups/2`,
`step/2`, `group_title/1`, `viewer_index/2`, `video?/1` — logic out of the
callbacks, callbacks just wire results in.
- **Domain logic lives in contexts** (Maracá / Ybira / Jaci). The LiveView
*calls* the context and shapes the result for the view; it never reimplements
business rules. Always pass through `current_scope` (RLS boundary).
- `assign_new/3` for derived/cached state that must survive the HTTP → WebSocket
handoff (see `hooks.ex` `storage_stats` — computed once, not re-queried).
- **JS hooks are the edge, kept minimal.** Before writing a hook, check a native
binding (`phx-viewport-*`, `phx-drop-target`, `JS.*`) already solves it
(`hooks.js` opens with exactly this reminder). A hook only touches DOM events
and browser APIs (clipboard, share, keyboard/swipe); it `pushEvent`s and lets
the **server decide and own state** (see `Clipboard`, `ViewerNav`). Keep pure
transforms separate from DOM mutation. Functional Core / Imperative Shell —
including in JS.

### Components & layout

- **Reuse `core_components`** (`.button`, `.modal`, `.confirm_dialog`,
`.empty_state`, `.segmented`, `.icon`, `.icon_button`). Don't hand-roll a
primitive that already exists.
- CSS: **BEM** for component classes (`block__element--modifier`, e.g.
`photo-grid__item`); **utility classes** for layout/spacing (`row between`,
`col gap-5`, `center`, `mt-4`, `type-*`). One LiveView per screen; wrap in
`Layouts.app`.
- **Every screen ships empty / error / permission states** — not just the happy
path. `<.empty_state>` exists for this; use it.
- All user-facing strings go through `gettext` / `ngettext`. **pt-BR first.**
Never anglicize the Tupi-Guarani names (Tekoa, Maracá, Ybira, Jaci, Guará).

### Motion & accessibility

- Motion is **CSS-first and restrained** — one high-impact moment beats
scattered micro-animations. Respect `prefers-reduced-motion`.
- Touch targets ≥ `--size-touch` (44px). Icon buttons need a `label`. Images
need `alt`. Keyboard + swipe navigation where it matters (the `ViewerNav`
pattern). Visible focus states.

### Build for the audience (communitarian, non-technical)

These are build directives distilled from `community-ux-reviewer` — apply them
*while building*, not only in review.

- **Design for the least-technical person in the community**, not power users.
No jargon, no assumed technical knowledge, no dead-ends.
- **Commons, not a personal account.** Favor shared spaces, collective memory,
and stewardship ("who keeps this organized") over individualist patterns:
profiles, vanity metrics, likes/followers, algorithmic feeds, and
attention-bait notifications. Default to shared-by-norm with **explicit,
legible consent**, not private silos.
- **One community per box** — hard-enforced. No multi-community switching, no
public discovery, no cross-instance feed. The "network" is this one Tekoa.
- **Onboarding is by invitation** — arriving as belonging, not signing up alone.
- **LiveView / Raspberry Pi feasibility lens on every interaction.** Server-
rendered, mobile-first, low client state. No offline-PWA assumptions, no heavy
client-side state machines. Don't fight the architecture.
- **Respect the scope cuts.** MVP = the community's file + photo vault. Chat
(Guará) is out of MVP — never propose chat as the way to be "more
communitarian"; find that value in files / photos / people / governance.

### Plain text: no AI-typography, no emoji

Text Tainá ships or that people read as docs reads like a human wrote it: UI
copy / gettext, docs (including the `tekoa` repo), production comments and
moduledocs under `lib/`, and commit/PR prose. No AI-typography tells, no emoji.
Use plain ASCII:

- `—` / `–`: comma for an aside, colon for a definition (`**Label**:`, headings,
titles), or a period to split a sentence. Choose by context.
- `·` / `•`: comma in metadata (`PDF, 2,4 MB`), or ` / ` when a comma clashes.
- `…` to `...`; curly quotes `" " ' '` to straight `" '`; `×` to `x`; `→` to
`->` or a word ("para", "depois").
- Emoji: remove. Do/don't markers become text (`✅` / `❌` to `Bom:` / `Evite:`).
- Keep intentional ASCII-art diagrams (box-drawing `─ │ └ ├` and their arrows):
deliberate, not typography.

Humanize the sentence, don't just swap the glyph; align to the docs voice and
`community-ux-reviewer`. `test/**` comments are out of scope (the `→` / `—`
shorthand there is useful and stays); only fix a test when an assertion must
match changed UI copy. Verify with a unicode grep after editing. Note: `rtk`
rewrites a *leading* `grep`/`find`, which silently breaks unicode scans, so run
`find … -print0 | xargs -0 grep -P '…'` or `perl -CSD -i -pe`.

### Social model & transparency (Tainá-specific, non-negotiable)

Decided 2026-06-15 (canonical record: `tekoa/tecnico/RFC_003_GOVERNANCA_E_TRANSPARENCIA.md`).

- **Two zones, not one pile.** Content lives in **praça** (commons —
shared-by-norm, every morador reads; placing a file there IS the reversible
consent) or **casa** (personal — private-by-default; only the owner reads,
and *everyone else, including the zelador,* must `request_access` → owner
approves). New files default to **casa**. Read paths in Ybira/Jaci enforce
this (`zone == :praca OR Maraca.authorize?`), never tekoa-wide.
- **Zelador, not admin.** Reframe the role as **zelador(a)** — caretaker of the
machine (disk, backups, updates, invites) with **zero data authority and zero
unilateral social power**. `member`→**morador(a)**. A tekoa may have
**multiple zeladores**. Tainá is pre-alpha → **rename the enum cleanly** in a
migration (`admin`→`:zelador`, `member`→`:morador`) — no cookie-session
caution needed. Predicates `Maraca.zelador?/1`/`morador?/1`. The zelador has
**no read shortcut** to a morador's casa — confirm this stays true.
- **Transparency is a core pillar, not a feature.** Three layers, all MVP:
the **Mural** (append-only social ledger — who did what), the **Painel /
"Saúde da comunidade"** (system state in plain pt-BR), and the **telemetry +
structured-logging substrate** (`:telemetry`, `telemetry_metrics`, `Logger`
metadata). **SOVEREIGN — never phone home.** Telemetry/metrics/logs stay on
the community's box; shipping community data to any SaaS (PostHog, etc.)
betrays data sovereignty. Accessibility (WCAG contrast/labels/focus) is *part
of* accessible transparency, not separate polish.
- **Honest framing.** No encryption exists yet. UI must say casa privacy is a
*software + trust* promise ("promessa de software, não cadeado matemático"),
never a cryptographic guarantee. Only future Ybira **E2E** (not convergent)
would let us honestly say the zelador *cannot* read casa files.
- **Governance is collective.** Sensitive acts (remove member, appoint/revoke
zelador) are community-voted *proposals* (assembleia), never a god-button —
but that track is beyond RFC_002 and ships only after its tekoa RFC lands.
- **Naming is tiered — one source of truth.** **Tupi-Guarani** for
subsystem/brand proper nouns only (Tekoa, Maracá, Ybira, Jaci, Guará).
**Familiar pt-BR** for the everyday social vocabulary, where the *same word*
is the DB atom, the context term, **and** the UI label — `:casa`, `:praca`,
`:zelador`, `:morador`, mural, pedido, assembleia — no English-atom →
pt-BR-label translation seam. **English** only for invisible plumbing users
never read (Repo, Scope, Telemetry). This is the deliberate exception to
"code in English" — same justification as the intentional Tupi naming, and it
serves the non-technical pt-BR audience.

### Penpot is the UI source of truth — verify, then build

- The Penpot design system is **complete and code-implemented** (foundations,
components, tokens, mobile + desktop); `assets/css/tokens.css` is imported
directly from Penpot. **Do not re-import tokens or invent foundations** —
compose new screens from the existing components + tokens.
- The Penpot **MCP is live**. **Before non-trivial UI work, verify the live
file first** via the `community-ux-reviewer` agent (it holds the
`mcp__penpot__*` tools; the main loop does not). **Editing Penpot is
authorized** — refactor/improve boards within the existing token/component set
when it helps.
42 changes: 42 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# CLAUDE.md — Tainá

Project memory for the Tainá app (this git repo). Overrides the user-level `~/.claude/CLAUDE.md` where they conflict. The workspace-level `../CLAUDE.md` is just a pointer to this file, to `AGENTS.md`, and to the `community-ux-reviewer` agent.

## Source of truth: `../tekoa/`

**`../tekoa/` is the canonical documentation and the source of truth for this project.** Before proposing architecture, scope, or behavior, read the relevant doc in `../tekoa/` and align to it. If code and `../tekoa/` disagree, `../tekoa/` wins — surface the drift rather than inventing a third answer.

- `../tekoa/ROADMAP.md` — current MVP roadmap (v3.0). What's in/out of scope, and ordering. **Check this before suggesting features.**
- `../tekoa/tecnico/RFC_ARQUITETURA.md` — the full architecture RFC. The authority on structure and subsystem boundaries.
- `../tekoa/tecnico/RFC_002_MVP.md` — the MVP RFC that supersedes the older "PWA Monolith" v2.0 design; holds the rationale for current decisions.
- `../tekoa/guias/contribuindo-github.md` — contribution workflow.
- `../tekoa/CONTRIBUTING.md`, `../tekoa/CODE_OF_CONDUCT.md` — process and conduct.

`../tekoa/` is a separate docs repo (`taina-labs/tekoa`) checked out as a sibling at the workspace root — it is **not** part of this app repo. Do not edit it as a side effect of app work; doc changes are deliberate and go through the docs repo.

## What Tainá is

Self-hosted private-cloud platform — a community's file/photo vault with painless install (per `ROADMAP.md`). Modular monolith, backend-first. AGPL. Tupi-Guarani-named subsystems: **Maracá** (auth/invites/authz), **Ybira** (content-addressed storage, source of truth for all file services — extend by composition, not inheritance), **Jaci** (photo gallery), **Guará** (chat, post-MVP).

## Stack

- Elixir 1.20 / OTP 28, **Phoenix 1.8 + LiveView**.
- **PostgreSQL 18 with Row-Level Security** — per-community isolation. Treat RLS as a hard boundary, not an optimization.
Comment thread
zoedsoupe marked this conversation as resolved.
- Oban (jobs), Bandit (server), bcrypt, nanoid.
- Nix flake + direnv for the dev env (`flake.nix`, `.envrc`); `docker-compose.yml` for Postgres.

## Commands (run here, in the app dir)

- `mix setup` — deps + `ecto.create` + `ecto.migrate` + seeds.
- `mix test` — auto-creates/migrates the test DB first.
- `mix test test/path_test.exs` / `mix test --failed` — focused runs.
- `mix precommit` — **run when done.** Compiles `--warning-as-errors`, `deps.unlock --unused`, `format`, `test`. Fix everything it reports.
- `mix ecto.reset` — drop + recreate when migrations get messy.
- Quality tooling present: Credo, Dialyxir (PLTs in `priv/plts`), Styler.

## Conventions

- Phoenix/Ecto/Elixir specifics live in `AGENTS.md` — follow it. Notably: **`Req` is the HTTP client here** (Phoenix default), not Finch — this repo overrides my usual global preference.
- **UI/frontend work**: follow the **Frontend / UI** section of `AGENTS.md` (tokens-only/Penpot as source of truth, Functional Core / Imperative Shell on LiveViews *and* JS hooks, `core_components` reuse, communitarian build directives). Run the `community-ux-reviewer` agent for non-trivial UI.
- Otherwise my user-level conventions apply: Functional Core / Imperative Shell, `@behaviour` over `use` macros, `{:ok,_}`/`{:error,_}` tuples, minimal deps, pattern matching over validators.
- One module per file. Predicate functions end in `?` (never `is_` prefix outside guards).
23 changes: 23 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* Entry point, empacotado pelo esbuild junto com js/app.js.
Ordem importa: tokens primeiro, utilitários por último (precedência). */

@import "./tokens.css";
@import "./base.css";
@import "./layouts.css";
@import "./components.css";
@import "./utilities.css";

/* Estados de conexão do LiveView (feedback discreto, sem JS extra) */
.phx-loading {
cursor: wait;
}

.phx-click-loading {
opacity: var(--opacity-muted);
pointer-events: none;
}

/* uploads: entrada inválida */
.phx-no-feedback .field__error {
display: none;
}
Loading
Loading