Commit Graph

26 Commits

Author SHA1 Message Date
pigui
bca3e4bcc6 Show who signs a post and its tags, and read the blog by tag
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:20:57 +02:00
pigui
918465a8ab Keep posts with code inside a phone's width
The single-column layout grew to its widest line, so a code block pushed the
whole text past the edge of the screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:12:19 +02:00
pigui
e9082b90ee Restore the numeric field's left inset
The value in "Horas por semana" sat 12px to the left of every other
field -- and, more visibly, 12px left of its own label, which stayed
where the others were.

It is a defect in @pi/ui, measured rather than guessed:

  field           has-icon-prefix   prefix   value starts at
  text-field      false             --       16px
  email-field     false             --       16px
  number-field    true              4px      4px

`pi-number-field` declares `<ng-container matPrefix>` unconditionally,
so Material treats the field as having an icon prefix even with nothing
projected into it. The empty 4px prefix displaces the 16px inset the
other fields get. Only two of the library's twenty field templates
declare a prefix, both numeric, and only `incremental-number-field`
actually fills one -- so it is an oversight, not a pattern.

This is the stopgap, marked as such and scoped to that field. The real
fix is already written in the library (`pireon-ui`,
`number-field.component.html`, guarded by a `contentChild`) but is not
built or published yet; this rule comes out when that version lands.

Verified in both themes: all four fields now start their value and their
label at 16px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:36:00 +02:00
pigui
aea8f5675d Make the button-shaped links actual links
"Web oficial" did nothing when clicked, and it was not alone: six places
wrapped `<pi-action-button>` in an `<a>`. That component renders a
`<button>`, so each one was a button inside a link -- invalid HTML, and
the button swallows the click. Besides "Web oficial" it covered Demo,
Repositorio, the contact CTA on every detail page, the 404's way home
and a call card's apply button.

They become one `app-link-button`, which renders a real anchor. The
click was only part of it: a link can also be opened in a new tab,
middle-clicked, copied, tabbed to and followed by a crawler.

Its look does not come from @pi/ui's button classes, and not for want of
trying: those rules are scoped to the element (`html button:not(…)` in
`_pi-button.scss`), so on an anchor they contribute nothing except
`pi-color__primary` -- a *text* colour, which painted the label violet on
a violet fill. The stylesheet reads the same `--pi-theme-button-*`
tokens the library reads instead, so a change of shell or theme still
moves these buttons and dark mode needs nothing of its own.

Verified: the anchor reports href, target and a real navigation to the
external site; no `<button>` inside an `<a>` remains on the page; and
the rendered pill matches the header's button in radius, gradient fill,
white text and weight, in both themes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:07:15 +02:00
pigui
e2e90d5512 Fix lists in posts and pages, and style images inside the text
Every list in a post rendered its bullet alone on one line and the
item's text on the next. The panel's editor wraps each item's text in
a <p>, and @pi/ui's reset puts the bullet inside the line: a block
after an inline marker falls below it, paragraph margins and all.
Bullets now sit outside, with the paragraph inside an item unspaced.

Images in the text — attached in the panel or written in by URL through
the API — are never wider than the column and sit apart from the prose,
with a caption under a figure. Inline code and code blocks get a tinted
surface; quotes keep the library's own quotation marks.

The mock's posts carry such a body, so the typography can be reviewed
without writing one in the panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:11:46 +02:00
pigui
e5d91d673a Put the blog where people look for it
The blog was reachable only from a link in the footer, which is to say
hardly at all. Once there is a post:

- "Blog" joins the header menu, on desktop and in the mobile menu, on
  every page.
- The landing shows the three latest posts under "Novedades", with a
  link to the rest.

The post card moves into its own component, post-card, shared by the
blog (image beside the text) and the landing (image above).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:02:55 +02:00
pigui
5ea9914583 Give every technology a page of its own
A technology's name appeared in nine places -- the chips on project,
call, service and member cards and detail pages, the landing's ring
cards, the radar -- and not one was clickable. Now each leads to
`/tecnologias/:slug`, with the logo, what it is, where the collective
places it on the radar and a link to its own site.

Two constraints from the API shaped this, both verified against it:

Its address is the site's own. Technologies carry no `path` and no
`alternates`, and `/slugs` cannot list them -- its type vocabulary is a
closed enum. `routes.ts` opens by stating the API decides every record's
address; this is the first type where that is not true, so `SECTIONS`
gains the one entry and the comment says why. The sitemap composes these
URLs locally for the same reason, and an unreachable technologies call
there is not fatal the way `/slugs` is: it costs a handful of URLs
rather than misrepresenting the site.

There is no detail endpoint. `GET /technologies/{slug}` answers "route
not found" -- the route does not exist. `pireonApi.technology` filters
the collection instead, passing `in_use: false` because the default
returns only technologies attached to a project, which today is none.
An outage and an unknown slug stay distinguishable so the first is not
reported as a 404.

`website` is the only link the payload has: the project's own home page,
where its documentation lives. There is no separate docs field, so the
label says what the link is.

The chip markup was identical in all nine places, so it becomes one
`app-technology-chip` rather than nine anchors. `project-card` and
`radar-page` gain a `locale` input, which is all they lacked to address
it.

Verified: lint, 54 unit tests, build and 11 e2e. Against the mock, the
three addresses answer 200 and an unknown slug 404; the switcher moves
between them; canonical and hreflang are right; the sitemap lists all
three per technology.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-29 20:13:05 +02:00
pigui
240ad149e7 Bring the landing to life as it scrolls
- Each section assembles itself on arrival: heading first, then what it
  holds, and a grid's cards one after another — instead of the whole
  section fading in as one block.
- The hero enters on load, piece by piece, in CSS, so it plays with or
  without JavaScript; the stats come last and count up from zero.
- The hero's lights drift and fade as it scrolls away, and the header
  gains a shadow once the page moves — scroll-driven animations, no
  listeners; browsers without them simply do not animate.

The count only runs if the page hydrates before the stats appear at
0.8s: after that the figure has been read, and dropping it to zero to
count again would be a flicker. Server-rendered HTML carries the real
numbers and nothing hidden, and prefers-reduced-motion turns all of it
off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:21:48 +02:00
pigui
fea60a168c Add a blog, built from pages
A post is a page in the panel's "Blog" category, so it is written,
translated, scheduled and reviewed like any page, and the MCP server
can publish one the same way. Nothing new in the API.

- /blog (/en/blog, /ca/blog): posts newest first, with image, date and
  excerpt.
- A post shows its date and leads back to the blog; its structured data
  is a BlogPosting.
- /blog/rss.xml?lang=xx: one feed per language, announced in <head>.
- Posts stay out of the header and the footer, which links the blog
  once there is any post instead of growing with every one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:45:08 +02:00
pigui
86b7485b32 Tell search engines and link previews what each page is
- /sitemap.xml, from the API's /slugs plus the landing and listings:
  every address in every language, with hreflang and x-default. A 503
  when the API is down, rather than a sitemap missing half the site.
- /robots.txt, naming the sitemap on whatever domain this is served on.
- Slugs renamed in the panel now redirect. The API has always kept the
  old address the moment one changed; middleware/redirects.ts answers
  it with a 301, from a list kept for a minute and through outages.
- og:url, og:image (the record's own picture, the logo otherwise) and
  schema.org JSON-LD: Organization on the landing; WebPage,
  CreativeWork, Person or Service elsewhere. Shared through core/head.
- The proxy-derived origin moves to core/origin.ts, used by the page
  shell, the landing and both server routes.

In development only /api/* reaches Nitro, so the two files 404 under
`npm start`; they were checked against a production build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:21:59 +02:00
pigui
399b8d5450 Add a page for every way to help
/colabora (/en/contribute, /ca/col-labora) — the calls' own section, so
it lists what /colabora/{slug} are — brings the open calls, the good
first issues across the collective's repositories, and a five-step
guide from picking a task to having it reviewed. With nothing open and
nothing labelled, the guide and the other ways to help are still there.
The landing's "contribute" section links to it.

Also fixes the landing's issue list, which @pi/ui's `li + li` margin
spaced twice over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:14:16 +02:00
pigui
daacc2c5d6 Give each service its own page
A service now has its address, like a project: icon, tagline, what the
collective does and how, the projects it was delivered on, what the
people it was for said, and the technologies behind it. The landing's
service cards link to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:10:19 +02:00
pigui
06eeefc144 Give the tech radar its own page
/radar (/en/radar, /ca/radar) shows every technology the collective has
placed, by ring: logo, category, what it is, why it sits there, its
website and how many projects use it. The landing's radar summary links
to it.

It is the first listing — a page this site composes rather than one
record's — so resolveRoute learns about those (LISTINGS), and builds
their addresses and hreflang alternates from the same table.

Logos sit on a light tile in both themes. A brand's mark is not ours to
recolour, and Rust's, which is black, would vanish in dark mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:07:19 +02:00
pigui
b5681a12ac Give each call its own page, and a form to answer it
A call now has its address — description, what the volunteer gets, the
commitment added up, level, closing date, project, technologies — and
the application form right there. The landing's call card leads to it
instead of scrolling to the general form, and shows the level in the
visitor's language rather than the API's `mid` or `any`.

The form is the landing's, with a `callSlug` that hides the kind
selector and posts to the new /api/calls/{slug}/apply handler: the API
refuses a `kind` on an application, which is always an offer of time.
Both handlers share one check, core/submission.ts, now tested on its
own.

A call that is filled or closed keeps its page — a link to it may
still be out there — but shows why there is no form, and where to
write instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:02:06 +02:00
pigui
439f951f2e Give projects and members their own pages
The API has always served a project's and a member's detail, with
`path` and `alternates` in three languages, and the site had nowhere to
show it. A project now has its page — tagline, status, technologies,
description, technical decisions, what was learned, gallery, client,
dates, lead, team, repositories and open calls — and so does a member:
headline, bio, links, technologies and projects. Cards on the landing
link to both.

Addresses follow the API's rule read backwards (core/routes.ts):
pages at the root, records under a section named in their language,
the default language unprefixed. One catch-all route serves them all.
Not a route per shape: Analog registers each loader as an internal
Nitro route, and Nitro's router cannot hold `/:prefix/:slug` beside
`/:prefix/:section/:slug` — the first became unreachable.

The frame around a record (header, footer, lights, 404/503, title,
description, canonical, hreflang) is one component now, PageShell, and
each type only brings its body, all sharing one layout partial.

Also fixes chips that stepped down one row after the first, on the
landing's cards too: @pi/ui's reset gives every `li + li` a top margin,
right for prose and wrong for a row spaced by `gap`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:57:13 +02:00
pigui
9af6b5aff9 Give the content pages the landing's look
Moving from the landing to a page read as changing sites: a narrow
centred column on a flat background, light headings, no cards. The
page now opens like the landing's hero — the same two lights, the same
left edge as the logo, a bold title with the excerpt as its lead — and
the body's headings are bold like every section title on the landing.

The lights move into a shared mixin (ui/_lights.scss) so the hero and
the pages cannot drift apart. On a page they sit on the whole <main>
with `overflow: clip`, so they fade out on their own instead of ending
in a hard edge right where the text starts, and `clip` rather than
`hidden` keeps the sticky side column working.

A side column keeps a page from being a dead end: "keep reading" cards
for the other pages, its own category first, and a card to get in
touch, both on the landing's card surface.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 10:38:04 +02:00
pigui
80a1924519 Serve the pages the panel publishes
The landing was one address. It now also answers on the pages the panel
publishes -- "Sobre nosotros" and the like -- at the addresses the API
itself chooses: `path` and `alternates` put the default language
unprefixed and every other one under its code, so `/sobre-nosotros`,
`/en/about-us`, `/ca/qui-som` are the routes. `core/api.ts#contentRoute`
is the single rule for that shape, and `/es/…` is deliberately not one
of them, so a page has one address rather than two.

Two routes share one load function, because Analog wants a `.server.ts`
beside each `.page.ts`: `[slug]` for the default language, and
`[locale]/[slug]` for the rest.

Whether a slug exists is the API's answer, not something the route table
knows, so the 404 middleware lets page-shaped addresses through and
`ui/content-page.ts` sets the status itself from `ngOnInit` -- the same
`injectResponse()` the landing uses for its 503, and `ngOnInit` because
an input has no value in the constructor. Body HTML comes from the
panel's editor and goes through Angular's sanitiser.

The header shows the featured pages beside the section anchors; the
footer lists them all. On a page the language switcher follows the API's
`alternates`, and a language with no translation leads to the landing in
that language instead of a 404.

Verified: lint, 20 unit tests, build, and 6 e2e -- including a new one
that a page-shaped address the API does not know answers 404 with
`robots: noindex`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 09:55:10 +02:00
pigui
47c9af682c Remove the build credit from the footer
Drops "Hecho con Angular, Analog y Nx." and the API link beside it,
leaving the copyright line on its own.

`footer.built` goes with it in all three locales. So does `footer.api`,
which was already dead -- declared and translated, never rendered.

The row's flex and space-between existed only to push the credit to the
far end, so they go too.

Verified in es/en/ca: no trace of the credit or the docs link in the
served HTML, and the copyright line renders in each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 15:37:55 +02:00
pigui
7bb40d32f1 Drop the gradient focus ring, which doubled every field's left edge
The doubled edge was not the textarea's alone -- it was on every field,
and the previous commit's explanation of why the inputs were exempt was
wrong. I asserted that at 999px the ring and the outline clamp to the
same curve without measuring it. They do not:

  ring     999px -> a 24px circle      (half the field's height)
  outline  999px -> a 12x24 ellipse    (leading piece width x half height)

The ring is one continuous rounded rectangle over the whole field; the
outline beneath it is split into three pieces and its leading piece is
12px wide. No radius makes a full-width curve agree with a 12px-wide
one, so there was nothing to tune -- the ring had to go. It was mine,
lifted from the library's `gradient` theme onto a shell that does not
use it, and `alternative` already expresses focus through the outline's
own colour.

The textarea's radius goes back to 1.25rem, since it was only lowered to
dodge the clamp.

Verified at 3x, one curve per field: valid+focus violet, error+focus
red, no ring anywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 14:06:59 +02:00
pigui
4d2a8dd26b Fix the doubled edge down the left of the textarea
Focusing the textarea while it had an error drew two red curves down its
left side, a couple of pixels apart. Not a positioning problem -- the
ring and the outline share the same box to the pixel. It is clamping.

The outline's leading piece is 12px wide, so the 20px corner radius it
was given got clamped to 12, while the focus ring, drawn across the full
width, kept a true 20px curve. Same declared radius, two different
curves. At 10px nothing is clamped and they land on top of each other.

The single-line fields never showed this: at 999px both the outline and
the ring clamp to half the field's height, so they already agreed.

Verified at 2x: one continuous outline, ring and leading piece both
reporting 10px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:31:58 +02:00
pigui
3316ac89fd Stop drawing an errored field twice when it takes focus
Focusing a field that already had an error painted two rings at once:
the notched outline stayed in the error colour and the gradient focus
ring went on top of it in the brand's violet-to-cyan. Two rings, two
colours, one field -- and on the textarea at two different radii, which
is what made it visible enough to report.

The ring itself is worth keeping; it is how focus reads everywhere else.
It just has to agree with the state it decorates, so while the field is
invalid it is built from the warn colour. The result is one red
indication that brightens on focus, and valid fields keep the brand
gradient.

Verified in both themes: an errored focused field's ring and outline are
both the warn colour, a valid focused one still gets the brand gradient.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:15:07 +02:00
pigui
216987bb0c Group each error message with the field it belongs to
On the deployed site "Falta el nombre" sat 0px below its own field and
6px above the *next* field's label, so the three lines read as one block
and the message looked like a caption for "HORAS POR SEMANA".

Material renders a field's message flush underneath it, so the space
between rows has to be visibly larger than the space inside one. The
form's row gap goes from 1.25rem to 2rem: 0px to its own field, 18px to
the next label.

Measured against https://pireon.dev before and after. The earlier fixes
in this series are already live there and verified correct on the
deployed build -- labels uppercase at weight 700, the checkbox message
red at 12px on its own line, the textarea's ring at 20px, and no growth
on submit. This one needs a redeploy to show.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:04:32 +02:00
pigui
6004110fcf Stop the form moving when it reports errors
Pressing Enviar on an incomplete form pushed everything below each
errored field downwards. Material's subscript strip is 10px tall while a
field is valid and 24px once it carries a message, so the form grew by
14px per field at the moment of the click -- the submit button moved out
from under the cursor. The taller height is now reserved up front.

The consent message had two problems of its own. It rendered as an
inline span straight after the label, so it read as part of the
sentence: "Acepto que guardéis mis datos para responderme. Hace falta tu
consentimiento". And nothing in @pi/ui styles `.pi-checkbox__error`, so
it inherited the body colour and size -- ordinary text sitting beside
four red ones. It now gets its own reserved line, in the warn colour at
the same 12px as every other message.

Verified in both themes: the form grows 0px on submit, and all five
messages match at 12px in the warn colour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 09:51:47 +02:00
pigui
7557894b49 Keep field labels stable on focus, and fix the textarea's ring
Focusing a field visibly reflowed its label: "MENSAJE *" turned into
"Mensaje *" in a lighter face under the cursor. @pi/ui applies the
theme's label typography under `:not(.mdc-text-field--focused)`, so
text-transform, letter-spacing and weight all fall away the moment a
field takes focus. Restated without that guard. Colour is left alone --
Material tints the label on focus and that part is wanted.

The textarea was also drawn twice, once in the error colour and once in
the gradient, the two outlines offset from each other. Both take their
radius from `--pi-theme-field-radius`, which the `alternative` shell
sets to 999px: right for a 48px input, a stadium on a 112px textarea. A
fixed radius makes the two shapes coincide.

Both only appear in states the previous fix did not check -- it verified
the resting form only. Measured now in error+focus: all four labels
uppercase at weight 700, and the textarea's ring and outline both at
20px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 09:08:54 +02:00
pigui
25d0fe500b Fix the hours field's label sitting on the outline
The label read "Horas por semana" struck through by the field's own
border: 5px above the wrapper instead of 18, straddling the 2px outline.

Angular Material measures an outlined field's floating label once on
init and writes the result to the element as an inline transform. This
field is the only one inside an @if, so it is measured before it has any
geometry and keeps the unfloated offset. @pi/ui already publishes the
right values for every other field and its rule matches this one too --
it simply loses to an inline style, so the fix restates the library's
own numbers with `!important`, which is what outranks one.

It also gains a placeholder, which every other field already had.

The e2e config now derives the dev server's port from BASE_URL instead
of pinning 4200. With `reuseExistingServer`, an unrelated project's dev
server on that port gets adopted silently and all five tests fail with
"element(s) not found" while the page under test was never served --
which is exactly what happened while verifying this.

Verified: all four labels now land at -18 with a 48px box, light and
dark; lint, 15 unit tests, build and 5 e2e green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:13:12 +02:00
pigui
3f930bb46a Initial commit 2026-08-30 21:11:59 +02:00