Remove DaisyUI; consolidate the UI on Basecoat + sarah.css

7285e05753b2 · AtlantisPleb · · parent 8b04c2a324d0

Remove DaisyUI; consolidate the UI on Basecoat + sarah.css

Two component systems cannot share this bundle. DaisyUI emitted flat
component rules -- `.btn` setting background-color, color, and border-color --
into a cascade layer that has to sit above `components` for Tailwind utilities
to keep working. That put it permanently above every `.btn[data-variant=...]`
in sarah.css, which is why all eight SarahUI button variants rendered
identically on staging. No layer ordering fixes that while both are present;
one of them has to go, and the owner chose Basecoat.

Drop the dep and both `@plugin` blocks. In their place, port Sarah's token
ladder into `:root` in app.css. This turns out to be load-bearing rather than
cosmetic: sarah.css consumes `--ink-void`, `--text-primary`, `--accent`,
`--line-strong` and the rest but never defines them -- it only aliases them
onto the Basecoat names. Nothing in this repo defined the primitives, so the
whole Sarah palette was resolving to nothing and DaisyUI's theme was silently
carrying the colour. The variants now resolve to distinct declarations in the
built CSS.

Restyle the surfaces that spoke DaisyUI onto the data-attribute vocabulary:
`btn btn-primary btn-sm` becomes `class="btn" data-variant="primary"
data-size="sm"`, `alert alert-info` becomes `alert` + `data-variant="info"`,
`card-body`/`card-actions` become the `<header>`/`<footer>` the Basecoat card
already styles. Where DaisyUI supplied a widget with no counterpart, build the
honest thing instead of a lookalike: the repo tabs become page navigation
carrying `aria-current` (they change the URL, so they were never tabs), the
milestone bar becomes a `role="progressbar"` meter (the value is a measured
ratio, so `<progress>`'s indeterminate state never applied), the account
dropdown becomes a native `<details>`, and form errors move to `aria-invalid`.

Import basecoat/components/table.css for `CoreComponents.table/1`.

Two things this deliberately does not do. The palette is now dark-only --
DaisyUI shipped the only light theme and Sarah has no light variant, so the
`data-theme` plumbing still runs but no longer repaints; `theme_toggle/1`
documents that rather than pretending. And `--color-base-100/200/300` and
`--color-base-content` survive as aliases onto the three surface rungs,
because the component catalog and the marketing home template still speak
them. They are names for Sarah tokens now, not a second palette.

AGENTS.md previously told agents to reach for DaisyUI classes. It now states
the single system, the data-attribute rule, the individual-import discipline,
and why a second component library breaks this specific bundle.

No test regressions: 899/949 before, 899/949 after on identical buckets. The
cluster/Raft/Horde tests are load-sensitive and flake identically on both
sides of the change (10/12 in each).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149rBWy7br1Z7bbz9NrQhEr
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified AGENTS.md
  • modified assets/css/app.css
  • modified lib/openagents_web/components/core_components.ex
  • modified lib/openagents_web/components/layouts.ex
  • modified lib/openagents_web/components/repo_header.ex
  • modified lib/openagents_web/live/assignee_index_live.ex
  • modified lib/openagents_web/live/docs_live.ex
  • modified lib/openagents_web/live/home_live.ex
  • modified lib/openagents_web/live/icon_index_live.ex
  • modified lib/openagents_web/live/issue_index_live.ex
  • modified lib/openagents_web/live/issue_new_live.ex
  • modified lib/openagents_web/live/issue_show_live.ex
  • modified lib/openagents_web/live/label_index_live.ex
  • modified lib/openagents_web/live/milestone_index_live.ex
  • modified lib/openagents_web/live/project_index_live.ex
  • modified lib/openagents_web/live/project_show_live.ex
  • modified mix.exs
  • modified mix.lock

Diff

18 files changed, +533 -469

AGENTS.md modified +7 -1

@@ -42,7 +42,13 @@ custom classes must fully style the input

42 42
43 43
- **Always use and maintain this import syntax** in the app.css file for projects generated with `phx.new`
44 44
- **Never** use `@apply` when writing raw css
45
- **Use DaisyUI component classes and patterns** where they already cover the needed UI. Add custom Tailwind only when the design is unique to OpenAgents.
45
- **There is exactly one component system: vendored Basecoat plus Sarah's style pack.** Basecoat lives in `assets/vendor/basecoat/components/` and carries structure (display, padding, min-height); `assets/css/sarah.css` carries OpenAgents' identity (motion tokens, radius scale, colour, the notched variant, corner frames) and must stay the last import so its declarations win.
46
  - **Reach for `OpenAgentsWeb.SarahUI` first.** It wraps that CSS in nineteen ready primitives — `button/1`, `card/1`, `badge/1`, `alert/1`, `input/1`, `textarea/1`, `label/1`, `field/1`, `avatar/1`, `menu/1`, `empty/1`, `kbd/1`, and the rest. Fall back to hand-written classes only when no primitive covers the shape.
47
  - **Variants are data attributes, not classes.** A control is `class="btn"` plus `data-variant="primary"` / `data-size="sm"` / `data-tone="danger"`. Never invent `btn-primary`-style variant classes; they defeat the whole point of the split.
48
  - **Never add a second component library — DaisyUI above all.** DaisyUI was removed deliberately. It emitted flat `.btn` rules (setting background, colour, border) from a cascade layer that outranked every `.btn[data-variant=…]` in `sarah.css`, so all eight SarahUI button variants rendered identically on staging. Any library with the same shape will do the same thing again.
49
  - **Import Basecoat components individually**, one `@import "../vendor/basecoat/components/<name>.css"` per component a surface actually uses. Component CSS lands in `@layer components` and ships whether or not the class appears in markup, so the list is a budget. **Never** import `basecoat.css`, `basecoat-base.css`, or `basecoat-components.css` — each pulls in all 39 components at once.
50
  - The palette is the token ladder at the top of `app.css` (`--ink-void` / `--ink-surface` / `--ink-raised`, the text and line tiers, and the four semantic colours). It is dark-only. Prefer the Basecoat utility names built on it — `bg-background`, `bg-card`, `text-foreground`, `text-muted-foreground`, `border-border` — over the older `base-100`/`base-content` aliases, which are kept only so unconverted surfaces keep resolving.
51
- Add custom Tailwind only when the design is unique to OpenAgents and no primitive fits.
46 52
- Out of the box **only the app.js and app.css bundles are supported**
47 53
  - You cannot reference an external vendor'd script `src` or link `href` in the layouts
48 54
  - You must import the vendor deps into app.js and app.css to use them
assets/css/app.css modified +122 -99

@@ -1,18 +1,15 @@

1 1
/* See the Tailwind configuration guide for advanced usage
2 2
   https://tailwindcss.com/docs/configuration */
3 3
4
/* Cascade-layer order, declared before any @import so it is the FIRST @layer
5
 * statement in the bundle — only the first one establishes order; later ones
6
 * can add names but cannot reorder existing layers.
4
/* There is exactly one component system here: Basecoat structure plus Sarah's
5
 * style pack. DaisyUI was removed (see AGENTS.md) — it emitted flat component
6
 * rules from its own cascade layer that outranked every `.btn[data-variant=…]`
7
 * in sarah.css, so all eight SarahUI button variants rendered identically. Do
8
 * not reintroduce it, and do not add a second component library beside it.
7 9
 *
8
 * DaisyUI emits into its own `daisyui` layer. Without this line that layer is
9
 * declared after `components`, and a later-declared layer beats an earlier one
10
 * regardless of specificity, so DaisyUI's flat `.btn` overrode every
11
 * `.btn[data-variant=...]` rule in sarah.css and all eight SarahUI button
12
 * variants rendered identically. Listing `daisyui` before `components` keeps
13
 * Sarah's component styles authoritative, which is what sarah.css's own
14
 * "must stay last so its declarations win" note requires. */
15
@layer theme, base, daisyui, components, utilities;
10
 * With DaisyUI gone the layer order Tailwind itself declares
11
 * (`theme, base, components, utilities`) is correct as-is, so this file no
12
 * longer restates it. */
16 13
17 14
@import "tailwindcss" source(none);
18 15
@import "phoenix-colocated/openagents/colocated.css";

@@ -24,8 +21,9 @@

24 21
 *
25 22
 * These carry the structure (display, padding, min-height, background) that
26 23
 * sarah.css deliberately does not set — its .btn only adds gap, border,
27
 * radius, colour, font, and transition on top. Without them SarahUI controls
28
 * fall back to DaisyUI's geometry. This list matches Sarah's exactly. */
24
 * radius, colour, font, and transition on top. With DaisyUI gone they are the
25
 * only source of that geometry, so nothing here is optional. This list is
26
 * Sarah's, plus `table` for `CoreComponents.table/1`. */
29 27
@import "../vendor/basecoat/base/base.css";
30 28
@import "../vendor/basecoat/components/button.css";
31 29
@import "../vendor/basecoat/components/input.css";

@@ -40,6 +38,7 @@

40 38
@import "../vendor/basecoat/components/item.css";
41 39
@import "../vendor/basecoat/components/empty.css";
42 40
@import "../vendor/basecoat/components/kbd.css";
41
@import "../vendor/basecoat/components/table.css";
43 42
44 43
/* Sarah's identity. Must stay last so its declarations win. */
45 44
@import "./sarah.css";

@@ -53,82 +52,106 @@

53 52
   The heroicons installation itself is managed by your mix.exs */
54 53
@plugin "../vendor/heroicons";
55 54
56
/* daisyUI Tailwind Plugin. */
57
@plugin "daisyui/packages/bundle/daisyui" {
58
  themes: false;
59
}
60
61
/* daisyUI theme plugin.
62
  We ship with two themes, a light one inspired on Phoenix colors and a dark one inspired
63
  on Elixir colors. Build your own at: https://daisyui.com/theme-generator/ */
64
@plugin "daisyui/packages/bundle/daisyui-theme" {
65
  name: "dark";
66
  default: true;
67
  prefersdark: true;
68
  color-scheme: "dark";
69
  --color-base-100: #08090a;
70
  --color-base-200: #0f1011;
71
  --color-base-300: #1c1c1f;
72
  --color-base-content: #f7f8f8;
73
  --color-primary: #5e6ad2;
74
  --color-primary-content: #f7f8f8;
75
  --color-secondary: #1f2023;
76
  --color-secondary-content: #f7f8f8;
77
  --color-accent: #5e6ad2;
78
  --color-accent-content: #f7f8f8;
79
  --color-neutral: #62666d;
80
  --color-neutral-content: #f7f8f8;
81
  --color-info: #5e6ad2;
82
  --color-info-content: #f7f8f8;
83
  --color-success: #4cb782;
84
  --color-success-content: #08090a;
85
  --color-warning: #f2994a;
86
  --color-warning-content: #08090a;
87
  --color-error: #eb5757;
88
  --color-error-content: #f7f8f8;
89
  --radius-selector: 0.25rem;
90
  --radius-field: 0.375rem;
91
  --radius-box: 0.5rem;
92
  --size-selector: 0.21875rem;
93
  --size-field: 0.21875rem;
94
  --border: 1px;
95
  --depth: 1;
96
  --noise: 0;
97
}
98
99
@plugin "daisyui/packages/bundle/daisyui-theme" {
100
  name: "light";
101
  default: false;
102
  prefersdark: false;
103
  color-scheme: "light";
104
  --color-base-100: oklch(98% 0 0);
105
  --color-base-200: oklch(96% 0.001 286.375);
106
  --color-base-300: oklch(92% 0.004 286.32);
107
  --color-base-content: oklch(21% 0.006 285.885);
108
  --color-primary: oklch(70% 0.213 47.604);
109
  --color-primary-content: oklch(98% 0.016 73.684);
110
  --color-secondary: oklch(55% 0.027 264.364);
111
  --color-secondary-content: oklch(98% 0.002 247.839);
112
  --color-accent: oklch(0% 0 0);
113
  --color-accent-content: oklch(100% 0 0);
114
  --color-neutral: oklch(44% 0.017 285.786);
115
  --color-neutral-content: oklch(98% 0 0);
116
  --color-info: oklch(62% 0.214 259.815);
117
  --color-info-content: oklch(97% 0.014 254.604);
118
  --color-success: oklch(70% 0.14 182.503);
119
  --color-success-content: oklch(98% 0.014 180.72);
120
  --color-warning: oklch(66% 0.179 58.318);
121
  --color-warning-content: oklch(98% 0.022 95.277);
122
  --color-error: oklch(58% 0.253 17.585);
123
  --color-error-content: oklch(96% 0.015 12.422);
124
  --radius-selector: 0.25rem;
125
  --radius-field: 0.25rem;
126
  --radius-box: 0.5rem;
127
  --size-selector: 0.21875rem;
128
  --size-field: 0.21875rem;
129
  --border: 1.5px;
130
  --depth: 1;
131
  --noise: 0;
55
/* ── Token ladder ───────────────────────────────────────────────────────────
56
 * Ported from Sarah, where these are the two tiers of DESIGN.md "Color
57
 * Tokens": PRIMITIVES are the hand-picked Aiur oklch values; DERIVATIONS are
58
 * color-mix() steps off one ink, so the hover/focus/border grammar is
59
 * systematic rather than accumulated.
60
 *
61
 * sarah.css consumes these but does not define them — it aliases them onto
62
 * the Basecoat names (`--background`, `--card`, `--primary`, `--border`, …)
63
 * in its own `:root`. Before this block existed those aliases resolved to
64
 * nothing and the DaisyUI theme was carrying the palette instead, which is
65
 * exactly the collision that made every SarahUI variant render identically.
66
 *
67
 * The three surfaces are a fixed relationship, not three loose colors: page
68
 * and sidebar sit on the darkest (--ink-void), the working canvas one step up
69
 * (--ink-surface), elevated components another step up (--ink-raised). New
70
 * surfaces take a rung; they do not invent a fourth.
71
 *
72
 * The palette is dark-only, as Sarah's is (`color-scheme: dark`). The
73
 * `data-theme` plumbing in root.html.heex still runs, but there is no second
74
 * light palette behind it any more — DaisyUI's was the only one, and Sarah
75
 * ships no light variant to replace it with.
76
 */
77
:root {
78
  color-scheme: dark;
79
80
  /* PRIMITIVES — the Aiur palette, verbatim. */
81
  --ink-void: oklch(12.901% 0.01498 266.378); /* darkest: page, sidebar */
82
  --ink-surface: oklch(18.311% 0.03089 263.383); /* +1: canvas, chrome */
83
  --ink-raised: oklch(24.185% 0.04699 263.85); /* +2: elevated components */
84
  --text-ink: oklch(96.386% 0.01695 267.792); /* the one text/icon/wash ink */
85
  --accent: oklch(62.308% 0.18801 259.815);
86
  --accent-bright: oklch(71.374% 0.14338 254.624);
87
  --info: oklch(75.351% 0.13899 232.661);
88
  --success: oklch(72.275% 0.19201 149.579);
89
  --warning: oklch(76.859% 0.16466 70.08);
90
  --danger: oklch(71.063% 0.16615 22.216);
91
92
  /* DERIVATIONS — text tiers as alpha steps of the ink. The literals are the
93
     Aiur values; the percentages beside them are the measured best-fit alpha
94
     steps, recorded so a later theme can choose the ladder deliberately. */
95
  --text-primary: color-mix(in oklab, var(--text-ink) 100%, transparent); /* 100% */
96
  --text-body: oklch(90.551% 0.0307 265.325); /* exception; best fit 91% */
97
  --text-muted: oklch(71.613% 0.04926 262.439); /* exception; best fit 65% */
98
  --text-dim: oklch(65.775% 0.04389 274.477); /* exception; best fit 57% */
99
100
  /* Icon tiers. Glyphs read lighter than text at the same alpha, so they get
101
     their own ladder rather than borrowing the text one. */
102
  --icon-primary: color-mix(in oklab, var(--text-ink) 100%, transparent); /* 100% */
103
  --icon-secondary: color-mix(in oklab, var(--text-ink) 66%, transparent); /* 66% */
104
  --icon-tertiary: color-mix(in oklab, var(--text-ink) 52%, transparent); /* 52% */
105
  --icon-faint: color-mix(in oklab, var(--text-ink) 28%, transparent); /* 28% */
106
107
  /* Washes — translucent ink laid over whatever surface is beneath, so hover
108
     and selection read as the same gesture on every rung of the surface
109
     ladder. Hover < active < selected < strong. */
110
  --wash-hover: color-mix(in oklab, var(--text-ink) 6%, transparent); /* 6% */
111
  --wash-active: color-mix(in oklab, var(--text-ink) 8%, transparent); /* 8% */
112
  --wash-selected: color-mix(in oklab, var(--text-ink) 10%, transparent); /* 10% */
113
  --wash-strong: color-mix(in oklab, var(--text-ink) 14%, transparent); /* 14% */
114
115
  /* The hover surface is the wash resolved opaquely over the raised rung. */
116
  --ink-hover: color-mix(in oklab, var(--text-ink) 4%, var(--ink-raised)); /* 4% over raised */
117
118
  /* The one inverted control: the strongest ink as ground, the darkest rung as
119
     glyph. State changes ride opacity so the fill never shifts hue. */
120
  --control-inverse-bg: var(--text-primary);
121
  --control-inverse-fg: var(--ink-void);
122
123
  /* Borders. --line and --line-strong are the 12% and 20% rungs by role but
124
     keep their literals (same chroma accounting as the text exceptions); the
125
     soft and faint rungs are true ladder steps. */
126
  --line: oklch(29.154% 0.0506 264.607); /* exception; best fit 11% */
127
  --line-soft: color-mix(in oklab, var(--text-ink) 8%, transparent); /* 8% */
128
  --line-faint: color-mix(in oklab, var(--text-ink) 4%, transparent); /* 4% */
129
  --line-strong: oklch(36.455% 0.0696 264.888); /* exception; best fit 20% */
130
}
131
132
/* Utility namespaces on top of the ladder.
133
 *
134
 * `base-100/200/300` and `base-content` were DaisyUI's surface vocabulary and
135
 * are still spoken by surfaces this change did not restyle (the component
136
 * catalog, the marketing home template, and the sidebar/docs/chat CSS at the
137
 * foot of this file). They are kept as aliases onto the three surface rungs so
138
 * that vocabulary keeps resolving; they are now names for Sarah tokens, not a
139
 * second palette. Prefer Basecoat's own `background` / `card` / `foreground` /
140
 * `border` names in new markup.
141
 *
142
 * The semantic four are Sarah's, not DaisyUI's, and have no Basecoat
143
 * equivalent — `--color-destructive` covers danger, but there is no success,
144
 * warning, or info in the Basecoat base. */
145
@theme {
146
  --color-base-100: var(--ink-void);
147
  --color-base-200: var(--ink-surface);
148
  --color-base-300: var(--ink-raised);
149
  --color-base-content: var(--text-primary);
150
151
  --color-success: var(--success);
152
  --color-warning: var(--warning);
153
  --color-info: var(--info);
154
  --color-danger: var(--danger);
132 155
}
133 156
134 157
/* Add variants based on LiveView classes */

@@ -283,7 +306,7 @@

283 306
  min-height: 32px;
284 307
  padding-inline: 16px;
285 308
  background: var(--color-base-200);
286
  color: var(--color-neutral);
309
  color: var(--text-muted);
287 310
  font-size: 0.75rem;
288 311
  font-weight: 600;
289 312
  letter-spacing: 0.06em;

@@ -343,7 +366,7 @@

343 366
344 367
.docs-sidebar__section-title {
345 368
  padding-inline: 8px;
346
  color: var(--color-neutral);
369
  color: var(--text-muted);
347 370
  font-size: 0.75rem;
348 371
  font-weight: 600;
349 372
  letter-spacing: 0.06em;

@@ -407,7 +430,7 @@

407 430
}
408 431
409 432
.message-time {
410
  color: var(--color-neutral);
433
  color: var(--text-muted);
411 434
  font-size: 0.75rem;
412 435
  line-height: 1rem;
413 436
}

@@ -425,8 +448,8 @@

425 448
  padding: 10px 14px;
426 449
  border-radius: 16px;
427 450
  border-bottom-right-radius: 4px;
428
  background: var(--color-primary);
429
  color: var(--color-primary-content);
451
  background: var(--accent);
452
  color: var(--text-primary);
430 453
}
431 454
432 455
.composer {

@@ -447,8 +470,8 @@

447 470
}
448 471
449 472
.composer-card:focus-within {
450
  border-color: var(--color-primary);
451
  box-shadow: 0 0 0 2px var(--color-primary) / 20%;
473
  border-color: var(--accent-bright);
474
  box-shadow: 0 0 0 2px color-mix(in oklch, var(--accent-bright) 22%, transparent);
452 475
}
453 476
454 477
.composer-grid {

@@ -475,7 +498,7 @@

475 498
}
476 499
477 500
.composer-input::placeholder {
478
  color: var(--color-neutral);
501
  color: var(--text-muted);
479 502
}
480 503
481 504
.composer-trailing {

@@ -495,8 +518,8 @@

495 518
  justify-content: center;
496 519
  border: 0;
497 520
  border-radius: 50%;
498
  background: var(--color-primary);
499
  color: var(--color-primary-content);
521
  background: var(--accent);
522
  color: var(--text-primary);
500 523
  cursor: pointer;
501 524
  transition: filter 150ms ease, transform 150ms ease;
502 525
}
lib/openagents_web/components/core_components.ex modified +94 -81

@@ -9,11 +9,18 @@ defmodule OpenAgentsWeb.CoreComponents do

9 9
  them in any way you want, based on your application growth and needs.
10 10
11 11
  The foundation for styling is Tailwind CSS, a utility-first CSS framework,
12
  augmented with daisyUI, a Tailwind CSS plugin that provides UI components
13
  and themes. Here are useful references:
14
15
    * [daisyUI](https://daisyui.com/docs/intro/) - a good place to get
16
      started and see the available components.
12
  on top of the vendored Basecoat component structure in
13
  `assets/vendor/basecoat/components/` and Sarah's style pack in
14
  `assets/css/sarah.css`. Basecoat expresses variants as data attributes
15
  (`data-variant`, `data-size`), so a control is `class="btn"` plus a data
16
  attribute rather than a stack of variant classes. There is no second
17
  component library — DaisyUI was removed, and reintroducing it would put flat
18
  `.btn`-style rules back above `.btn[data-variant=…]`. Here are useful
19
  references:
20
21
    * `OpenAgentsWeb.SarahUI` - nineteen ready primitives over that CSS
22
      (`button/1`, `card/1`, `badge/1`, `alert/1`, `input/1`, …). Prefer them
23
      to hand-written component classes.
17 24
18 25
    * [Tailwind CSS](https://tailwindcss.com) - the foundational framework
19 26
      we build on. You will use it for layout, sizing, flexbox, grid, and

@@ -63,21 +70,22 @@ defmodule OpenAgentsWeb.CoreComponents do

63 70
      id={@id}
64 71
      phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
65 72
      role="alert"
66
      class="toast toast-top toast-end z-50"
73
      class="fixed top-4 right-4 z-50 flex flex-col items-end gap-2"
67 74
      {@rest}
68 75
    >
69
      <div class={[
70
        "alert w-80 sm:w-96 max-w-80 sm:max-w-96 text-wrap",
71
        @kind == :info && "alert-info",
72
        @kind == :error && "alert-error"
73
      ]}>
76
      <%!-- Basecoat/Sarah `.alert` is a three-column grid: leading marker,
77
      body `<section>`, trailing action. Keeping exactly three children is what
78
      lets the close control sit in the third column instead of wrapping. --%>
79
      <div
80
        class="alert w-80 sm:w-96 max-w-80 sm:max-w-96 text-wrap"
81
        data-variant={if @kind == :error, do: "danger", else: "info"}
82
      >
74 83
        <.icon :if={@kind == :info} name="hero-information-circle" class="size-5 shrink-0" />
75 84
        <.icon :if={@kind == :error} name="hero-exclamation-circle" class="size-5 shrink-0" />
76
        <div>
85
        <section>
77 86
          <p :if={@title} class="font-semibold">{@title}</p>
78 87
          <p>{msg}</p>
79
        </div>
80
        <div class="flex-1" />
88
        </section>
81 89
        <button type="button" class="group self-start cursor-pointer" aria-label={gettext("close")}>
82 90
          <.icon name="hero-x-mark" class="size-5 opacity-40 group-hover:opacity-70" />
83 91
        </button>

@@ -89,6 +97,15 @@ defmodule OpenAgentsWeb.CoreComponents do

89 97
  @doc """
90 98
  Renders a button with navigation support.
91 99
100
  A thin wrapper over the Basecoat/Sarah `.btn` recipe: the class is always
101
  `btn` and the look comes from `data-variant`, never from a stack of variant
102
  classes. `class` adds layout utilities alongside it rather than replacing it,
103
  so a caller cannot accidentally drop the component styling.
104
105
  `OpenAgentsWeb.SarahUI.button/1` is the fuller control (eight variants, three
106
  sizes, a danger tone). This one exists so surfaces on `CoreComponents` alone
107
  still get the same two shapes.
108
92 109
  ## Examples
93 110
94 111
      <.button>Send!</.button>

@@ -98,33 +115,21 @@ defmodule OpenAgentsWeb.CoreComponents do

98 115
  attr :rest, :global,
99 116
    include: ~w(href navigate patch method download name value disabled type id phx-click)
100 117
101
  attr :class, :any
102
  attr :variant, :string, values: ~w(primary)
118
  attr :class, :any, default: nil
119
  attr :variant, :string, values: ~w(primary secondary ghost outline), default: "secondary"
120
  attr :size, :string, values: [nil | ~w(xs sm lg)], default: nil
103 121
  slot :inner_block, required: true
104 122
105 123
  def button(%{rest: rest} = assigns) do
106
    variants = %{
107
      "primary" => "bg-primary text-primary-content hover:bg-[#4c57b5]",
108
      nil => "bg-[#1c1c1f] text-base-content border border-[#26262a] hover:bg-[#26262a]"
109
    }
110
111
    assigns =
112
      assign_new(assigns, :class, fn ->
113
        [
114
          "inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed",
115
          Map.fetch!(variants, assigns[:variant])
116
        ]
117
      end)
118
119 124
    if rest[:href] || rest[:navigate] || rest[:patch] do
120 125
      ~H"""
121
      <.link class={@class} {@rest}>
126
      <.link class={["btn", @class]} data-variant={@variant} data-size={@size} {@rest}>
122 127
        {render_slot(@inner_block)}
123 128
      </.link>
124 129
      """
125 130
    else
126 131
      ~H"""
127
      <button class={@class} {@rest}>
132
      <button class={["btn", @class]} data-variant={@variant} data-size={@size} {@rest}>
128 133
        {render_slot(@inner_block)}
129 134
      </button>
130 135
      """

@@ -220,7 +225,7 @@ defmodule OpenAgentsWeb.CoreComponents do

220 225
      end)
221 226
222 227
    ~H"""
223
    <div class="fieldset mb-2">
228
    <div class="field mb-2">
224 229
      <label for={@id}>
225 230
        <input
226 231
          type="hidden"

@@ -229,14 +234,17 @@ defmodule OpenAgentsWeb.CoreComponents do

229 234
          disabled={@rest[:disabled]}
230 235
          form={@rest[:form]}
231 236
        />
232
        <span class="label">
237
        <span class="label inline-flex items-center gap-2">
238
          <%!-- The one native control the style pack does not restyle. Letting
239
          the browser draw it and tinting it with accent-color keeps it
240
          keyboard- and platform-correct for free. --%>
233 241
          <input
234 242
            type="checkbox"
235 243
            id={@id}
236 244
            name={@name}
237 245
            value="true"
238 246
            checked={@checked}
239
            class={@class || "checkbox checkbox-sm"}
247
            class={@class || "size-4 shrink-0 accent-primary"}
240 248
            {@rest}
241 249
          />{@label}
242 250
        </span>

@@ -248,13 +256,14 @@ defmodule OpenAgentsWeb.CoreComponents do

248 256
249 257
  def input(%{type: "select"} = assigns) do
250 258
    ~H"""
251
    <div class="fieldset mb-2">
259
    <div class="field mb-2">
252 260
      <label for={@id}>
253 261
        <span :if={@label} class="label mb-1">{@label}</span>
254 262
        <select
255 263
          id={@id}
256 264
          name={@name}
257
          class={[@class || "w-full select", @errors != [] && (@error_class || "select-error")]}
265
          class={[@class || "w-full input", "aria-invalid:border-destructive", @error_class]}
266
          aria-invalid={@errors != [] && "true"}
258 267
          multiple={@multiple}
259 268
          {@rest}
260 269
        >

@@ -269,16 +278,14 @@ defmodule OpenAgentsWeb.CoreComponents do

269 278
270 279
  def input(%{type: "textarea"} = assigns) do
271 280
    ~H"""
272
    <div class="fieldset mb-2">
281
    <div class="field mb-2">
273 282
      <label for={@id}>
274 283
        <span :if={@label} class="label mb-1">{@label}</span>
275 284
        <textarea
276 285
          id={@id}
277 286
          name={@name}
278
          class={[
279
            @class || "w-full textarea",
280
            @errors != [] && (@error_class || "textarea-error")
281
          ]}
287
          class={[@class || "w-full textarea", "aria-invalid:border-destructive", @error_class]}
288
          aria-invalid={@errors != [] && "true"}
282 289
          {@rest}
283 290
        >{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
284 291
      </label>

@@ -290,7 +297,7 @@ defmodule OpenAgentsWeb.CoreComponents do

290 297
  # All other inputs text, datetime-local, url, password, etc. are handled here...
291 298
  def input(assigns) do
292 299
    ~H"""
293
    <div class="fieldset mb-2">
300
    <div class="field mb-2">
294 301
      <label for={@id}>
295 302
        <span :if={@label} class="label mb-1">{@label}</span>
296 303
        <input

@@ -298,10 +305,8 @@ defmodule OpenAgentsWeb.CoreComponents do

298 305
          name={@name}
299 306
          id={@id}
300 307
          value={Phoenix.HTML.Form.normalize_value(@type, @value)}
301
          class={[
302
            @class || "w-full input",
303
            @errors != [] && (@error_class || "input-error")
304
          ]}
308
          class={[@class || "w-full input", "aria-invalid:border-destructive", @error_class]}
309
          aria-invalid={@errors != [] && "true"}
305 310
          {@rest}
306 311
        />
307 312
      </label>

@@ -313,7 +318,7 @@ defmodule OpenAgentsWeb.CoreComponents do

313 318
  # Helper used by inputs to generate form errors
314 319
  defp error(assigns) do
315 320
    ~H"""
316
    <p class="mt-1.5 flex gap-2 items-center text-sm text-error">
321
    <p class="mt-1.5 flex gap-2 items-center text-sm text-destructive">
317 322
      <.icon name="hero-exclamation-circle" class="size-5" />
318 323
      {render_slot(@inner_block)}
319 324
    </p>

@@ -334,7 +339,7 @@ defmodule OpenAgentsWeb.CoreComponents do

334 339
        <h1 class="text-lg font-semibold leading-8">
335 340
          {render_slot(@inner_block)}
336 341
        </h1>
337
        <p :if={@subtitle != []} class="text-sm text-base-content/70">
342
        <p :if={@subtitle != []} class="text-sm text-muted-foreground">
338 343
          {render_slot(@subtitle)}
339 344
        </p>
340 345
      </div>

@@ -375,34 +380,42 @@ defmodule OpenAgentsWeb.CoreComponents do

375 380
      end
376 381
377 382
    ~H"""
378
    <table class="table table-zebra">
379
      <thead>
380
        <tr>
381
          <th :for={col <- @col}>{col[:label]}</th>
382
          <th :if={@action != []}>
383
            <span class="sr-only">{gettext("Actions")}</span>
384
          </th>
385
        </tr>
386
      </thead>
387
      <tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
388
        <tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
389
          <td
390
            :for={col <- @col}
391
            phx-click={@row_click && @row_click.(row)}
392
            class={@row_click && "hover:cursor-pointer"}
393
          >
394
            {render_slot(col, @row_item.(row))}
395
          </td>
396
          <td :if={@action != []} class="w-0 font-semibold">
397
            <div class="flex gap-4">
398
              <%= for action <- @action do %>
399
                {render_slot(action, @row_item.(row))}
400
              <% end %>
401
            </div>
402
          </td>
403
        </tr>
404
      </tbody>
405
    </table>
383
    <%!-- Basecoat's `.table` carries structure only — widths, alignment, and
384
    the row rule. Rhythm and colour are utilities here rather than a zebra
385
    variant: Sarah separates rows with the hairline, not with alternating
386
    fills. --%>
387
    <div class="table-container">
388
      <table class="table text-sm">
389
        <thead>
390
          <tr>
391
            <th :for={col <- @col} class="px-3 py-2 text-left text-muted-foreground">
392
              {col[:label]}
393
            </th>
394
            <th :if={@action != []} class="px-3 py-2">
395
              <span class="sr-only">{gettext("Actions")}</span>
396
            </th>
397
          </tr>
398
        </thead>
399
        <tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
400
          <tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="hover:bg-muted/40">
401
            <td
402
              :for={col <- @col}
403
              phx-click={@row_click && @row_click.(row)}
404
              class={["px-3 py-2", @row_click && "hover:cursor-pointer"]}
405
            >
406
              {render_slot(col, @row_item.(row))}
407
            </td>
408
            <td :if={@action != []} class="w-0 px-3 py-2 font-semibold">
409
              <div class="flex gap-4">
410
                <%= for action <- @action do %>
411
                  {render_slot(action, @row_item.(row))}
412
                <% end %>
413
              </div>
414
            </td>
415
          </tr>
416
        </tbody>
417
      </table>
418
    </div>
406 419
    """
407 420
  end
408 421

@@ -422,11 +435,11 @@ defmodule OpenAgentsWeb.CoreComponents do

422 435
423 436
  def list(assigns) do
424 437
    ~H"""
425
    <ul class="list">
426
      <li :for={item <- @item} class="list-row">
427
        <div class="list-col-grow">
428
          <div class="font-bold">{item.title}</div>
429
          <div>{render_slot(item)}</div>
438
    <ul class="divide-y divide-border">
439
      <li :for={item <- @item} class="flex items-start gap-4 py-3">
440
        <div class="min-w-0 grow">
441
          <div class="font-semibold">{item.title}</div>
442
          <div class="text-muted-foreground">{render_slot(item)}</div>
430 443
        </div>
431 444
      </li>
432 445
    </ul>
lib/openagents_web/components/layouts.ex modified +30 -20

@@ -45,7 +45,7 @@ defmodule OpenAgentsWeb.Layouts do

45 45
46 46
  def app(assigns) do
47 47
    ~H"""
48
    <div class="h-screen flex overflow-hidden bg-base-100">
48
    <div class="h-screen flex overflow-hidden bg-background">
49 49
      <%= if @current_scope do %>
50 50
        <.sidebar current_scope={@current_scope} />
51 51
      <% end %>

@@ -55,7 +55,7 @@ defmodule OpenAgentsWeb.Layouts do

55 55
56 56
        <main class={[
57 57
          "flex-1 min-w-0 overflow-y-auto overscroll-none p-4",
58
          @current_scope && "bg-base-100"
58
          @current_scope && "bg-background"
59 59
        ]}>
60 60
          <%= if @current_scope do %>
61 61
            {render_slot(@inner_block)}

@@ -78,29 +78,29 @@ defmodule OpenAgentsWeb.Layouts do

78 78
79 79
  defp openagents_command_bar(assigns) do
80 80
    ~H"""
81
    <header class="navbar bg-base-100 border-b border-base-300 px-4 h-16 shrink-0">
82
      <div class="navbar-start">
81
    <header class="flex items-center gap-2 bg-background border-b border-border px-4 h-16 shrink-0">
82
      <div class="flex flex-1 min-w-0 items-center gap-2">
83 83
        <%= if !@current_scope do %>
84
          <.link navigate={~p"/"} class="btn btn-ghost text-xl">
84
          <.link navigate={~p"/"} class="btn text-xl text-foreground" data-variant="ghost">
85 85
            OpenAgents
86 86
          </.link>
87 87
        <% end %>
88 88
        <%= if @title do %>
89
          <div class="flex flex-col justify-center">
90
            <h1 class="text-base font-semibold leading-tight">{@title}</h1>
89
          <div class="flex flex-col justify-center min-w-0">
90
            <h1 class="text-base font-semibold leading-tight truncate">{@title}</h1>
91 91
            <%= if @subtitle do %>
92
              <p class="text-xs text-base-content/60">{@subtitle}</p>
92
              <p class="text-xs text-muted-foreground truncate">{@subtitle}</p>
93 93
            <% end %>
94 94
          </div>
95 95
        <% end %>
96 96
      </div>
97 97
98
      <div class="navbar-end gap-2">
98
      <div class="flex items-center gap-2">
99 99
        <%= if @current_scope do %>
100 100
          <.account_dropdown current_scope={@current_scope} />
101 101
        <% else %>
102 102
          <.form for={%{}} as={:auth} action={~p"/auth/github"} method="post" class="m-0">
103
            <.button type="submit" class="btn btn-primary btn-sm">Sign in with GitHub</.button>
103
            <.button type="submit" variant="primary" size="sm">Sign in with GitHub</.button>
104 104
          </.form>
105 105
        <% end %>
106 106
      </div>

@@ -189,25 +189,31 @@ defmodule OpenAgentsWeb.Layouts do

189 189
190 190
  defp account_dropdown(assigns) do
191 191
    ~H"""
192
    <details class="dropdown dropdown-end">
193
      <summary class="btn btn-ghost btn-circle avatar list-none cursor-pointer">
192
    <%!-- A native <details> disclosure rather than a JavaScript dropdown, for
193
    the same reason `SarahUI.menu/1` uses the popover API: the account control
194
    has to work before any client script has run. --%>
195
    <details class="relative">
196
      <summary class="btn list-none cursor-pointer !p-1" data-variant="ghost">
194 197
        <img
195 198
          src={@current_scope.github_avatar_url}
196 199
          alt={"GitHub avatar for @#{@current_scope.github_login}"}
197 200
          class="w-8 h-8 rounded-full"
198 201
        />
199 202
      </summary>
200
      <ul class="menu dropdown-content bg-base-100 rounded-box z-10 w-56 p-2 shadow border border-base-300">
201
        <li class="p-2">
203
      <ul class="absolute end-0 z-10 mt-2 w-56 rounded-lg border border-border bg-popover p-2 shadow-lg">
204
        <li class="flex flex-col p-2">
202 205
          <span class="font-semibold">{account_display_name(@current_scope)}</span>
203
          <span :if={@current_scope.github_name} class="text-sm text-base-content/70">
206
          <span :if={@current_scope.github_name} class="text-sm text-muted-foreground">
204 207
            @{@current_scope.github_login}
205 208
          </span>
206 209
        </li>
207 210
        <li>
208 211
          <.form for={%{}} as={:logout} action={~p"/logout"} method="post" class="m-0 w-full">
209 212
            <input type="hidden" name="_method" value="delete" />
210
            <button type="submit" class="w-full text-left flex items-center gap-2">
213
            <button
214
              type="submit"
215
              class="w-full rounded-md px-2 py-1.5 text-left flex items-center gap-2 hover:bg-muted"
216
            >
211 217
              <.icon name="hero-arrow-right-start-on-rectangle" class="size-4" /> Log out
212 218
            </button>
213 219
          </.form>

@@ -366,17 +372,21 @@ defmodule OpenAgentsWeb.Layouts do

366 372
  end
367 373
368 374
  @doc """
369
  Provides dark vs light theme toggle based on themes defined in app.css.
375
  The `data-theme` segmented control.
370 376
371
  See <head> in root.html.heex which applies the theme before page load.
377
  The attribute plumbing in `root.html.heex` still runs, but since DaisyUI was
378
  removed there is only one palette behind it: Sarah's token ladder is
379
  dark-only (`color-scheme: dark` in app.css) and ships no light variant. The
380
  control therefore moves its indicator without repainting the page. Keep it or
381
  remove it deliberately — do not reintroduce a second theme to make it work.
372 382
  """
373 383
  def theme_toggle(assigns) do
374 384
    ~H"""
375 385
    <%!-- w-24 is load-bearing: the three buttons are w-1/3, so without an explicit
376 386
    width this stretches to fill any block-level parent and the segments stretch
377 387
    with it. 24 (6rem) is the natural content width: 3 buttons x (p-2 + size-4). --%>
378
    <div class="card relative flex flex-row items-center w-24 shrink-0 border-2 border-base-300 bg-base-300 rounded-full">
379
      <div class="absolute w-1/3 h-full rounded-full border-1 border-base-200 bg-base-100 brightness-200 left-0 [[data-theme=light]_&]:left-1/3 [[data-theme=dark]_&]:left-2/3 [[data-theme-source=system]_&]:!left-0 transition-[left]" />
388
    <div class="relative flex flex-row items-center w-24 shrink-0 border-2 border-muted bg-muted rounded-full">
389
      <div class="absolute w-1/3 h-full rounded-full border-1 border-card bg-background brightness-200 left-0 [[data-theme=light]_&]:left-1/3 [[data-theme=dark]_&]:left-2/3 [[data-theme-source=system]_&]:!left-0 transition-[left]" />
380 390
381 391
      <button
382 392
        class="flex p-2 cursor-pointer w-1/3"
lib/openagents_web/components/repo_header.ex modified +53 -37

@@ -12,52 +12,68 @@ defmodule OpenAgentsWeb.Components.RepoHeader do

12 12
13 13
  def repo_header(assigns) do
14 14
    ~H"""
15
    <div class="border-b border-base-300 pb-4 mb-6">
15
    <div class="border-b border-border pb-4 mb-6">
16 16
      <div class="flex flex-wrap items-center gap-3 mb-3">
17
        <div class="avatar avatar-placeholder">
18
          <div class="bg-primary text-primary-content w-10 h-10 rounded-full flex items-center justify-center font-semibold">
19
            {@owner |> String.first() |> String.upcase()}
20
          </div>
21
        </div>
22
        <div class="text-sm breadcrumbs">
23
          <ul>
17
        <span class="avatar size-10 bg-primary text-primary-foreground font-semibold">
18
          <span>{@owner |> String.first() |> String.upcase()}</span>
19
        </span>
20
        <nav class="text-sm" aria-label="Breadcrumb">
21
          <ol class="flex items-center gap-1">
24 22
            <li>
25
              <.link navigate={~p"/"} class="link link-primary">
26
                {@owner}
27
              </.link>
23
              <.link navigate={~p"/"} class="btn px-0" data-variant="link">{@owner}</.link>
28 24
            </li>
29
            <li class="font-semibold">{@repo}</li>
30
          </ul>
31
        </div>
32
        <span class="badge badge-sm badge-ghost">Public</span>
25
            <li aria-hidden="true" class="text-muted-foreground">/</li>
26
            <li class="font-semibold" aria-current="page">{@repo}</li>
27
          </ol>
28
        </nav>
29
        <span class="badge" data-variant="dim">Public</span>
33 30
      </div>
34
      <div class="tabs tabs-bordered">
35
        <.link
31
      <%!-- Page navigation, not a tab widget: these are links that change the
32
      URL, so they carry `aria-current` rather than tab roles, and the selected
33
      state is the accent underline. --%>
34
      <nav class="flex gap-1 -mb-4 overflow-x-auto" aria-label={"#{@repo} sections"}>
35
        <.repo_tab
36 36
          patch={~p"/#{@owner}/#{@repo}/issues"}
37
          class={["tab", @active == "issues" && "tab-active"]}
38
        >
39
          Issues {if is_integer(@open_count), do: "(#{@open_count})"}
40
        </.link>
41
        <.link
37
          selected={@active == "issues"}
38
          label={"Issues #{if is_integer(@open_count), do: "(#{@open_count})", else: ""}"}
39
        />
40
        <.repo_tab
42 41
          patch={~p"/#{@owner}/#{@repo}/labels"}
43
          class={["tab", @active == "labels" && "tab-active"]}
44
        >
45
          Labels
46
        </.link>
47
        <.link
42
          selected={@active == "labels"}
43
          label="Labels"
44
        />
45
        <.repo_tab
48 46
          patch={~p"/#{@owner}/#{@repo}/milestones"}
49
          class={["tab", @active == "milestones" && "tab-active"]}
50
        >
51
          Milestones
52
        </.link>
53
        <.link
47
          selected={@active == "milestones"}
48
          label="Milestones"
49
        />
50
        <.repo_tab
54 51
          patch={~p"/#{@owner}/#{@repo}/projects"}
55
          class={["tab", @active == "projects" && "tab-active"]}
56
        >
57
          Projects
58
        </.link>
59
      </div>
52
          selected={@active == "projects"}
53
          label="Projects"
54
        />
55
      </nav>
60 56
    </div>
61 57
    """
62 58
  end
59
60
  attr :patch, :string, required: true
61
  attr :selected, :boolean, required: true
62
  attr :label, :string, required: true
63
64
  defp repo_tab(assigns) do
65
    ~H"""
66
    <.link
67
      patch={@patch}
68
      aria-current={@selected && "page"}
69
      class={[
70
        "whitespace-nowrap border-b-2 px-3 pb-3 text-sm transition-colors",
71
        @selected && "border-primary text-foreground font-semibold",
72
        !@selected && "border-transparent text-muted-foreground hover:text-foreground"
73
      ]}
74
    >
75
      {@label}
76
    </.link>
77
    """
78
  end
63 79
end
lib/openagents_web/live/assignee_index_live.ex modified +5 -7

@@ -27,19 +27,17 @@ defmodule OpenAgentsWeb.AssigneeIndexLive do

27 27
      <h1 class="text-2xl font-bold mb-4">Assignees</h1>
28 28
29 29
      <%= if @assignees == [] do %>
30
        <div class="alert alert-info">
30
        <div class="alert" data-variant="info" role="status">
31 31
          <.icon name="hero-information-circle" class="size-5" />
32
          <span>No assignees have been assigned to issues yet.</span>
32
          <section>No assignees have been assigned to issues yet.</section>
33 33
        </div>
34 34
      <% else %>
35 35
        <.table id="assignees" rows={@assignees}>
36 36
          <:col :let={{login, _count}} label="Assignee">
37 37
            <div class="flex items-center gap-2">
38
              <div class="avatar avatar-placeholder">
39
                <div class="bg-neutral text-neutral-content w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold">
40
                  {login |> String.first() |> String.upcase()}
41
                </div>
42
              </div>
38
              <span class="avatar size-8 font-semibold">
39
                <span>{login |> String.first() |> String.upcase()}</span>
40
              </span>
43 41
              <span class="font-semibold">{login}</span>
44 42
            </div>
45 43
          </:col>
lib/openagents_web/live/docs_live.ex modified +9 -5

@@ -15,7 +15,7 @@ defmodule OpenAgentsWeb.DocsLive do

15 15
    ~H"""
16 16
    <div class="max-w-3xl mx-auto">
17 17
      <h1 class="text-3xl font-semibold mb-4">Documentation</h1>
18
      <p class="text-base-content/70 mb-8">
18
      <p class="text-muted-foreground mb-8">
19 19
        The OpenAgents docs are a work in progress. This placeholder shows the structure we will build out next.
20 20
      </p>
21 21

@@ -31,10 +31,14 @@ defmodule OpenAgentsWeb.DocsLive do

31 31
32 32
  defp card(assigns) do
33 33
    ~H"""
34
    <div class="card bg-base-200 border border-base-300 p-4">
35
      <h2 class="text-lg font-medium mb-1">{@title}</h2>
36
      <p class="text-sm text-base-content/70">{@description}</p>
37
    </div>
34
    <%!-- `.card` brings its own padding and lift; the surrounding grid supplies
35
    the gaps, so the panel margin is dropped here. --%>
36
    <article class="card !m-0">
37
      <header>
38
        <h2 class="card-title">{@title}</h2>
39
        <p>{@description}</p>
40
      </header>
41
    </article>
38 42
    """
39 43
  end
40 44
end
lib/openagents_web/live/home_live.ex modified +3 -42

@@ -11,13 +11,13 @@ defmodule OpenAgentsWeb.HomeLive do

11 11
  def render(assigns) do
12 12
    ~H"""
13 13
    <Layouts.app flash={@flash} current_scope={@current_scope} wide>
14
      <section class="hero min-h-[60vh]">
15
        <div class="hero-content text-center">
14
      <section class="flex min-h-[60vh] items-center justify-center px-4">
15
        <div class="text-center">
16 16
          <div class="max-w-4xl space-y-6">
17 17
            <h1 class="text-5xl md:text-7xl font-bold tracking-tight">
18 18
              The Agent Forge
19 19
            </h1>
20
            <p class="text-xl md:text-2xl text-base-content/70">
20
            <p class="text-xl md:text-2xl text-muted-foreground">
21 21
              Purpose-built for planning and shipping issues. Designed for the agent era.
22 22
            </p>
23 23
            <div class="flex flex-wrap justify-center gap-4">

@@ -43,45 +43,6 @@ defmodule OpenAgentsWeb.HomeLive do

43 43
          </div>
44 44
        </div>
45 45
      </section>
46
47
      <%!--
48
      <div class="flex justify-center -mt-20 mb-20 px-4">
49
        <div class="mockup-browser border bg-base-200 w-full max-w-4xl">
50
          <div class="mockup-browser-toolbar">
51
            <div class="input">openagents.com/OpenAgents/openagents/issues</div>
52
          </div>
53
          <div class="p-6">
54
            <div class="card bg-base-100 border border-base-300 mt-4">
55
              <div class="card-body">
56
                <div class="flex items-start gap-4">
57
                  <.icon name="hero-exclamation-circle" class="size-6 text-success shrink-0" />
58
                  <div class="flex-1">
59
                    <h2 class="text-2xl font-bold">
60
                      Issue preview
61
                      <span class="text-base-content/50 text-lg font-normal">#1</span>
62
                    </h2>
63
                    <p class="text-sm text-base-content/70 mt-1">
64
                      Opened on Aug 19, 2026 by OpenAgents
65
                    </p>
66
                    <div class="flex flex-wrap gap-1 mt-3">
67
                      <span class="badge badge-sm">preview</span>
68
                    </div>
69
                  </div>
70
                </div>
71
                <p class="whitespace-pre-wrap mt-4 text-base-content/90">
72
                  This is a placeholder for the live issue preview.
73
                </p>
74
                <div class="card-actions justify-end mt-4">
75
                  <.button navigate={~p"/OpenAgents/openagents/issues/1"} variant="primary">
76
                    Open issue
77
                  </.button>
78
                </div>
79
              </div>
80
            </div>
81
          </div>
82
        </div>
83
      </div>
84
      --%>
85 46
    </Layouts.app>
86 47
    """
87 48
  end
lib/openagents_web/live/icon_index_live.ex modified +2 -2

@@ -23,9 +23,9 @@ defmodule OpenAgentsWeb.IconIndexLive do

23 23
24 24
      <div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-4">
25 25
        <%= for name <- @icons do %>
26
          <div class="flex flex-col items-center gap-2 p-3 border border-base-300 rounded-lg">
26
          <div class="flex flex-col items-center gap-2 p-3 border border-border rounded-lg">
27 27
            <.icon name={name} class="size-8" />
28
            <span class="text-xs text-center break-all text-base-content/70">{name}</span>
28
            <span class="text-xs text-center break-all text-muted-foreground">{name}</span>
29 29
          </div>
30 30
        <% end %>
31 31
      </div>
lib/openagents_web/live/issue_index_live.ex modified +41 -32

@@ -32,34 +32,37 @@ defmodule OpenAgentsWeb.IssueIndexLive do

32 32
  def render(assigns) do
33 33
    ~H"""
34 34
    <Layouts.app flash={@flash} current_scope={@current_scope}>
35
      <div class="bg-base-100 border border-base-300 rounded-lg overflow-hidden">
36
        <div class="flex flex-wrap items-center gap-3 p-4 border-b border-base-300">
35
      <div class="bg-background border border-border rounded-lg overflow-hidden">
36
        <div class="flex flex-wrap items-center gap-3 p-4 border-b border-border">
37 37
          <div class="flex items-center gap-1">
38
            <%!-- These two are the current filter, so the selected one carries
39
            `aria-current` and the accent tone rather than a pressed-button
40
            class; the style pack has no active state for a link. --%>
38 41
            <.link
39 42
              patch={~p"/#{@owner}/#{@repo}/issues?state=open"}
40
              class={[
41
                "btn btn-sm btn-ghost gap-1",
42
                @state == "open" && "btn-active"
43
              ]}
43
              class={["btn gap-1", @state == "open" && "text-foreground font-semibold"]}
44
              data-variant="ghost"
45
              data-size="sm"
46
              aria-current={@state == "open" && "page"}
44 47
            >
45 48
              <.icon name="hero-exclamation-circle" class="size-4 text-success" />
46 49
              <span class="font-semibold">{@open_count}</span> Open
47 50
            </.link>
48 51
            <.link
49 52
              patch={~p"/#{@owner}/#{@repo}/issues?state=closed"}
50
              class={[
51
                "btn btn-sm btn-ghost gap-1",
52
                @state == "closed" && "btn-active"
53
              ]}
53
              class={["btn gap-1", @state == "closed" && "text-foreground font-semibold"]}
54
              data-variant="ghost"
55
              data-size="sm"
56
              aria-current={@state == "closed" && "page"}
54 57
            >
55
              <.icon name="hero-check-circle" class="size-4 text-base-content/50" />
58
              <.icon name="hero-check-circle" class="size-4 text-muted-foreground" />
56 59
              <span class="font-semibold">{@closed_count}</span> Closed
57 60
            </.link>
58 61
          </div>
59 62
60 63
          <div class="flex-1 min-w-[12rem]">
61
            <label class="input input-sm input-ghost w-full max-w-xs flex items-center gap-2">
62
              <.icon name="hero-magnifying-glass" class="size-4 text-base-content/50" />
64
            <label class="input w-full max-w-xs flex items-center gap-2">
65
              <.icon name="hero-magnifying-glass" class="size-4 text-muted-foreground" />
63 66
              <input
64 67
                type="text"
65 68
                placeholder="Search or filter results..."

@@ -69,21 +72,26 @@ defmodule OpenAgentsWeb.IssueIndexLive do

69 72
          </div>
70 73
71 74
          <div class="flex items-center gap-1">
72
            <button class="btn btn-sm btn-ghost gap-1">
75
            <button class="btn gap-1" data-variant="ghost" data-size="sm">
73 76
              <.icon name="hero-tag" class="size-4" /> Labels
74 77
            </button>
75
            <button class="btn btn-sm btn-ghost gap-1">
78
            <button class="btn gap-1" data-variant="ghost" data-size="sm">
76 79
              <.icon name="hero-flag" class="size-4" /> Milestones
77 80
            </button>
78
            <button class="btn btn-sm btn-ghost gap-1">
81
            <button class="btn gap-1" data-variant="ghost" data-size="sm">
79 82
              <.icon name="hero-user" class="size-4" /> Assignees
80 83
            </button>
81
            <button class="btn btn-sm btn-ghost gap-1">
84
            <button class="btn gap-1" data-variant="ghost" data-size="sm">
82 85
              <.icon name="hero-bars-arrow-down" class="size-4" /> Sort
83 86
            </button>
84 87
          </div>
85 88
86
          <.link navigate={~p"/#{@owner}/#{@repo}/issues/new"} class="btn btn-primary btn-sm">
89
          <.link
90
            navigate={~p"/#{@owner}/#{@repo}/issues/new"}
91
            class="btn"
92
            data-variant="primary"
93
            data-size="sm"
94
          >
87 95
            New issue
88 96
          </.link>
89 97
        </div>

@@ -92,21 +100,21 @@ defmodule OpenAgentsWeb.IssueIndexLive do

92 100
          <div class="p-8 text-center">
93 101
            <.icon
94 102
              name="hero-clipboard-document-list"
95
              class="size-12 mx-auto mb-3 text-base-content/30"
103
              class="size-12 mx-auto mb-3 text-muted-foreground/50"
96 104
            />
97 105
            <h3 class="text-lg font-medium mb-1">
98 106
              No {if(@state == "open", do: "open", else: "closed")} issues
99 107
            </h3>
100
            <p class="text-sm text-base-content/70">
108
            <p class="text-sm text-muted-foreground">
101 109
              Issues will show up here once they are created.
102 110
            </p>
103 111
          </div>
104 112
        <% else %>
105
          <div id="issues" phx-update="stream" class="divide-y divide-base-300">
113
          <div id="issues" phx-update="stream" class="divide-y divide-border">
106 114
            <div
107 115
              :for={{id, issue} <- @streams.issues}
108 116
              id={id}
109
              class="p-4 hover:bg-base-200"
117
              class="p-4 hover:bg-card"
110 118
            >
111 119
              <div class="flex items-start gap-4">
112 120
                <.icon

@@ -119,22 +127,23 @@ defmodule OpenAgentsWeb.IssueIndexLive do

119 127
                  class={[
120 128
                    "size-5 shrink-0",
121 129
                    issue.state == "open" && "text-success",
122
                    issue.state == "closed" && "text-base-content/50"
130
                    issue.state == "closed" && "text-muted-foreground"
123 131
                  ]}
124 132
                />
125 133
                <div class="flex-1 min-w-0">
126 134
                  <h3 class="font-semibold text-base">
127 135
                    <.link
128 136
                      navigate={~p"/#{@owner}/#{@repo}/issues/#{issue.number}"}
129
                      class="link link-primary"
137
                      class="btn px-0 text-base font-semibold"
138
                      data-variant="link"
130 139
                    >
131 140
                      {issue.title}
132 141
                    </.link>
133
                    <span class="text-base-content/50 font-normal text-sm ml-1">
142
                    <span class="text-muted-foreground font-normal text-sm ml-1">
134 143
                      #{issue.number}
135 144
                    </span>
136 145
                  </h3>
137
                  <p class="text-sm text-base-content/70 mt-1">
146
                  <p class="text-sm text-muted-foreground mt-1">
138 147
                    Opened on {Calendar.strftime(issue.inserted_at, "%b %d, %Y")} by {(issue.user &&
139 148
                                                                                         issue.user[
140 149
                                                                                           "login"

@@ -150,7 +159,7 @@ defmodule OpenAgentsWeb.IssueIndexLive do

150 159
                  <div class="flex flex-wrap gap-1 mt-2">
151 160
                    <%= for label <- issue.labels || [] do %>
152 161
                      <span
153
                        class="badge badge-sm"
162
                        class="badge rounded-full px-2 py-0.5"
154 163
                        style={"background-color: ##{label["color"]}; color: #000;"}
155 164
                      >
156 165
                        {label["name"]}

@@ -158,13 +167,13 @@ defmodule OpenAgentsWeb.IssueIndexLive do

158 167
                    <% end %>
159 168
                  </div>
160 169
                </div>
161
                <div class="flex -space-x-2 shrink-0">
170
                <div class="avatar-group shrink-0">
162 171
                  <%= for assignee <- issue.assignees || [] do %>
163
                    <div class="avatar avatar-placeholder" title={assignee["login"]}>
164
                      <div class="bg-neutral text-neutral-content w-6 h-6 rounded-full flex items-center justify-center text-xs">
172
                    <span class="avatar size-6" title={assignee["login"]}>
173
                      <span class="!text-xs">
165 174
                        {assignee["login"] |> String.first() |> String.upcase()}
166
                      </div>
167
                    </div>
175
                      </span>
176
                    </span>
168 177
                  <% end %>
169 178
                </div>
170 179
              </div>
lib/openagents_web/live/issue_new_live.ex modified +4 -4

@@ -79,7 +79,7 @@ defmodule OpenAgentsWeb.IssueNewLive do

79 79
        for={@form}
80 80
        id="new-issue-form"
81 81
        phx-submit="save"
82
        class="card bg-base-100 border border-base-300 p-4 space-y-4"
82
        class="card !m-0 space-y-4"
83 83
      >
84 84
        <.input field={@form[:title]} label="Title" required />
85 85
        <.input field={@form[:body]} type="textarea" label="Body" />

@@ -97,12 +97,12 @@ defmodule OpenAgentsWeb.IssueNewLive do

97 97
          options={@label_options}
98 98
          multiple
99 99
        />
100
        <div class="card-actions justify-end gap-2">
101
          <.link navigate={~p"/#{@owner}/#{@repo}/issues"} class="btn btn-ghost">
100
        <footer class="flex justify-end gap-2">
101
          <.link navigate={~p"/#{@owner}/#{@repo}/issues"} class="btn" data-variant="ghost">
102 102
            Cancel
103 103
          </.link>
104 104
          <.button variant="primary">Create issue</.button>
105
        </div>
105
        </footer>
106 106
      </.form>
107 107
    </Layouts.app>
108 108
    """
lib/openagents_web/live/issue_show_live.ex modified +44 -39

@@ -107,35 +107,31 @@ defmodule OpenAgentsWeb.IssueShowLive do

107 107
          for={@form}
108 108
          id="issue-edit-form"
109 109
          phx-submit="save"
110
          class="card bg-base-100 p-4 border border-base-300 mb-6"
110
          class="card !mx-0 !mt-0 mb-6"
111 111
        >
112 112
          <.input field={@form[:title]} label="Title" />
113 113
          <.input field={@form[:body]} type="textarea" label="Body" />
114
          <div class="card-actions justify-end mt-2 gap-2">
115
            <button type="button" class="btn btn-ghost" phx-click="toggle_edit">
114
          <footer class="flex justify-end mt-2 gap-2">
115
            <button type="button" class="btn" data-variant="ghost" phx-click="toggle_edit">
116 116
              Cancel
117 117
            </button>
118 118
            <.button variant="primary">Save</.button>
119
          </div>
119
          </footer>
120 120
        </.form>
121 121
      <% else %>
122 122
        <div class="flex flex-col md:flex-row md:items-start md:justify-between gap-4 mb-6">
123 123
          <div class="flex-1">
124 124
            <h1 class="text-3xl font-bold mb-2">
125 125
              {@issue.title}
126
              <span class="text-base-content/50 text-xl font-normal">
126
              <span class="text-muted-foreground text-xl font-normal">
127 127
                #{@issue.number}
128 128
              </span>
129 129
            </h1>
130 130
            <div class="flex items-center gap-2 mb-3">
131
              <span class={[
132
                "badge",
133
                @issue.state == "open" && "badge-success",
134
                @issue.state == "closed" && "badge-ghost"
135
              ]}>
131
              <span class="badge" data-variant={if @issue.state == "open", do: "success", else: "dim"}>
136 132
                {@issue.state}
137 133
              </span>
138
              <p class="text-sm text-base-content/70">
134
              <p class="text-sm text-muted-foreground">
139 135
                Opened on {Calendar.strftime(@issue.inserted_at, "%b %d, %Y")} by {(@issue.user &&
140 136
                                                                                      @issue.user[
141 137
                                                                                        "login"

@@ -145,13 +141,25 @@ defmodule OpenAgentsWeb.IssueShowLive do

145 141
            </div>
146 142
          </div>
147 143
          <div class="flex gap-2 shrink-0">
148
            <button :if={@issue.state == "open"} class="btn btn-sm btn-success" phx-click="close">
144
            <button
145
              :if={@issue.state == "open"}
146
              class="btn"
147
              data-variant="primary"
148
              data-size="sm"
149
              phx-click="close"
150
            >
149 151
              Close issue
150 152
            </button>
151
            <button :if={@issue.state == "closed"} class="btn btn-sm btn-ghost" phx-click="reopen">
153
            <button
154
              :if={@issue.state == "closed"}
155
              class="btn"
156
              data-variant="ghost"
157
              data-size="sm"
158
              phx-click="reopen"
159
            >
152 160
              Reopen issue
153 161
            </button>
154
            <button class="btn btn-sm btn-ghost" phx-click="toggle_edit">
162
            <button class="btn" data-variant="ghost" data-size="sm" phx-click="toggle_edit">
155 163
              Edit
156 164
            </button>
157 165
          </div>

@@ -160,8 +168,8 @@ defmodule OpenAgentsWeb.IssueShowLive do

160 168
161 169
      <div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
162 170
        <div class="lg:col-span-3 space-y-6">
163
          <div class="card bg-base-100 border border-base-300 p-4">
164
            <p class="whitespace-pre-wrap text-base-content/90">
171
          <div class="card !m-0">
172
            <p class="whitespace-pre-wrap text-foreground">
165 173
              {@issue.body || "No description provided."}
166 174
            </p>
167 175
          </div>

@@ -170,22 +178,22 @@ defmodule OpenAgentsWeb.IssueShowLive do

170 178
171 179
          <div class="space-y-4">
172 180
            <%= for comment <- @comments do %>
173
              <div class="card bg-base-100 border border-base-300 p-4">
181
              <div class="card !m-0">
174 182
                <div class="flex items-center gap-2 mb-2">
175
                  <div class="avatar avatar-placeholder">
176
                    <div class="bg-neutral text-neutral-content w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold">
183
                  <span class="avatar size-8 font-semibold">
184
                    <span>
177 185
                      {(comment.user && comment.user["login"] |> String.first() |> String.upcase()) ||
178 186
                        "?"}
179
                    </div>
180
                  </div>
187
                    </span>
188
                  </span>
181 189
                  <span class="font-semibold text-sm">
182 190
                    {(comment.user && comment.user["login"]) || "anonymous"}
183 191
                  </span>
184
                  <span class="text-sm text-base-content/50">
192
                  <span class="text-sm text-muted-foreground">
185 193
                    {Calendar.strftime(comment.created_at, "%b %d, %Y %H:%M")}
186 194
                  </span>
187 195
                </div>
188
                <p class="whitespace-pre-wrap text-base-content/90">
196
                <p class="whitespace-pre-wrap text-foreground">
189 197
                  {comment.body}
190 198
                </p>
191 199
              </div>

@@ -196,23 +204,23 @@ defmodule OpenAgentsWeb.IssueShowLive do

196 204
            for={@comment_form}
197 205
            id="comment-form"
198 206
            phx-submit="add_comment"
199
            class="card bg-base-100 border border-base-300 p-4"
207
            class="card !m-0"
200 208
          >
201 209
            <.input field={@comment_form[:body]} type="textarea" label="Write a comment" />
202
            <div class="card-actions justify-end mt-2">
210
            <footer class="flex justify-end mt-2">
203 211
              <.button variant="primary">Comment</.button>
204
            </div>
212
            </footer>
205 213
          </.form>
206 214
        </div>
207 215
208 216
        <div class="space-y-4">
209 217
          <%= if @issue.labels != [] do %>
210 218
            <div>
211
              <h3 class="font-semibold text-sm text-base-content/70 mb-2">Labels</h3>
219
              <h3 class="font-semibold text-sm text-muted-foreground mb-2">Labels</h3>
212 220
              <div class="flex flex-wrap gap-1">
213 221
                <%= for label <- @issue.labels do %>
214 222
                  <span
215
                    class="badge badge-sm"
223
                    class="badge rounded-full px-2 py-0.5"
216 224
                    style={"background-color: ##{label["color"]}; color: #000;"}
217 225
                  >
218 226
                    {label["name"]}

@@ -224,14 +232,14 @@ defmodule OpenAgentsWeb.IssueShowLive do

224 232
225 233
          <%= if @issue.assignees != [] do %>
226 234
            <div>
227
              <h3 class="font-semibold text-sm text-base-content/70 mb-2">Assignees</h3>
228
              <div class="flex -space-x-2">
235
              <h3 class="font-semibold text-sm text-muted-foreground mb-2">Assignees</h3>
236
              <div class="avatar-group">
229 237
                <%= for assignee <- @issue.assignees do %>
230
                  <div class="avatar avatar-placeholder" title={assignee["login"]}>
231
                    <div class="bg-neutral text-neutral-content w-7 h-7 rounded-full flex items-center justify-center text-xs font-semibold">
238
                  <span class="avatar size-7 font-semibold" title={assignee["login"]}>
239
                    <span class="!text-xs">
232 240
                      {assignee["login"] |> String.first() |> String.upcase()}
233
                    </div>
234
                  </div>
241
                    </span>
242
                  </span>
235 243
                <% end %>
236 244
              </div>
237 245
            </div>

@@ -239,11 +247,8 @@ defmodule OpenAgentsWeb.IssueShowLive do

239 247
240 248
          <%= if @issue.milestone do %>
241 249
            <div>
242
              <h3 class="font-semibold text-sm text-base-content/70 mb-2">Milestone</h3>
243
              <.link
244
                navigate={~p"/#{@owner}/#{@repo}/milestones"}
245
                class="badge badge-ghost badge-sm"
246
              >
250
              <h3 class="font-semibold text-sm text-muted-foreground mb-2">Milestone</h3>
251
              <.link navigate={~p"/#{@owner}/#{@repo}/milestones"} class="badge" data-variant="dim">
247 252
                {@issue.milestone["title"]}
248 253
              </.link>
249 254
            </div>
lib/openagents_web/live/label_index_live.ex modified +13 -7

@@ -52,34 +52,40 @@ defmodule OpenAgentsWeb.LabelIndexLive do

52 52
        for={@form}
53 53
        id="new-label-form"
54 54
        phx-submit="save"
55
        class="card bg-base-100 border border-base-300 p-4 mb-6"
55
        class="card !mx-0 !mt-0 mb-6"
56 56
      >
57 57
        <div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
58 58
          <.input field={@form[:name]} label="Name" required />
59 59
          <.input field={@form[:color]} label="Color" required />
60 60
          <.input field={@form[:description]} label="Description" />
61 61
        </div>
62
        <div class="card-actions justify-end mt-2">
62
        <footer class="flex justify-end mt-2">
63 63
          <.button variant="primary">Add label</.button>
64
        </div>
64
        </footer>
65 65
      </.form>
66 66
67 67
      <%= if @labels == [] do %>
68
        <div class="alert alert-info">
68
        <div class="alert" data-variant="info" role="status">
69 69
          <.icon name="hero-information-circle" class="size-5" />
70
          <span>No labels yet.</span>
70
          <section>No labels yet.</section>
71 71
        </div>
72 72
      <% else %>
73 73
        <.table id="labels" rows={@labels}>
74 74
          <:col :let={label} label="Label">
75
            <span class="badge badge-md" style={"background-color: ##{label.color}; color: #000;"}>
75
            <span
76
              class="badge rounded-full px-2 py-0.5"
77
              style={"background-color: ##{label.color}; color: #000;"}
78
            >
76 79
              {label.name}
77 80
            </span>
78 81
          </:col>
79 82
          <:col :let={label} label="Description">{label.description || "—"}</:col>
80 83
          <:col :let={label} label="">
81 84
            <button
82
              class="btn btn-ghost btn-sm text-error"
85
              class="btn"
86
              data-variant="ghost"
87
              data-size="sm"
88
              data-tone="danger"
83 89
              phx-click="delete"
84 90
              phx-value-id={label.id}
85 91
              data-confirm="Delete this label?"
lib/openagents_web/live/milestone_index_live.ex modified +35 -24

@@ -90,72 +90,80 @@ defmodule OpenAgentsWeb.MilestoneIndexLive do

90 90
        for={@form}
91 91
        id="new-milestone-form"
92 92
        phx-submit="save"
93
        class="card bg-base-100 border border-base-300 p-4 mb-6"
93
        class="card !mx-0 !mt-0 mb-6"
94 94
      >
95 95
        <div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
96 96
          <.input field={@form[:title]} label="Title" required />
97 97
          <.input field={@form[:due_on]} label="Due on" />
98 98
          <.input field={@form[:description]} label="Description" />
99 99
        </div>
100
        <div class="card-actions justify-end mt-2">
100
        <footer class="flex justify-end mt-2">
101 101
          <.button variant="primary">Add milestone</.button>
102
        </div>
102
        </footer>
103 103
      </.form>
104 104
105 105
      <%= if @milestones == [] do %>
106
        <div class="alert alert-info">
106
        <div class="alert" data-variant="info" role="status">
107 107
          <.icon name="hero-information-circle" class="size-5" />
108
          <span>No milestones yet.</span>
108
          <section>No milestones yet.</section>
109 109
        </div>
110 110
      <% else %>
111 111
        <div class="space-y-4">
112 112
          <%= for milestone <- @milestones do %>
113
            <div class="card bg-base-100 border border-base-300 p-4">
113
            <div class="card !m-0">
114 114
              <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 mb-2">
115 115
                <h3 class="text-lg font-semibold">{milestone.title}</h3>
116 116
                <div class="flex gap-2">
117
                  <span class={[
118
                    "badge badge-sm",
119
                    milestone.state == "open" && "badge-success",
120
                    milestone.state == "closed" && "badge-ghost"
121
                  ]}>
117
                  <span
118
                    class="badge"
119
                    data-variant={if milestone.state == "open", do: "success", else: "dim"}
120
                  >
122 121
                    {milestone.state}
123 122
                  </span>
124 123
                  <%= if milestone.due_on do %>
125
                    <span class="badge badge-ghost badge-sm">Due {milestone.due_on}</span>
124
                    <span class="badge" data-variant="dim">Due {milestone.due_on}</span>
126 125
                  <% end %>
127 126
                </div>
128 127
              </div>
129 128
130 129
              <%= if milestone.description do %>
131
                <p class="text-sm text-base-content/70 mb-3">
130
                <p class="text-sm text-muted-foreground mb-3">
132 131
                  {milestone.description}
133 132
                </p>
134 133
              <% end %>
135 134
136 135
              <div class="flex items-center gap-4 mb-2">
137
                <div class="flex-1">
138
                  <progress
139
                    class="progress progress-success w-full"
140
                    value={milestone.progress}
141
                    max="100"
142
                  ></progress>
136
                <%!-- A meter, not task progress: the value is a measured
137
                ratio of closed to total, so `<progress>`'s indeterminate state
138
                never applies. Drawn from the surface rungs rather than a
139
                component variant, since the style pack has no bar. --%>
140
                <div
141
                  class="flex-1 h-2 rounded-full bg-muted overflow-hidden"
142
                  role="progressbar"
143
                  aria-valuenow={milestone.progress}
144
                  aria-valuemin="0"
145
                  aria-valuemax="100"
146
                  aria-label={"#{milestone.title} progress"}
147
                >
148
                  <div class="h-full bg-success" style={"width: #{milestone.progress}%;"}></div>
143 149
                </div>
144 150
                <span class="text-sm font-semibold w-12 text-right">
145 151
                  {milestone.progress}%
146 152
                </span>
147 153
              </div>
148 154
149
              <div class="flex items-center gap-4 text-sm text-base-content/70 mb-3">
155
              <div class="flex items-center gap-4 text-sm text-muted-foreground mb-3">
150 156
                <span>{milestone.open_count} open</span>
151 157
                <span>{milestone.closed_count} closed</span>
152 158
                <span>{milestone.total_count} total</span>
153 159
              </div>
154 160
155
              <div class="card-actions justify-end">
161
              <footer class="flex justify-end gap-2">
156 162
                <%= if milestone.state == "open" do %>
157 163
                  <button
158
                    class="btn btn-ghost btn-sm"
164
                    class="btn"
165
                    data-variant="ghost"
166
                    data-size="sm"
159 167
                    phx-click="close"
160 168
                    phx-value-id={milestone.id}
161 169
                  >

@@ -163,14 +171,17 @@ defmodule OpenAgentsWeb.MilestoneIndexLive do

163 171
                  </button>
164 172
                <% end %>
165 173
                <button
166
                  class="btn btn-ghost btn-sm text-error"
174
                  class="btn"
175
                  data-variant="ghost"
176
                  data-size="sm"
177
                  data-tone="danger"
167 178
                  phx-click="delete"
168 179
                  phx-value-id={milestone.id}
169 180
                  data-confirm="Delete this milestone?"
170 181
                >
171 182
                  Delete
172 183
                </button>
173
              </div>
184
              </footer>
174 185
            </div>
175 186
          <% end %>
176 187
        </div>
lib/openagents_web/live/project_index_live.ex modified +33 -29

@@ -60,53 +60,57 @@ defmodule OpenAgentsWeb.ProjectIndexLive do

60 60
        for={@form}
61 61
        id="new-project-form"
62 62
        phx-submit="save"
63
        class="card bg-base-100 border border-base-300 p-4 mb-6"
63
        class="card !mx-0 !mt-0 mb-6"
64 64
      >
65 65
        <.input field={@form[:title]} label="Title" required />
66
        <div class="card-actions justify-end mt-2">
66
        <footer class="flex justify-end mt-2">
67 67
          <.button variant="primary">Add project</.button>
68
        </div>
68
        </footer>
69 69
      </.form>
70 70
71 71
      <%= if @projects == [] do %>
72
        <div class="alert alert-info">
72
        <div class="alert" data-variant="info" role="status">
73 73
          <.icon name="hero-information-circle" class="size-5" />
74
          <span>No projects yet.</span>
74
          <section>No projects yet.</section>
75 75
        </div>
76 76
      <% else %>
77 77
        <div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
78 78
          <%= for project <- @projects do %>
79
            <div class="card bg-base-100 border border-base-300">
80
              <div class="card-body">
79
            <article class="card !m-0">
80
              <header>
81 81
                <h3 class="card-title">
82 82
                  <.link
83 83
                    navigate={~p"/#{@owner}/#{@repo}/projects/#{project.number}"}
84
                    class="link link-primary"
84
                    class="btn px-0"
85
                    data-variant="link"
85 86
                  >
86 87
                    {project.title}
87 88
                  </.link>
88 89
                </h3>
89
                <p class="text-sm text-base-content/70">
90
                  {project.state}
91
                </p>
92
                <div class="card-actions justify-end">
93
                  <.link
94
                    navigate={~p"/#{@owner}/#{@repo}/projects/#{project.number}"}
95
                    class="btn btn-sm btn-ghost"
96
                  >
97
                    View
98
                  </.link>
99
                  <button
100
                    class="btn btn-ghost btn-sm text-error"
101
                    phx-click="delete"
102
                    phx-value-id={project.id}
103
                    data-confirm="Delete this project?"
104
                  >
105
                    Delete
106
                  </button>
107
                </div>
108
              </div>
109
            </div>
90
                <p>{project.state}</p>
91
              </header>
92
              <footer class="flex justify-end gap-2 mt-4">
93
                <.link
94
                  navigate={~p"/#{@owner}/#{@repo}/projects/#{project.number}"}
95
                  class="btn"
96
                  data-variant="ghost"
97
                  data-size="sm"
98
                >
99
                  View
100
                </.link>
101
                <button
102
                  class="btn"
103
                  data-variant="ghost"
104
                  data-size="sm"
105
                  data-tone="danger"
106
                  phx-click="delete"
107
                  phx-value-id={project.id}
108
                  data-confirm="Delete this project?"
109
                >
110
                  Delete
111
                </button>
112
              </footer>
113
            </article>
110 114
          <% end %>
111 115
        </div>
112 116
      <% end %>
lib/openagents_web/live/project_show_live.ex modified +38 -32

@@ -67,7 +67,12 @@ defmodule OpenAgentsWeb.ProjectShowLive do

67 67
    <Layouts.app flash={@flash} current_scope={@current_scope}>
68 68
      <div class="flex items-center justify-between mb-4">
69 69
        <h1 class="text-2xl font-bold">{@project.title}</h1>
70
        <.link navigate={~p"/#{@owner}/#{@repo}/projects"} class="btn btn-ghost btn-sm">
70
        <.link
71
          navigate={~p"/#{@owner}/#{@repo}/projects"}
72
          class="btn"
73
          data-variant="ghost"
74
          data-size="sm"
75
        >
71 76
          Back to projects
72 77
        </.link>
73 78
      </div>

@@ -76,7 +81,7 @@ defmodule OpenAgentsWeb.ProjectShowLive do

76 81
        for={@form}
77 82
        id="new-project-item-form"
78 83
        phx-submit="add_item"
79
        class="card bg-base-100 border border-base-300 p-4 mb-6"
84
        class="card !mx-0 !mt-0 mb-6"
80 85
      >
81 86
        <div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
82 87
          <.input

@@ -95,43 +100,44 @@ defmodule OpenAgentsWeb.ProjectShowLive do

95 100
            required
96 101
          />
97 102
        </div>
98
        <div class="card-actions justify-end mt-2">
103
        <footer class="flex justify-end mt-2">
99 104
          <.button variant="primary">Add to board</.button>
100
        </div>
105
        </footer>
101 106
      </.form>
102 107
103 108
      <div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-start">
104 109
        <%= for status <- @statuses do %>
105
          <div class="card bg-base-200 border border-base-300">
106
            <div class="card-body p-3">
107
              <h3 class="card-title text-sm font-bold mb-2">{status}</h3>
108
              <div class="space-y-2 min-h-24">
109
                <%= for item <- @items, item.status == status do %>
110
                  <div class="card bg-base-100 border border-base-300 shadow-sm p-3">
111
                    <.link
112
                      navigate={~p"/#{@owner}/#{@repo}/issues/#{item.issue.number}"}
113
                      class="link link-primary text-sm font-semibold"
114
                    >
115
                      {item.issue.title}
116
                    </.link>
117
                    <span class="text-xs text-base-content/50 block">
118
                      #{item.issue.number}
119
                    </span>
120
                    <div class="flex flex-wrap gap-1 mt-2">
121
                      <%= for label <- item.issue.labels || [] do %>
122
                        <span
123
                          class="badge badge-sm"
124
                          style={"background-color: ##{label["color"]}; color: #000;"}
125
                        >
126
                          {label["name"]}
127
                        </span>
128
                      <% end %>
129
                    </div>
110
          <section class="card !m-0 !p-3">
111
            <header class="mb-2">
112
              <h3 class="card-title !text-sm">{status}</h3>
113
            </header>
114
            <div class="space-y-2 min-h-24">
115
              <%= for item <- @items, item.status == status do %>
116
                <article class="card !m-0 !p-3">
117
                  <.link
118
                    navigate={~p"/#{@owner}/#{@repo}/issues/#{item.issue.number}"}
119
                    class="btn px-0 text-sm font-semibold"
120
                    data-variant="link"
121
                  >
122
                    {item.issue.title}
123
                  </.link>
124
                  <span class="text-xs text-muted-foreground block">
125
                    #{item.issue.number}
126
                  </span>
127
                  <div class="flex flex-wrap gap-1 mt-2">
128
                    <%= for label <- item.issue.labels || [] do %>
129
                      <span
130
                        class="badge rounded-full px-2 py-0.5"
131
                        style={"background-color: ##{label["color"]}; color: #000;"}
132
                      >
133
                        {label["name"]}
134
                      </span>
135
                    <% end %>
130 136
                  </div>
131
                <% end %>
132
              </div>
137
                </article>
138
              <% end %>
133 139
            </div>
134
          </div>
140
          </section>
135 141
        <% end %>
136 142
      </div>
137 143
    </Layouts.app>
mix.exs modified -7

@@ -72,13 +72,6 @@ defmodule OpenAgents.MixProject do

72 72
       app: false,
73 73
       compile: false,
74 74
       depth: 1},
75
      {:daisyui,
76
       github: "saadeghi/daisyui",
77
       tag: "v5.5.20",
78
       sparse: "packages/bundle",
79
       app: false,
80
       compile: false,
81
       depth: 1},
82 75
      {:swoosh, "~> 1.16"},
83 76
      {:req, "~> 0.5"},
84 77
      {:earmark, "~> 1.4"},
mix.lock modified -1

@@ -3,7 +3,6 @@

3 3
  "bandit": {:hex, :bandit, "1.12.4", "10bbab488edf8162318d736c19c5837077b8fca2bf5d95b07b33830387124f62", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "84513318c5752a2a8017664450f889b47fae5d53d64698ddf1e4fb09a7449e8d"},
4 4
  "castle": {:hex, :castle, "0.3.1", "e5d4f20696d878052a23c13158e1d372b24d9b30a6ea6f52fa6063c21c5ad67e", [:mix], [{:forecastle, "~> 0.1.3", [hex: :forecastle, repo: "hexpm", optional: false]}], "hexpm", "3ee9ca04b069280ab4197fe753562958729c83b3aa08125255116a989e133835"},
5 5
  "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
6
  "daisyui": {:git, "https://github.com/saadeghi/daisyui.git", "22ecff57f2c391b80a75617325748cf4d13fdf47", [tag: "v5.5.20", sparse: "packages/bundle", depth: 1]},
7 6
  "db_connection": {:hex, :db_connection, "2.10.2", "ae391e803a5adff104da913c2fc1c0c14a37f8b10001dcef568796e1fb7bf95c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "510b14482330f1af6490a2fa0efd8d4f1435d1529b165647df22ac0f2df0fa93"},
8 7
  "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"},
9 8
  "delta_crdt": {:hex, :delta_crdt, "0.6.5", "c7bb8c2c7e60f59e46557ab4e0224f67ba22f04c02826e273738f3dcc4767adc", [:mix], [{:merkle_map, "~> 0.2.0", [hex: :merkle_map, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c6ae23a525d30f96494186dd11bf19ed9ae21d9fe2c1f1b217d492a7cc7294ae"},

This page updates live while a promote is in flight · changelog