Compare commits

...
13 Commits
Author SHA1 Message Date
bvandeusen da6bb815bb Buttons: one definition, aligned to the design system — plus local prior-art recall (#94)
CI & Build / integration (push) Successful in 21s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / Python tests (push) Successful in 58s
CI & Build / Build & push image (push) Successful in 20s
2026-08-02 18:50:04 -04:00
bvandeusenandClaude Opus 5 5f8b824523 refactor(ui): the remaining views migrate; 2 dead classes, 2 off-palette hovers
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 30s
CI & Build / TypeScript typecheck (push) Successful in 45s
CI & Build / integration (push) Successful in 1m50s
CI & Build / Python tests (push) Successful in 2m13s
CI & Build / Build & push image (push) Successful in 42s
Twelve more files onto the shared buttons. What the pass turned up:

DEAD, verified not merely unnamed:
- .btn-reconsolidate (TaskEditorView). A comment eleven hundred lines up in the
  same file says the feature was removed in Phase 8. The CSS outlived it.
- .btn-remove-slot (SettingsView), style rules only, no template anywhere.

OFF-PALETTE, the #2319 shape: TrashView's restore and purge hovers used
`var(--color-primary, #6366f1)` and `var(--color-danger, #ef4444)` — Tailwind
indigo and Tailwind red, from no palette in this system. The fallback is what
renders if the token is ever absent, and it renders something plausible
forever. Now the action and destructive colours, no fallback.

A REAL BREAKAGE MY OWN CHECK COULD NOT SEE, worth recording. Deleting a rule
whose selector was part of a comma-separated group left the leading selectors
behind:

    .btn-log-edit,
    <nothing>
    .log-textarea { … }

which silently swallows the next rule. Brace counting passed — there are no
braces in a dangling fragment. Found by scanning for selector lines ending in
`,` not followed by another selector; three instances across two files, one of
them interleaved with comments so the first sweep missed it. The sweep is now
part of the verification, not a one-off.

Kept bespoke, deliberately: .btn-pin/.btn-unpin (pill-shaped history badges),
.btn-add-share and .btn-new-note (gradient CTAs — brand moments, which the
house style does sanction), .btn-icon/.btn-bell (icon buttons, a different
component), .btn-add-system/.btn-add-milestone (dashed "add" affordances).
These are not drift; they are other things wearing a btn- prefix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-08-01 22:53:13 -04:00
bvandeusenandClaude Opus 5 3fc693443e refactor(ui): the settings family migrates; two colour bugs and a dead class
CI & Build / Python lint (push) Successful in 7s
CI & Build / Plugin hooks (push) Successful in 36s
CI & Build / TypeScript typecheck (push) Successful in 49s
CI & Build / integration (push) Successful in 2m16s
CI & Build / Python tests (push) Successful in 2m51s
CI & Build / Build & push image (push) Successful in 1m15s
SettingsView and UserManagementView had near-identical button vocabularies —
btn-delete, btn-cancel-delete, btn-confirm-delete, btn-toggle/-open/-close —
defined separately in each. Parallel duplication (#2278's shape), and it had
already diverged twice:

- .btn-confirm-delete used --color-danger in UserManagement and
  --color-action-destructive in Settings. Those are different colours on
  purpose: the house style keeps error (something went wrong) distinct from
  destructive (something is about to). A delete confirmation is destructive.
  UserManagement was showing the error colour for a button nothing had failed
  in yet.

- .btn-remove-slot's hover reached for --color-danger for the same reason, and
  is the same correction. It turned out to be dead anyway — style rules only,
  no template reference anywhere in the app — so it is gone.

.btn-danger-outline was defined TWICE inside SettingsView, at 0.4rem 0.9rem and
0.45rem 1rem. One file, one class, two geometries, ~1200 lines apart. That is
the clearest single argument for this whole task that I have found: the drift
does not need two files, only enough distance that nobody sees both at once.

The registration toggle keeps .btn-toggle-close, and only that. It is bound
dynamically (:class="registrationOpen ? … : …"), so a name-based scan reads it
as unused — checked before deleting. .btn-toggle-open went, because btn-primary
now says the same thing; the close state stays because it must NOT read as the
primary action it sits on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-08-01 22:48:35 -04:00
bvandeusenandClaude Opus 5 3d6931b838 refactor(ui): editor-shared buttons alias onto the variants; 3 dead classes go
CI & Build / Python lint (push) Successful in 7s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 32s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 1m51s
CI & Build / Build & push image (push) Canceled after 38s
editor-shared.css defined thirteen button rules with hand-written geometry.
Ten are now thin aliases onto the shared variants — same class names, because
these are used across six views and pointing a name somewhere is cheaper than
rewriting every call site (the .btn-small precedent).

Three were DEAD: .btn-assist-toggle, .btn-close-assist and .btn-toggle-view had
no template reference and no dynamic binding anywhere in the app. Verified
before deleting rather than assumed from the name — a class with no user is
indistinguishable from one bound dynamically until you look.

Named honestly in the file: CSS has no @extend, so each alias carries the
variant's declarations rather than inheriting them. That is duplication this
migration cannot remove. But it is duplication of a REFERENCE — var(--color-
action-primary) — not of a value, so a palette change still moves everything at
once, which is the property that actually mattered.

Also gone: eight hardcoded geometries (0.4rem 1rem, 0.85rem, and so on) that
now come from --fs-space and --fs-size tokens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-08-01 22:46:01 -04:00
bvandeusenandClaude Opus 5 c7cf07824a refactor(ui): the two workspace panels migrate onto the shared buttons
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 38s
CI & Build / Python tests (push) Canceled after 1m35s
CI & Build / integration (push) Canceled after 1m35s
CI & Build / Build & push image (push) Canceled after 0s
Eighteen bespoke button rules across the two densest components, replaced by
composition in the template. Net -279 lines.

These files are where the size taxonomy earns itself: almost every button here
is an inline affordance — a dismiss ×, a confirm tick, an add-chip — sitting
inside a card or a line of text. Forcing them to the standard 8/16px would have
broken the layouts, which is why the previous commit measured the clusters
before assuming a button is a button.

What the migration left behind is the useful signal. Each residual rule is now
one line stating only what the shared classes genuinely cannot:

  .btn-add            { font-size: 1rem; }      a '+' glyph, not a label
  .btn-search-clear   { padding: 0; flex-shrink: 0; }   sits in the field
  .btn-suggest-tags   { flex-shrink: 0; align-self: center; }
  .btn-delete-task    { margin-left: 0.25rem; }

Four residuals were deleted rather than kept, because the shared sheet already
said the same thing: a disabled opacity, two hover colours, and a danger-outline
hover fill. Keeping them would have recreated the drift in miniature.

Two accent hovers went with them. .btn-suggest-tags tinted its border and label
with the accent on hover, which is the same house-style violation corrected in
f491b6d — it survived that pass because it was a hover, not a fill.

.btn-tag-suggestion and .btn-chip-link stay bespoke, deliberately. They are
tag-shaped rather than button-shaped, and the house style does put the accent on
tags — so they are not drift, they are a different component wearing a btn-
prefix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-08-01 22:44:21 -04:00
bvandeusenandClaude Opus 5 67fdf7c55b refactor(ui): auth views migrate onto the shared buttons; two variants added
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 42s
CI & Build / integration (push) Successful in 1m39s
CI & Build / Python tests (push) Successful in 2m4s
CI & Build / Build & push image (push) Canceled after 0s
The five auth views each defined .btn-submit identically — full-width, filled,
0.6rem — and LoginView additionally defined .btn-oauth. Those rules are now
gone entirely rather than tokenised: the template composes `btn-primary
btn-block` and `btn-ghost btn-block`, and there is nothing left per-file to
drift.

That is the difference between this and the earlier chunks. Consolidating the
core four moved geometry into one place but left every semantic name defining
its own; this removes the definition.

Two variants added, both earned rather than invented:

- .btn-text — no fill, no border. The most common shape in the dense surfaces
  (dismiss, cancel-beside-confirm, clear-search) where a border would draw a
  box around something that should read as an action on the adjacent text.
  Distinct from ghost, which IS a box.
- .btn-danger-outline — already existed independently in three views before
  this sheet, which is what makes it a variant and not a one-off. It is what a
  delete looks like when it must not shout.

.btn-block composes with a variant rather than being one, because width is
orthogonal to appearance.

Also corrected the sheet's own header, which claimed "no template changes" —
true when it was written, false as of this commit. It now states the actual
model: a button is variant + size, composed in the template. Semantic per-view
names are named as the thing that drifted, and why: a name says what a button
is FOR and nothing about what it should look like, so two buttons doing the
same job in two views had no reason to match, and didn't.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-08-01 22:42:14 -04:00
bvandeusenandClaude Opus 5 37616682f0 feat(ui): three button sizes, because the app has three kinds of button
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 31s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 45s
Measured before deciding: across the ~100 bespoke button rules, vertical
padding does not spread — it clusters. ~27 at 0.4–0.45rem, ~28 at 0.25–0.3rem,
~23 at 0.1–0.15rem. Those are three different components that happen to share a
name prefix: a page action, a row action, and an affordance that lives inside a
card.

Collapsing them to the single size the shared sheet had would have visibly
broken every card layout, which is why the one-off migration stopped here for a
decision rather than proceeding on the assumption that a button is a button.

  default        8px 16px   page action — what the house style specifies
  .btn-compact   4px 12px   toolbar, table row, list item controls
  .btn-inline    2px 4px    dismiss ×, confirm tick, add-chip

.btn-small and .btn-sm already sat at the compact step, so they are kept as
aliases for it — no template churn, and the two spellings stop being a third
thing that might drift.

.btn-inline is deliberately below the spacing scale's first step on the
vertical axis: 4px of padding on an 11px label already exceeds the line box
these sit in. Stated in the file so it reads as a measured exception rather
than someone ignoring the scale.

DesignView renders all three as real specimens. A size scale described in prose
is one nobody can check; rendered from the actual classes, it cannot claim
something the app does not do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-08-01 22:21:44 -04:00
bvandeusenandClaude Opus 5 97b93bcaea refactor(ui): buttons stop using weights the system doesn't have
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 21s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 1m10s
CI & Build / Build & push image (push) Successful in 36s
Eleven buttons set font-weight 600. The house style permits exactly two
weights, 400 and 500, and says so explicitly — 600 and 700 are not part of the
system. Every auth Submit, plus Invite, Toggle, Confirm-delete, Add-share,
the OAuth button and the assist Reject.

Now var(--fs-weight-medium), which is 500. Buttons get very slightly lighter.

Small on its own, but it is the third kind of drift the same five auth views
have now produced: geometry that differed per file, an accent fill the style
forbids, and a weight the system does not define. None of the three was a
deliberate choice — each is what happens when a button is written by copying
the nearest existing one.

Weight declarations only. No geometry, no colour, no templates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-08-01 22:19:16 -04:00
bvandeusenandClaude Opus 5 f491b6d7b9 refactor(ui): action buttons stop wearing the accent
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 46s
CI & Build / Build & push image (push) Successful in 47s
21 buttons filled with --color-primary, which resolves to Scribe's violet
accent: every auth Submit (Login, Register, Invite, Forgot, Reset), plus
Invite, Add, Confirm, Restore, Generate, Log-save, Subtask-confirm,
Toggle-open, the modal primary, the version-restore, the inline-assist button,
the milestone-plan actions, the task-advance hover, and both empty-state CTAs.

The house style is explicit that the accent never appears on an action button:
action colours are universal across the family precisely so a Save button looks
identical in every app, while the accent carries identity. Doing both makes the
accent mean two things and neither clearly.

Operator's call, and the reasoning is worth keeping: the violet-on-Scribe-
actions treatment was a deliberate early choice to give the web UI its own
personality, made when much more of the app was user-facing. That is no longer
true, so consistency is now worth more than the distinction it was buying.

Found in two passes, which is the part worth noting. The first scan looked for
`.btn-*` and found 13. Seven more were the same thing under different names —
.modal-btn-primary, .vh-btn-restore, .inline-assist-btn, .empty-action,
.task-advance-btn — plus .ms-plan-actions .btn-primary, a compound override
flagged in the previous commit. Searching by naming convention finds what was
named consistently, which is never the whole set.

Deliberately NOT changed: progress-bar fills, active tab / page / selection
states, tag-pill hover, the duration badge, the skip link, the assist pulse.
Those are identity and active-state, which is exactly where the accent belongs.
After this the accent appears only there, which is what makes it read as
identity rather than as decoration.

Colour swaps only — 25 lines changed, no geometry, no structure, no templates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-08-01 22:13:58 -04:00
bvandeusenandClaude Opus 5 1a959b1db0 refactor(ui): one button definition, aligned to the design system
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 36s
CI & Build / integration (push) Successful in 39s
CI & Build / Python tests (push) Successful in 1m12s
CI & Build / Build & push image (push) Successful in 1m12s
`.btn-primary` was defined five times in five scoped stylesheets and all five
had drifted: three paddings, three font sizes, three disabled opacities — and
ProjectListView had no disabled style at all, so a disabled button there looked
enabled. Nothing detected any of it. A scoped duplicate is not a rule
violation, not a broken reference, and not a recorded snippet, so no existing
check could see it (#2273).

assets/components.css is now the single definition of the core four —
primary, secondary, ghost, danger — plus the small modifier, in design-system
tokens throughout. Operator's call to align to the system rather than to the
majority of current values, so buttons move to the 8px radius and 12px label
the system specifies, from the app's 4px/14.4px.

Class names are unchanged, so there are no template edits: the existing surface
is repurposed, not rebuilt.

STAGING PROPERTY that makes this safe to land ahead of the rest: a Vue
`<style scoped>` rule compiles to `.btn-primary[data-v-…]` (specificity 0,2,0)
and beats a plain global selector (0,1,0). So the shared sheet changes nothing
for a view still carrying its own copy, and every intermediate state of the
remaining migration is coherent rather than half-applied.

Two divergences corrected on the way, both worth naming:

- SnippetEditorView's `.btn-secondary` was a GHOST in disguise — outline
  styling under the secondary name, while the house style says secondary is
  filled bronze. It now looks like what it is called.
- SnippetDetailView and SnippetListView tinted a ghost button's label with the
  ACCENT on hover. The house style reserves the accent for identity and active
  state, never general chrome.

DesignView reported "no shared button exists" as an honest gap and declined to
draw a look-alike. That gap is closed, so it now renders the app's real
classes — the specimens cannot drift from the app without drifting the app.

Net -177 lines. Follows: the ~20 semantic one-offs (.btn-save, .btn-delete,
.btn-toggle …) and the compound overrides in ProjectView, one of which puts the
accent on a primary action button.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-08-01 19:52:35 -04:00
bvandeusenandClaude Opus 5 6d01788326 test(plugin): pin both halves of the local prior-art arm
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 28s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 54s
CI & Build / Build & push image (push) Successful in 21s
CI caught the previous commit: the fail-open contract asserts a hook stays
SILENT with no working instance, and the new local arm deliberately speaks
there. The invariant is right and the hook is right — the smoke event was
wrong. It used `def f`, which this repo really does define, so the hook found
something and "silent" was asserting the wrong thing.

Fixed by asserting the two properties separately:

- SILENT for a symbol that genuinely does not exist.
- SPEAKING, with NO credentials, for one that does. That is the point of the
  arm — the other arms ask Scribe what was RECORDED; this one asks the repo
  what EXISTS, which needs no instance. Were it to start depending on
  configuration it would stop covering the case it was built for, and only
  this assertion would notice.

Second trap, hit while fixing the first: spelling the absent symbol out in full
wrote `def <name>(` into check_plugin.py, so the smoke event DEFINED the very
symbol it claimed was missing, and the hook found it again. The name is now
assembled from fragments so the contiguous string never appears in the source.

scribe_prior_art.sh joins scribe_session_context.sh as a hook that legitimately
produces output without credentials — for the same reason, that it carries
something needing neither network nor config.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-07-31 23:49:10 -04:00
bvandeusenandClaude Opus 5 17d59fa3e0 feat(prior-art): ask the repo, not just the record, before writing a definition
CI & Build / Plugin hooks (push) Failing after 6s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python lint (push) Successful in 2s
CI & Build / integration (push) Successful in 31s
CI & Build / Python tests (push) Successful in 56s
CI & Build / Build & push image (push) Successful in 21s
Scribe has never read a line of the codebase. Every Drafter surface recalls
from the RECORD — things someone deliberately recorded — so a helper nobody
thought to record is invisible to all of them. That is how `.btn-primary` came
to be defined four times, in four scoped stylesheets, already diverged: it was
never a snippet, so no threshold and no query rewrite could ever have surfaced
it (#2280).

The write-path hook already runs on the developer's machine, inside the repo,
holding the code about to be written. It can simply look. No index, no storage,
no staleness story, no server round-trip.

Verified against this repo with Scribe unconfigured:

    .btn-primary is already defined in 4 other file(s):
      DesignSystemsView.vue ProjectListView.vue SettingsView.vue
      SnippetEditorView.vue

Three properties it needs, all checked by hand:

- DEFINITION-shaped patterns only. Grepping bare occurrences would match every
  call site and bury the real finding, and a hint that is mostly noise is one
  people learn to skip — worse than none. A payload containing only calls to
  embed_note() stays silent; one containing `def embed_note` does not.
- The target file is excluded, so editing the file that already defines
  something doesn't report it against itself.
- It runs when Scribe is UNCONFIGURED, and a failed request no longer discards
  it. The remote arms answer "what was recorded"; this one answers "what
  exists", and that question needs no instance to produce an answer. `curl ||
  exit 0` became `curl || true` for the same reason.

Plugin 0.1.21 -> 0.1.22.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-07-31 23:43:58 -04:00
bvandeusen 6a6a388ecd docs: Fabled-Git, not Forgejo, in ci-requirements
The instance has run Gitea since the migration. Prose only — no workflow or
path change. Scribe issue #2272.
2026-07-31 23:42:25 -04:00
37 changed files with 617 additions and 1736 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ real Postgres), build (docker buildx).
- uv (test + integration jobs run `uv sync --locked`; installed in the
image since the ci-python Dockerfile started pip-installing it)
- docker CLI + buildx (build job pushes the production image to the
Forgejo registry)
Fabled-Git registry)
## Per-job tool installs
+193
View File
@@ -0,0 +1,193 @@
/* Shared component styles — the house recipes, in one place.
* ===========================================================================
*
* WHY THIS FILE EXISTS
*
* `.btn-primary` was defined five times, in five scoped stylesheets, and all
* five had drifted: three paddings, three font sizes, three disabled opacities,
* and one view with no disabled style at all (#2273). Nothing detected that,
* because a scoped duplicate is invisible to every tool — it isn't a rule
* violation, isn't a broken reference, and isn't a recorded snippet.
*
* GEOMETRY LIVES HERE. Every value is a design-system token, so a palette or
* scale change moves the buttons rather than stranding a copy that no longer
* matches.
*
* A button is `variant + size`, composed in the template:
* btn-primary a page action
* btn-primary btn-compact a row action
* btn-ghost btn-inline an affordance inside a card
* btn-primary btn-block a form's single submitting action
* Semantic per-view names (.btn-save, .btn-delete-task, …) were the thing that
* drifted, because a name says what a button is FOR and nothing about what it
* should look like — so two buttons doing the same job in two views had no
* reason to match, and didn't.
*
* MIGRATION NOTE — this file is deliberately safe to land ahead of the removals.
* These are plain selectors (specificity 0,1,0); a Vue `<style scoped>` rule
* compiles to `.btn-primary[data-v-…]` (0,2,0) and therefore WINS. So a view
* still carrying its own copy is unaffected until that copy is deleted, and
* every intermediate state of the migration is coherent.
*/
/* --- the shared shape ---------------------------------------------------- */
.btn-primary,
.btn-secondary,
.btn-ghost,
.btn-danger,
.btn-danger-outline {
padding: var(--fs-space-2) var(--fs-space-4); /* 8px 16px */
border: none;
border-radius: var(--fs-radius-md); /* 8px — the system's button radius */
font-family: var(--fs-font-body);
font-size: var(--fs-size-label); /* 12px */
font-weight: var(--fs-weight-medium); /* 500 — the heaviest the system goes */
line-height: var(--fs-leading-body);
white-space: nowrap;
cursor: pointer;
transition: background var(--fs-dur-fast) var(--fs-ease),
border-color var(--fs-dur-fast) var(--fs-ease),
color var(--fs-dur-fast) var(--fs-ease);
}
/* One rule, so a disabled button can never look enabled in one view and
* disabled in another — which is exactly what ProjectListView shipped, having
* no disabled style at all. */
.btn-primary:disabled,
.btn-secondary:disabled,
.btn-ghost:disabled,
.btn-danger:disabled,
.btn-danger-outline:disabled {
opacity: var(--fs-disabled-opacity);
cursor: not-allowed;
}
.btn-primary:focus-visible,
.btn-secondary:focus-visible,
.btn-ghost:focus-visible,
.btn-danger:focus-visible,
.btn-danger-outline:focus-visible {
outline: none;
box-shadow: var(--fs-focus-ring);
}
/* --- variants ------------------------------------------------------------ */
/* The accent is deliberately ABSENT from every filled variant. Action colours
* are universal across the family so a Save button looks identical in every
* app — the accent is identity, not action. */
.btn-primary {
background: var(--color-action-primary);
color: var(--fs-text-on-action);
}
.btn-primary:not(:disabled):hover {
background: var(--color-action-primary-hover);
}
.btn-secondary {
background: var(--color-action-secondary);
color: var(--fs-text-on-action);
}
.btn-secondary:not(:disabled):hover {
background: var(--color-action-secondary-hover);
}
/* Ghost is an OUTLINE, which is why its border and the tertiary action colour
* are the same token rather than two values that happen to match. Hover moves
* the BORDER, not the text to the accent — SnippetDetailView tinted the label
* with the accent on hover, which the house style reserves for identity and
* active state, not for general chrome. */
.btn-ghost {
background: none;
border: var(--fs-border);
color: var(--color-text);
}
.btn-ghost:not(:disabled):hover {
border: var(--fs-border-hover);
background: var(--color-hover);
}
/* Destructive is NOT the error colour: an error is a failure that happened, a
* destructive action is one about to happen. Pair with an icon. */
.btn-danger {
background: var(--color-action-destructive);
color: var(--fs-text-on-action);
}
.btn-danger:not(:disabled):hover {
background: var(--color-action-destructive-hover);
}
/* A bare text button: no fill, no border. The most common shape in the dense
* surfaces — a dismiss, a cancel next to a confirm, a clear-search — where a
* border would draw a box around something that should read as an action on
* the text beside it. Distinct from ghost, which IS a box. */
.btn-text {
background: none;
border: none;
color: var(--color-text-muted);
padding: var(--fs-space-1) var(--fs-space-2);
font-family: var(--fs-font-body);
font-size: var(--fs-size-tiny);
line-height: 1;
cursor: pointer;
transition: color var(--fs-dur-fast) var(--fs-ease);
}
.btn-text:not(:disabled):hover { color: var(--color-text); }
.btn-text:disabled { opacity: var(--fs-disabled-opacity); cursor: not-allowed; }
.btn-text:focus-visible { outline: none; box-shadow: var(--fs-focus-ring); }
/* Destructive, outlined — fills on hover. Already existed independently in
* three views before this sheet, which is what makes it a variant rather than
* a one-off: it is what a delete looks like when it must not shout. */
.btn-danger-outline {
background: none;
border: 1px solid var(--color-action-destructive);
color: var(--color-action-destructive);
}
.btn-danger-outline:not(:disabled):hover {
background: var(--color-action-destructive);
color: var(--fs-text-on-action);
}
/* --- size modifiers ------------------------------------------------------
*
* THREE sizes, because the app genuinely has three. Measured across the ~100
* bespoke button rules before this scale existed, vertical padding fell into
* clusters rather than a spread: ~27 at 0.40.45rem, ~28 at 0.250.3rem, ~23
* at 0.10.15rem. Those are three different components — a page action, a row
* action, and an affordance living inside a card — that happen to share a name
* prefix. Collapsing them to one size would visibly break the card layouts.
*
* A button carries its size modifier; the DEFAULT (no modifier) is the page
* action, which is the one the house style specifies.
*/
/* Row actions: a toolbar, a table row, a list item's controls. */
.btn-compact,
.btn-small, /* pre-existing spellings, kept so no template churns */
.btn-sm {
padding: var(--fs-space-1) var(--fs-space-3); /* 4px 12px */
font-size: var(--fs-size-tiny);
}
/* Inline affordances: a dismiss ×, a confirm tick, an add-chip — things that
* sit INSIDE a line of text or a card and must not disturb its rhythm. Below
* the spacing scale's first step on the vertical axis by necessity: 4px of
* padding on a 11px label already exceeds the line box these live in. */
.btn-inline {
padding: 2px var(--fs-space-1); /* 2px 4px */
font-size: var(--fs-size-tiny);
line-height: 1;
}
/* Full width, for a form's single submitting action — the auth screens. Width
* is orthogonal to size, so it composes: `btn-primary btn-block`. */
.btn-block {
display: block;
width: 100%;
padding: var(--fs-space-3) var(--fs-space-4); /* 12px 16px — a touch taller,
because a full-width button
is the page's main action */
font-size: var(--fs-size-body-sm);
}
+94 -179
View File
@@ -34,86 +34,11 @@
gap: 0.5rem;
align-items: center;
}
.btn-back {
display: inline-flex;
align-items: center;
padding: 0.45rem 1rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: none;
color: var(--color-text-secondary);
text-decoration: none;
cursor: pointer;
font-size: 0.9rem;
}
.btn-back:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
/* Save: Moss action-primary per the Hybrid rule. Saving is "operating
the software" — not a brand moment. Accent gradient is reserved for
Send / empty-state CTAs. */
.btn-save {
padding: 0.45rem 1.1rem;
background: var(--color-action-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-weight: 500;
font-size: 0.875rem;
transition: background 0.15s, opacity 0.15s;
}
.btn-save:hover:not(:disabled) {
background: var(--color-action-primary-hover);
}
.btn-save:disabled {
opacity: 0.55;
cursor: default;
}
/* Delete: Oxblood action-destructive per Hybrid rule. Should be paired
with a Trash icon at the call site to reinforce intent. */
.btn-delete {
padding: 0.45rem 1rem;
background: var(--color-action-destructive);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-weight: 500;
}
.btn-delete:hover { background: var(--color-action-destructive-hover); }
.btn-assist-toggle {
margin-left: auto;
padding: 0.4rem 0.9rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: none;
color: var(--color-text-secondary);
cursor: pointer;
font-size: 0.85rem;
}
.btn-assist-toggle.active {
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
border-color: var(--color-primary);
color: var(--color-primary);
}
.title-input {
padding: 0.4rem 0;
border: none;
border-bottom: 1.5px solid var(--color-border);
border-radius: 0;
font-size: 1.5rem;
font-weight: 500;
font-family: "Fraunces", Georgia, serif;
background: transparent;
color: var(--color-text);
width: 100%;
transition: border-color 0.15s;
}
.title-input:focus {
outline: none;
border-bottom-color: var(--color-primary);
@@ -155,23 +80,6 @@
align-items: center;
gap: 0.4rem;
}
.btn-suggest-tags {
padding: 0.3rem 0.7rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-bg-card);
color: var(--color-text-secondary);
cursor: pointer;
font-size: 0.8rem;
}
.btn-suggest-tags:hover:not(:disabled) {
border-color: var(--color-primary);
color: var(--color-primary);
}
.btn-suggest-tags:disabled {
opacity: 0.6;
cursor: wait;
}
.tag-pill {
display: inline-flex;
align-items: center;
@@ -198,18 +106,6 @@
.tag-check {
font-size: 0.7rem;
}
.btn-dismiss-tags {
padding: 0.1rem 0.4rem;
border: none;
background: none;
color: var(--color-text-muted);
cursor: pointer;
font-size: 1rem;
line-height: 1;
}
.btn-dismiss-tags:hover {
color: var(--color-text);
}
/* ── Assist panel ── */
.assist-panel {
@@ -237,32 +133,6 @@
text-transform: uppercase;
letter-spacing: 0.05em;
}
.btn-proofread {
padding: 0.3rem 0.65rem;
font-size: 0.78rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: none;
color: var(--color-text-secondary);
cursor: pointer;
}
.btn-proofread:hover:not(:disabled) {
border-color: var(--color-primary);
color: var(--color-primary);
}
.btn-proofread:disabled {
opacity: 0.5;
cursor: default;
}
.btn-close-assist {
padding: 0.1rem 0.4rem;
border: none;
background: none;
color: var(--color-text-muted);
cursor: pointer;
font-size: 1rem;
line-height: 1;
}
.assist-panel-body {
flex: 1;
min-height: 0;
@@ -342,28 +212,6 @@
display: flex;
gap: 0.5rem;
}
.btn-generate {
padding: 0.4rem 0.9rem;
background: var(--color-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.85rem;
}
.btn-generate:disabled {
opacity: 0.5;
cursor: default;
}
.btn-clear {
padding: 0.4rem 0.9rem;
background: none;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.85rem;
color: var(--color-text-secondary);
}
/* Streaming */
.assist-streaming-label {
@@ -419,14 +267,6 @@
font-weight: 500;
color: var(--color-text-secondary);
}
.btn-toggle-view {
font-size: 0.75rem;
color: var(--color-primary);
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.diff-view {
border: 1px solid var(--color-input-border);
border-radius: var(--radius-sm);
@@ -475,24 +315,6 @@
display: flex;
gap: 0.5rem;
}
.btn-accept {
padding: 0.4rem 1rem;
background: var(--color-success);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.85rem;
}
.btn-reject {
padding: 0.4rem 1rem;
background: none;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.85rem;
color: var(--color-text-secondary);
}
/* ── Modal ── */
.modal-overlay {
@@ -547,7 +369,7 @@
z-index: 100;
transform: translateX(-50%);
padding: 0.3rem 0.75rem;
background: var(--color-primary);
background: var(--color-action-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
@@ -640,3 +462,96 @@
padding: 0.5rem 1rem 1rem;
}
}
/* ---------------------------------------------------------------------------
* Editor button aliases.
*
* These names are used across six views, so they alias onto the shared variants
* rather than every call site being rewritten — the class stays the app's, the
* appearance comes from components.css. Same reasoning as .btn-small: a name
* already in the templates is cheaper to point somewhere than to replace.
*
* They are @extend-shaped, which CSS lacks, so each carries the variant's own
* declarations. That is the one duplication this migration cannot remove — but
* it is duplication of a REFERENCE (a var()), not of a value, so a palette
* change still moves everything at once.
* ------------------------------------------------------------------------ */
.btn-accept,
.btn-generate,
.btn-save {
background: var(--color-action-primary);
color: var(--fs-text-on-action);
border: none;
}
.btn-accept:not(:disabled):hover,
.btn-generate:not(:disabled):hover,
.btn-save:not(:disabled):hover {
background: var(--color-action-primary-hover);
}
.btn-back,
.btn-clear,
.btn-reject,
.btn-proofread,
.btn-suggest-tags {
background: none;
border: var(--fs-border);
color: var(--color-text);
}
.btn-back:not(:disabled):hover,
.btn-clear:not(:disabled):hover,
.btn-reject:not(:disabled):hover,
.btn-proofread:not(:disabled):hover,
.btn-suggest-tags:not(:disabled):hover {
border: var(--fs-border-hover);
background: var(--color-hover);
}
.btn-delete {
background: var(--color-action-destructive);
color: var(--fs-text-on-action);
border: none;
}
.btn-delete:not(:disabled):hover {
background: var(--color-action-destructive-hover);
}
/* Shared geometry for every alias above. */
.btn-accept, .btn-generate, .btn-save, .btn-back, .btn-clear, .btn-reject,
.btn-delete, .btn-dismiss-tags, .btn-proofread, .btn-suggest-tags {
border-radius: var(--fs-radius-md);
font-family: var(--fs-font-body);
font-weight: var(--fs-weight-medium);
cursor: pointer;
white-space: nowrap;
transition: background var(--fs-dur-fast) var(--fs-ease),
border-color var(--fs-dur-fast) var(--fs-ease),
color var(--fs-dur-fast) var(--fs-ease);
}
.btn-accept, .btn-generate, .btn-save, .btn-back, .btn-clear, .btn-reject,
.btn-delete {
padding: var(--fs-space-2) var(--fs-space-4);
font-size: var(--fs-size-label);
}
.btn-proofread, .btn-suggest-tags {
padding: var(--fs-space-1) var(--fs-space-3);
font-size: var(--fs-size-tiny);
}
.btn-dismiss-tags {
background: none;
border: none;
color: var(--color-text-muted);
padding: 2px var(--fs-space-1);
font-size: var(--fs-size-tiny);
line-height: 1;
}
.btn-dismiss-tags:hover { color: var(--color-text); }
.btn-accept:disabled, .btn-generate:disabled, .btn-save:disabled,
.btn-back:disabled, .btn-clear:disabled, .btn-reject:disabled,
.btn-delete:disabled, .btn-proofread:disabled, .btn-suggest-tags:disabled,
.btn-dismiss-tags:disabled {
opacity: var(--fs-disabled-opacity);
cursor: not-allowed;
}
+1 -14
View File
@@ -281,7 +281,7 @@ onMounted(loadVersions);
<div class="history-footer">
<button
class="btn-restore"
class="btn-primary"
:disabled="!selectedVersion?.body"
@click="restore"
>Restore this version</button>
@@ -393,19 +393,6 @@ onMounted(loadVersions);
border-top: 1px solid var(--color-border);
}
.btn-restore {
padding: 0.45rem 1rem;
background: var(--color-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
}
.btn-restore:disabled {
opacity: 0.5;
cursor: default;
}
/* ── Pin badges + label rendering ───────────────────────────────────────── */
.pin-badge {
@@ -188,7 +188,7 @@ const markers: Record<DiffLine["type"], string> = {
cursor: pointer;
font-size: 0.8rem;
font-family: inherit;
font-weight: 600;
font-weight: var(--fs-weight-medium);
}
.iap-btn-accept {
background: var(--color-success, #22c55e);
+2 -35
View File
@@ -50,7 +50,7 @@ onMounted(() => store.fetchAll())
<span class="notif-panel-title">Notifications</span>
<button
v-if="store.count > 0"
class="btn-mark-all"
class="btn-text"
@click="store.markAll()"
>Mark all read</button>
</header>
@@ -70,7 +70,7 @@ onMounted(() => store.fetchAll())
</p>
<span class="notif-time">{{ relativeTime(n.created_at) }}</span>
</div>
<button class="btn-notif-close" @click.stop="store.markRead(n.id)" aria-label="Dismiss"><X :size="16" /></button>
<button class="btn-text" @click.stop="store.markRead(n.id)" aria-label="Dismiss"><X :size="16" /></button>
</li>
</ul>
<div v-else class="notif-empty">No unread notifications</div>
@@ -108,21 +108,6 @@ onMounted(() => store.fetchAll())
font-size: 0.9rem;
}
.btn-mark-all {
background: none;
border: none;
color: var(--color-primary);
font-size: 0.78rem;
cursor: pointer;
padding: 0;
}
.btn-mark-all:hover { text-decoration: underline; }
.notif-list {
list-style: none;
padding: 0;
margin: 0;
}
.notif-item {
display: flex;
@@ -148,22 +133,4 @@ onMounted(() => store.fetchAll())
}
.notif-time { font-size: 0.75rem; color: var(--color-muted); }
.btn-notif-close {
background: none;
border: none;
color: var(--color-muted);
cursor: pointer;
font-size: 0.8rem;
padding: 0.1rem 0.25rem;
flex-shrink: 0;
transition: color 0.1s;
}
.btn-notif-close:hover { color: var(--color-text); }
.notif-empty {
padding: 1.5rem;
text-align: center;
color: var(--color-muted);
font-size: 0.88rem;
}
</style>
+3 -39
View File
@@ -117,7 +117,7 @@ onMounted(async () => {
<div class="share-dialog" role="dialog" :aria-label="`Share ${resourceTitle}`">
<header class="share-header">
<h2 class="share-title">Share "{{ resourceTitle }}"</h2>
<button class="btn-close" @click="emit('close')" aria-label="Close"><X :size="16" /></button>
<button class="btn-text" @click="emit('close')" aria-label="Close"><X :size="16" /></button>
</header>
<!-- Add share form -->
@@ -184,7 +184,7 @@ onMounted(async () => {
<option value="editor">Editor</option>
<option value="admin">Admin</option>
</select>
<button class="btn-remove-share" @click="removeShare(share)" aria-label="Remove"><X :size="16" /></button>
<button class="btn-text" @click="removeShare(share)" aria-label="Remove"><X :size="16" /></button>
</li>
<li v-if="!shares.length" class="shares-empty">Not shared with anyone yet</li>
</ul>
@@ -231,23 +231,6 @@ onMounted(async () => {
color: var(--color-text);
}
.btn-close {
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
font-size: 1.1rem;
padding: 0.25rem;
line-height: 1;
border-radius: 4px;
transition: color 0.15s;
}
.btn-close:hover { color: var(--color-text); }
.share-add {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--color-border);
}
.share-tabs {
display: flex;
@@ -343,7 +326,7 @@ onMounted(async () => {
border: none;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
font-weight: var(--fs-weight-medium);
cursor: pointer;
transition: opacity 0.15s;
white-space: nowrap;
@@ -394,23 +377,4 @@ onMounted(async () => {
cursor: pointer;
}
.btn-remove-share {
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
font-size: 0.9rem;
padding: 0.15rem 0.3rem;
border-radius: 4px;
transition: color 0.15s;
flex-shrink: 0;
}
.btn-remove-share:hover { color: var(--color-danger, #ef4444); }
.shares-empty {
color: var(--color-text-muted);
font-size: 0.85rem;
text-align: center;
padding: 0.75rem;
}
</style>
+6 -51
View File
@@ -163,7 +163,7 @@ async function confirmDelete() {
<!-- Toolbar -->
<div class="systems-toolbar">
<button v-if="!showCreate" class="btn-add-system" @click="openCreate">
<button v-if="!showCreate" class="btn-ghost btn-inline btn-add-system" @click="openCreate">
+ System
</button>
<label v-if="archivedSystems.length" class="archived-toggle">
@@ -190,10 +190,10 @@ async function confirmDelete() {
aria-label="System description"
></textarea>
<div class="system-form-actions">
<button type="submit" class="btn-confirm" :disabled="!newName.trim() || creating">
<button type="submit" class="btn-primary btn-compact" :disabled="!newName.trim() || creating">
{{ creating ? "Creating…" : "Create" }}
</button>
<button type="button" class="btn-cancel" @click="cancelCreate">Cancel</button>
<button type="button" class="btn-ghost btn-compact" @click="cancelCreate">Cancel</button>
</div>
</form>
@@ -211,7 +211,7 @@ async function confirmDelete() {
<div v-else-if="!visibleSystems.length" class="systems-empty">
<p class="empty-title">No systems yet</p>
<p class="empty-sub">Define a reusable subsystem or area to organize issues against.</p>
<button v-if="!showCreate" class="btn-confirm" @click="openCreate">+ Create a system</button>
<button v-if="!showCreate" class="btn-primary btn-compact" @click="openCreate">+ Create a system</button>
</div>
<!-- List -->
@@ -241,10 +241,10 @@ async function confirmDelete() {
aria-label="System description"
></textarea>
<div class="system-form-actions">
<button type="submit" class="btn-confirm" :disabled="!editName.trim() || savingEdit">
<button type="submit" class="btn-primary btn-compact" :disabled="!editName.trim() || savingEdit">
{{ savingEdit ? "Saving…" : "Save" }}
</button>
<button type="button" class="btn-cancel" @click="cancelEdit">Cancel</button>
<button type="button" class="btn-ghost btn-compact" @click="cancelEdit">Cancel</button>
</div>
</form>
</template>
@@ -387,51 +387,6 @@ async function confirmDelete() {
.system-textarea { resize: vertical; }
.system-form-actions { display: flex; gap: 0.4rem; }
.btn-confirm {
padding: 0.35rem 0.8rem;
background: var(--color-action-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.82rem;
font-family: inherit;
transition: background 0.15s;
}
.btn-confirm:hover:not(:disabled) { background: var(--color-action-primary-hover); }
.btn-confirm:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.btn-confirm:disabled { opacity: 0.5; cursor: default; }
.btn-cancel {
padding: 0.35rem 0.8rem;
background: var(--color-action-secondary);
border: none;
color: var(--fs-text-on-action);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.82rem;
font-family: inherit;
transition: background 0.15s;
}
.btn-cancel:hover { background: var(--color-action-secondary-hover); }
.btn-cancel:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
/* ── List ─────────────────────────────────────────────────────── */
.systems-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.system-card {
display: flex;
align-items: flex-start;
gap: 0.65rem;
padding: 0.65rem 0.85rem;
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
transition: border-color 0.12s, box-shadow 0.15s;
}
.system-card:hover {
border-color: color-mix(in srgb, var(--color-primary) 50%, var(--color-border));
box-shadow: 0 3px 10px rgba(0,0,0,0.07);
}
.system-card--archived { opacity: 0.6; }
.system-swatch {
+5 -58
View File
@@ -122,8 +122,8 @@ onMounted(loadLogs);
class="log-duration-input"
placeholder="min"
/>
<button class="btn-log-save" @click="saveEdit(log)">Save</button>
<button class="btn-log-cancel" @click="cancelEdit">Cancel</button>
<button class="btn-primary btn-compact" @click="saveEdit(log)">Save</button>
<button class="btn-ghost btn-compact" @click="cancelEdit">Cancel</button>
</div>
</template>
<template v-else>
@@ -133,8 +133,8 @@ onMounted(loadLogs);
{{ formatDuration(log.duration_minutes) }}
</span>
<div class="log-entry-actions">
<button class="btn-log-edit" @click="startEdit(log)" title="Edit">Edit</button>
<button class="btn-log-delete" aria-label="Delete log entry" @click="deleteLog(log)">&times;</button>
<button class="btn-text" @click="startEdit(log)" title="Edit">Edit</button>
<button class="btn-text" aria-label="Delete log entry" @click="deleteLog(log)">&times;</button>
</div>
</div>
<div class="log-content prose" v-html="renderMarkdown(log.content)"></div>
@@ -162,7 +162,7 @@ onMounted(loadLogs);
/>
</label>
<button
class="btn-log-submit"
class="btn-primary btn-compact"
@click="submitLog"
:disabled="!newContent.trim() || submitting"
>
@@ -230,34 +230,7 @@ onMounted(loadLogs);
gap: 0.25rem;
}
.btn-log-edit,
.btn-log-delete {
background: none;
border: none;
cursor: pointer;
color: var(--color-text-muted);
font-size: 0.8rem;
font-family: inherit;
padding: 0.1rem 0.25rem;
line-height: 1;
}
.btn-log-edit:hover { color: var(--color-primary); }
.btn-log-delete:hover { color: var(--color-danger, #e74c3c); }
.log-content {
font-size: 0.875rem;
}
.log-content :deep(p) { margin: 0; }
.log-add {
border-top: 1px solid var(--color-border);
padding-top: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.log-textarea {
width: 100%;
@@ -307,32 +280,6 @@ onMounted(loadLogs);
border-color: var(--color-primary);
}
.btn-log-submit,
.btn-log-save {
margin-left: auto;
padding: 0.3rem 0.75rem;
background: var(--color-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.8rem;
font-family: inherit;
}
.btn-log-submit:disabled {
opacity: 0.5;
cursor: default;
}
.btn-log-cancel {
padding: 0.3rem 0.6rem;
background: none;
border: 1px solid var(--color-border);
color: var(--color-text-secondary);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.8rem;
font-family: inherit;
}
</style>
@@ -236,7 +236,7 @@ function restore() {
.vh-btn-back:hover { border-color: var(--color-primary); color: var(--color-primary); }
.vh-btn-restore {
background: var(--color-primary);
background: var(--color-action-primary);
border: none;
border-radius: var(--radius-sm);
padding: 0.25rem 0.6rem;
+15 -198
View File
@@ -314,14 +314,14 @@ defineExpose({ reload: loadProjectNotes });
@keydown.enter="createNote"
@keydown.escape="cancelNewNote"
/>
<button class="btn-confirm" :disabled="creatingNote || !newNoteTitle.trim()" @click="createNote">
<button class="btn-primary btn-inline" :disabled="creatingNote || !newNoteTitle.trim()" @click="createNote">
{{ creatingNote ? '' : '+' }}
</button>
<button class="btn-cancel" aria-label="Cancel new note" @click="cancelNewNote"><X :size="16" /></button>
<button class="btn-text" aria-label="Cancel new note" @click="cancelNewNote"><X :size="16" /></button>
</template>
<template v-else>
<span class="rail-title">Notes</span>
<button class="btn-new-note" @click="startNewNote" title="New note">+ New</button>
<button class="btn-ghost btn-inline" @click="startNewNote" title="New note">+ New</button>
</template>
</div>
@@ -333,7 +333,7 @@ defineExpose({ reload: loadProjectNotes });
type="search"
aria-label="Search notes"
/>
<button v-if="searchQuery" class="btn-search-clear" aria-label="Clear search" @click="searchQuery = ''"><X :size="16" /></button>
<button v-if="searchQuery" class="btn-text btn-search-clear" aria-label="Clear search" @click="searchQuery = ''"><X :size="16" /></button>
</div>
<div v-if="listLoading" class="rail-state">Loading</div>
@@ -358,12 +358,12 @@ defineExpose({ reload: loadProjectNotes });
</div>
<div class="note-row-actions" @click.stop>
<template v-if="deletingId === note.id">
<button class="btn-confirm-delete" :disabled="pendingDelete === note.id" @click="requestDelete(note.id, $event)">
<button class="btn-danger-outline btn-inline" :disabled="pendingDelete === note.id" @click="requestDelete(note.id, $event)">
{{ pendingDelete === note.id ? '' : 'Delete?' }}
</button>
<button class="btn-cancel-delete" aria-label="Cancel delete" @click="cancelDelete($event)"><X :size="16" /></button>
<button class="btn-text" aria-label="Cancel delete" @click="cancelDelete($event)"><X :size="16" /></button>
</template>
<button v-else class="btn-delete" title="Delete note" @click="requestDelete(note.id, $event)">
<button v-else class="btn-text" title="Delete note" @click="requestDelete(note.id, $event)">
<Trash2 :size="16" />
</button>
</div>
@@ -382,7 +382,7 @@ defineExpose({ reload: loadProjectNotes });
<WordCount :body="noteBody" />
<span v-if="dirty && !saving" class="unsaved">Unsaved</span>
<span v-if="saving" class="saving-txt">Saving</span>
<button class="btn-save" :disabled="saving || !dirty" @click="saveNote">Save</button>
<button class="btn-primary btn-compact" :disabled="saving || !dirty" @click="saveNote">Save</button>
</div>
</div>
@@ -400,7 +400,7 @@ defineExpose({ reload: loadProjectNotes });
<div class="tag-row">
<TagInput v-model="noteTags" :fetchTags="(q: string) => notesStore.fetchAllTags(q)" />
<button
class="btn-suggest-tags"
class="btn-ghost btn-compact"
:disabled="tagSuggestions.suggestingTags.value"
title="Auto-suggest tags from title and body"
@click="tagSuggestions.fetchTagSuggestions()"
@@ -417,7 +417,7 @@ defineExpose({ reload: loadProjectNotes });
:class="['btn-tag-suggestion', { applied: tagSuggestions.appliedTags.value.has(tag) }]"
@click="tagSuggestions.applyTagSuggestion(tag)"
>#{{ tag }}{{ tagSuggestions.appliedTags.value.has(tag) ? ' ✓' : '' }}</button>
<button class="btn-dismiss-suggestions" aria-label="Dismiss tag suggestions" @click="tagSuggestions.dismissTagSuggestions()"><X :size="16" /></button>
<button class="btn-text" aria-label="Dismiss tag suggestions" @click="tagSuggestions.dismissTagSuggestions()"><X :size="16" /></button>
</div>
<div class="toolbar-row">
@@ -429,8 +429,8 @@ defineExpose({ reload: loadProjectNotes });
<span v-for="s in linkSuggestions" :key="s.note_id" class="link-suggest-chip" :title="`Appears ${s.count}× unlinked`">
<button class="btn-chip-link" @click="applyLink(s)">[[{{ s.title }}]]</button>
</span>
<button class="btn-link-all" @click="applyAllLinks" title="Link all suggestions">All</button>
<button class="btn-dismiss-suggestions" aria-label="Dismiss link suggestions" @click="linkSuggestions = []"><X :size="16" /></button>
<button class="btn-ghost btn-inline" @click="applyAllLinks" title="Link all suggestions">All</button>
<button class="btn-text" aria-label="Dismiss link suggestions" @click="linkSuggestions = []"><X :size="16" /></button>
</div>
<div class="editor-area" @keydown.ctrl.s.prevent="saveNote" @keydown.ctrl.e.prevent="editorRef?.editor?.commands.focus()">
@@ -484,26 +484,6 @@ defineExpose({ reload: loadProjectNotes });
flex: 1;
}
.btn-new-note {
background: none;
border: 1px solid var(--color-border);
border-radius: 4px;
padding: 0.15rem 0.4rem;
font-size: 0.7rem;
color: var(--color-text-muted);
cursor: pointer;
white-space: nowrap;
}
.btn-new-note:hover { border-color: var(--color-primary); color: var(--color-primary); }
.rail-search {
display: flex;
align-items: center;
gap: 0.2rem;
padding: 0.3rem 0.5rem;
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
.rail-search-input {
flex: 1;
@@ -517,17 +497,8 @@ defineExpose({ reload: loadProjectNotes });
.rail-search-input:focus { outline: none; }
.rail-search-input::-webkit-search-cancel-button { display: none; }
.btn-search-clear {
background: none;
border: none;
color: var(--color-text-muted);
font-size: 0.68rem;
cursor: pointer;
padding: 0;
line-height: 1;
flex-shrink: 0;
}
.btn-search-clear:hover { color: var(--color-text); }
/* Sits inside the search field: no padding, and must not flex. */
.btn-search-clear { padding: 0; flex-shrink: 0; }
.rail-state {
padding: 1rem 0.65rem;
@@ -617,99 +588,7 @@ defineExpose({ reload: loadProjectNotes });
align-items: center;
}
.btn-delete {
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
padding: 0.1rem;
border-radius: 3px;
opacity: 0;
transition: opacity 0.1s, color 0.1s;
display: flex;
align-items: center;
}
.note-row:hover .btn-delete { opacity: 1; }
.btn-delete:hover { color: var(--color-action-destructive); }
.btn-confirm-delete {
background: none;
border: 1px solid var(--color-action-destructive);
color: var(--color-action-destructive);
font-size: 0.65rem;
font-weight: 500;
cursor: pointer;
padding: 0.1rem 0.3rem;
border-radius: 3px;
transition: background 0.15s, color 0.15s;
}
.btn-confirm-delete:hover:not(:disabled) { background: var(--color-action-destructive); color: var(--fs-text-on-action); }
.btn-confirm-delete:disabled { opacity: 0.5; cursor: default; }
.btn-cancel-delete {
background: none;
border: none;
color: var(--color-text-muted);
font-size: 0.7rem;
cursor: pointer;
padding: 0.1rem;
}
.btn-cancel-delete:hover { color: var(--color-text); }
/* Inline new note */
.new-note-input {
flex: 1;
background: var(--color-input-bg, var(--color-bg));
border: 1px solid var(--color-primary);
border-radius: 4px;
padding: 0.15rem 0.35rem;
font-size: 0.78rem;
color: var(--color-text);
min-width: 0;
}
.new-note-input:focus { outline: none; }
.btn-confirm {
background: var(--color-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: 4px;
padding: 0.15rem 0.35rem;
font-size: 0.85rem;
cursor: pointer;
flex-shrink: 0;
line-height: 1;
}
.btn-confirm:disabled { opacity: 0.4; cursor: default; }
.btn-cancel {
background: none;
border: none;
color: var(--color-text-muted);
font-size: 0.72rem;
cursor: pointer;
padding: 0.1rem;
flex-shrink: 0;
}
.btn-cancel:hover { color: var(--color-text); }
/* ── Right editor pane ── */
.note-editor-pane {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
}
.editor-empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-muted);
font-size: 0.85rem;
}
/* Editor UI */
.panel-header {
@@ -731,23 +610,6 @@ defineExpose({ reload: loadProjectNotes });
.saving-txt { font-size: 0.72rem; color: var(--color-primary); }
/* Moss action-primary per Hybrid */
.btn-save {
background: var(--color-action-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: 5px;
padding: 0.25rem 0.7rem;
font-size: 0.8rem;
cursor: pointer;
transition: background 0.15s;
}
.btn-save:hover:not(:disabled) { background: var(--color-action-primary-hover); }
.btn-save:disabled { opacity: 0.4; cursor: default; }
.note-title-row {
padding: 0.9rem 1.1rem 0.5rem;
flex-shrink: 0;
}
.note-title-input {
width: 100%;
@@ -775,20 +637,7 @@ defineExpose({ reload: loadProjectNotes });
}
.tag-row > :first-child { flex: 1; min-width: 0; }
.btn-suggest-tags {
background: none;
border: 1px solid var(--color-border);
border-radius: 5px;
padding: 0.25rem 0.55rem;
font-size: 0.75rem;
color: var(--color-text-muted);
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
align-self: center;
}
.btn-suggest-tags:hover:not(:disabled) { border-color: var(--color-primary); color: var(--color-primary); }
.btn-suggest-tags:disabled { opacity: 0.5; cursor: default; }
.btn-suggest-tags { flex-shrink: 0; align-self: center; }
.tag-suggestions {
display: flex;
@@ -817,21 +666,6 @@ defineExpose({ reload: loadProjectNotes });
color: var(--color-primary);
}
.btn-dismiss-suggestions {
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
font-size: 0.75rem;
margin-left: auto;
padding: 0.1rem 0.3rem;
}
.btn-dismiss-suggestions:hover { color: var(--color-text); }
.toolbar-row {
padding: 0.3rem 0.6rem;
flex-shrink: 0;
}
.link-suggest-strip {
display: flex;
@@ -865,21 +699,4 @@ defineExpose({ reload: loadProjectNotes });
}
.btn-chip-link:hover { background: color-mix(in srgb, var(--color-primary) 15%, transparent); }
.btn-link-all {
background: none;
border: 1px solid var(--color-border);
border-radius: 4px;
padding: 0.1rem 0.4rem;
font-size: 0.7rem;
color: var(--color-text-muted);
cursor: pointer;
margin-left: 0.1rem;
}
.btn-link-all:hover { border-color: var(--color-primary); color: var(--color-primary); }
.editor-area {
flex: 1;
overflow-y: auto;
padding: 0.5rem 0.6rem;
}
</style>
+11 -81
View File
@@ -232,7 +232,7 @@ defineExpose({ reload: loadAll });
placeholder="New task..."
@keydown.enter="addTask"
/>
<button class="btn-add" :disabled="addingTask || !newTaskTitle.trim()" @click="addTask">+</button>
<button class="btn-primary btn-inline btn-add" :disabled="addingTask || !newTaskTitle.trim()" @click="addTask">+</button>
</div>
<div v-if="loading" class="state-msg">Loading...</div>
@@ -293,18 +293,18 @@ defineExpose({ reload: loadAll });
<Transition name="detail-fade">
<div v-if="activeTask" class="task-detail">
<div class="detail-header">
<RouterLink :to="`/tasks/${activeTask.id}/edit`" target="_blank" class="btn-edit-task" title="Open full editor">Edit </RouterLink>
<RouterLink :to="`/tasks/${activeTask.id}/edit`" target="_blank" class="btn-text btn-edit-task" title="Open full editor">Edit </RouterLink>
<span :class="['status-badge', `status-${activeTask.status}`]" @click="cycleStatus(activeTask, $event)" title="Click to cycle status">
{{ STATUS_ICON[activeTask.status] ?? "○" }} {{ activeTask.status.replace("_", " ") }}
</span>
<template v-if="deleteConfirmPending">
<button class="btn-delete-confirm" :disabled="deletingTask" @click="deleteActiveTask">{{ deletingTask ? '...' : 'Delete?' }}</button>
<button class="btn-delete-cancel" aria-label="Cancel delete" @click="cancelDeleteTask"><X :size="16" /></button>
<button class="btn-danger-outline btn-inline btn-delete-confirm" :disabled="deletingTask" @click="deleteActiveTask">{{ deletingTask ? '...' : 'Delete?' }}</button>
<button class="btn-text" aria-label="Cancel delete" @click="cancelDeleteTask"><X :size="16" /></button>
</template>
<button v-else class="btn-delete-task" title="Delete task" @click="deleteActiveTask">
<button v-else class="btn-text btn-delete-task" title="Delete task" @click="deleteActiveTask">
<Trash2 :size="16" />
</button>
<button class="btn-close-detail" @click="closeTask" aria-label="Close detail"><X :size="16" /></button>
<button class="btn-text btn-close-detail" @click="closeTask" aria-label="Close detail"><X :size="16" /></button>
</div>
<h3 class="detail-title">{{ activeTask.title }}</h3>
@@ -396,17 +396,7 @@ defineExpose({ reload: loadAll });
}
.task-add-input:focus { outline: none; border-color: var(--color-primary); }
.btn-add {
background: var(--color-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: 5px;
padding: 0.28rem 0.55rem;
font-size: 1rem;
cursor: pointer;
line-height: 1;
}
.btn-add:disabled { opacity: 0.4; cursor: default; }
.btn-add { font-size: 1rem; } /* a '+' glyph, not a label */
.groups-scroll {
flex: 1;
@@ -534,17 +524,7 @@ defineExpose({ reload: loadAll });
.status-badge.status-in_progress { border-color: var(--color-primary); color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 10%, transparent); }
.status-badge.status-done { border-color: var(--color-success, #27ae60); color: var(--color-success, #27ae60); background: color-mix(in srgb, var(--color-success, #27ae60) 10%, transparent); }
.btn-edit-task {
background: none;
border: none;
color: var(--color-primary);
font-size: 0.78rem;
cursor: pointer;
padding: 0.1rem 0.3rem;
border-radius: 3px;
text-decoration: none;
flex-shrink: 0;
}
.btn-edit-task { margin-left: 0.25rem; }
.btn-edit-task:hover { text-decoration: underline; }
.detail-body {
@@ -566,51 +546,11 @@ defineExpose({ reload: loadAll });
color: var(--color-text);
}
.btn-delete-task {
background: none;
border: none;
color: var(--color-text-muted);
font-size: 0.8rem;
cursor: pointer;
padding: 0.15rem 0.3rem;
border-radius: 3px;
margin-left: 0.25rem;
}
.btn-delete-task { margin-left: 0.25rem; }
.btn-delete-task:hover { color: var(--color-action-destructive); }
.btn-delete-confirm {
background: none;
border: 1px solid var(--color-action-destructive);
color: var(--color-action-destructive);
font-size: 0.72rem;
font-weight: 500;
cursor: pointer;
padding: 0.15rem 0.5rem;
border-radius: 4px;
margin-left: 0.25rem;
transition: background 0.15s, color 0.15s;
}
.btn-delete-confirm:hover:not(:disabled) { background: var(--color-action-destructive); color: var(--fs-text-on-action); }
.btn-delete-confirm:disabled { opacity: 0.5; cursor: default; }
.btn-delete-confirm { margin-left: 0.25rem; }
.btn-delete-cancel {
background: none;
border: none;
color: var(--color-text-muted);
font-size: 0.75rem;
cursor: pointer;
padding: 0.1rem 0.3rem;
}
.btn-delete-cancel:hover { color: var(--color-text); }
.detail-title {
padding: 0.75rem 0.75rem 0.25rem;
font-size: 0.95rem;
font-weight: 500;
margin: 0;
color: var(--color-text);
flex-shrink: 0;
}
.detail-meta {
display: flex;
@@ -679,15 +619,5 @@ defineExpose({ reload: loadAll });
}
/* Close detail button */
.btn-close-detail {
background: none;
border: none;
color: var(--color-text-muted);
font-size: 0.75rem;
cursor: pointer;
padding: 0.15rem 0.3rem;
border-radius: 3px;
margin-left: 0.2rem;
}
.btn-close-detail:hover { color: var(--color-text); }
.btn-close-detail { margin-left: 0.2rem; }
</style>
+2
View File
@@ -3,6 +3,8 @@ import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";
import "./assets/theme.css";
// After theme.css — it consumes the tokens declared there.
import "./assets/components.css";
import "./assets/prose.css";
const app = createApp(App);
-41
View File
@@ -1262,47 +1262,6 @@ textarea.input {
/* Buttons ---------------------------------------------------------------- */
.btn-primary,
.btn-ghost,
.btn-danger {
border-radius: var(--radius-sm);
padding: 0.45rem 0.9rem;
font: inherit;
font-size: 0.85rem;
cursor: pointer;
border: 1px solid transparent;
}
.btn-primary {
background: var(--color-action-primary);
color: var(--fs-text-on-action);
}
.btn-primary:not(:disabled):hover {
background: var(--color-action-primary-hover);
}
.btn-ghost {
background: none;
border-color: var(--color-border);
color: var(--color-text-secondary);
}
.btn-danger {
background: var(--color-action-destructive);
color: #E8E4D8;
}
.btn-small {
padding: 0.25rem 0.55rem;
font-size: 0.75rem;
}
.btn-primary:disabled,
.btn-ghost:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.row-actions {
+45 -17
View File
@@ -12,11 +12,12 @@
* every specimen below is either a REAL component imported from the app, or a
* real token read from the browser, or it is explicitly marked as missing.
*
* Buttons are the case where that bites. Rule 65 specifies four variants, but
* `.btn-primary` is defined four separate times in four `<style scoped>` blocks
* and 30 of 54 SFCs carry their own button CSS (#2273). There is no shared
* button to import, so rendering one here would just make this a fifth copy.
* It is reported as a gap instead.
* Buttons WERE the case where that bit: `.btn-primary` was defined five times
* in five `<style scoped>` blocks, all five drifted, and this page reported it
* as a gap because drawing a look-alike would have made it a sixth copy.
* `assets/components.css` is now the single definition (#2273), so the
* specimens below are the app's real classes — they cannot drift from the app
* without drifting the app itself.
*/
import { computed, onMounted, ref } from "vue";
@@ -225,25 +226,33 @@ const TYPE_SPECIMENS = [
</div>
</section>
<!-- The honest gap. -->
<!-- No longer a gap: these are the app's real classes, from the shared
sheet. Nothing here is a look-alike — change components.css and these
specimens change with it, which is the only way this page stays true. -->
<section class="design-section">
<h2>Buttons</h2>
<div class="gap-notice">
<strong>Not implemented as a shared component.</strong>
<p>
Rule 65 specifies four variants. In practice <code>.btn-primary</code> is
defined four separate times in four <code>&lt;style scoped&gt;</code>
blocks, and 30 of 54 single-file components carry their own button CSS.
There is no shared button to render here, and drawing one would make
this page a fifth copy of it the exact drift this surface exists to
catch. Tracked as issue #2273.
</p>
<div class="button-specimens">
<button class="btn-primary">Save</button>
<button class="btn-secondary">Detect</button>
<button class="btn-ghost">Cancel</button>
<button class="btn-danger">Delete</button>
<button class="btn-primary" disabled>Disabled</button>
</div>
<p class="spec-caption">
Three sizes, because the app has three kinds of button: a page action, a
row action, and an affordance that sits inside a card without disturbing
its rhythm.
</p>
<div class="button-specimens">
<button class="btn-primary">Default — page action</button>
<button class="btn-primary btn-compact">Compact — row action</button>
<button class="btn-primary btn-inline">Inline</button>
</div>
<ul class="spec-list">
<li v-for="b in RULEBOOK_BUTTONS" :key="b.name">
<span class="spec-name">{{ b.name }}</span>
<span class="spec-detail">{{ b.spec }}</span>
<span class="spec-status missing">missing</span>
<span class="spec-status ok">shared</span>
</li>
</ul>
</section>
@@ -361,6 +370,25 @@ const TYPE_SPECIMENS = [
/* Gaps ------------------------------------------------------------------- */
.spec-caption {
margin: 0 0 0.75rem;
color: var(--color-text-secondary);
font-size: 0.85rem;
line-height: 1.5;
max-width: 60ch;
}
/* Layout only. The buttons inside style themselves from the shared sheet —
adding any appearance rule here would recreate the copy this section
just stopped being. */
.button-specimens {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--fs-space-3);
margin-bottom: 1rem;
}
.gap-notice {
background: var(--color-surface);
border: 1px solid var(--color-border);
+1 -19
View File
@@ -49,7 +49,7 @@ async function handleSubmit() {
/>
</div>
<p v-if="error" class="error-msg">{{ error }}</p>
<button type="submit" class="btn-submit" :disabled="submitting">
<button type="submit" class="btn-primary btn-block" :disabled="submitting">
{{ submitting ? "Sending..." : "Send Reset Link" }}
</button>
</form>
@@ -137,24 +137,6 @@ async function handleSubmit() {
.success-msg p {
margin: 0.5rem 0;
}
.btn-submit {
width: 100%;
padding: 0.6rem;
background: var(--color-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.95rem;
font-weight: 600;
}
.btn-submit:disabled {
opacity: 0.6;
cursor: default;
}
.btn-submit:hover:not(:disabled) {
opacity: 0.9;
}
.auth-footer {
text-align: center;
font-size: 0.9rem;
+3 -39
View File
@@ -381,7 +381,7 @@ onUnmounted(() => {
<option value="alpha">Alphabetical</option>
<option value="type">By type</option>
</select>
<button class="btn-graph" :class="{ active: graphOpen }" @click="toggleGraph" title="Toggle graph view">
<button class="btn-ghost btn-compact" :class="{ active: graphOpen }" @click="toggleGraph" title="Toggle graph view">
<Share2 :size="16" />
Graph
</button>
@@ -463,14 +463,14 @@ onUnmounted(() => {
<span>Graph</span>
<div style="display:flex;gap:4px;align-items:center">
<button
class="btn-icon-sm"
class="btn-text"
@click="toggleGraphExpand"
:title="graphExpanded ? 'Narrow panel' : 'Expand panel'"
>
<ChevronLeft v-if="graphExpanded" :size="16" />
<ChevronRight v-else :size="16" />
</button>
<button class="btn-icon-sm" @click="toggleGraph" title="Close graph">
<button class="btn-text" @click="toggleGraph" title="Close graph">
<X :size="16" />
</button>
</div>
@@ -719,26 +719,6 @@ onUnmounted(() => {
cursor: pointer;
outline: none;
}
.btn-graph {
display: flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
border-radius: 8px;
border: 1px solid var(--color-border, rgba(255,255,255,0.1));
background: transparent;
color: var(--color-muted);
cursor: pointer;
font-size: 0.85rem;
transition: all 0.15s;
white-space: nowrap;
}
.btn-graph:hover { color: var(--color-text); border-color: rgba(255,255,255,0.2); }
.btn-graph.active {
background: var(--color-primary-wash);
border-color: rgba(91, 74, 138, 0.35);
color: var(--color-primary);
}
/* ── Card grid ───────────────────────────────────────────── */
.card-grid {
@@ -956,22 +936,6 @@ onUnmounted(() => {
border-bottom: 1px solid var(--color-border, rgba(255,255,255,0.06));
flex-shrink: 0;
}
.btn-icon-sm {
background: none;
border: none;
color: var(--color-muted);
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.85rem;
transition: color 0.15s;
}
.btn-icon-sm:hover { color: var(--color-text); }
.graph-embed {
flex: 1;
overflow: hidden;
position: relative;
}
/* Override GraphView's 100vh height so it fills the panel instead */
.graph-embed :deep(.graph-page) {
height: 100%;
+2 -34
View File
@@ -84,7 +84,7 @@ function loginWithOAuth() {
<p class="forgot-link">
<router-link to="/forgot-password">Forgot your password?</router-link>
</p>
<button type="submit" class="btn-submit" :disabled="submitting">
<button type="submit" class="btn-primary btn-block" :disabled="submitting">
{{ submitting ? "Signing in..." : "Sign In" }}
</button>
</form>
@@ -98,7 +98,7 @@ function loginWithOAuth() {
<button
v-if="authStore.oauthEnabled"
class="btn-oauth"
class="btn-ghost btn-block"
@click="loginWithOAuth"
>
Login with Authentik
@@ -176,24 +176,6 @@ function loginWithOAuth() {
font-size: 0.9rem;
margin: 0 0 0.75rem;
}
.btn-submit {
width: 100%;
padding: 0.6rem;
background: var(--color-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.95rem;
font-weight: 600;
}
.btn-submit:disabled {
opacity: 0.6;
cursor: default;
}
.btn-submit:hover:not(:disabled) {
opacity: 0.9;
}
.divider {
display: flex;
align-items: center;
@@ -208,20 +190,6 @@ function loginWithOAuth() {
flex: 1;
border-top: 1px solid var(--color-border);
}
.btn-oauth {
width: 100%;
padding: 0.6rem;
background: transparent;
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.95rem;
font-weight: 600;
}
.btn-oauth:hover {
background: var(--color-bg-hover, var(--color-border));
}
.auth-footer {
text-align: center;
font-size: 0.9rem;
+1 -14
View File
@@ -177,7 +177,7 @@ function clearFilters() {
<input v-model="dateTo" type="date" class="filter-date" title="To date" />
<button
v-if="category || search || dateFrom || dateTo"
class="btn-clear"
class="btn-ghost btn-compact"
@click="clearFilters"
>
Clear
@@ -337,19 +337,6 @@ function clearFilters() {
.filter-date {
width: 140px;
}
.btn-clear {
padding: 0.4rem 0.75rem;
background: transparent;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.85rem;
}
.btn-clear:hover {
color: var(--color-text);
border-color: var(--color-text-muted);
}
/* Table */
.loading-msg,
+1 -1
View File
@@ -748,7 +748,7 @@ onUnmounted(() => assist.clearSelection());
cursor: pointer;
font-family: inherit;
}
.btn-link-all:hover { background: var(--color-primary); color: var(--fs-text-on-action); }
.btn-link-all:hover { background: var(--color-action-primary); color: var(--fs-text-on-action); }
.link-suggest-list {
display: flex;
+4 -75
View File
@@ -194,22 +194,22 @@ async function convertToTask() {
</div>
<template v-else-if="store.currentNote">
<div class="toolbar">
<router-link to="/notes" class="btn-back"> Notes</router-link>
<router-link to="/notes" class="btn-ghost"> Notes</router-link>
<router-link
:to="`/notes/${store.currentNote.id}/edit`"
class="btn-edit"
class="btn-primary"
>
Edit
</router-link>
<button
v-if="!store.currentNote.is_task"
class="btn-convert"
class="btn-secondary btn-compact"
@click="convertToTask"
:disabled="converting"
>
{{ converting ? "Converting..." : "Convert to Task" }}
</button>
<button class="btn-share" @click="showShare = true">Share</button>
<button class="btn-secondary btn-compact" @click="showShare = true">Share</button>
</div>
<!-- Breadcrumb: parent project milestone -->
@@ -329,81 +329,10 @@ async function convertToTask() {
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.btn-back {
display: inline-flex;
align-items: center;
padding: 0.45rem 1rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: none;
color: var(--color-text-secondary);
text-decoration: none;
cursor: pointer;
font-size: 0.9rem;
}
.btn-back:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
/* Edit: Moss action-primary — switching from view to edit is operating
the software, not a brand moment. */
.btn-edit {
display: inline-flex;
align-items: center;
padding: 0.45rem 1.1rem;
border: none;
border-radius: var(--radius-sm);
background: var(--color-action-primary);
color: var(--fs-text-on-action);
text-decoration: none;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: background 0.15s;
}
.btn-edit:hover {
background: var(--color-action-primary-hover);
color: var(--fs-text-on-action);
}
/* Convert + Share: Bronze action-secondary — alternate paths */
.btn-convert {
margin-left: auto;
padding: 0.3rem 0.75rem;
background: var(--color-action-secondary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.85rem;
transition: background 0.15s;
}
.btn-convert:hover { background: var(--color-action-secondary-hover); }
.btn-convert:disabled {
opacity: 0.6;
cursor: default;
}
.btn-share {
padding: 0.3rem 0.75rem;
background: var(--color-action-secondary);
border: none;
border-radius: var(--radius-sm);
color: var(--fs-text-on-action);
cursor: pointer;
font-size: 0.85rem;
font-family: inherit;
transition: background 0.15s;
}
.btn-share:hover { background: var(--color-action-secondary-hover); }
.note-title {
font-family: "Fraunces", Georgia, serif;
font-size: 2rem;
font-weight: 500;
line-height: 1.2;
margin: 0.25rem 0 0.5rem;
color: var(--color-text);
}
.meta {
display: flex;
+4 -18
View File
@@ -284,20 +284,6 @@ function overallPct(project: Project): { total: number; pct: number } {
/* Moss action-primary per Hybrid — list-view utility action,
not a brand moment. Empty-state .empty-action below keeps accent. */
.btn-primary {
padding: 0.45rem 1rem;
background: var(--color-action-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-family: inherit;
transition: background 0.15s;
}
.btn-primary:hover {
background: var(--color-action-primary-hover);
}
.filter-tabs {
display: flex;
@@ -340,8 +326,8 @@ function overallPct(project: Project): { total: number; pct: number } {
.empty-icon { font-size: 2.5rem; margin-bottom: 0.75rem; opacity: 0.3; }
.empty-title { font-size: 1rem; font-weight: 500; color: var(--color-text-secondary); margin: 0 0 0.35rem; }
.empty-sub { font-size: 0.85rem; margin: 0 0 1rem; }
.empty-action { display: inline-block; padding: 0.4rem 1rem; border: 1px solid var(--color-primary); border-radius: var(--radius-sm); color: var(--color-primary); background: none; cursor: pointer; font-size: 0.85rem; transition: background 0.15s, color 0.15s; }
.empty-action:hover { background: var(--color-primary); color: var(--fs-text-on-action); }
.empty-action { display: inline-block; padding: 0.4rem 1rem; border: 1px solid var(--color-action-primary); border-radius: var(--radius-sm); color: var(--color-action-primary); background: none; cursor: pointer; font-size: 0.85rem; transition: background 0.15s, color 0.15s; }
.empty-action:hover { background: var(--color-action-primary); color: var(--fs-text-on-action); }
.skeleton-card {
height: 140px;
@@ -592,8 +578,8 @@ function overallPct(project: Project): { total: number; pct: number } {
background: var(--color-bg);
}
.modal-btn-primary {
background: var(--color-primary);
border-color: var(--color-primary);
background: var(--color-action-primary);
border-color: var(--color-action-primary);
color: var(--fs-text-on-action);
}
.modal-btn-primary:hover:not(:disabled) {
+9 -147
View File
@@ -435,7 +435,7 @@ async function confirmDelete() {
<!-- Nav bar -->
<div class="page-header">
<router-link to="/projects" class="btn-back"> Projects</router-link>
<router-link to="/projects" class="btn-ghost"> Projects</router-link>
<div class="page-header-actions">
<template v-if="showStartPlanning">
<input
@@ -451,7 +451,7 @@ async function confirmDelete() {
>
Create plan
</button>
<button class="btn-share" @click="showStartPlanning = false; planTitle = ''">Cancel</button>
<button class="btn-secondary btn-compact" @click="showStartPlanning = false; planTitle = ''">Cancel</button>
</template>
<button
v-else-if="project"
@@ -464,7 +464,7 @@ async function confirmDelete() {
<LayoutGrid :size="16" />
Workspace
</router-link>
<button v-if="project && !showStartPlanning" class="btn-share" @click="showShare = true">Share</button>
<button v-if="project && !showStartPlanning" class="btn-secondary btn-compact" @click="showShare = true">Share</button>
<button v-if="project && !showStartPlanning" class="btn-danger-outline" @click="showDeleteConfirm = true">Delete</button>
</div>
</div>
@@ -548,7 +548,7 @@ async function confirmDelete() {
<option v-for="ds in designSystems" :key="ds.id" :value="ds.id">{{ ds.title }}</option>
</select>
</div>
<button class="btn-save-panel" @click="saveProject" :disabled="!editDirty || saving">
<button class="btn-primary" @click="saveProject" :disabled="!editDirty || saving">
{{ saving ? "Saving..." : "Save Changes" }}
</button>
</aside>
@@ -581,7 +581,7 @@ async function confirmDelete() {
</div>
<template v-else>
<div class="milestone-actions">
<button v-if="!showNewMilestone" class="btn-add-milestone" @click="showNewMilestone = true">
<button v-if="!showNewMilestone" class="btn-ghost btn-inline btn-add-milestone" @click="showNewMilestone = true">
+ Milestone
</button>
<div v-else class="new-milestone-row">
@@ -593,10 +593,10 @@ async function confirmDelete() {
@keydown.enter="createMilestone"
@keydown.escape="showNewMilestone = false; newMilestoneTitle = ''"
/>
<button class="btn-ms-confirm" @click="createMilestone" :disabled="!newMilestoneTitle.trim() || creatingMilestone">
<button class="btn-primary btn-compact" @click="createMilestone" :disabled="!newMilestoneTitle.trim() || creatingMilestone">
{{ creatingMilestone ? "..." : "Add" }}
</button>
<button class="btn-ms-cancel" @click="showNewMilestone = false; newMilestoneTitle = ''">Cancel</button>
<button class="btn-secondary btn-compact" @click="showNewMilestone = false; newMilestoneTitle = ''">Cancel</button>
</div>
</div>
@@ -856,80 +856,9 @@ async function confirmDelete() {
min-width: 200px;
}
.btn-back {
display: inline-flex;
align-items: center;
padding: 0.4rem 0.9rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: none;
color: var(--color-text-secondary);
text-decoration: none;
font-size: 0.875rem;
transition: border-color 0.15s, color 0.15s;
}
.btn-back:hover { border-color: var(--color-primary); color: var(--color-primary); }
/* Open Workspace: brand-moment CTA — keep accent gradient. Workspace is
the project's "central feature moment" — entering the focused workspace
is a Scribe-flavored action, not a plain operation. */
.btn-workspace {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.45rem 1rem;
background: var(--gradient-cta);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
box-shadow: var(--glow-cta);
transition: box-shadow 0.15s, opacity 0.15s;
}
.btn-workspace:hover { box-shadow: var(--glow-cta-hover); opacity: 0.95; color: var(--fs-text-on-action); }
/* Share: Bronze action-secondary — alternate path */
.btn-share {
padding: 0.4rem 0.8rem;
background: var(--color-action-secondary);
border: none;
color: var(--fs-text-on-action);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.85rem;
font-family: inherit;
transition: background 0.15s;
}
.btn-share:hover { background: var(--color-action-secondary-hover); }
/* Delete project: Oxblood action-destructive ghost — outline form since
the actual confirm modal carries the filled destructive treatment */
.btn-danger-outline {
padding: 0.4rem 0.8rem;
background: none;
border: 1px solid var(--color-action-destructive);
color: var(--color-action-destructive);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.85rem;
font-family: inherit;
transition: background 0.15s, color 0.15s;
}
.btn-danger-outline:hover { background: var(--color-action-destructive); color: var(--fs-text-on-action); }
.error-msg { color: var(--color-danger); font-size: 0.9rem; }
/* ── Project identity header ─────────────────────────────────── */
.project-header { margin-bottom: 1rem; }
.title-row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.4rem;
}
.project-title-input {
flex: 1;
font-size: 1.75rem;
@@ -1062,30 +991,6 @@ async function confirmDelete() {
.edit-textarea { resize: vertical; }
/* Save panel: Moss action-primary per Hybrid rule */
.btn-save-panel {
padding: 0.45rem 0.9rem;
background: var(--color-action-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
font-family: inherit;
width: 100%;
transition: background 0.15s;
}
.btn-save-panel:hover:not(:disabled) { background: var(--color-action-primary-hover); }
.btn-save-panel:disabled { opacity: 0.45; cursor: default; }
/* ── Content area ────────────────────────────────────────────── */
.content-area { display: flex; flex-direction: column; gap: 0.75rem; }
.tab-bar {
display: flex;
gap: 0;
border-bottom: 1px solid var(--color-border);
}
.tab-btn {
display: inline-flex;
align-items: center;
@@ -1148,49 +1053,6 @@ async function confirmDelete() {
}
.milestone-title-input:focus { outline: none; border-color: var(--color-primary); }
/* Milestone confirm: Moss action-primary; Cancel: Bronze action-secondary */
.btn-ms-confirm {
padding: 0.3rem 0.65rem;
background: var(--color-action-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.78rem;
font-family: inherit;
transition: background 0.15s;
}
.btn-ms-confirm:hover:not(:disabled) { background: var(--color-action-primary-hover); }
.btn-ms-confirm:disabled { opacity: 0.5; cursor: default; }
.btn-ms-cancel {
padding: 0.3rem 0.65rem;
background: var(--color-action-secondary);
border: none;
color: var(--fs-text-on-action);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.78rem;
font-family: inherit;
transition: background 0.15s;
}
.btn-ms-cancel:hover { background: var(--color-action-secondary-hover); }
/* ── Milestone group ─────────────────────────────────────────── */
.milestone-group {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
overflow: hidden;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
}
.milestone-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 0.85rem;
background: var(--color-bg-secondary);
font-size: 0.85rem;
user-select: none;
border-bottom: 1px solid var(--color-border);
}
.milestone-header.clickable { cursor: pointer; }
.milestone-header.clickable:hover { background: color-mix(in srgb, var(--color-primary) 4%, var(--color-bg-secondary)); }
@@ -1224,7 +1086,7 @@ async function confirmDelete() {
cursor: pointer;
border: 1px solid var(--color-border);
}
.ms-plan-actions .btn-primary { background: var(--color-primary); color: var(--fs-text-on-action); border-color: var(--color-primary); }
.ms-plan-actions .btn-primary { background: var(--color-action-primary); color: var(--fs-text-on-action); border-color: var(--color-action-primary); }
.ms-plan-actions .btn-primary:disabled { opacity: 0.6; cursor: default; }
.ms-plan-actions .btn-secondary { background: var(--color-bg-card); color: var(--color-text); }
@@ -1393,7 +1255,7 @@ async function confirmDelete() {
line-height: 1;
}
.task-card:hover .task-advance-btn { opacity: 1; }
.task-advance-btn:hover { background: var(--color-primary); border-color: var(--color-primary); color: var(--fs-text-on-action); }
.task-advance-btn:hover { background: var(--color-action-primary); border-color: var(--color-action-primary); color: var(--fs-text-on-action); }
.task-advance-btn--done:hover { background: var(--color-success, #22c55e); border-color: var(--color-success, #22c55e); color: var(--fs-text-on-action); }
.task-advance-btn:disabled { opacity: 0.4; cursor: default; }
+1 -19
View File
@@ -144,7 +144,7 @@ async function handleSubmit() {
<p v-if="passwordMismatch" class="error-hint">Passwords do not match</p>
</div>
<p v-if="error" class="error-msg">{{ error }}</p>
<button type="submit" class="btn-submit" :disabled="!canSubmit">
<button type="submit" class="btn-primary btn-block" :disabled="!canSubmit">
{{ submitting ? "Creating Account..." : "Create Account" }}
</button>
</form>
@@ -247,24 +247,6 @@ async function handleSubmit() {
font-size: 0.9rem;
margin: 0 0 0.75rem;
}
.btn-submit {
width: 100%;
padding: 0.6rem;
background: var(--color-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.95rem;
font-weight: 600;
}
.btn-submit:disabled {
opacity: 0.6;
cursor: default;
}
.btn-submit:hover:not(:disabled) {
opacity: 0.9;
}
.auth-footer {
text-align: center;
font-size: 0.9rem;
+1 -19
View File
@@ -117,7 +117,7 @@ async function handleSubmit() {
<p v-if="passwordMismatch" class="error-hint">Passwords do not match</p>
</div>
<p v-if="error" class="error-msg">{{ error }}</p>
<button type="submit" class="btn-submit" :disabled="!canSubmit">
<button type="submit" class="btn-primary btn-block" :disabled="!canSubmit">
{{ submitting ? "Creating account..." : "Create Account" }}
</button>
</form>
@@ -216,24 +216,6 @@ async function handleSubmit() {
font-size: 0.9rem;
margin: 0 0 0.75rem;
}
.btn-submit {
width: 100%;
padding: 0.6rem;
background: var(--color-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.95rem;
font-weight: 600;
}
.btn-submit:disabled {
opacity: 0.6;
cursor: default;
}
.btn-submit:hover:not(:disabled) {
opacity: 0.9;
}
.auth-footer {
text-align: center;
font-size: 0.9rem;
+1 -19
View File
@@ -88,7 +88,7 @@ async function handleSubmit() {
<p v-if="passwordMismatch" class="error-hint">Passwords do not match</p>
</div>
<p v-if="error" class="error-msg">{{ error }}</p>
<button type="submit" class="btn-submit" :disabled="!canSubmit">
<button type="submit" class="btn-primary btn-block" :disabled="!canSubmit">
{{ submitting ? "Resetting..." : "Reset Password" }}
</button>
</form>
@@ -195,24 +195,6 @@ async function handleSubmit() {
font-size: 0.9rem;
margin: 0 0 0.75rem;
}
.btn-submit {
width: 100%;
padding: 0.6rem;
background: var(--color-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.95rem;
font-weight: 600;
}
.btn-submit:disabled {
opacity: 0.6;
cursor: default;
}
.btn-submit:hover:not(:disabled) {
opacity: 0.9;
}
.auth-footer {
text-align: center;
font-size: 0.9rem;
+26 -239
View File
@@ -1165,7 +1165,7 @@ function formatUserDate(iso: string): string {
<p class="field-hint">Click Detect to auto-fill from your browser.</p>
</div>
<div class="actions">
<button class="btn-save" @click="saveTimezone" :disabled="savingTimezone">
<button class="btn-primary" @click="saveTimezone" :disabled="savingTimezone">
{{ savingTimezone ? 'Saving…' : 'Save' }}
</button>
<span v-if="timezoneSaved" class="saved-msg">Saved!</span>
@@ -1190,7 +1190,7 @@ function formatUserDate(iso: string): string {
<p class="field-hint">Set to <strong>0</strong> to keep deleted items forever (never auto-purge).</p>
</div>
<div class="actions">
<button class="btn-save" @click="saveRetention" :disabled="savingRetention">
<button class="btn-primary" @click="saveRetention" :disabled="savingRetention">
{{ savingRetention ? 'Saving' : 'Save' }}
</button>
<span v-if="retentionSaved" class="saved-msg">Saved!</span>
@@ -1313,7 +1313,7 @@ function formatUserDate(iso: string): string {
</p>
</div>
<div class="actions">
<button class="btn-save" @click="saveKbInject" :disabled="savingKbInject">
<button class="btn-primary" @click="saveKbInject" :disabled="savingKbInject">
{{ savingKbInject ? 'Saving' : 'Save' }}
</button>
<span v-if="kbInjectSaved" class="saved-msg">Saved!</span>
@@ -1361,7 +1361,7 @@ function formatUserDate(iso: string): string {
</div>
<div class="actions">
<button
class="btn-save"
class="btn-primary"
@click="changeEmail"
:disabled="changingEmail || !emailPassword"
>
@@ -1409,7 +1409,7 @@ function formatUserDate(iso: string): string {
</div>
<div class="actions">
<button
class="btn-save"
class="btn-primary"
@click="changePassword"
:disabled="changingPassword || !currentPassword || newPassword.length < 8 || newPassword !== confirmNewPassword"
>
@@ -1465,7 +1465,7 @@ function formatUserDate(iso: string): string {
</div>
</div>
<div class="actions">
<button class="btn-save" @click="saveProfile" :disabled="profileSaving">{{ profileSaving ? 'Saving' : 'Save' }}</button>
<button class="btn-primary" @click="saveProfile" :disabled="profileSaving">{{ profileSaving ? 'Saving' : 'Save' }}</button>
<span v-if="profileSaved" class="saved-msg">Saved!</span>
</div>
</section>
@@ -1491,7 +1491,7 @@ function formatUserDate(iso: string): string {
</div>
</div>
<div class="actions">
<button class="btn-save" @click="saveProfile" :disabled="profileSaving">{{ profileSaving ? 'Saving' : 'Save' }}</button>
<button class="btn-primary" @click="saveProfile" :disabled="profileSaving">{{ profileSaving ? 'Saving' : 'Save' }}</button>
<span v-if="profileSaved" class="saved-msg">Saved!</span>
</div>
</section>
@@ -1501,7 +1501,7 @@ function formatUserDate(iso: string): string {
<p class="section-desc">Topics you care about — used to personalise the journal's daily prep and chat responses.</p>
<TagInput v-model="profile.interests" placeholder="Add an interest…" :fetchTags="emptyTagsFetch" />
<div class="actions">
<button class="btn-save" @click="saveProfile" :disabled="profileSaving">{{ profileSaving ? 'Saving…' : 'Save' }}</button>
<button class="btn-primary" @click="saveProfile" :disabled="profileSaving">{{ profileSaving ? 'Saving…' : 'Save' }}</button>
<span v-if="profileSaved" class="saved-msg">Saved!</span>
</div>
</section>
@@ -1533,7 +1533,7 @@ function formatUserDate(iso: string): string {
</div>
</div>
<div class="actions">
<button class="btn-save" @click="saveProfile" :disabled="profileSaving">{{ profileSaving ? 'Saving…' : 'Save' }}</button>
<button class="btn-primary" @click="saveProfile" :disabled="profileSaving">{{ profileSaving ? 'Saving…' : 'Save' }}</button>
<span v-if="profileSaved" class="saved-msg">Saved!</span>
</div>
</section>
@@ -1562,7 +1562,7 @@ function formatUserDate(iso: string): string {
<p class="field-hint">Emails for logins, logouts, and password changes.</p>
</div>
<div class="actions">
<button class="btn-save" @click="saveNotifications" :disabled="savingNotifications">
<button class="btn-primary" @click="saveNotifications" :disabled="savingNotifications">
{{ savingNotifications ? "Saving..." : "Save" }}
</button>
<span v-if="notificationsSaved" class="saved-msg">Saved!</span>
@@ -1589,7 +1589,7 @@ function formatUserDate(iso: string): string {
placeholder="Enter a search query..."
@keydown="onSearchKeydown"
/>
<button class="btn-save" @click="testSearch" :disabled="searchLoading || !searchQuery.trim()">
<button class="btn-primary" @click="testSearch" :disabled="searchLoading || !searchQuery.trim()">
{{ searchLoading ? "Searching..." : "Search" }}
</button>
</div>
@@ -1891,7 +1891,7 @@ function formatUserDate(iso: string): string {
/>
</div>
<div class="actions">
<button class="btn-save" @click="saveBaseUrl" :disabled="savingBaseUrl">
<button class="btn-primary" @click="saveBaseUrl" :disabled="savingBaseUrl">
{{ savingBaseUrl ? "Saving..." : "Save" }}
</button>
<span v-if="baseUrlSaved" class="saved-msg">Saved!</span>
@@ -1916,7 +1916,7 @@ function formatUserDate(iso: string): string {
/>
</div>
<div class="actions">
<button class="btn-save" @click="saveMarketplaceUrl" :disabled="savingMarketplaceUrl">
<button class="btn-primary" @click="saveMarketplaceUrl" :disabled="savingMarketplaceUrl">
{{ savingMarketplaceUrl ? "Saving..." : "Save" }}
</button>
<span v-if="marketplaceUrlSaved" class="saved-msg">Saved!</span>
@@ -1946,10 +1946,10 @@ function formatUserDate(iso: string): string {
</select>
</div>
<div class="actions">
<button class="btn-save" @click="saveDbMaintenance" :disabled="savingDbMaint">
<button class="btn-primary" @click="saveDbMaintenance" :disabled="savingDbMaint">
{{ savingDbMaint ? "Saving..." : "Save" }}
</button>
<button class="btn-save btn-secondary" @click="runDbMaintenanceNow" :disabled="runningDbMaint">
<button class="btn-secondary" @click="runDbMaintenanceNow" :disabled="runningDbMaint">
{{ runningDbMaint ? "Running..." : "Run now" }}
</button>
<span v-if="dbMaintSaved" class="saved-msg">Saved!</span>
@@ -2040,7 +2040,7 @@ function formatUserDate(iso: string): string {
<p class="field-hint">Recommended for port 587. Implicit TLS is used automatically for port 465.</p>
</div>
<div class="actions" style="margin-bottom: 1.25rem;">
<button class="btn-save" @click="saveSmtp" :disabled="savingSmtp">
<button class="btn-primary" @click="saveSmtp" :disabled="savingSmtp">
{{ savingSmtp ? "Saving..." : "Save SMTP Settings" }}
</button>
<span v-if="smtpSaved" class="saved-msg">Saved!</span>
@@ -2054,7 +2054,7 @@ function formatUserDate(iso: string): string {
placeholder="test@example.com"
class="input"
/>
<button class="btn-save" @click="sendTestEmail" :disabled="sendingTest || !testRecipient.trim()">
<button class="btn-primary" @click="sendTestEmail" :disabled="sendingTest || !testRecipient.trim()">
{{ sendingTest ? "Sending..." : "Send Test" }}
</button>
</div>
@@ -2079,7 +2079,7 @@ function formatUserDate(iso: string): string {
<p class="field-hint">When closed, new users can only be added by an administrator.</p>
</div>
<button
class="btn-toggle"
class="btn-primary btn-toggle"
:class="registrationOpen ? 'btn-toggle-close' : 'btn-toggle-open'"
@click="toggleRegistration"
:disabled="toggling"
@@ -2100,7 +2100,7 @@ function formatUserDate(iso: string): string {
required
:disabled="sendingInvite"
/>
<button type="submit" class="btn-save" :disabled="sendingInvite || !inviteEmail.trim()">
<button type="submit" class="btn-primary" :disabled="sendingInvite || !inviteEmail.trim()">
{{ sendingInvite ? "Sending..." : "Send Invite" }}
</button>
</form>
@@ -2122,7 +2122,7 @@ function formatUserDate(iso: string): string {
<td class="hide-mobile cell-date">{{ formatUserDate(inv.created_at) }}</td>
<td class="hide-mobile cell-date">{{ formatUserDate(inv.expires_at) }}</td>
<td class="cell-actions">
<button class="btn-delete" @click="revokeInvitation(inv.id)" :disabled="revokingId !== null">
<button class="btn-ghost btn-compact" @click="revokeInvitation(inv.id)" :disabled="revokingId !== null">
{{ revokingId === inv.id ? "Revoking..." : "Revoke" }}
</button>
</td>
@@ -2161,13 +2161,13 @@ function formatUserDate(iso: string): string {
<span class="you-label">You</span>
</template>
<template v-else-if="confirmDeleteId === u.id">
<button class="btn-confirm-delete" @click="confirmDelete(u.id)" :disabled="deleting !== null">
<button class="btn-danger btn-compact" @click="confirmDelete(u.id)" :disabled="deleting !== null">
{{ deleting === u.id ? "Deleting..." : "Confirm" }}
</button>
<button class="btn-cancel-delete" @click="cancelDelete">Cancel</button>
<button class="btn-ghost btn-compact" @click="cancelDelete">Cancel</button>
</template>
<template v-else>
<button class="btn-delete" @click="confirmDelete(u.id)" :disabled="deleting !== null">Delete</button>
<button class="btn-ghost btn-compact" @click="confirmDelete(u.id)" :disabled="deleting !== null">Delete</button>
</template>
</td>
</tr>
@@ -2305,10 +2305,10 @@ function formatUserDate(iso: string): string {
<span v-if="g.description" class="group-desc">{{ g.description }}</span>
</div>
<div class="group-card-actions">
<button class="btn-sm" @click="toggleGroupExpand(g)">
<button class="btn-ghost btn-compact" @click="toggleGroupExpand(g)">
{{ expandedGroupId === g.id ? 'Collapse' : 'Manage' }}
</button>
<button class="btn-sm btn-danger-sm" @click="deleteGroupConfirm(g)">Delete</button>
<button class="btn-danger-outline btn-compact" @click="deleteGroupConfirm(g)">Delete</button>
</div>
</div>
@@ -2338,7 +2338,7 @@ function formatUserDate(iso: string): string {
<li v-for="m in (groupMembers[g.id] || [])" :key="m.user_id" class="member-row">
<span class="member-name">{{ m.username }}</span>
<span class="member-role-badge" :class="`role-${m.role}`">{{ m.role }}</span>
<button class="btn-sm btn-danger-sm" @click="removeMemberFromGroup(g.id, m.user_id)">Remove</button>
<button class="btn-danger-outline btn-compact" @click="removeMemberFromGroup(g.id, m.user_id)">Remove</button>
</li>
<li v-if="!(groupMembers[g.id]?.length)" class="members-empty">No members yet.</li>
</ul>
@@ -2564,82 +2564,6 @@ function formatUserDate(iso: string): string {
flex-wrap: wrap;
}
/* Save: Moss action-primary per Hybrid */
.btn-save {
padding: 0.4rem 0.9rem;
background: var(--color-action-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.875rem;
font-family: inherit;
white-space: nowrap;
transition: background 0.15s;
}
.btn-save:disabled { opacity: 0.6; cursor: default; }
.btn-save:hover:not(:disabled) { background: var(--color-action-primary-hover); }
/* Danger outline (Invalidate sessions, Clear observations, etc.):
Oxblood action-destructive ghost — fills on hover */
.btn-danger-outline {
padding: 0.4rem 0.9rem;
background: none;
color: var(--color-action-destructive);
border: 1px solid var(--color-action-destructive);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.875rem;
font-family: inherit;
white-space: nowrap;
transition: background 0.15s, color 0.15s;
}
.btn-danger-outline:hover:not(:disabled) {
background: var(--color-action-destructive);
color: var(--fs-text-on-action);
}
.btn-danger-outline:disabled { opacity: 0.5; cursor: default; }
/* Filled destructive: Oxblood action-destructive */
.btn-danger {
padding: 0.4rem 0.9rem;
background: var(--color-action-destructive);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.875rem;
font-family: inherit;
white-space: nowrap;
transition: background 0.15s;
}
.btn-danger:hover:not(:disabled) { background: var(--color-action-destructive-hover); }
.btn-danger:disabled { opacity: 0.5; cursor: default; }
/* Secondary: Bronze action-secondary — alternate paths (Detect, Test,
Refresh, Add slot, etc.). Outline form for visual lightness. */
.btn-secondary {
padding: 0.4rem 0.9rem;
background: var(--color-action-secondary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.875rem;
font-family: inherit;
white-space: nowrap;
transition: background 0.15s;
}
.btn-secondary:hover:not(:disabled) { background: var(--color-action-secondary-hover); }
.btn-secondary:disabled { opacity: 0.6; cursor: default; }
/* DB maintenance last-run summary */
.db-maint-last { margin-top: 1rem; }
.db-maint-last-label {
display: block;
font-size: 0.8rem;
color: var(--color-text-muted);
margin-bottom: 0.4rem;
}
.db-maint-table-list {
list-style: none;
margin: 0;
@@ -2985,68 +2909,6 @@ function formatUserDate(iso: string): string {
}
.you-label { font-size: 0.8rem; color: var(--color-text-muted); }
/* Per-row delete (users / invitations / etc.): ghost → Oxblood on hover */
.btn-delete {
padding: 0.25rem 0.6rem;
background: transparent;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.8rem;
}
.btn-delete:hover:not(:disabled) { border-color: var(--color-action-destructive); color: var(--color-action-destructive); }
.btn-delete:disabled { opacity: 0.4; cursor: default; }
/* Two-stage destructive: Confirm = Oxblood filled, Cancel = Bronze ghost */
.btn-confirm-delete {
padding: 0.25rem 0.6rem;
background: var(--color-action-destructive);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.8rem;
font-weight: 500;
margin-right: 0.25rem;
transition: background 0.15s;
}
.btn-confirm-delete:hover:not(:disabled) { background: var(--color-action-destructive-hover); }
.btn-confirm-delete:disabled { opacity: 0.6; cursor: default; }
.btn-cancel-delete {
padding: 0.25rem 0.6rem;
background: transparent;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.8rem;
}
.btn-cancel-delete:hover { color: var(--color-text); border-color: var(--color-text-muted); }
/* Toggle (Open/Close registration, etc.): Open = Moss, Close = Pewter ghost */
.btn-toggle {
padding: 0.45rem 1rem;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
white-space: nowrap;
transition: background 0.15s;
}
.btn-toggle:disabled { opacity: 0.6; cursor: default; }
.btn-toggle-open { background: var(--color-action-primary); color: var(--fs-text-on-action); }
.btn-toggle-open:hover:not(:disabled) { background: var(--color-action-primary-hover); }
.btn-toggle-close {
background: var(--color-bg-secondary);
color: var(--color-text);
border: 1px solid var(--color-border);
}
.btn-toggle-close:hover:not(:disabled) { border-color: var(--color-warning); color: var(--color-warning); }
.loading-msg, .empty-msg {
text-align: center;
color: var(--color-text-muted);
font-size: 0.9rem;
padding: 1rem 0;
}
/* Logs panel */
.stats-section { padding: 1rem 1.25rem; }
@@ -3148,20 +3010,6 @@ function formatUserDate(iso: string): string {
/* ── Groups tab ──────────────────────────────────────────────── */
/* Moss action-primary per Hybrid */
.btn-primary {
padding: 0.4rem 0.9rem;
background: var(--color-action-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.875rem;
font-family: inherit;
white-space: nowrap;
transition: background 0.15s;
}
.btn-primary:disabled { opacity: 0.6; cursor: default; }
.btn-primary:hover:not(:disabled) { background: var(--color-action-primary-hover); }
.input-field {
width: 100%;
@@ -3244,32 +3092,6 @@ function formatUserDate(iso: string): string {
flex-shrink: 0;
}
.btn-sm {
padding: 0.25rem 0.6rem;
background: none;
border: 1px solid var(--color-border);
border-radius: 4px;
font-size: 0.78rem;
color: var(--color-text-secondary);
cursor: pointer;
font-family: inherit;
transition: border-color 0.15s, color 0.15s;
}
.btn-sm:hover { border-color: var(--color-primary); color: var(--color-primary); }
.btn-danger-sm:hover { border-color: var(--color-action-destructive); color: var(--color-action-destructive); }
.group-members-panel {
padding: 0.75rem 1rem 1rem;
border-top: 1px solid var(--color-border);
background: var(--color-surface);
}
.members-search {
display: flex;
gap: 0.5rem;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.member-search-wrap {
flex: 1;
@@ -3782,21 +3604,6 @@ function formatUserDate(iso: string): string {
text-align: right;
color: var(--color-text-secondary);
}
.btn-remove-slot {
background: none;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
color: var(--color-text-muted);
cursor: pointer;
font-size: 0.75rem;
padding: 0.2rem 0.45rem;
line-height: 1;
transition: color 0.15s, border-color 0.15s;
}
.btn-remove-slot:hover {
color: var(--color-danger, #e05555);
border-color: var(--color-danger, #e05555);
}
.blend-actions {
margin-top: 0.25rem;
}
@@ -3850,24 +3657,4 @@ function formatUserDate(iso: string): string {
color: var(--color-text-muted);
margin-bottom: 0.75rem;
}
.btn-danger-outline {
padding: 0.45rem 1rem;
background: none;
border: 1px solid var(--color-action-destructive);
border-radius: var(--radius-sm);
color: var(--color-action-destructive);
font-size: 0.9rem;
cursor: pointer;
font-family: inherit;
transition: background 0.15s, color 0.15s;
}
.btn-danger-outline:hover:not(:disabled) {
background: var(--color-action-destructive);
color: var(--fs-text-on-action);
}
.btn-danger-outline:disabled { opacity: 0.5; cursor: default; }
@keyframes va-dot-bounce {
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
40% { transform: scale(1); opacity: 1; }
}
</style>
-28
View File
@@ -249,34 +249,6 @@ async function confirmDelete() {
flex-shrink: 0;
}
.btn-ghost {
padding: 0.35rem 0.8rem;
border: 1px solid var(--color-border);
background: transparent;
color: var(--color-text);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.85rem;
font-family: inherit;
transition: border-color 0.15s, color 0.15s;
}
.btn-ghost:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
.btn-danger {
padding: 0.35rem 0.8rem;
border: none;
background: var(--color-action-destructive, #6B2118);
color: var(--fs-text-on-action);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.85rem;
font-family: inherit;
}
.btn-danger:hover {
opacity: 0.9;
}
.when-to-use {
margin: 0.75rem 0 1.25rem;
-31
View File
@@ -572,37 +572,6 @@ function cancel() {
gap: 0.5rem;
margin-top: 0.25rem;
}
.btn-primary {
padding: 0.5rem 1.1rem;
background: var(--color-action-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-family: inherit;
transition: background 0.15s;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-action-primary-hover);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: default;
}
.btn-secondary {
padding: 0.5rem 1.1rem;
background: var(--color-bg-secondary);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-family: inherit;
}
.btn-secondary:hover {
background: var(--color-bg);
}
@media (max-width: 600px) {
.field-row,
+3 -37
View File
@@ -543,21 +543,6 @@ function usageTitle(s: SnippetListItem): string {
}
/* Moss action-primary per Hybrid — utility action, not a brand moment. */
.btn-primary {
padding: 0.45rem 1rem;
background: var(--color-action-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-family: inherit;
transition: background 0.15s;
white-space: nowrap;
}
.btn-primary:hover {
background: var(--color-action-primary-hover);
}
.search-row {
margin-bottom: 1.25rem;
@@ -669,16 +654,16 @@ function usageTitle(s: SnippetListItem): string {
.empty-action {
display: inline-block;
padding: 0.4rem 1rem;
border: 1px solid var(--color-primary);
border: 1px solid var(--color-action-primary);
border-radius: var(--radius-sm);
color: var(--color-primary);
color: var(--color-action-primary);
background: none;
cursor: pointer;
font-size: 0.85rem;
transition: background 0.15s, color 0.15s;
}
.empty-action:hover {
background: var(--color-primary);
background: var(--color-action-primary);
color: var(--fs-text-on-action);
}
@@ -870,21 +855,6 @@ function usageTitle(s: SnippetListItem): string {
gap: 0.5rem;
align-items: center;
}
.btn-ghost {
padding: 0.4rem 0.85rem;
border: 1px solid var(--color-border);
background: transparent;
color: var(--color-text);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-family: inherit;
transition: border-color 0.15s, color 0.15s;
}
.btn-ghost:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
/* Selected card = 2px accent border per the design system (featured/active). */
.snippet-card.selected {
@@ -931,10 +901,6 @@ function usageTitle(s: SnippetListItem): string {
flex: 1;
min-width: 0;
}
.select-bar .btn-primary:disabled {
opacity: 0.5;
cursor: default;
}
/* Merge modal */
.modal-overlay {
+4 -68
View File
@@ -645,7 +645,7 @@ useEditorGuards(dirty, save);
@focus="onParentFocus"
@blur="hideParentDropdown"
/>
<button v-if="parentId" class="btn-clear-parent" @click="clearParentTask" title="Clear">&times;</button>
<button v-if="parentId" class="btn-text btn-clear-parent" @click="clearParentTask" title="Clear">&times;</button>
</div>
<div v-if="showParentDropdown" class="parent-dropdown">
<div v-if="parentSearchLoading" class="parent-dropdown-item parent-empty">Searching...</div>
@@ -666,7 +666,7 @@ useEditorGuards(dirty, save);
<div v-if="isEditing" class="subtasks-section">
<div class="subtasks-header">
<span class="subtasks-label">Sub-tasks</span>
<button class="btn-add-subtask" @click="addingSubTask = !addingSubTask">+ Add</button>
<button class="btn-text" @click="addingSubTask = !addingSubTask">+ Add</button>
</div>
<div v-if="subTasksLoading" class="subtasks-loading">Loading...</div>
<template v-else>
@@ -686,8 +686,8 @@ useEditorGuards(dirty, save);
@keydown.escape="addingSubTask = false; newSubTaskTitle = ''"
autofocus
/>
<button class="btn-subtask-confirm" @click="createSubTask" :disabled="!newSubTaskTitle.trim()">Add</button>
<button class="btn-subtask-cancel" @click="addingSubTask = false; newSubTaskTitle = ''">Cancel</button>
<button class="btn-primary btn-compact" @click="createSubTask" :disabled="!newSubTaskTitle.trim()">Add</button>
<button class="btn-ghost btn-compact" @click="addingSubTask = false; newSubTaskTitle = ''">Cancel</button>
</div>
</template>
</div>
@@ -921,23 +921,6 @@ useEditorGuards(dirty, save);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.btn-add-subtask {
background: none;
border: none;
cursor: pointer;
color: var(--color-primary);
font-size: 0.78rem;
font-family: inherit;
padding: 0.1rem 0.2rem;
}
.btn-add-subtask:hover { opacity: 0.8; }
.subtasks-loading { font-size: 0.78rem; color: var(--color-text-muted); }
.subtask-row {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.2rem 0;
}
.subtask-checkbox { flex-shrink: 0; cursor: pointer; }
.subtask-title {
font-size: 0.83rem;
@@ -968,33 +951,6 @@ useEditorGuards(dirty, save);
font-family: inherit;
}
.subtask-input:focus { outline: none; border-color: var(--color-primary); }
.btn-subtask-confirm {
padding: 0.25rem 0.5rem;
background: var(--color-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.78rem;
font-family: inherit;
}
.btn-subtask-confirm:disabled { opacity: 0.5; cursor: default; }
.btn-subtask-cancel {
padding: 0.25rem 0.5rem;
background: none;
border: 1px solid var(--color-border);
color: var(--color-text-secondary);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.78rem;
font-family: inherit;
}
/* Streaming preview */
.stream-label {
font-size: 0.8rem;
color: var(--color-text-muted);
}
.stream-preview {
border: 1px solid var(--color-input-border);
border-radius: var(--radius-sm);
@@ -1125,24 +1081,4 @@ useEditorGuards(dirty, save);
color: var(--color-primary, #6366f1);
font-style: normal;
}
.btn-reconsolidate {
margin-left: auto;
padding: 0.25rem 0.7rem;
font-size: 0.78rem;
font-style: normal;
color: inherit;
background: transparent;
border: 1px solid var(--color-border, rgba(255, 255, 255, 0.12));
border-radius: 999px;
cursor: pointer;
transition: background 120ms ease;
}
.btn-reconsolidate:hover:not(:disabled) {
background: rgba(99, 102, 241, 0.12);
border-color: var(--color-primary, #6366f1);
}
.btn-reconsolidate:disabled {
opacity: 0.5;
cursor: progress;
}
</style>
+5 -82
View File
@@ -263,29 +263,29 @@ const subTaskProgress = computed(() => {
<div class="toolbar">
<router-link
:to="store.currentTask.project_id ? `/projects/${store.currentTask.project_id}` : '/tasks'"
class="btn-back"
class="btn-ghost"
>{{ store.currentTask.project_id ? "← Project" : "← Tasks" }}</router-link>
<router-link
:to="`/tasks/${store.currentTask.id}/edit`"
class="btn-edit"
class="btn-primary"
>
Edit
</router-link>
<button
v-if="advanceLabel"
class="btn-advance"
class="btn-primary"
@click="advanceStatus"
>
{{ advanceLabel }}
</button>
<button
class="btn-convert"
class="btn-secondary btn-compact"
@click="convertToNote"
:disabled="converting"
>
{{ converting ? "Converting..." : "Convert to Note" }}
</button>
<button class="btn-share" @click="showShare = true">Share</button>
<button class="btn-secondary btn-compact" @click="showShare = true">Share</button>
</div>
<!-- Breadcrumb: parent task project milestone -->
@@ -475,83 +475,6 @@ const subTaskProgress = computed(() => {
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.btn-back {
display: inline-flex;
align-items: center;
padding: 0.45rem 1rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: none;
color: var(--color-text-secondary);
text-decoration: none;
cursor: pointer;
font-size: 0.9rem;
}
.btn-back:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
/* Edit + Advance: Moss action-primary — both are "operating the software"
workflow actions, not brand moments. */
.btn-edit,
.btn-advance {
display: inline-flex;
align-items: center;
padding: 0.45rem 1.1rem;
border: none;
border-radius: var(--radius-sm);
background: var(--color-action-primary);
color: var(--fs-text-on-action);
text-decoration: none;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: background 0.15s;
}
.btn-edit:hover,
.btn-advance:hover {
background: var(--color-action-primary-hover);
color: var(--fs-text-on-action);
}
/* Convert + Share: Bronze action-secondary — alternate paths */
.btn-convert {
margin-left: auto;
padding: 0.3rem 0.75rem;
background: var(--color-action-secondary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.85rem;
transition: background 0.15s;
}
.btn-convert:hover { background: var(--color-action-secondary-hover); }
.btn-convert:disabled {
opacity: 0.6;
cursor: default;
}
.btn-share {
padding: 0.3rem 0.75rem;
background: var(--color-action-secondary);
border: none;
border-radius: var(--radius-sm);
color: var(--fs-text-on-action);
cursor: pointer;
font-size: 0.85rem;
font-family: inherit;
transition: background 0.15s;
}
.btn-share:hover { background: var(--color-action-secondary-hover); }
.task-title {
font-family: "Fraunces", Georgia, serif;
font-size: 2rem;
font-weight: 500;
line-height: 1.2;
margin: 0.25rem 0 0.5rem;
color: var(--color-text);
}
.meta {
display: flex;
+5 -19
View File
@@ -25,7 +25,7 @@ onMounted(() => store.fetchTrash());
<h1>Trash</h1>
<button
v-if="store.batches.length"
class="btn-empty"
class="btn-ghost btn-compact"
@click="empty"
>Empty trash</button>
</header>
@@ -52,8 +52,8 @@ onMounted(() => store.fetchTrash());
</div>
</div>
<div class="batch-actions">
<button class="btn-restore" @click="store.restore(b.batch_id)">Restore</button>
<button class="btn-purge" @click="purge(b.batch_id)">Delete permanently</button>
<button class="btn-ghost btn-compact btn-restore" @click="store.restore(b.batch_id)">Restore</button>
<button class="btn-ghost btn-compact btn-purge" @click="purge(b.batch_id)">Delete permanently</button>
</div>
</li>
</ul>
@@ -64,25 +64,11 @@ onMounted(() => store.fetchTrash());
.trash-page { max-width: 900px; margin: 0 auto; padding: 1.5rem; }
.trash-header { display: flex; align-items: center; justify-content: space-between; }
.trash-header h1 { font-family: Fraunces, serif; font-style: italic; margin: 0; }
.btn-empty {
background: none; border: 1px solid var(--color-border, #2a2a2e);
color: inherit; border-radius: 6px; padding: 0.4rem 0.8rem; cursor: pointer;
}
.btn-empty:hover { border-color: var(--color-danger, #ef4444); color: var(--color-danger, #ef4444); }
.trash-note { opacity: 0.7; font-size: 0.9em; margin: 0.5rem 0 1.5rem; }
.trash-loading, .trash-empty { opacity: 0.6; font-style: italic; padding: 2rem 0; }
.batch-list { list-style: none; padding: 0; margin: 0; }
.batch {
display: flex; align-items: center; justify-content: space-between;
gap: 1rem; padding: 0.85rem 1rem; margin-bottom: 0.5rem;
background: var(--color-surface, #18181b); border-radius: 8px;
border-left: 2px solid var(--color-border, #2a2a2e);
}
.batch-summary { font-weight: 500; }
.batch-count { opacity: 0.6; font-weight: 400; font-size: 0.9em; margin-left: 0.35rem; }
.batch-meta { font-size: 0.82em; opacity: 0.6; margin-top: 0.25rem; }
.batch-actions { display: flex; gap: 0.5rem; flex-shrink: 0; }
.batch-actions button { border-radius: 6px; padding: 0.35rem 0.7rem; cursor: pointer; border: 1px solid var(--color-border, #2a2a2e); background: none; color: inherit; }
.btn-restore:hover { border-color: var(--color-primary, #6366f1); color: var(--color-primary, #6366f1); }
.btn-purge:hover { border-color: var(--color-danger, #ef4444); color: var(--color-danger, #ef4444); }
.btn-restore:hover { border-color: var(--color-action-primary); color: var(--color-action-primary); }
.btn-purge:hover { border-color: var(--color-action-destructive); color: var(--color-action-destructive); }
</style>
+8 -92
View File
@@ -167,7 +167,7 @@ function formatDate(iso: string): string {
</p>
</div>
<button
class="btn-toggle"
class="btn-primary btn-toggle"
:class="registrationOpen ? 'btn-toggle-close' : 'btn-toggle-open'"
@click="toggleRegistration"
:disabled="toggling"
@@ -190,7 +190,7 @@ function formatDate(iso: string): string {
/>
<button
type="submit"
class="btn-invite"
class="btn-primary"
:disabled="sendingInvite || !inviteEmail.trim()"
>
{{ sendingInvite ? "Sending..." : "Send Invite" }}
@@ -216,7 +216,7 @@ function formatDate(iso: string): string {
<td class="hide-mobile cell-date">{{ formatDate(inv.expires_at) }}</td>
<td class="cell-actions">
<button
class="btn-delete"
class="btn-ghost btn-compact"
@click="revokeInvitation(inv.id)"
:disabled="revokingId !== null"
>
@@ -262,17 +262,17 @@ function formatDate(iso: string): string {
</template>
<template v-else-if="confirmDeleteId === u.id">
<button
class="btn-confirm-delete"
class="btn-danger btn-compact"
@click="confirmDelete(u.id)"
:disabled="deleting !== null"
>
{{ deleting === u.id ? "Deleting..." : "Confirm" }}
</button>
<button class="btn-cancel-delete" @click="cancelDelete">Cancel</button>
<button class="btn-ghost btn-compact" @click="cancelDelete">Cancel</button>
</template>
<template v-else>
<button
class="btn-delete"
class="btn-ghost btn-compact"
@click="confirmDelete(u.id)"
:disabled="deleting !== null"
>
@@ -328,24 +328,6 @@ function formatDate(iso: string): string {
outline: none;
border-color: var(--color-primary);
}
.btn-invite {
padding: 0.45rem 1rem;
background: var(--color-primary);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
white-space: nowrap;
}
.btn-invite:disabled {
opacity: 0.6;
cursor: default;
}
.btn-invite:hover:not(:disabled) {
opacity: 0.9;
}
.invite-list {
margin-top: 1rem;
}
@@ -380,26 +362,8 @@ function formatDate(iso: string): string {
font-size: 0.8rem;
color: var(--color-text-muted);
}
.btn-toggle {
padding: 0.45rem 1rem;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
white-space: nowrap;
}
.btn-toggle:disabled {
opacity: 0.6;
cursor: default;
}
.btn-toggle-open {
background: var(--color-primary);
color: var(--fs-text-on-action);
}
.btn-toggle-open:hover:not(:disabled) {
opacity: 0.9;
}
/* The one genuine override: 'close registration' must NOT read as the
primary action it sits on. Scoped, so it beats the shared variant. */
.btn-toggle-close {
background: var(--color-bg-secondary);
color: var(--color-text);
@@ -478,54 +442,6 @@ function formatDate(iso: string): string {
font-size: 0.8rem;
color: var(--color-text-muted);
}
.btn-delete {
padding: 0.25rem 0.6rem;
background: transparent;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.8rem;
}
.btn-delete:hover:not(:disabled) {
border-color: var(--color-danger);
color: var(--color-danger);
}
.btn-delete:disabled {
opacity: 0.4;
cursor: default;
}
.btn-confirm-delete {
padding: 0.25rem 0.6rem;
background: var(--color-danger);
color: var(--fs-text-on-action);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
margin-right: 0.25rem;
}
.btn-confirm-delete:hover:not(:disabled) {
filter: brightness(0.9);
}
.btn-confirm-delete:disabled {
opacity: 0.6;
cursor: default;
}
.btn-cancel-delete {
padding: 0.25rem 0.6rem;
background: transparent;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.8rem;
}
.btn-cancel-delete:hover {
color: var(--color-text);
border-color: var(--color-text-muted);
}
@media (max-width: 768px) {
.registration-row {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "scribe",
"description": "Scribe system-of-record for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your always-on rules + active-project context, process-skills (writing-plans, systematic-debugging, verification, brainstorming, reusing-code), and your saved Scribe Processes auto-surfaced as skills (/scribe:sync). Replaces superpowers + file-memory with one app-backed plugin.",
"version": "0.1.21",
"version": "0.1.22",
"author": { "name": "Bryan Van Deusen" },
"mcpServers": {
"scribe": {
+101 -18
View File
@@ -48,16 +48,9 @@ case "$file_path" in
exit 0 ;;
esac
url=${SCRIBE_URL:-${CLAUDE_PLUGIN_OPTION_API_ENDPOINT:-}}
token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_API_TOKEN:-}}
# Guard against an unexpanded ${...} placeholder arriving as a literal.
case "$url" in *'${'*) url="" ;; esac
case "$token" in *'${'*) token="" ;; esac
# Unconfigured install → silent. Prior-art recall is pure enrichment.
[ -n "$url" ] && [ -n "$token" ] || exit 0
# Snippet locations are recorded repo-relative, so send a repo-relative path —
# an absolute one would simply match nothing.
# an absolute one would simply match nothing. Resolved BEFORE the config gate
# because the local arm below needs the repo root and needs no server at all.
lookup_dir=$(dirname -- "$file_path" 2>/dev/null || true)
[ -d "$lookup_dir" ] || lookup_dir=${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}
repo_root=$(git -C "$lookup_dir" rev-parse --show-toplevel 2>/dev/null || true)
@@ -68,6 +61,84 @@ if [ -n "$repo_root" ]; then
esac
fi
# ---------------------------------------------------------------------------
# ARM 1 — BY NAME, LOCALLY (#2280). Does a definition of this already exist?
#
# The other two arms ask Scribe what was RECORDED. Scribe has never read a line
# of the codebase, so a helper nobody thought to record is invisible to them —
# which is how `.btn-primary` came to be defined four times, in four scoped
# stylesheets, already diverged. It was never a snippet, so no threshold and no
# query rewrite could ever have surfaced it.
#
# This arm closes that by asking the only question the record cannot answer,
# in the only place that can: the hook already runs on the developer's machine,
# inside the repo, holding the code about to be written. No index, no storage,
# no staleness, and no server — it deliberately runs even on an install that
# has never configured Scribe.
#
# Definition-shaped patterns only. Grepping for bare occurrences would match
# every CALL site and drown the real finding — and a hint that is mostly noise
# is one people learn to skip, which is worse than none.
# ---------------------------------------------------------------------------
local_lines=""
if [ -n "$repo_root" ] && [ -n "$code" ]; then
# kind<TAB>name for each thing this payload DEFINES.
names=$(printf '%s' "$code" | awk '
match($0, /^[[:space:]]*\.[A-Za-z][A-Za-z0-9_-]*[[:space:]]*[,{]/) {
t = $0; sub(/^[[:space:]]*\./, "", t); sub(/[[:space:]]*[,{].*$/, "", t);
if (t != "") print "css\t" t; next }
match($0, /^[[:space:]]*(export[[:space:]]+)?(default[[:space:]]+)?(async[[:space:]]+)?function[[:space:]]+[A-Za-z_$][A-Za-z0-9_$]*/) {
t = $0; sub(/^.*function[[:space:]]+/, "", t); sub(/[^A-Za-z0-9_$].*$/, "", t);
if (t != "") print "sym\t" t; next }
match($0, /^[[:space:]]*(export[[:space:]]+)?class[[:space:]]+[A-Za-z_$][A-Za-z0-9_$]*/) {
t = $0; sub(/^.*class[[:space:]]+/, "", t); sub(/[^A-Za-z0-9_$].*$/, "", t);
if (t != "") print "sym\t" t; next }
match($0, /^[[:space:]]*(async[[:space:]]+)?def[[:space:]]+[A-Za-z_][A-Za-z0-9_]*/) {
t = $0; sub(/^.*def[[:space:]]+/, "", t); sub(/[^A-Za-z0-9_].*$/, "", t);
if (t != "") print "sym\t" t; next }
match($0, /^[[:space:]]*(export[[:space:]]+)?(const|let)[[:space:]]+[A-Za-z_$][A-Za-z0-9_$]*[[:space:]]*=[[:space:]]*(async[[:space:]]*)?[(<]/) {
t = $0; sub(/^[[:space:]]*(export[[:space:]]+)?(const|let)[[:space:]]+/, "", t);
sub(/[^A-Za-z0-9_$].*$/, "", t);
if (t != "") print "sym\t" t; next }
' 2>/dev/null | sort -u | head -12) || names=""
while IFS=$'\t' read -r kind name; do
[ -n "${name:-}" ] || continue
case "$kind" in
css) pat="^[[:space:]]*\.${name}[[:space:]]*[,{]" ;;
*) pat="(function|class|def)[[:space:]]+${name}[^A-Za-z0-9_]|(const|let)[[:space:]]+${name}[[:space:]]*=" ;;
esac
# -I skips binaries; :(exclude) drops the file being written, which would
# otherwise always match itself on an Edit.
hits=$(git -C "$repo_root" grep -I -l -E -e "$pat" -- . ":(exclude)${rel_path}" 2>/dev/null | head -4) || hits=""
[ -n "$hits" ] || continue
count=$(printf '%s\n' "$hits" | grep -c . 2>/dev/null || echo 0)
label=$([ "$kind" = css ] && printf '.%s' "$name" || printf '%s' "$name")
files=$(printf '%s' "$hits" | tr '\n' ' ' | sed 's/ $//')
local_lines="${local_lines}> - \`${label}\` is already defined in ${count} other file(s): ${files}"$'\n'
done <<< "$names"
fi
local_context=""
if [ -n "$local_lines" ]; then
local_context="> Already defined elsewhere in this repo — check before adding another copy (\`git grep\` shown; this is a nudge, not a gate):"$'\n'"${local_lines}"
fi
url=${SCRIBE_URL:-${CLAUDE_PLUGIN_OPTION_API_ENDPOINT:-}}
token=${SCRIBE_TOKEN:-${CLAUDE_PLUGIN_OPTION_API_TOKEN:-}}
# Guard against an unexpanded ${...} placeholder arriving as a literal.
case "$url" in *'${'*) url="" ;; esac
case "$token" in *'${'*) token="" ;; esac
# Unconfigured install → the recorded-prior-art arms are skipped, but the local
# arm above already ran and may have something to say.
if [ -z "$url" ] || [ -z "$token" ]; then
if [ -n "$local_context" ]; then
jq -n --arg c "$local_context" \
'{hookSpecificOutput: {hookEventName: "PreToolUse", additionalContext: $c}}'
fi
exit 0
fi
# Cap the code sent as the semantic query. The embedder truncates at its own
# token limit well before this, so a bigger slice buys no extra signal — and the
# payload has to stay a GET (a read-scoped API key cannot POST, and every other
@@ -110,20 +181,32 @@ if [ -n "$session_id" ]; then
fi
fi
# `|| true`, not `|| exit 0`: an unreachable instance must not discard a local
# finding that needed no instance to produce.
body=$(curl -fsS --max-time 5 \
-H "Authorization: Bearer ${token}" \
"${url%/}/api/plugin/prior-art?path=${path_enc}&code=${code_enc}${repo_q}${exclude_q}" 2>/dev/null) || exit 0
[ -n "$body" ] || exit 0
"${url%/}/api/plugin/prior-art?path=${path_enc}&code=${code_enc}${repo_q}${exclude_q}" 2>/dev/null) || body=""
context=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null) || exit 0
[ -n "$context" ] || exit 0
# Remember what was surfaced so it isn't shown again this session.
if [ -n "$idfile" ]; then
printf '%s' "$body" | jq -r '.note_ids[]? // empty' 2>/dev/null >> "$idfile" || true
context=""
if [ -n "$body" ]; then
context=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null) || context=""
# Remember what was surfaced so it isn't shown again this session.
if [ -n "$idfile" ] && [ -n "$context" ]; then
printf '%s' "$body" | jq -r '.note_ids[]? // empty' 2>/dev/null >> "$idfile" || true
fi
fi
# Local first. It answers "this already EXISTS", which is a stronger claim than
# "this resembles something recorded" — and it is the one the recorded arms are
# structurally unable to make.
combined="$local_context"
if [ -n "$context" ]; then
[ -n "$combined" ] && combined="${combined}"$'\n'
combined="${combined}${context}"
fi
[ -n "$combined" ] || exit 0
# No permissionDecision: this is a nudge, not a gate. The write goes ahead.
jq -n --arg c "$context" \
jq -n --arg c "$combined" \
'{hookSpecificOutput: {hookEventName: "PreToolUse", additionalContext: $c}}'
exit 0
+56 -1
View File
@@ -181,13 +181,25 @@ def check_shellcheck() -> None:
# pinning: the bug and the healthy no-results case look identical from outside.
# Pinning it does NOT make the failure visible; it makes sure the fail-open
# behaviour is deliberate rather than accidental.
# A symbol that exists nowhere, ASSEMBLED rather than written literally.
# The prior-art hook's local arm (#2280) fires with no credentials, so the
# silence assertion below needs a name the repo genuinely lacks. Two traps,
# both hit while writing this:
# - `def f` matched real code, so the hook spoke and "silent" was asserting
# the wrong thing;
# - spelling the replacement out in full put `def <name>(` INTO this file,
# so the smoke event defined the very symbol it claimed was absent.
# Concatenating keeps the contiguous string out of the source.
_ABSENT_SYM = "zz" + "_absent_" + "9f3a2b"
SMOKE_EVENTS: dict[str, str] = {
"scribe_autoinject.sh": json.dumps(
{"session_id": "smoke", "cwd": ".", "prompt": "a multi-line\nprompt\nhere"}
),
"scribe_prior_art.sh": json.dumps(
{"session_id": "smoke", "cwd": ".", "tool_name": "Edit",
"tool_input": {"file_path": "src/x.py", "new_string": "def f():\n pass\n"}}
"tool_input": {"file_path": "src/x.py",
"new_string": f"def {_ABSENT_SYM}():\n pass\n"}}
),
"scribe_sync_processes.sh": json.dumps({"source": "startup"}),
"scribe_session_context.sh": json.dumps({"source": "startup"}),
@@ -253,6 +265,48 @@ def check_fail_open() -> None:
ok(f"{rel} [{label}]: exit 0, silent")
def check_local_prior_art_needs_no_instance() -> None:
"""The prior-art hook's local arm must answer with no credentials (#2280).
The other arms ask Scribe what was RECORDED. This one asks the repo what
EXISTS, which needs no instance — and that is the whole reason it catches
the case the recorded arms structurally cannot: a helper nobody thought to
record. If it ever silently starts depending on configuration, it stops
covering that case and nothing else would notice.
Paired with the silence assertion in check_fail_open, which uses a symbol
that cannot exist. Together they pin both halves: silent when there is
nothing to say, and speaking when there is — both with no instance at all.
"""
script = HOOKS_DIR / "scribe_prior_art.sh"
if not script.is_file() or not shutil.which("jq"):
skip("prior-art local arm: hook or jq missing")
return
# A definition this repo really does contain, written into a DIFFERENT file
# so the self-match exclusion doesn't suppress it.
event = json.dumps({
"session_id": "smoke", "cwd": ".", "tool_name": "Write",
"tool_input": {
"file_path": "scripts/_probe_not_real.py",
"content": "def check_local_prior_art_needs_no_instance():\n pass\n",
},
})
try:
proc = _run_hook(script, event, {}) # NO credentials, on purpose
except subprocess.TimeoutExpired:
fail("prior-art local arm: hung")
return
if proc.returncode != 0:
fail(f"prior-art local arm: exited {proc.returncode}, must be 0")
elif "already defined" not in proc.stdout:
fail("prior-art local arm: found nothing for a symbol this repo "
"defines, with no credentials — the arm that needs no instance "
"has stopped working, and the recorded arms cannot cover for it")
else:
ok("prior-art local arm: answers with no instance configured")
def _git(*args: str) -> tuple[int, str]:
proc = subprocess.run(
["git", *args], capture_output=True, text=True, cwd=ROOT
@@ -344,6 +398,7 @@ def main() -> int:
check_patterns()
check_shellcheck()
check_fail_open()
check_local_prior_art_needs_no_instance()
if not args.no_version:
check_version_bump(args.base)