Buttons: one definition, aligned to the design system — plus local prior-art recall #94
+1
-1
@@ -22,7 +22,7 @@ real Postgres), build (docker buildx).
|
|||||||
- uv (test + integration jobs run `uv sync --locked`; installed in the
|
- uv (test + integration jobs run `uv sync --locked`; installed in the
|
||||||
image since the ci-python Dockerfile started pip-installing it)
|
image since the ci-python Dockerfile started pip-installing it)
|
||||||
- docker CLI + buildx (build job pushes the production image to the
|
- docker CLI + buildx (build job pushes the production image to the
|
||||||
Forgejo registry)
|
Fabled-Git registry)
|
||||||
|
|
||||||
## Per-job tool installs
|
## Per-job tool installs
|
||||||
|
|
||||||
|
|||||||
@@ -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.4–0.45rem, ~28 at 0.25–0.3rem, ~23
|
||||||
|
* at 0.1–0.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);
|
||||||
|
}
|
||||||
@@ -34,86 +34,11 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
align-items: center;
|
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
|
/* Save: Moss action-primary per the Hybrid rule. Saving is "operating
|
||||||
the software" — not a brand moment. Accent gradient is reserved for
|
the software" — not a brand moment. Accent gradient is reserved for
|
||||||
Send / empty-state CTAs. */
|
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
|
/* Delete: Oxblood action-destructive per Hybrid rule. Should be paired
|
||||||
with a Trash icon at the call site to reinforce intent. */
|
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 {
|
.title-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-bottom-color: var(--color-primary);
|
border-bottom-color: var(--color-primary);
|
||||||
@@ -155,23 +80,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
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 {
|
.tag-pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -198,18 +106,6 @@
|
|||||||
.tag-check {
|
.tag-check {
|
||||||
font-size: 0.7rem;
|
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 ── */
|
||||||
.assist-panel {
|
.assist-panel {
|
||||||
@@ -237,32 +133,6 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
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 {
|
.assist-panel-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -342,28 +212,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
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 */
|
/* Streaming */
|
||||||
.assist-streaming-label {
|
.assist-streaming-label {
|
||||||
@@ -419,14 +267,6 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--color-text-secondary);
|
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 {
|
.diff-view {
|
||||||
border: 1px solid var(--color-input-border);
|
border: 1px solid var(--color-input-border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
@@ -475,24 +315,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
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 ── */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
@@ -547,7 +369,7 @@
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
padding: 0.3rem 0.75rem;
|
padding: 0.3rem 0.75rem;
|
||||||
background: var(--color-primary);
|
background: var(--color-action-primary);
|
||||||
color: var(--fs-text-on-action);
|
color: var(--fs-text-on-action);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
@@ -640,3 +462,96 @@
|
|||||||
padding: 0.5rem 1rem 1rem;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ onMounted(loadVersions);
|
|||||||
|
|
||||||
<div class="history-footer">
|
<div class="history-footer">
|
||||||
<button
|
<button
|
||||||
class="btn-restore"
|
class="btn-primary"
|
||||||
:disabled="!selectedVersion?.body"
|
:disabled="!selectedVersion?.body"
|
||||||
@click="restore"
|
@click="restore"
|
||||||
>Restore this version</button>
|
>Restore this version</button>
|
||||||
@@ -393,19 +393,6 @@ onMounted(loadVersions);
|
|||||||
border-top: 1px solid var(--color-border);
|
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 badges + label rendering ───────────────────────────────────────── */
|
||||||
.pin-badge {
|
.pin-badge {
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ const markers: Record<DiffLine["type"], string> = {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-weight: 600;
|
font-weight: var(--fs-weight-medium);
|
||||||
}
|
}
|
||||||
.iap-btn-accept {
|
.iap-btn-accept {
|
||||||
background: var(--color-success, #22c55e);
|
background: var(--color-success, #22c55e);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ onMounted(() => store.fetchAll())
|
|||||||
<span class="notif-panel-title">Notifications</span>
|
<span class="notif-panel-title">Notifications</span>
|
||||||
<button
|
<button
|
||||||
v-if="store.count > 0"
|
v-if="store.count > 0"
|
||||||
class="btn-mark-all"
|
class="btn-text"
|
||||||
@click="store.markAll()"
|
@click="store.markAll()"
|
||||||
>Mark all read</button>
|
>Mark all read</button>
|
||||||
</header>
|
</header>
|
||||||
@@ -70,7 +70,7 @@ onMounted(() => store.fetchAll())
|
|||||||
</p>
|
</p>
|
||||||
<span class="notif-time">{{ relativeTime(n.created_at) }}</span>
|
<span class="notif-time">{{ relativeTime(n.created_at) }}</span>
|
||||||
</div>
|
</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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div v-else class="notif-empty">No unread notifications</div>
|
<div v-else class="notif-empty">No unread notifications</div>
|
||||||
@@ -108,21 +108,6 @@ onMounted(() => store.fetchAll())
|
|||||||
font-size: 0.9rem;
|
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 {
|
.notif-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -148,22 +133,4 @@ onMounted(() => store.fetchAll())
|
|||||||
}
|
}
|
||||||
.notif-time { font-size: 0.75rem; color: var(--color-muted); }
|
.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>
|
</style>
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ onMounted(async () => {
|
|||||||
<div class="share-dialog" role="dialog" :aria-label="`Share ${resourceTitle}`">
|
<div class="share-dialog" role="dialog" :aria-label="`Share ${resourceTitle}`">
|
||||||
<header class="share-header">
|
<header class="share-header">
|
||||||
<h2 class="share-title">Share "{{ resourceTitle }}"</h2>
|
<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>
|
</header>
|
||||||
|
|
||||||
<!-- Add share form -->
|
<!-- Add share form -->
|
||||||
@@ -184,7 +184,7 @@ onMounted(async () => {
|
|||||||
<option value="editor">Editor</option>
|
<option value="editor">Editor</option>
|
||||||
<option value="admin">Admin</option>
|
<option value="admin">Admin</option>
|
||||||
</select>
|
</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>
|
||||||
<li v-if="!shares.length" class="shares-empty">Not shared with anyone yet</li>
|
<li v-if="!shares.length" class="shares-empty">Not shared with anyone yet</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -231,23 +231,6 @@ onMounted(async () => {
|
|||||||
color: var(--color-text);
|
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 {
|
.share-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -343,7 +326,7 @@ onMounted(async () => {
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: var(--fs-weight-medium);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 0.15s;
|
transition: opacity 0.15s;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -394,23 +377,4 @@ onMounted(async () => {
|
|||||||
cursor: pointer;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ async function confirmDelete() {
|
|||||||
|
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<div class="systems-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
|
+ System
|
||||||
</button>
|
</button>
|
||||||
<label v-if="archivedSystems.length" class="archived-toggle">
|
<label v-if="archivedSystems.length" class="archived-toggle">
|
||||||
@@ -190,10 +190,10 @@ async function confirmDelete() {
|
|||||||
aria-label="System description"
|
aria-label="System description"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="system-form-actions">
|
<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" }}
|
{{ creating ? "Creating…" : "Create" }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn-cancel" @click="cancelCreate">Cancel</button>
|
<button type="button" class="btn-ghost btn-compact" @click="cancelCreate">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ async function confirmDelete() {
|
|||||||
<div v-else-if="!visibleSystems.length" class="systems-empty">
|
<div v-else-if="!visibleSystems.length" class="systems-empty">
|
||||||
<p class="empty-title">No systems yet</p>
|
<p class="empty-title">No systems yet</p>
|
||||||
<p class="empty-sub">Define a reusable subsystem or area to organize issues against.</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>
|
</div>
|
||||||
|
|
||||||
<!-- List -->
|
<!-- List -->
|
||||||
@@ -241,10 +241,10 @@ async function confirmDelete() {
|
|||||||
aria-label="System description"
|
aria-label="System description"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="system-form-actions">
|
<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" }}
|
{{ savingEdit ? "Saving…" : "Save" }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn-cancel" @click="cancelEdit">Cancel</button>
|
<button type="button" class="btn-ghost btn-compact" @click="cancelEdit">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
@@ -387,51 +387,6 @@ async function confirmDelete() {
|
|||||||
.system-textarea { resize: vertical; }
|
.system-textarea { resize: vertical; }
|
||||||
|
|
||||||
.system-form-actions { display: flex; gap: 0.4rem; }
|
.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-card--archived { opacity: 0.6; }
|
||||||
|
|
||||||
.system-swatch {
|
.system-swatch {
|
||||||
|
|||||||
@@ -122,8 +122,8 @@ onMounted(loadLogs);
|
|||||||
class="log-duration-input"
|
class="log-duration-input"
|
||||||
placeholder="min"
|
placeholder="min"
|
||||||
/>
|
/>
|
||||||
<button class="btn-log-save" @click="saveEdit(log)">Save</button>
|
<button class="btn-primary btn-compact" @click="saveEdit(log)">Save</button>
|
||||||
<button class="btn-log-cancel" @click="cancelEdit">Cancel</button>
|
<button class="btn-ghost btn-compact" @click="cancelEdit">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -133,8 +133,8 @@ onMounted(loadLogs);
|
|||||||
{{ formatDuration(log.duration_minutes) }}
|
{{ formatDuration(log.duration_minutes) }}
|
||||||
</span>
|
</span>
|
||||||
<div class="log-entry-actions">
|
<div class="log-entry-actions">
|
||||||
<button class="btn-log-edit" @click="startEdit(log)" title="Edit">Edit</button>
|
<button class="btn-text" @click="startEdit(log)" title="Edit">Edit</button>
|
||||||
<button class="btn-log-delete" aria-label="Delete log entry" @click="deleteLog(log)">×</button>
|
<button class="btn-text" aria-label="Delete log entry" @click="deleteLog(log)">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="log-content prose" v-html="renderMarkdown(log.content)"></div>
|
<div class="log-content prose" v-html="renderMarkdown(log.content)"></div>
|
||||||
@@ -162,7 +162,7 @@ onMounted(loadLogs);
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
class="btn-log-submit"
|
class="btn-primary btn-compact"
|
||||||
@click="submitLog"
|
@click="submitLog"
|
||||||
:disabled="!newContent.trim() || submitting"
|
:disabled="!newContent.trim() || submitting"
|
||||||
>
|
>
|
||||||
@@ -230,34 +230,7 @@ onMounted(loadLogs);
|
|||||||
gap: 0.25rem;
|
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 {
|
.log-textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -307,32 +280,6 @@ onMounted(loadLogs);
|
|||||||
border-color: var(--color-primary);
|
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>
|
</style>
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ function restore() {
|
|||||||
.vh-btn-back:hover { border-color: var(--color-primary); color: var(--color-primary); }
|
.vh-btn-back:hover { border-color: var(--color-primary); color: var(--color-primary); }
|
||||||
|
|
||||||
.vh-btn-restore {
|
.vh-btn-restore {
|
||||||
background: var(--color-primary);
|
background: var(--color-action-primary);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
padding: 0.25rem 0.6rem;
|
padding: 0.25rem 0.6rem;
|
||||||
|
|||||||
@@ -314,14 +314,14 @@ defineExpose({ reload: loadProjectNotes });
|
|||||||
@keydown.enter="createNote"
|
@keydown.enter="createNote"
|
||||||
@keydown.escape="cancelNewNote"
|
@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 ? '…' : '+' }}
|
{{ creatingNote ? '…' : '+' }}
|
||||||
</button>
|
</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>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="rail-title">Notes</span>
|
<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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -333,7 +333,7 @@ defineExpose({ reload: loadProjectNotes });
|
|||||||
type="search"
|
type="search"
|
||||||
aria-label="Search notes"
|
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>
|
||||||
|
|
||||||
<div v-if="listLoading" class="rail-state">Loading…</div>
|
<div v-if="listLoading" class="rail-state">Loading…</div>
|
||||||
@@ -358,12 +358,12 @@ defineExpose({ reload: loadProjectNotes });
|
|||||||
</div>
|
</div>
|
||||||
<div class="note-row-actions" @click.stop>
|
<div class="note-row-actions" @click.stop>
|
||||||
<template v-if="deletingId === note.id">
|
<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?' }}
|
{{ pendingDelete === note.id ? '…' : 'Delete?' }}
|
||||||
</button>
|
</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>
|
</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" />
|
<Trash2 :size="16" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -382,7 +382,7 @@ defineExpose({ reload: loadProjectNotes });
|
|||||||
<WordCount :body="noteBody" />
|
<WordCount :body="noteBody" />
|
||||||
<span v-if="dirty && !saving" class="unsaved">Unsaved</span>
|
<span v-if="dirty && !saving" class="unsaved">Unsaved</span>
|
||||||
<span v-if="saving" class="saving-txt">Saving…</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -400,7 +400,7 @@ defineExpose({ reload: loadProjectNotes });
|
|||||||
<div class="tag-row">
|
<div class="tag-row">
|
||||||
<TagInput v-model="noteTags" :fetchTags="(q: string) => notesStore.fetchAllTags(q)" />
|
<TagInput v-model="noteTags" :fetchTags="(q: string) => notesStore.fetchAllTags(q)" />
|
||||||
<button
|
<button
|
||||||
class="btn-suggest-tags"
|
class="btn-ghost btn-compact"
|
||||||
:disabled="tagSuggestions.suggestingTags.value"
|
:disabled="tagSuggestions.suggestingTags.value"
|
||||||
title="Auto-suggest tags from title and body"
|
title="Auto-suggest tags from title and body"
|
||||||
@click="tagSuggestions.fetchTagSuggestions()"
|
@click="tagSuggestions.fetchTagSuggestions()"
|
||||||
@@ -417,7 +417,7 @@ defineExpose({ reload: loadProjectNotes });
|
|||||||
:class="['btn-tag-suggestion', { applied: tagSuggestions.appliedTags.value.has(tag) }]"
|
:class="['btn-tag-suggestion', { applied: tagSuggestions.appliedTags.value.has(tag) }]"
|
||||||
@click="tagSuggestions.applyTagSuggestion(tag)"
|
@click="tagSuggestions.applyTagSuggestion(tag)"
|
||||||
>#{{ tag }}{{ tagSuggestions.appliedTags.value.has(tag) ? ' ✓' : '' }}</button>
|
>#{{ 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>
|
||||||
|
|
||||||
<div class="toolbar-row">
|
<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`">
|
<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>
|
<button class="btn-chip-link" @click="applyLink(s)">[[{{ s.title }}]]</button>
|
||||||
</span>
|
</span>
|
||||||
<button class="btn-link-all" @click="applyAllLinks" title="Link all suggestions">All</button>
|
<button class="btn-ghost btn-inline" @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-text" aria-label="Dismiss link suggestions" @click="linkSuggestions = []"><X :size="16" /></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="editor-area" @keydown.ctrl.s.prevent="saveNote" @keydown.ctrl.e.prevent="editorRef?.editor?.commands.focus()">
|
<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;
|
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 {
|
.rail-search-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -517,17 +497,8 @@ defineExpose({ reload: loadProjectNotes });
|
|||||||
.rail-search-input:focus { outline: none; }
|
.rail-search-input:focus { outline: none; }
|
||||||
.rail-search-input::-webkit-search-cancel-button { display: none; }
|
.rail-search-input::-webkit-search-cancel-button { display: none; }
|
||||||
|
|
||||||
.btn-search-clear {
|
/* Sits inside the search field: no padding, and must not flex. */
|
||||||
background: none;
|
.btn-search-clear { padding: 0; flex-shrink: 0; }
|
||||||
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); }
|
|
||||||
|
|
||||||
.rail-state {
|
.rail-state {
|
||||||
padding: 1rem 0.65rem;
|
padding: 1rem 0.65rem;
|
||||||
@@ -617,99 +588,7 @@ defineExpose({ reload: loadProjectNotes });
|
|||||||
align-items: center;
|
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; }
|
.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 */
|
/* Editor UI */
|
||||||
.panel-header {
|
.panel-header {
|
||||||
@@ -731,23 +610,6 @@ defineExpose({ reload: loadProjectNotes });
|
|||||||
.saving-txt { font-size: 0.72rem; color: var(--color-primary); }
|
.saving-txt { font-size: 0.72rem; color: var(--color-primary); }
|
||||||
|
|
||||||
/* Moss action-primary per Hybrid */
|
/* 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 {
|
.note-title-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -775,20 +637,7 @@ defineExpose({ reload: loadProjectNotes });
|
|||||||
}
|
}
|
||||||
.tag-row > :first-child { flex: 1; min-width: 0; }
|
.tag-row > :first-child { flex: 1; min-width: 0; }
|
||||||
|
|
||||||
.btn-suggest-tags {
|
.btn-suggest-tags { flex-shrink: 0; align-self: center; }
|
||||||
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; }
|
|
||||||
|
|
||||||
.tag-suggestions {
|
.tag-suggestions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -817,21 +666,6 @@ defineExpose({ reload: loadProjectNotes });
|
|||||||
color: var(--color-primary);
|
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 {
|
.link-suggest-strip {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -865,21 +699,4 @@ defineExpose({ reload: loadProjectNotes });
|
|||||||
}
|
}
|
||||||
.btn-chip-link:hover { background: color-mix(in srgb, var(--color-primary) 15%, transparent); }
|
.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>
|
</style>
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ defineExpose({ reload: loadAll });
|
|||||||
placeholder="New task..."
|
placeholder="New task..."
|
||||||
@keydown.enter="addTask"
|
@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>
|
||||||
|
|
||||||
<div v-if="loading" class="state-msg">Loading...</div>
|
<div v-if="loading" class="state-msg">Loading...</div>
|
||||||
@@ -293,18 +293,18 @@ defineExpose({ reload: loadAll });
|
|||||||
<Transition name="detail-fade">
|
<Transition name="detail-fade">
|
||||||
<div v-if="activeTask" class="task-detail">
|
<div v-if="activeTask" class="task-detail">
|
||||||
<div class="detail-header">
|
<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">
|
<span :class="['status-badge', `status-${activeTask.status}`]" @click="cycleStatus(activeTask, $event)" title="Click to cycle status">
|
||||||
{{ STATUS_ICON[activeTask.status] ?? "○" }} {{ activeTask.status.replace("_", " ") }}
|
{{ STATUS_ICON[activeTask.status] ?? "○" }} {{ activeTask.status.replace("_", " ") }}
|
||||||
</span>
|
</span>
|
||||||
<template v-if="deleteConfirmPending">
|
<template v-if="deleteConfirmPending">
|
||||||
<button class="btn-delete-confirm" :disabled="deletingTask" @click="deleteActiveTask">{{ deletingTask ? '...' : 'Delete?' }}</button>
|
<button class="btn-danger-outline btn-inline 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-text" aria-label="Cancel delete" @click="cancelDeleteTask"><X :size="16" /></button>
|
||||||
</template>
|
</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" />
|
<Trash2 :size="16" />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
<h3 class="detail-title">{{ activeTask.title }}</h3>
|
<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); }
|
.task-add-input:focus { outline: none; border-color: var(--color-primary); }
|
||||||
|
|
||||||
.btn-add {
|
.btn-add { font-size: 1rem; } /* a '+' glyph, not a label */
|
||||||
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; }
|
|
||||||
|
|
||||||
.groups-scroll {
|
.groups-scroll {
|
||||||
flex: 1;
|
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-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); }
|
.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 {
|
.btn-edit-task { margin-left: 0.25rem; }
|
||||||
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:hover { text-decoration: underline; }
|
.btn-edit-task:hover { text-decoration: underline; }
|
||||||
|
|
||||||
.detail-body {
|
.detail-body {
|
||||||
@@ -566,51 +546,11 @@ defineExpose({ reload: loadAll });
|
|||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-delete-task {
|
.btn-delete-task { margin-left: 0.25rem; }
|
||||||
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:hover { color: var(--color-action-destructive); }
|
.btn-delete-task:hover { color: var(--color-action-destructive); }
|
||||||
|
|
||||||
.btn-delete-confirm {
|
.btn-delete-confirm { margin-left: 0.25rem; }
|
||||||
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-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 {
|
.detail-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -679,15 +619,5 @@ defineExpose({ reload: loadAll });
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Close detail button */
|
/* Close detail button */
|
||||||
.btn-close-detail {
|
.btn-close-detail { margin-left: 0.2rem; }
|
||||||
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); }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { createPinia } from "pinia";
|
|||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import "./assets/theme.css";
|
import "./assets/theme.css";
|
||||||
|
// After theme.css — it consumes the tokens declared there.
|
||||||
|
import "./assets/components.css";
|
||||||
import "./assets/prose.css";
|
import "./assets/prose.css";
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|||||||
@@ -1262,47 +1262,6 @@ textarea.input {
|
|||||||
|
|
||||||
/* Buttons ---------------------------------------------------------------- */
|
/* 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 {
|
.row-actions {
|
||||||
|
|||||||
@@ -12,11 +12,12 @@
|
|||||||
* every specimen below is either a REAL component imported from the app, or a
|
* 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.
|
* 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
|
* Buttons WERE the case where that bit: `.btn-primary` was defined five times
|
||||||
* `.btn-primary` is defined four separate times in four `<style scoped>` blocks
|
* in five `<style scoped>` blocks, all five drifted, and this page reported it
|
||||||
* and 30 of 54 SFCs carry their own button CSS (#2273). There is no shared
|
* as a gap because drawing a look-alike would have made it a sixth copy.
|
||||||
* button to import, so rendering one here would just make this a fifth copy.
|
* `assets/components.css` is now the single definition (#2273), so the
|
||||||
* It is reported as a gap instead.
|
* 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";
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
|
||||||
@@ -225,25 +226,33 @@ const TYPE_SPECIMENS = [
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section class="design-section">
|
||||||
<h2>Buttons</h2>
|
<h2>Buttons</h2>
|
||||||
<div class="gap-notice">
|
<div class="button-specimens">
|
||||||
<strong>Not implemented as a shared component.</strong>
|
<button class="btn-primary">Save</button>
|
||||||
<p>
|
<button class="btn-secondary">Detect</button>
|
||||||
Rule 65 specifies four variants. In practice <code>.btn-primary</code> is
|
<button class="btn-ghost">Cancel</button>
|
||||||
defined four separate times in four <code><style scoped></code>
|
<button class="btn-danger">Delete</button>
|
||||||
blocks, and 30 of 54 single-file components carry their own button CSS.
|
<button class="btn-primary" disabled>Disabled</button>
|
||||||
There is no shared button to render here, and drawing one would make
|
</div>
|
||||||
this page a fifth copy of it — the exact drift this surface exists to
|
<p class="spec-caption">
|
||||||
catch. Tracked as issue #2273.
|
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>
|
</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>
|
</div>
|
||||||
<ul class="spec-list">
|
<ul class="spec-list">
|
||||||
<li v-for="b in RULEBOOK_BUTTONS" :key="b.name">
|
<li v-for="b in RULEBOOK_BUTTONS" :key="b.name">
|
||||||
<span class="spec-name">{{ b.name }}</span>
|
<span class="spec-name">{{ b.name }}</span>
|
||||||
<span class="spec-detail">{{ b.spec }}</span>
|
<span class="spec-detail">{{ b.spec }}</span>
|
||||||
<span class="spec-status missing">missing</span>
|
<span class="spec-status ok">shared</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
@@ -361,6 +370,25 @@ const TYPE_SPECIMENS = [
|
|||||||
|
|
||||||
/* Gaps ------------------------------------------------------------------- */
|
/* 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 {
|
.gap-notice {
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ async function handleSubmit() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="error" class="error-msg">{{ error }}</p>
|
<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" }}
|
{{ submitting ? "Sending..." : "Send Reset Link" }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -137,24 +137,6 @@ async function handleSubmit() {
|
|||||||
.success-msg p {
|
.success-msg p {
|
||||||
margin: 0.5rem 0;
|
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 {
|
.auth-footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ onUnmounted(() => {
|
|||||||
<option value="alpha">Alphabetical</option>
|
<option value="alpha">Alphabetical</option>
|
||||||
<option value="type">By type</option>
|
<option value="type">By type</option>
|
||||||
</select>
|
</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" />
|
<Share2 :size="16" />
|
||||||
Graph
|
Graph
|
||||||
</button>
|
</button>
|
||||||
@@ -463,14 +463,14 @@ onUnmounted(() => {
|
|||||||
<span>Graph</span>
|
<span>Graph</span>
|
||||||
<div style="display:flex;gap:4px;align-items:center">
|
<div style="display:flex;gap:4px;align-items:center">
|
||||||
<button
|
<button
|
||||||
class="btn-icon-sm"
|
class="btn-text"
|
||||||
@click="toggleGraphExpand"
|
@click="toggleGraphExpand"
|
||||||
:title="graphExpanded ? 'Narrow panel' : 'Expand panel'"
|
:title="graphExpanded ? 'Narrow panel' : 'Expand panel'"
|
||||||
>
|
>
|
||||||
<ChevronLeft v-if="graphExpanded" :size="16" />
|
<ChevronLeft v-if="graphExpanded" :size="16" />
|
||||||
<ChevronRight v-else :size="16" />
|
<ChevronRight v-else :size="16" />
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-icon-sm" @click="toggleGraph" title="Close graph">
|
<button class="btn-text" @click="toggleGraph" title="Close graph">
|
||||||
<X :size="16" />
|
<X :size="16" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -719,26 +719,6 @@ onUnmounted(() => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
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 ───────────────────────────────────────────── */
|
||||||
.card-grid {
|
.card-grid {
|
||||||
@@ -956,22 +936,6 @@ onUnmounted(() => {
|
|||||||
border-bottom: 1px solid var(--color-border, rgba(255,255,255,0.06));
|
border-bottom: 1px solid var(--color-border, rgba(255,255,255,0.06));
|
||||||
flex-shrink: 0;
|
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 */
|
/* Override GraphView's 100vh height so it fills the panel instead */
|
||||||
.graph-embed :deep(.graph-page) {
|
.graph-embed :deep(.graph-page) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function loginWithOAuth() {
|
|||||||
<p class="forgot-link">
|
<p class="forgot-link">
|
||||||
<router-link to="/forgot-password">Forgot your password?</router-link>
|
<router-link to="/forgot-password">Forgot your password?</router-link>
|
||||||
</p>
|
</p>
|
||||||
<button type="submit" class="btn-submit" :disabled="submitting">
|
<button type="submit" class="btn-primary btn-block" :disabled="submitting">
|
||||||
{{ submitting ? "Signing in..." : "Sign In" }}
|
{{ submitting ? "Signing in..." : "Sign In" }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -98,7 +98,7 @@ function loginWithOAuth() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="authStore.oauthEnabled"
|
v-if="authStore.oauthEnabled"
|
||||||
class="btn-oauth"
|
class="btn-ghost btn-block"
|
||||||
@click="loginWithOAuth"
|
@click="loginWithOAuth"
|
||||||
>
|
>
|
||||||
Login with Authentik
|
Login with Authentik
|
||||||
@@ -176,24 +176,6 @@ function loginWithOAuth() {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
margin: 0 0 0.75rem;
|
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 {
|
.divider {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -208,20 +190,6 @@ function loginWithOAuth() {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
border-top: 1px solid var(--color-border);
|
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 {
|
.auth-footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ function clearFilters() {
|
|||||||
<input v-model="dateTo" type="date" class="filter-date" title="To date" />
|
<input v-model="dateTo" type="date" class="filter-date" title="To date" />
|
||||||
<button
|
<button
|
||||||
v-if="category || search || dateFrom || dateTo"
|
v-if="category || search || dateFrom || dateTo"
|
||||||
class="btn-clear"
|
class="btn-ghost btn-compact"
|
||||||
@click="clearFilters"
|
@click="clearFilters"
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
@@ -337,19 +337,6 @@ function clearFilters() {
|
|||||||
.filter-date {
|
.filter-date {
|
||||||
width: 140px;
|
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 */
|
/* Table */
|
||||||
.loading-msg,
|
.loading-msg,
|
||||||
|
|||||||
@@ -748,7 +748,7 @@ onUnmounted(() => assist.clearSelection());
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: inherit;
|
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 {
|
.link-suggest-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -194,22 +194,22 @@ async function convertToTask() {
|
|||||||
</div>
|
</div>
|
||||||
<template v-else-if="store.currentNote">
|
<template v-else-if="store.currentNote">
|
||||||
<div class="toolbar">
|
<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
|
<router-link
|
||||||
:to="`/notes/${store.currentNote.id}/edit`"
|
:to="`/notes/${store.currentNote.id}/edit`"
|
||||||
class="btn-edit"
|
class="btn-primary"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</router-link>
|
</router-link>
|
||||||
<button
|
<button
|
||||||
v-if="!store.currentNote.is_task"
|
v-if="!store.currentNote.is_task"
|
||||||
class="btn-convert"
|
class="btn-secondary btn-compact"
|
||||||
@click="convertToTask"
|
@click="convertToTask"
|
||||||
:disabled="converting"
|
:disabled="converting"
|
||||||
>
|
>
|
||||||
{{ converting ? "Converting..." : "Convert to Task" }}
|
{{ converting ? "Converting..." : "Convert to Task" }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-share" @click="showShare = true">Share</button>
|
<button class="btn-secondary btn-compact" @click="showShare = true">Share</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Breadcrumb: parent → project → milestone -->
|
<!-- Breadcrumb: parent → project → milestone -->
|
||||||
@@ -329,81 +329,10 @@ async function convertToTask() {
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-bottom: 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
|
/* Edit: Moss action-primary — switching from view to edit is operating
|
||||||
the software, not a brand moment. */
|
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 */
|
/* 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 {
|
.meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -284,20 +284,6 @@ function overallPct(project: Project): { total: number; pct: number } {
|
|||||||
|
|
||||||
/* Moss action-primary per Hybrid — list-view utility action,
|
/* Moss action-primary per Hybrid — list-view utility action,
|
||||||
not a brand moment. Empty-state .empty-action below keeps accent. */
|
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 {
|
.filter-tabs {
|
||||||
display: flex;
|
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-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-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-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 { 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-primary); color: var(--fs-text-on-action); }
|
.empty-action:hover { background: var(--color-action-primary); color: var(--fs-text-on-action); }
|
||||||
|
|
||||||
.skeleton-card {
|
.skeleton-card {
|
||||||
height: 140px;
|
height: 140px;
|
||||||
@@ -592,8 +578,8 @@ function overallPct(project: Project): { total: number; pct: number } {
|
|||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
}
|
}
|
||||||
.modal-btn-primary {
|
.modal-btn-primary {
|
||||||
background: var(--color-primary);
|
background: var(--color-action-primary);
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-action-primary);
|
||||||
color: var(--fs-text-on-action);
|
color: var(--fs-text-on-action);
|
||||||
}
|
}
|
||||||
.modal-btn-primary:hover:not(:disabled) {
|
.modal-btn-primary:hover:not(:disabled) {
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ async function confirmDelete() {
|
|||||||
|
|
||||||
<!-- Nav bar -->
|
<!-- Nav bar -->
|
||||||
<div class="page-header">
|
<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">
|
<div class="page-header-actions">
|
||||||
<template v-if="showStartPlanning">
|
<template v-if="showStartPlanning">
|
||||||
<input
|
<input
|
||||||
@@ -451,7 +451,7 @@ async function confirmDelete() {
|
|||||||
>
|
>
|
||||||
Create plan
|
Create plan
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-share" @click="showStartPlanning = false; planTitle = ''">Cancel</button>
|
<button class="btn-secondary btn-compact" @click="showStartPlanning = false; planTitle = ''">Cancel</button>
|
||||||
</template>
|
</template>
|
||||||
<button
|
<button
|
||||||
v-else-if="project"
|
v-else-if="project"
|
||||||
@@ -464,7 +464,7 @@ async function confirmDelete() {
|
|||||||
<LayoutGrid :size="16" />
|
<LayoutGrid :size="16" />
|
||||||
Workspace
|
Workspace
|
||||||
</router-link>
|
</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>
|
<button v-if="project && !showStartPlanning" class="btn-danger-outline" @click="showDeleteConfirm = true">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -548,7 +548,7 @@ async function confirmDelete() {
|
|||||||
<option v-for="ds in designSystems" :key="ds.id" :value="ds.id">{{ ds.title }}</option>
|
<option v-for="ds in designSystems" :key="ds.id" :value="ds.id">{{ ds.title }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-save-panel" @click="saveProject" :disabled="!editDirty || saving">
|
<button class="btn-primary" @click="saveProject" :disabled="!editDirty || saving">
|
||||||
{{ saving ? "Saving..." : "Save Changes" }}
|
{{ saving ? "Saving..." : "Save Changes" }}
|
||||||
</button>
|
</button>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -581,7 +581,7 @@ async function confirmDelete() {
|
|||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="milestone-actions">
|
<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
|
+ Milestone
|
||||||
</button>
|
</button>
|
||||||
<div v-else class="new-milestone-row">
|
<div v-else class="new-milestone-row">
|
||||||
@@ -593,10 +593,10 @@ async function confirmDelete() {
|
|||||||
@keydown.enter="createMilestone"
|
@keydown.enter="createMilestone"
|
||||||
@keydown.escape="showNewMilestone = false; newMilestoneTitle = ''"
|
@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" }}
|
{{ creatingMilestone ? "..." : "Add" }}
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -856,80 +856,9 @@ async function confirmDelete() {
|
|||||||
min-width: 200px;
|
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); }
|
.btn-workspace:hover { box-shadow: var(--glow-cta-hover); opacity: 0.95; color: var(--fs-text-on-action); }
|
||||||
|
|
||||||
/* Share: Bronze action-secondary — alternate path */
|
/* 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 {
|
.project-title-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
@@ -1062,30 +991,6 @@ async function confirmDelete() {
|
|||||||
.edit-textarea { resize: vertical; }
|
.edit-textarea { resize: vertical; }
|
||||||
|
|
||||||
/* Save panel: Moss action-primary per Hybrid rule */
|
/* 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 {
|
.tab-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1148,49 +1053,6 @@ async function confirmDelete() {
|
|||||||
}
|
}
|
||||||
.milestone-title-input:focus { outline: none; border-color: var(--color-primary); }
|
.milestone-title-input:focus { outline: none; border-color: var(--color-primary); }
|
||||||
/* Milestone confirm: Moss action-primary; Cancel: Bronze action-secondary */
|
/* 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 { cursor: pointer; }
|
||||||
.milestone-header.clickable:hover { background: color-mix(in srgb, var(--color-primary) 4%, var(--color-bg-secondary)); }
|
.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;
|
cursor: pointer;
|
||||||
border: 1px solid var(--color-border);
|
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-primary:disabled { opacity: 0.6; cursor: default; }
|
||||||
.ms-plan-actions .btn-secondary { background: var(--color-bg-card); color: var(--color-text); }
|
.ms-plan-actions .btn-secondary { background: var(--color-bg-card); color: var(--color-text); }
|
||||||
|
|
||||||
@@ -1393,7 +1255,7 @@ async function confirmDelete() {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.task-card:hover .task-advance-btn { opacity: 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--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; }
|
.task-advance-btn:disabled { opacity: 0.4; cursor: default; }
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ async function handleSubmit() {
|
|||||||
<p v-if="passwordMismatch" class="error-hint">Passwords do not match</p>
|
<p v-if="passwordMismatch" class="error-hint">Passwords do not match</p>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="error" class="error-msg">{{ error }}</p>
|
<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" }}
|
{{ submitting ? "Creating Account..." : "Create Account" }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -247,24 +247,6 @@ async function handleSubmit() {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
margin: 0 0 0.75rem;
|
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 {
|
.auth-footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ async function handleSubmit() {
|
|||||||
<p v-if="passwordMismatch" class="error-hint">Passwords do not match</p>
|
<p v-if="passwordMismatch" class="error-hint">Passwords do not match</p>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="error" class="error-msg">{{ error }}</p>
|
<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" }}
|
{{ submitting ? "Creating account..." : "Create Account" }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -216,24 +216,6 @@ async function handleSubmit() {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
margin: 0 0 0.75rem;
|
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 {
|
.auth-footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ async function handleSubmit() {
|
|||||||
<p v-if="passwordMismatch" class="error-hint">Passwords do not match</p>
|
<p v-if="passwordMismatch" class="error-hint">Passwords do not match</p>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="error" class="error-msg">{{ error }}</p>
|
<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" }}
|
{{ submitting ? "Resetting..." : "Reset Password" }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -195,24 +195,6 @@ async function handleSubmit() {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
margin: 0 0 0.75rem;
|
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 {
|
.auth-footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|||||||
@@ -1165,7 +1165,7 @@ function formatUserDate(iso: string): string {
|
|||||||
<p class="field-hint">Click Detect to auto-fill from your browser.</p>
|
<p class="field-hint">Click Detect to auto-fill from your browser.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn-save" @click="saveTimezone" :disabled="savingTimezone">
|
<button class="btn-primary" @click="saveTimezone" :disabled="savingTimezone">
|
||||||
{{ savingTimezone ? 'Saving…' : 'Save' }}
|
{{ savingTimezone ? 'Saving…' : 'Save' }}
|
||||||
</button>
|
</button>
|
||||||
<span v-if="timezoneSaved" class="saved-msg">Saved!</span>
|
<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>
|
<p class="field-hint">Set to <strong>0</strong> to keep deleted items forever (never auto-purge).</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn-save" @click="saveRetention" :disabled="savingRetention">
|
<button class="btn-primary" @click="saveRetention" :disabled="savingRetention">
|
||||||
{{ savingRetention ? 'Saving…' : 'Save' }}
|
{{ savingRetention ? 'Saving…' : 'Save' }}
|
||||||
</button>
|
</button>
|
||||||
<span v-if="retentionSaved" class="saved-msg">Saved!</span>
|
<span v-if="retentionSaved" class="saved-msg">Saved!</span>
|
||||||
@@ -1313,7 +1313,7 @@ function formatUserDate(iso: string): string {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn-save" @click="saveKbInject" :disabled="savingKbInject">
|
<button class="btn-primary" @click="saveKbInject" :disabled="savingKbInject">
|
||||||
{{ savingKbInject ? 'Saving…' : 'Save' }}
|
{{ savingKbInject ? 'Saving…' : 'Save' }}
|
||||||
</button>
|
</button>
|
||||||
<span v-if="kbInjectSaved" class="saved-msg">Saved!</span>
|
<span v-if="kbInjectSaved" class="saved-msg">Saved!</span>
|
||||||
@@ -1361,7 +1361,7 @@ function formatUserDate(iso: string): string {
|
|||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button
|
<button
|
||||||
class="btn-save"
|
class="btn-primary"
|
||||||
@click="changeEmail"
|
@click="changeEmail"
|
||||||
:disabled="changingEmail || !emailPassword"
|
:disabled="changingEmail || !emailPassword"
|
||||||
>
|
>
|
||||||
@@ -1409,7 +1409,7 @@ function formatUserDate(iso: string): string {
|
|||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button
|
<button
|
||||||
class="btn-save"
|
class="btn-primary"
|
||||||
@click="changePassword"
|
@click="changePassword"
|
||||||
:disabled="changingPassword || !currentPassword || newPassword.length < 8 || newPassword !== confirmNewPassword"
|
:disabled="changingPassword || !currentPassword || newPassword.length < 8 || newPassword !== confirmNewPassword"
|
||||||
>
|
>
|
||||||
@@ -1465,7 +1465,7 @@ function formatUserDate(iso: string): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<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>
|
<span v-if="profileSaved" class="saved-msg">Saved!</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1491,7 +1491,7 @@ function formatUserDate(iso: string): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<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>
|
<span v-if="profileSaved" class="saved-msg">Saved!</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
<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" />
|
<TagInput v-model="profile.interests" placeholder="Add an interest…" :fetchTags="emptyTagsFetch" />
|
||||||
<div class="actions">
|
<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>
|
<span v-if="profileSaved" class="saved-msg">Saved!</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1533,7 +1533,7 @@ function formatUserDate(iso: string): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<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>
|
<span v-if="profileSaved" class="saved-msg">Saved!</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1562,7 +1562,7 @@ function formatUserDate(iso: string): string {
|
|||||||
<p class="field-hint">Emails for logins, logouts, and password changes.</p>
|
<p class="field-hint">Emails for logins, logouts, and password changes.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn-save" @click="saveNotifications" :disabled="savingNotifications">
|
<button class="btn-primary" @click="saveNotifications" :disabled="savingNotifications">
|
||||||
{{ savingNotifications ? "Saving..." : "Save" }}
|
{{ savingNotifications ? "Saving..." : "Save" }}
|
||||||
</button>
|
</button>
|
||||||
<span v-if="notificationsSaved" class="saved-msg">Saved!</span>
|
<span v-if="notificationsSaved" class="saved-msg">Saved!</span>
|
||||||
@@ -1589,7 +1589,7 @@ function formatUserDate(iso: string): string {
|
|||||||
placeholder="Enter a search query..."
|
placeholder="Enter a search query..."
|
||||||
@keydown="onSearchKeydown"
|
@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" }}
|
{{ searchLoading ? "Searching..." : "Search" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1891,7 +1891,7 @@ function formatUserDate(iso: string): string {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn-save" @click="saveBaseUrl" :disabled="savingBaseUrl">
|
<button class="btn-primary" @click="saveBaseUrl" :disabled="savingBaseUrl">
|
||||||
{{ savingBaseUrl ? "Saving..." : "Save" }}
|
{{ savingBaseUrl ? "Saving..." : "Save" }}
|
||||||
</button>
|
</button>
|
||||||
<span v-if="baseUrlSaved" class="saved-msg">Saved!</span>
|
<span v-if="baseUrlSaved" class="saved-msg">Saved!</span>
|
||||||
@@ -1916,7 +1916,7 @@ function formatUserDate(iso: string): string {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn-save" @click="saveMarketplaceUrl" :disabled="savingMarketplaceUrl">
|
<button class="btn-primary" @click="saveMarketplaceUrl" :disabled="savingMarketplaceUrl">
|
||||||
{{ savingMarketplaceUrl ? "Saving..." : "Save" }}
|
{{ savingMarketplaceUrl ? "Saving..." : "Save" }}
|
||||||
</button>
|
</button>
|
||||||
<span v-if="marketplaceUrlSaved" class="saved-msg">Saved!</span>
|
<span v-if="marketplaceUrlSaved" class="saved-msg">Saved!</span>
|
||||||
@@ -1946,10 +1946,10 @@ function formatUserDate(iso: string): string {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn-save" @click="saveDbMaintenance" :disabled="savingDbMaint">
|
<button class="btn-primary" @click="saveDbMaintenance" :disabled="savingDbMaint">
|
||||||
{{ savingDbMaint ? "Saving..." : "Save" }}
|
{{ savingDbMaint ? "Saving..." : "Save" }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-save btn-secondary" @click="runDbMaintenanceNow" :disabled="runningDbMaint">
|
<button class="btn-secondary" @click="runDbMaintenanceNow" :disabled="runningDbMaint">
|
||||||
{{ runningDbMaint ? "Running..." : "Run now" }}
|
{{ runningDbMaint ? "Running..." : "Run now" }}
|
||||||
</button>
|
</button>
|
||||||
<span v-if="dbMaintSaved" class="saved-msg">Saved!</span>
|
<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>
|
<p class="field-hint">Recommended for port 587. Implicit TLS is used automatically for port 465.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions" style="margin-bottom: 1.25rem;">
|
<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" }}
|
{{ savingSmtp ? "Saving..." : "Save SMTP Settings" }}
|
||||||
</button>
|
</button>
|
||||||
<span v-if="smtpSaved" class="saved-msg">Saved!</span>
|
<span v-if="smtpSaved" class="saved-msg">Saved!</span>
|
||||||
@@ -2054,7 +2054,7 @@ function formatUserDate(iso: string): string {
|
|||||||
placeholder="test@example.com"
|
placeholder="test@example.com"
|
||||||
class="input"
|
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" }}
|
{{ sendingTest ? "Sending..." : "Send Test" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
<p class="field-hint">When closed, new users can only be added by an administrator.</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="btn-toggle"
|
class="btn-primary btn-toggle"
|
||||||
:class="registrationOpen ? 'btn-toggle-close' : 'btn-toggle-open'"
|
:class="registrationOpen ? 'btn-toggle-close' : 'btn-toggle-open'"
|
||||||
@click="toggleRegistration"
|
@click="toggleRegistration"
|
||||||
:disabled="toggling"
|
:disabled="toggling"
|
||||||
@@ -2100,7 +2100,7 @@ function formatUserDate(iso: string): string {
|
|||||||
required
|
required
|
||||||
:disabled="sendingInvite"
|
: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" }}
|
{{ sendingInvite ? "Sending..." : "Send Invite" }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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.created_at) }}</td>
|
||||||
<td class="hide-mobile cell-date">{{ formatUserDate(inv.expires_at) }}</td>
|
<td class="hide-mobile cell-date">{{ formatUserDate(inv.expires_at) }}</td>
|
||||||
<td class="cell-actions">
|
<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" }}
|
{{ revokingId === inv.id ? "Revoking..." : "Revoke" }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -2161,13 +2161,13 @@ function formatUserDate(iso: string): string {
|
|||||||
<span class="you-label">You</span>
|
<span class="you-label">You</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="confirmDeleteId === u.id">
|
<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" }}
|
{{ deleting === u.id ? "Deleting..." : "Confirm" }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-cancel-delete" @click="cancelDelete">Cancel</button>
|
<button class="btn-ghost btn-compact" @click="cancelDelete">Cancel</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<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>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -2305,10 +2305,10 @@ function formatUserDate(iso: string): string {
|
|||||||
<span v-if="g.description" class="group-desc">{{ g.description }}</span>
|
<span v-if="g.description" class="group-desc">{{ g.description }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="group-card-actions">
|
<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' }}
|
{{ expandedGroupId === g.id ? 'Collapse' : 'Manage' }}
|
||||||
</button>
|
</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>
|
||||||
</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">
|
<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-name">{{ m.username }}</span>
|
||||||
<span class="member-role-badge" :class="`role-${m.role}`">{{ m.role }}</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>
|
||||||
<li v-if="!(groupMembers[g.id]?.length)" class="members-empty">No members yet.</li>
|
<li v-if="!(groupMembers[g.id]?.length)" class="members-empty">No members yet.</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -2564,82 +2564,6 @@ function formatUserDate(iso: string): string {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
/* Save: Moss action-primary per Hybrid */
|
/* 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 {
|
.db-maint-table-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -2985,68 +2909,6 @@ function formatUserDate(iso: string): string {
|
|||||||
}
|
}
|
||||||
.you-label { font-size: 0.8rem; color: var(--color-text-muted); }
|
.you-label { font-size: 0.8rem; color: var(--color-text-muted); }
|
||||||
/* Per-row delete (users / invitations / etc.): ghost → Oxblood on hover */
|
/* 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 */
|
/* Logs panel */
|
||||||
.stats-section { padding: 1rem 1.25rem; }
|
.stats-section { padding: 1rem 1.25rem; }
|
||||||
@@ -3148,20 +3010,6 @@ function formatUserDate(iso: string): string {
|
|||||||
|
|
||||||
/* ── Groups tab ──────────────────────────────────────────────── */
|
/* ── Groups tab ──────────────────────────────────────────────── */
|
||||||
/* Moss action-primary per Hybrid */
|
/* 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 {
|
.input-field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -3244,32 +3092,6 @@ function formatUserDate(iso: string): string {
|
|||||||
flex-shrink: 0;
|
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 {
|
.member-search-wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -3782,21 +3604,6 @@ function formatUserDate(iso: string): string {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
color: var(--color-text-secondary);
|
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 {
|
.blend-actions {
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
@@ -3850,24 +3657,4 @@ function formatUserDate(iso: string): string {
|
|||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
margin-bottom: 0.75rem;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -249,34 +249,6 @@ async function confirmDelete() {
|
|||||||
flex-shrink: 0;
|
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 {
|
.when-to-use {
|
||||||
margin: 0.75rem 0 1.25rem;
|
margin: 0.75rem 0 1.25rem;
|
||||||
|
|||||||
@@ -572,37 +572,6 @@ function cancel() {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 0.25rem;
|
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) {
|
@media (max-width: 600px) {
|
||||||
.field-row,
|
.field-row,
|
||||||
|
|||||||
@@ -543,21 +543,6 @@ function usageTitle(s: SnippetListItem): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Moss action-primary per Hybrid — utility action, not a brand moment. */
|
/* 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 {
|
.search-row {
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.25rem;
|
||||||
@@ -669,16 +654,16 @@ function usageTitle(s: SnippetListItem): string {
|
|||||||
.empty-action {
|
.empty-action {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.4rem 1rem;
|
padding: 0.4rem 1rem;
|
||||||
border: 1px solid var(--color-primary);
|
border: 1px solid var(--color-action-primary);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
color: var(--color-primary);
|
color: var(--color-action-primary);
|
||||||
background: none;
|
background: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
transition: background 0.15s, color 0.15s;
|
transition: background 0.15s, color 0.15s;
|
||||||
}
|
}
|
||||||
.empty-action:hover {
|
.empty-action:hover {
|
||||||
background: var(--color-primary);
|
background: var(--color-action-primary);
|
||||||
color: var(--fs-text-on-action);
|
color: var(--fs-text-on-action);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -870,21 +855,6 @@ function usageTitle(s: SnippetListItem): string {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
align-items: center;
|
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). */
|
/* Selected card = 2px accent border per the design system (featured/active). */
|
||||||
.snippet-card.selected {
|
.snippet-card.selected {
|
||||||
@@ -931,10 +901,6 @@ function usageTitle(s: SnippetListItem): string {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.select-bar .btn-primary:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Merge modal */
|
/* Merge modal */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
|
|||||||
@@ -645,7 +645,7 @@ useEditorGuards(dirty, save);
|
|||||||
@focus="onParentFocus"
|
@focus="onParentFocus"
|
||||||
@blur="hideParentDropdown"
|
@blur="hideParentDropdown"
|
||||||
/>
|
/>
|
||||||
<button v-if="parentId" class="btn-clear-parent" @click="clearParentTask" title="Clear">×</button>
|
<button v-if="parentId" class="btn-text btn-clear-parent" @click="clearParentTask" title="Clear">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showParentDropdown" class="parent-dropdown">
|
<div v-if="showParentDropdown" class="parent-dropdown">
|
||||||
<div v-if="parentSearchLoading" class="parent-dropdown-item parent-empty">Searching...</div>
|
<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 v-if="isEditing" class="subtasks-section">
|
||||||
<div class="subtasks-header">
|
<div class="subtasks-header">
|
||||||
<span class="subtasks-label">Sub-tasks</span>
|
<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>
|
||||||
<div v-if="subTasksLoading" class="subtasks-loading">Loading...</div>
|
<div v-if="subTasksLoading" class="subtasks-loading">Loading...</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -686,8 +686,8 @@ useEditorGuards(dirty, save);
|
|||||||
@keydown.escape="addingSubTask = false; newSubTaskTitle = ''"
|
@keydown.escape="addingSubTask = false; newSubTaskTitle = ''"
|
||||||
autofocus
|
autofocus
|
||||||
/>
|
/>
|
||||||
<button class="btn-subtask-confirm" @click="createSubTask" :disabled="!newSubTaskTitle.trim()">Add</button>
|
<button class="btn-primary btn-compact" @click="createSubTask" :disabled="!newSubTaskTitle.trim()">Add</button>
|
||||||
<button class="btn-subtask-cancel" @click="addingSubTask = false; newSubTaskTitle = ''">Cancel</button>
|
<button class="btn-ghost btn-compact" @click="addingSubTask = false; newSubTaskTitle = ''">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -921,23 +921,6 @@ useEditorGuards(dirty, save);
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.04em;
|
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-checkbox { flex-shrink: 0; cursor: pointer; }
|
||||||
.subtask-title {
|
.subtask-title {
|
||||||
font-size: 0.83rem;
|
font-size: 0.83rem;
|
||||||
@@ -968,33 +951,6 @@ useEditorGuards(dirty, save);
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
.subtask-input:focus { outline: none; border-color: var(--color-primary); }
|
.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 {
|
.stream-preview {
|
||||||
border: 1px solid var(--color-input-border);
|
border: 1px solid var(--color-input-border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
@@ -1125,24 +1081,4 @@ useEditorGuards(dirty, save);
|
|||||||
color: var(--color-primary, #6366f1);
|
color: var(--color-primary, #6366f1);
|
||||||
font-style: normal;
|
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>
|
</style>
|
||||||
@@ -263,29 +263,29 @@ const subTaskProgress = computed(() => {
|
|||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<router-link
|
<router-link
|
||||||
:to="store.currentTask.project_id ? `/projects/${store.currentTask.project_id}` : '/tasks'"
|
: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>
|
>{{ store.currentTask.project_id ? "← Project" : "← Tasks" }}</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/tasks/${store.currentTask.id}/edit`"
|
:to="`/tasks/${store.currentTask.id}/edit`"
|
||||||
class="btn-edit"
|
class="btn-primary"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</router-link>
|
</router-link>
|
||||||
<button
|
<button
|
||||||
v-if="advanceLabel"
|
v-if="advanceLabel"
|
||||||
class="btn-advance"
|
class="btn-primary"
|
||||||
@click="advanceStatus"
|
@click="advanceStatus"
|
||||||
>
|
>
|
||||||
{{ advanceLabel }}
|
{{ advanceLabel }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn-convert"
|
class="btn-secondary btn-compact"
|
||||||
@click="convertToNote"
|
@click="convertToNote"
|
||||||
:disabled="converting"
|
:disabled="converting"
|
||||||
>
|
>
|
||||||
{{ converting ? "Converting..." : "Convert to Note" }}
|
{{ converting ? "Converting..." : "Convert to Note" }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-share" @click="showShare = true">Share</button>
|
<button class="btn-secondary btn-compact" @click="showShare = true">Share</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Breadcrumb: parent task → project → milestone -->
|
<!-- Breadcrumb: parent task → project → milestone -->
|
||||||
@@ -475,83 +475,6 @@ const subTaskProgress = computed(() => {
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin-bottom: 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 {
|
.meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ onMounted(() => store.fetchTrash());
|
|||||||
<h1>Trash</h1>
|
<h1>Trash</h1>
|
||||||
<button
|
<button
|
||||||
v-if="store.batches.length"
|
v-if="store.batches.length"
|
||||||
class="btn-empty"
|
class="btn-ghost btn-compact"
|
||||||
@click="empty"
|
@click="empty"
|
||||||
>Empty trash</button>
|
>Empty trash</button>
|
||||||
</header>
|
</header>
|
||||||
@@ -52,8 +52,8 @@ onMounted(() => store.fetchTrash());
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="batch-actions">
|
<div class="batch-actions">
|
||||||
<button class="btn-restore" @click="store.restore(b.batch_id)">Restore</button>
|
<button class="btn-ghost btn-compact 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-purge" @click="purge(b.batch_id)">Delete permanently</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -64,25 +64,11 @@ onMounted(() => store.fetchTrash());
|
|||||||
.trash-page { max-width: 900px; margin: 0 auto; padding: 1.5rem; }
|
.trash-page { max-width: 900px; margin: 0 auto; padding: 1.5rem; }
|
||||||
.trash-header { display: flex; align-items: center; justify-content: space-between; }
|
.trash-header { display: flex; align-items: center; justify-content: space-between; }
|
||||||
.trash-header h1 { font-family: Fraunces, serif; font-style: italic; margin: 0; }
|
.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-summary { font-weight: 500; }
|
||||||
.batch-count { opacity: 0.6; font-weight: 400; font-size: 0.9em; margin-left: 0.35rem; }
|
.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-meta { font-size: 0.82em; opacity: 0.6; margin-top: 0.25rem; }
|
||||||
.batch-actions { display: flex; gap: 0.5rem; flex-shrink: 0; }
|
.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; }
|
.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-restore:hover { border-color: var(--color-action-primary); color: var(--color-action-primary); }
|
||||||
.btn-purge:hover { border-color: var(--color-danger, #ef4444); color: var(--color-danger, #ef4444); }
|
.btn-purge:hover { border-color: var(--color-action-destructive); color: var(--color-action-destructive); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ function formatDate(iso: string): string {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="btn-toggle"
|
class="btn-primary btn-toggle"
|
||||||
:class="registrationOpen ? 'btn-toggle-close' : 'btn-toggle-open'"
|
:class="registrationOpen ? 'btn-toggle-close' : 'btn-toggle-open'"
|
||||||
@click="toggleRegistration"
|
@click="toggleRegistration"
|
||||||
:disabled="toggling"
|
:disabled="toggling"
|
||||||
@@ -190,7 +190,7 @@ function formatDate(iso: string): string {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn-invite"
|
class="btn-primary"
|
||||||
:disabled="sendingInvite || !inviteEmail.trim()"
|
:disabled="sendingInvite || !inviteEmail.trim()"
|
||||||
>
|
>
|
||||||
{{ sendingInvite ? "Sending..." : "Send Invite" }}
|
{{ 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="hide-mobile cell-date">{{ formatDate(inv.expires_at) }}</td>
|
||||||
<td class="cell-actions">
|
<td class="cell-actions">
|
||||||
<button
|
<button
|
||||||
class="btn-delete"
|
class="btn-ghost btn-compact"
|
||||||
@click="revokeInvitation(inv.id)"
|
@click="revokeInvitation(inv.id)"
|
||||||
:disabled="revokingId !== null"
|
:disabled="revokingId !== null"
|
||||||
>
|
>
|
||||||
@@ -262,17 +262,17 @@ function formatDate(iso: string): string {
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="confirmDeleteId === u.id">
|
<template v-else-if="confirmDeleteId === u.id">
|
||||||
<button
|
<button
|
||||||
class="btn-confirm-delete"
|
class="btn-danger btn-compact"
|
||||||
@click="confirmDelete(u.id)"
|
@click="confirmDelete(u.id)"
|
||||||
:disabled="deleting !== null"
|
:disabled="deleting !== null"
|
||||||
>
|
>
|
||||||
{{ deleting === u.id ? "Deleting..." : "Confirm" }}
|
{{ deleting === u.id ? "Deleting..." : "Confirm" }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-cancel-delete" @click="cancelDelete">Cancel</button>
|
<button class="btn-ghost btn-compact" @click="cancelDelete">Cancel</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button
|
<button
|
||||||
class="btn-delete"
|
class="btn-ghost btn-compact"
|
||||||
@click="confirmDelete(u.id)"
|
@click="confirmDelete(u.id)"
|
||||||
:disabled="deleting !== null"
|
:disabled="deleting !== null"
|
||||||
>
|
>
|
||||||
@@ -328,24 +328,6 @@ function formatDate(iso: string): string {
|
|||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--color-primary);
|
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 {
|
.invite-list {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
@@ -380,26 +362,8 @@ function formatDate(iso: string): string {
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
.btn-toggle {
|
/* The one genuine override: 'close registration' must NOT read as the
|
||||||
padding: 0.45rem 1rem;
|
primary action it sits on. Scoped, so it beats the shared variant. */
|
||||||
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;
|
|
||||||
}
|
|
||||||
.btn-toggle-close {
|
.btn-toggle-close {
|
||||||
background: var(--color-bg-secondary);
|
background: var(--color-bg-secondary);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
@@ -478,54 +442,6 @@ function formatDate(iso: string): string {
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--color-text-muted);
|
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) {
|
@media (max-width: 768px) {
|
||||||
.registration-row {
|
.registration-row {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "scribe",
|
"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.",
|
"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" },
|
"author": { "name": "Bryan Van Deusen" },
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"scribe": {
|
"scribe": {
|
||||||
|
|||||||
@@ -48,16 +48,9 @@ case "$file_path" in
|
|||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
esac
|
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 —
|
# 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)
|
lookup_dir=$(dirname -- "$file_path" 2>/dev/null || true)
|
||||||
[ -d "$lookup_dir" ] || lookup_dir=${event_cwd:-${CLAUDE_PROJECT_DIR:-$PWD}}
|
[ -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)
|
repo_root=$(git -C "$lookup_dir" rev-parse --show-toplevel 2>/dev/null || true)
|
||||||
@@ -68,6 +61,84 @@ if [ -n "$repo_root" ]; then
|
|||||||
esac
|
esac
|
||||||
fi
|
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
|
# 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
|
# 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
|
# 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
|
||||||
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 \
|
body=$(curl -fsS --max-time 5 \
|
||||||
-H "Authorization: Bearer ${token}" \
|
-H "Authorization: Bearer ${token}" \
|
||||||
"${url%/}/api/plugin/prior-art?path=${path_enc}&code=${code_enc}${repo_q}${exclude_q}" 2>/dev/null) || exit 0
|
"${url%/}/api/plugin/prior-art?path=${path_enc}&code=${code_enc}${repo_q}${exclude_q}" 2>/dev/null) || body=""
|
||||||
[ -n "$body" ] || exit 0
|
|
||||||
|
|
||||||
context=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null) || exit 0
|
|
||||||
[ -n "$context" ] || exit 0
|
|
||||||
|
|
||||||
|
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.
|
# Remember what was surfaced so it isn't shown again this session.
|
||||||
if [ -n "$idfile" ]; then
|
if [ -n "$idfile" ] && [ -n "$context" ]; then
|
||||||
printf '%s' "$body" | jq -r '.note_ids[]? // empty' 2>/dev/null >> "$idfile" || true
|
printf '%s' "$body" | jq -r '.note_ids[]? // empty' 2>/dev/null >> "$idfile" || true
|
||||||
fi
|
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.
|
# 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}}'
|
'{hookSpecificOutput: {hookEventName: "PreToolUse", additionalContext: $c}}'
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
+56
-1
@@ -181,13 +181,25 @@ def check_shellcheck() -> None:
|
|||||||
# pinning: the bug and the healthy no-results case look identical from outside.
|
# 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
|
# Pinning it does NOT make the failure visible; it makes sure the fail-open
|
||||||
# behaviour is deliberate rather than accidental.
|
# 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] = {
|
SMOKE_EVENTS: dict[str, str] = {
|
||||||
"scribe_autoinject.sh": json.dumps(
|
"scribe_autoinject.sh": json.dumps(
|
||||||
{"session_id": "smoke", "cwd": ".", "prompt": "a multi-line\nprompt\nhere"}
|
{"session_id": "smoke", "cwd": ".", "prompt": "a multi-line\nprompt\nhere"}
|
||||||
),
|
),
|
||||||
"scribe_prior_art.sh": json.dumps(
|
"scribe_prior_art.sh": json.dumps(
|
||||||
{"session_id": "smoke", "cwd": ".", "tool_name": "Edit",
|
{"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_sync_processes.sh": json.dumps({"source": "startup"}),
|
||||||
"scribe_session_context.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")
|
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]:
|
def _git(*args: str) -> tuple[int, str]:
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
["git", *args], capture_output=True, text=True, cwd=ROOT
|
["git", *args], capture_output=True, text=True, cwd=ROOT
|
||||||
@@ -344,6 +398,7 @@ def main() -> int:
|
|||||||
check_patterns()
|
check_patterns()
|
||||||
check_shellcheck()
|
check_shellcheck()
|
||||||
check_fail_open()
|
check_fail_open()
|
||||||
|
check_local_prior_art_needs_no_instance()
|
||||||
if not args.no_version:
|
if not args.no_version:
|
||||||
check_version_bump(args.base)
|
check_version_bump(args.base)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user