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: 8 additions & 0 deletions .agents/skills/scaffold-code-app/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ globals into the production compile, yet they must still be type-checked by the
This is why the app build never needs `vitest/globals` bolted into `tsconfig.app.json`: the tests get
their globals from their own project, and a wrong fixture shape still fails `tsc` at the gate.

**e. Keep the Power Apps Vite plugin (CDN-safe asset base path).** The template's `vite.config.ts`
includes `powerApps()` from `@microsoft/power-apps-vite`. **Leave it in the `plugins` array.** A Code
App is served from the Power Platform CDN at a non-root base path, and this plugin sets the asset base
so Vite's bundled asset URLs and images resolve once deployed. Remove it (or drop the `base`) and the
app's images and hashed assets 404 on the CDN. If a project ever does not use the plugin, set
`base: './'` in `vite.config.ts` instead. Images are referenced by **bundled import from `src/`**, not
from `public/` by absolute path (see the instantiation's "Brand assets and wiring").

See **AGENTS.md (repo root) → Stack notes** for the exact pins and the test-authoring rules (typed
fixtures, one un-mocked smoke render). After this, `npm run verify` runs clean on the empty app, so
the first task gate (Task 1) has a working harness to run against rather than being set up live.
Expand Down
8 changes: 8 additions & 0 deletions .claude/skills/scaffold-code-app/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ globals into the production compile, yet they must still be type-checked by the
This is why the app build never needs `vitest/globals` bolted into `tsconfig.app.json`: the tests get
their globals from their own project, and a wrong fixture shape still fails `tsc` at the gate.

**e. Keep the Power Apps Vite plugin (CDN-safe asset base path).** The template's `vite.config.ts`
includes `powerApps()` from `@microsoft/power-apps-vite`. **Leave it in the `plugins` array.** A Code
App is served from the Power Platform CDN at a non-root base path, and this plugin sets the asset base
so Vite's bundled asset URLs and images resolve once deployed. Remove it (or drop the `base`) and the
app's images and hashed assets 404 on the CDN. If a project ever does not use the plugin, set
`base: './'` in `vite.config.ts` instead. Images are referenced by **bundled import from `src/`**, not
from `public/` by absolute path (see the instantiation's "Brand assets and wiring").

See **AGENTS.md (repo root) → Stack notes** for the exact pins and the test-authoring rules (typed
fixtures, one un-mocked smoke render). After this, `npm run verify` runs clean on the empty app, so
the first task gate (Task 1) has a working harness to run against rather than being set up live.
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ diverge from its spec.
- **Per task:** typecheck + lint (near-instant). **On the data-layer task:** the audit pass (the
checklist in tasks.md Task 1). The test suite exists from Task 1's smoke test and peaks at the
modal milestone (Task 3).
- **Design-bearing tasks get a visual check (`verify` is necessary, not sufficient).** `tsc`, lint,
and test cannot see a broken hero image, a rating picker whose stars never fill, a monospace input,
or stats rendered as boxes instead of the contract's cluster. On Tasks 2, 3, and 4, **run the app
and look at it** against the conformance checklist (contract §8) and the instantiation before
calling the task done. This is the design analogue of the data-layer audit pass: green `verify` plus
eyes on the rendered surface, not green `verify` alone.
- **Commit each task when its gate is green.** After `npm run verify` passes (and the audit pass where
the task requires it), commit that task with a short message naming it, before starting the next.
The git log is the build record; never bundle two tasks into one commit.
Expand Down
39 changes: 31 additions & 8 deletions specs/design-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,14 @@ one interactive mode and two static read-out modes. It is reusable wherever a qu
judgement, score, or progress-out-of-N is shown; an app that has no such concept simply does not use
it. The glyph and range are parameters; everything below is fixed.

> **Build it once.** This is **one component** with a `mode` (picker / inline / confirmation), not
> three look-alike blocks of star markup. Every surface that shows marks (the card read-out, the modal
> picker, the success confirmation) renders the *same* component; only `mode`, `value`, and the
> handlers differ. Per-surface bespoke star code is the failure this section exists to prevent: it is
> how the fills drift, how the picker ends up with unfilled outline marks, and how the same rating
> renders three different ways. If you find yourself writing a second set of star classes, stop and
> reuse the first.

Two fills carry meaning and must not be swapped:
- **Active fill** = `signal/primary` — used while the user is *choosing* and on the immediate
*confirmation*. It is the live signal.
Expand All @@ -360,13 +368,18 @@ Two fills carry meaning and must not be swapped:

**Picker (interactive).** A centred row of N mark-buttons, each `clamp(46px,11vw,60px)`, no fill or
border on the button itself, `motion/control` (slight overshoot, selected mark `scale(1.06)`). A
mark is "on" when its index is `<= max(hover, draft)`. On marks: glyph filled `signal/primary`,
stroke `signal/primary`. Off marks: glyph filled `rgba(255,255,255,.04)`, stroke
`rgba(255,255,255,.32)` at `stroke-width:1.4`. Hover/focus previews without committing; click
commits the draft; pointer-leave clears the preview. A live caption sits beneath at a reserved
height (~26px, so nothing shifts): it names the current value, tinted `signal/primary` at the top of
the range and `text/label` below, and is blank at zero. Every mark-button carries an `aria-label`
("3 stars").
mark is "on" when its index is `<= max(hover, draft)`. On marks: glyph **filled** `signal/primary`
(`fill: #F5400D`, not just a coloured stroke), stroke `signal/primary`. Off marks: glyph filled
`rgba(255,255,255,.04)`, stroke `rgba(255,255,255,.32)` at `stroke-width:1.4`. Hover/focus previews
without committing; click commits the draft; pointer-leave clears the preview. A live caption sits
beneath at a reserved height (~26px, so nothing shifts): it names the current value, tinted
`signal/primary` at the top of the range and `text/label` below, and is blank at zero. Every
mark-button carries an `aria-label` ("3 stars").

> The single most common build error here is a picker whose marks are **thin grey outlines that never
> fill** on hover or select. That is wrong: a selected mark is a solid orange star. If your stars look
> like the off state when a rating is chosen, the `fill` is missing or is being applied to a stroke
> only. A picker that does not visibly fill fails conformance.

**Inline read-out (settled).** A compact, non-interactive row of small marks (~17px) showing a
persisted rating: filled marks `accent/gold`, the remainder `accent/gold-empty`. Pairs with a short
Expand All @@ -385,6 +398,10 @@ filled marks `signal/primary` up to the chosen value, the remainder `accent/gold
`outline:none`, placeholder in `text/faint`. Label above in 14px 700 `text/muted-2`, with an
"(optional)" / "(required)" qualifier in `text/faint` 500 where relevant.

> Set `font-family` to `font/ui` (Barlow) explicitly on inputs and textareas. Browsers default a
> `<textarea>` to a monospace font, so an unset family renders the placeholder and entered text in
> mono, off-brand and obvious. The field inherits the UI font, never the user-agent default.

---

## 5. Behavioural rules
Expand Down Expand Up @@ -503,7 +520,13 @@ A build conforms when all of these are true:
- [ ] Exactly one `signal/primary` action is visible per card and per modal step.
- [ ] Accent colour appears only to encode category or status, never as decoration.
- [ ] Surfaces are flat; the only shadow is `signal/glow` under the active primary button.
- [ ] Both display and UI font families are loaded and used per the type scale; no ALL CAPS.
- [ ] Both display and UI font families are loaded and used per the type scale; no ALL CAPS. Inputs
and textareas render in the UI font, not a monospace fallback.
- [ ] The Rating is a **single reusable component** (one set of star markup, switched by mode), not
per-surface copies. Picker marks **fill** `signal/primary` on hover/select (not outline-only);
settled marks are `accent/gold`; confirmation marks are `signal/primary`.
- [ ] Brand media that the app requires (hero, logo) **actually renders** in the running app, with no
broken-image or `alt`-text fallback showing.
- [ ] Layout uses the centred bands and the fluid auto-fill grid; the intro overlaps the hero when a
hero is present.
- [ ] Cards show distinct incomplete (orange-led) and complete (muted + positive) states.
Expand Down
48 changes: 45 additions & 3 deletions specs/session-feedback-instantiation.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,45 @@ The rating word vocabulary (index 1-5): `Poor`, `Fair`, `Good`, `Great`, `Excell

## 3. Screens & content (verbatim copy)

### 3.0 Brand assets and wiring (do this, do not skip — Code Apps handle images specially)
The brand assets live in this spec folder: `specs/assets/eppc-mark-white.svg`,
`specs/assets/eppc-logo-white.svg`, and `specs/assets/hero.jpg`. A Power Apps Code App is served from
the Power Platform CDN at a **non-root base path**, so *how* you reference an image decides whether it
loads at all. Wire each asset like this:

- **Copy** the needed assets into the app's own source tree (`src/assets/`) and **`import`** them, e.g.
`import heroImage from "./assets/hero.jpg"`, then `src={heroImage}`. A bundled import is the correct
Code Apps method: Vite fingerprints the file and rewrites its URL against the deployed base path (the
`@microsoft/power-apps-vite` `powerApps()` plugin sets that base in `vite.config.ts`), so it resolves
on the CDN.
- **Do not** put images in `public/` and reference them by root-absolute path (`/hero.jpg`). Those
paths are not base-path-rewritten, so they break under the Code App's non-root base unless you
manually prefix `import.meta.env.BASE_URL`. Bundled imports avoid the trap entirely, and it is Vite's
own guidance to "prefer importing assets unless you specifically need the `public` directory".
- **Do not** `import` across the `specs/` boundary (`../specs/assets/...`): that path sits outside the
app root and is fragile under Vite's `server.fs.allow` and the production build. This is exactly how
the hero ended up a broken image. Copy into `src/assets/` first, then import from there.
- The `hero.jpg` is the EPPC "Power Up" key art (speaker columns + the wordmark). It is the headline
brand moment of the whole demo and the thing Vibe could not produce, so it is **required**, not the
contract's optional hero. A hero that renders as an empty box with `alt` text showing is a build
failure for Task 4, not an acceptable "skipped hero".

### 3.1 App bar
- Logo: `assets/eppc-mark-white.svg`, ~34px tall, `alt="EPPC"`.
- Logo: `eppc-mark-white.svg` (wired per §3.0), ~34px tall, `alt="EPPC"`.
- Context label: `Session feedback`. Product name: `European Power Platform Conference`.

### 3.2 Hero
- Image: `assets/hero.jpg`, `alt="Welcome to Copenhagen 2026 — Power Up"`, `object-position:center 44%`.
- Image: `hero.jpg` (wired per §3.0), `alt="Welcome to Copenhagen 2026 — Power Up"`,
`object-position:center 44%`. Full-bleed per contract §4.3 with the bottom scrim, and the intro
strip overlaps it upward per contract §3. Required for this app; confirm it renders (§3.0).

### 3.3 Intro strip
- Eyebrow: `Your voice powers the conference`.
- Headline (display): `Rate the sessions` / `you powered through.` (deliberate `<br>` between the lines).
- Lead: `Two minutes of feedback helps speakers level up and shapes the EPPC programme. Tap any session you attended today to leave your rating.`
- Stat cluster (governed by `showHeroStats`, default on):
- Stat cluster (governed by `showHeroStats`, default on). Render as the contract's stat cluster
(§4.4): each stat is a display-font number over a muted caption, **baseline-aligned to the right of
the headline and separated by a 1px vertical rule**. Not boxed/filled tiles, not stacked cards.
- `{ratedCount}/{totalCount}` over `Sessions rated` (the fraction denominator uses `text/faint-2`).
- `#EPPC26` over `Join the conversation`.

Expand Down Expand Up @@ -106,6 +133,21 @@ Per the contract content card (4.6), filled as:
({title} in `text/strong`.)
- Return button: `Back to sessions`.

### 3.8 Rating — one shared component (contract §4.9)
The stars are **not** three separate pieces of markup. Build the contract's Rating component (§4.9)
**once** and use the same component in all three places it appears in this app:

- **Card rated footer** (§3.5) — `mode=inline`, settled, marks filled `accent/gold` (gold), with the
rating word in `status/positive-label`.
- **Modal picker** (§3.6) — `mode=picker`, interactive, "on" marks **filled** `signal/primary`
(orange), off marks faint, live word caption beneath (`Poor`..`Excellent`).
- **Success confirmation** (§3.7) — `mode=confirmation`, marks filled `signal/primary`.

The mode switches the fill and interactivity; the markup is shared. A picker whose stars stay grey
outlines when a rating is selected, or card/modal stars that visibly differ because they are separate
code, is a defect (contract §4.9). This is the component the demo leans on, so it is worth getting
right rather than inlining.

---

## 4. Behaviour (this app's settings)
Expand Down
11 changes: 10 additions & 1 deletion specs/tasks/task-2-home-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,22 @@
- Track filtering: chips narrow the grid and `All sessions` resets it, because the feedback flow
starts from finding your session.
- A card footer that reflects feedback state per session — the not-rated and rated states — so the
list doubles as a progress read even before the modal exists (Task 3 fills in the write).
list doubles as a progress read even before the modal exists (Task 3 fills in the write). The rated
footer's stars are the **shared Rating component** in settled (`accent/gold`) mode, contract §4.9 and
instantiation §3.8 — build the component here, since this is the first surface that shows marks, so
Task 3's picker reuses it rather than re-rolling stars.
- The intro stat cluster is the contract's baseline cluster (§4.4, instantiation §3.3), not boxed
tiles.

**Done when:**
- `tsc` and lint are clean.
- Every seeded session renders as a card, sorted by `eppc_starttime`.
- Track chips filter correctly; the active chip shows the white-fill state.
- `ratedCount/totalCount` is correct against the data.
- **Visual check (run the app):** the stat cluster is baseline-aligned with a vertical rule (not
boxes), rated cards show gold stars from the shared Rating component, and the layout matches the
contract bands. The hero is still the placeholder here (real art is Task 4), but nothing else is
unstyled.

**Out of scope:** the rate modal, submitting feedback, and final brand assets (real logo and hero
image come in Task 4). Build against the contract tokens so nothing is ever unstyled, but the EPPC
Expand Down
9 changes: 8 additions & 1 deletion specs/tasks/task-3-rate-modal.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
lifecycle, persistence via `Eppc_feedbacksService.create`, and count updates.
- [../design-contract.md §4.8-4.10](../design-contract.md) — the modal, the Rating component, and the
text field.
- [../session-feedback-instantiation.md §3.8](../session-feedback-instantiation.md) — the modal picker
and success confirmation reuse the **shared** Rating component built in Task 2, not new star markup.
- [../design-contract.md §5.2-5.6](../design-contract.md) — state-driven surfaces, honest disabled
states, modal discipline, and optimistic persistence.
- [../dataverse-schema.md §8 `create_feedback`](../dataverse-schema.md) — the create payload and the
Expand All @@ -26,7 +28,9 @@

- The rate modal as specified — track-coloured top bar, header, identity row, star picker with live
word caption, optional comment field, and submit-button states that name the blocking reason in the
label until valid. The spec sections own the copy and the states.
label until valid. The spec sections own the copy and the states. The picker is the **shared Rating
component** (instantiation §3.8) in interactive mode: "on" marks **fill** `signal/primary` (orange),
not grey outlines (contract §4.9). The comment textarea renders in the Barlow UI font (contract §4.10).
- The write that closes the loop: `Eppc_feedbacksService.create` on a first submit with the lookup
bound via `@odata.bind`, the card reflecting the result optimistically, and counts updating — this is
the task's reason to exist.
Expand All @@ -44,6 +48,9 @@
- The suite includes **one un-mocked smoke render** of `App` through the real module graph (mocking
only the `Eppc_*Service` boundary), so a default-vs-named import or import-resolution break is caught
here rather than in the browser console (AGENTS.md Stack notes).
- **Visual check (run the app):** open the modal and confirm the picker stars **fill orange** on hover
and select (not grey outlines), the live caption tracks the rating, and the textarea is in the UI
font, not monospace. `tsc`/lint/test cannot see these (AGENTS.md How we work).
- The create payload satisfies the generated `Eppc_feedbacks` model: rating is the Choice **integer**,
the lookup is `"eppc_SessionId@odata.bind"` (never `_eppc_sessionid_value`), and required system
fields are present (`statecode: 0`). A quick re-run of the Task 1 audit checklist against the write
Expand Down
18 changes: 13 additions & 5 deletions specs/tasks/task-4-branding.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,32 @@ EPPC app. This is the design payoff, held to last on purpose.
**Implements:**
- [../session-feedback-instantiation.md §2](../session-feedback-instantiation.md) — the four tracks
with their line colours and avatar fills.
- [../session-feedback-instantiation.md §3.1-3.2](../session-feedback-instantiation.md) — the real
logo (`assets/eppc-mark-white.svg`) and hero image (`assets/hero.jpg`) with the exact `alt` text.
- [../session-feedback-instantiation.md §3.0-3.2](../session-feedback-instantiation.md) — the asset
wiring (§3.0), the real logo, and the required hero image with the exact `alt` text.
- [../design-contract.md §1-2](../design-contract.md) — confirm only contract tokens and brand
assignments are used.
- [../design-contract.md §8](../design-contract.md) — the conformance checklist.

**Scope (what must exist, not how to build it):**
- The real logo and hero image in place of the scaffold's generic placeholders — the visible payoff
the whole build was leading to.
- The brand assets **wired into the app** per instantiation §3.0 (copied into `src/assets/` and
referenced by **bundled import** — the Code Apps method, so the non-root CDN base path resolves — not
from `public/` by absolute path and not imported across the `specs/` boundary) in place of the
scaffold's generic placeholders. This is the visible payoff the whole build was leading to.
- The "Power Up" hero (`hero.jpg`) rendering full-bleed with the contract scrim and the intro strip
overlapping it. Required for this app, not the contract's optional hero.
- Track accent colours and avatar fills rendering per the track table, so the brand identity is
consistent rather than approximate.
- Motion and spacing polish taken to the point the conformance checklist passes — no further, since
features and data are out of scope here.

**Done when:**
- `tsc`, lint, and `npm run build` are clean.
- **Visual check (run the app):** the hero image and logo **actually render** — no empty box, no
broken-image or `alt`-text fallback. A hero that does not load is a failure of this task, not an
acceptable skipped hero.
- The conformance checklist (contract §8) passes: one signal per surface, flat elevation, both font
families loaded, no ALL CAPS, no stray hex.
families loaded (inputs not monospace), the Rating is one shared component with the correct fills,
no ALL CAPS, no stray hex.
- Side by side against Demo 1's generic tile, it reads as the EPPC brand.

**Out of scope:** new features or data. Identity and polish only.