phase: 89_source_ignore_paths
All verification complete — TODO.md was already cleared in the roadmap commit; the two extra unit-test diffs are necessary fake-signature adaptations for the new keywords. Everything is green, no fixes were needed. ## Phase 89 — final verification pass: ALL GREEN **Verified (all 6 task files present in `complete/`):** - `git_sources.ignore_paths` JSONB column + migration 0013; `alembic downgrade -1 && upgrade head` round-trips (head `0013`) - Importer: `normalize_ignore_path`/`is_ignored`/`_ignore_for_root`, `ignore` in walk + progress pre-walk, `ignore_by_root` in `import_sources` - API: GET/POST carry list; admin-only `PATCH` (replace, 404/422 fixed details, anonymous 403) - Pipelines wired: `_run_sync`, `_run_upload` re-upload, `scripts/import_docs.py` - Sources-page box: dialog, §7.4 save lifecycle, `N ignored` tag, a11y; env rows get no box **Test/lint results:** - `uv run pytest` → 1808 passed - `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (>90%) - `uv run pytest tests/e2e/test_source_ignore_paths.py -v --no-cov` → 6 passed (isolated, DB up) - Regressions in isolation: `test_git_sources_admin` 6, `test_archive_upload_sources` 5, `test_sync_button` 3, `test_smoke` 3 — all passed - `uv run ruff check . && uv run pyright` → clean (0 errors) **Completion criteria:** box→PATCH 200→count+GET round-trip ✅ · sync excludes `ignore/` (no docs/chunks/embeddings/summaries) + prunes newly-ignored (pruned==2) ✅ · no-mid-path rule E2E ✅ · PATCH 404/422/replace/clear/403 ✅ · full gate green ✅ · commit + phase move left to harness per rules. **Deviations:** none blocking — E2E pins `files == 4` (overview's "5" was an off-by-one vs its own 6-file tree, documented in-test); `tests/unit/test_importer.py` + `test_sync_button.py` test-double fakes extended for the new keywords (needed for the suite to stay green). **Next pending phase:** none — `todo/` holds only this phase.
This commit is contained in:
@@ -143,6 +143,35 @@
|
||||
* gate and never fetches /api/git-sources (the Sources-page gate
|
||||
* pattern).
|
||||
*
|
||||
* Phase 89 (task 05) — the per-source ignore-paths editor: every
|
||||
* STORED row (s.id truthy — env-fallback rows carry NO button, A3:
|
||||
* nothing is stored to edit) gets an "Ignore paths" button in the
|
||||
* actions cell (left of Remove) that opens the page-local alertdialog
|
||||
* (#ignore-editor-dialog — the EXACT #remove-confirm-dialog pattern,
|
||||
* phase 69): #ignore-editor-source names the source (textContent ONLY
|
||||
* — the same `value` expression makeRow uses — URLs may embed
|
||||
* user:pass@ credentials, phase 32), the mono textarea prefills the
|
||||
* row's stored list (one path per line — the phase-89 A1 prefix rule,
|
||||
* stated in plain words in the helper copy), and focus lands on
|
||||
* Cancel (the safe default); Escape / Cancel / the dim backdrop close
|
||||
* as CANCEL (no request — focus returns to the row's trigger button).
|
||||
* "Save" runs the §7.4 in-flight lifecycle: both buttons disable +
|
||||
* the save relabels "Saving…" while the PATCH
|
||||
* /api/git-sources/{id} is out (blank lines in the box are separators,
|
||||
* not entries — split + trim + drop empty client-side; the server
|
||||
* still rejects empty entries defensively, A4). 200 → close (focus
|
||||
* return), loadSources (the row's "N ignored" count tag lands — the
|
||||
* A5 replace semantics round-trip through GET), announce — the update
|
||||
* confirmation is the LAST announcement (the reload's "N sources
|
||||
* listed." must not overwrite it); non-2xx → the in-dialog
|
||||
* role="alert" line (the server detail, apiDetail 422-shape-aware) +
|
||||
* the buttons restored, the dialog STAYS open and the textarea
|
||||
* content is KEPT (the instruction survives — tuning-form
|
||||
* convention); network failure → the fixed reachable? line, same
|
||||
* restore. Rows whose list is non-empty show the "N ignored" count
|
||||
* tag next to the location <code> (text + a distinct background —
|
||||
* never color alone, WCAG 1.4.1).
|
||||
*
|
||||
* Scope boundary (phase locked decisions): adding a git repo does
|
||||
* NOT clone — the sync service (server-side) does that. Removing a
|
||||
* source, however, performs the FULL cleanup server-side (phase 69):
|
||||
@@ -203,6 +232,17 @@ export async function mount(root) {
|
||||
const removeError = root.querySelector("#remove-confirm-error");
|
||||
const removeCancelBtn = root.querySelector("#remove-confirm-cancel");
|
||||
const removeRemoveBtn = root.querySelector("#remove-confirm-remove");
|
||||
/* Phase 89: the per-source ignore-paths editor — the SAME page-local
|
||||
alertdialog idiom as the remove-confirm modal (phase 69): static
|
||||
markup in the shell's Sources view; this module owns the open /
|
||||
cancel / save lifecycle. */
|
||||
const ignoreDialog = root.querySelector("#ignore-editor-dialog");
|
||||
const ignoreBackdrop = root.querySelector(".ignore-editor-backdrop");
|
||||
const ignoreSourceEl = root.querySelector("#ignore-editor-source");
|
||||
const ignoreTextarea = root.querySelector("#ignore-editor-textarea");
|
||||
const ignoreErrorEl = root.querySelector("#ignore-editor-error");
|
||||
const ignoreCancelBtn = root.querySelector("#ignore-editor-cancel");
|
||||
const ignoreSaveBtn = root.querySelector("#ignore-editor-save");
|
||||
|
||||
/* Polite live region: the screen-reader confirmation for loads, adds,
|
||||
and removals (the phase-15 announcer pattern). */
|
||||
@@ -321,6 +361,16 @@ export async function mount(root) {
|
||||
const code = document.createElement("code");
|
||||
code.textContent = value; // rendered as text, never as HTML
|
||||
urlTd.append(badge, code);
|
||||
/* Phase 89: the "N ignored" count tag — stored rows with a
|
||||
non-empty list only (text + a distinct background, never color
|
||||
alone — WCAG 1.4.1). The list round-trips: (s.ignore_paths ||
|
||||
[]) is the same expression openIgnoreEditor prefills from. */
|
||||
if (s.id && (s.ignore_paths || []).length > 0) {
|
||||
const count = document.createElement("span");
|
||||
count.className = "git-source-ignore-count";
|
||||
count.textContent = `${s.ignore_paths.length} ignored`;
|
||||
urlTd.append(count);
|
||||
}
|
||||
tr.appendChild(urlTd);
|
||||
|
||||
const addedTd = document.createElement("td");
|
||||
@@ -330,6 +380,22 @@ export async function mount(root) {
|
||||
const actTd = document.createElement("td");
|
||||
actTd.className = "git-source-actions-cell";
|
||||
if (s.id) {
|
||||
/* Phase 89: the per-row ignore-paths editor trigger (A3: stored
|
||||
rows only — env-fallback rows fall through to the "from
|
||||
.env" tag below). The aria-label is the ONLY place `value`
|
||||
appears (setAttribute — never innerHTML); the text label is
|
||||
static. Sits LEFT of Remove — the non-destructive action
|
||||
first. */
|
||||
const ignoreBtn = document.createElement("button");
|
||||
ignoreBtn.type = "button";
|
||||
ignoreBtn.className = "git-source-ignore";
|
||||
ignoreBtn.setAttribute(
|
||||
"aria-label",
|
||||
`Edit ignored paths for ${kindLabel} source: ${value}`,
|
||||
);
|
||||
ignoreBtn.textContent = "Ignore paths";
|
||||
ignoreBtn.addEventListener("click", () => openIgnoreEditor(s, ignoreBtn));
|
||||
actTd.appendChild(ignoreBtn);
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "git-source-remove";
|
||||
@@ -514,6 +580,168 @@ export async function mount(root) {
|
||||
if (removeBackdrop) removeBackdrop.addEventListener("click", cancelRemoveConfirm);
|
||||
if (removeRemoveBtn) removeRemoveBtn.addEventListener("click", confirmRemove);
|
||||
|
||||
/* ---------- ignore paths (PATCH /api/git-sources/{id}) — the per-row editor ----------
|
||||
* Phase 89: a stored row's "Ignore paths" button (makeRow — left of
|
||||
* Remove) opens the page-local alertdialog
|
||||
* (#ignore-editor-dialog — the EXACT #remove-confirm-dialog idiom,
|
||||
* phase 69) via openIgnoreEditor(s, triggerBtn): #ignore-editor-
|
||||
* source names the source (textContent ONLY — the same `value`
|
||||
* expression makeRow uses: s.path ?? s.url for local rows, s.url
|
||||
* for git — URLs may embed user:pass@ credentials, phase 32), the
|
||||
* mono textarea PREFILLS the row's stored list (one path per line
|
||||
* — the phase-89 A1 prefix rule, stated in plain words in the
|
||||
* helper copy), the error line clears, and focus lands on Cancel
|
||||
* (the safe default). Escape / Cancel / the dim backdrop close as
|
||||
* CANCEL: no request, focus returns to the row's trigger button.
|
||||
*
|
||||
* "Save" (saveIgnorePaths) runs the §7.4 never-stale lifecycle:
|
||||
* both buttons disable and the save relabels "Saving…" while the
|
||||
* PATCH is out — a blank line in the box is a separator, not an
|
||||
* entry (split + trim + drop empty lines client-side; the server
|
||||
* still rejects empty entries defensively, A4). 200 → close
|
||||
* (focus return) → loadSources (the "N ignored" count tag lands —
|
||||
* the A5 replace round-trips through GET) → announce (the update
|
||||
* confirmation is the LAST announcement — the reload's "N sources
|
||||
* listed." must not overwrite it); non-2xx → the in-dialog
|
||||
* role="alert" line (the server detail, apiDetail 422-shape-aware)
|
||||
* + the buttons restored — the dialog STAYS open and the textarea
|
||||
* content is KEPT (the instruction survives — the tuning-form
|
||||
* convention); network failure → the fixed reachable? line, same
|
||||
* restore. Env-fallback rows (id null) carry NO button (A3 —
|
||||
* nothing is stored to edit). */
|
||||
let ignoreTarget = null; // the row object of the open editor
|
||||
let ignoreTriggerBtn = null; // the row's button — focus returns here on close
|
||||
let ignoreInFlight = false; // §7.4: a PATCH is out (both buttons disabled)
|
||||
|
||||
function openIgnoreEditor(s, triggerBtn) {
|
||||
if (!ignoreDialog || !ignoreTextarea) return; // defensive — the markup ships with the page
|
||||
if (ignoreInFlight) return; // one editor at a time
|
||||
const isLocal = s.kind === "local";
|
||||
// The same `value` expression makeRow uses — textContent ONLY.
|
||||
if (ignoreSourceEl) ignoreSourceEl.textContent = isLocal ? s.path ?? s.url : s.url;
|
||||
ignoreTextarea.value = (s.ignore_paths || []).join("\n");
|
||||
if (ignoreErrorEl) {
|
||||
ignoreErrorEl.textContent = "";
|
||||
ignoreErrorEl.hidden = true; // a new attempt starts clean
|
||||
}
|
||||
ignoreInFlight = false;
|
||||
ignoreTarget = s;
|
||||
ignoreTriggerBtn = triggerBtn; // recorded for the focus return on close
|
||||
ignoreDialog.hidden = false;
|
||||
document.addEventListener("keydown", onIgnoreDialogKeydown);
|
||||
// Cancel is the safe default — focus lands on it (the remove-
|
||||
// dialog precedent; visibly: the global 3px :focus-visible
|
||||
// outline).
|
||||
if (ignoreCancelBtn) ignoreCancelBtn.focus();
|
||||
}
|
||||
|
||||
/* Any close (cancel, success): hide the dialog, reset the textarea
|
||||
+ error line, clear the target, detach the keydown handling, and
|
||||
return focus to the row's "Ignore paths" button (WCAG 2.1). */
|
||||
function closeIgnoreEditor() {
|
||||
if (!ignoreDialog) return;
|
||||
ignoreDialog.hidden = true;
|
||||
ignoreInFlight = false;
|
||||
if (ignoreTextarea) ignoreTextarea.value = "";
|
||||
if (ignoreErrorEl) {
|
||||
ignoreErrorEl.textContent = "";
|
||||
ignoreErrorEl.hidden = true;
|
||||
}
|
||||
if (ignoreCancelBtn) ignoreCancelBtn.disabled = false;
|
||||
if (ignoreSaveBtn) {
|
||||
ignoreSaveBtn.disabled = false;
|
||||
ignoreSaveBtn.textContent = "Save";
|
||||
}
|
||||
document.removeEventListener("keydown", onIgnoreDialogKeydown);
|
||||
const trigger = ignoreTriggerBtn;
|
||||
ignoreTarget = null;
|
||||
ignoreTriggerBtn = null;
|
||||
if (trigger) trigger.focus(); // focus returns to the row's button
|
||||
}
|
||||
|
||||
/* Escape / Cancel / backdrop all close as cancel — NO request. A
|
||||
cancel is a no-op while a PATCH is in flight (no half-cancel of
|
||||
an in-progress save; the buttons are disabled anyway, the
|
||||
Escape/backdrop paths need this guard). */
|
||||
function cancelIgnoreEditor() {
|
||||
if (ignoreInFlight) return;
|
||||
closeIgnoreEditor();
|
||||
}
|
||||
|
||||
/* While open (attached in openIgnoreEditor, detached in
|
||||
closeIgnoreEditor): Escape closes as cancel. */
|
||||
function onIgnoreDialogKeydown(e) {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
cancelIgnoreEditor();
|
||||
}
|
||||
}
|
||||
|
||||
async function saveIgnorePaths() {
|
||||
if (!ignoreTarget || ignoreInFlight) return; // one request at a time
|
||||
// The box's lines: one path per line — a blank line is a
|
||||
// separator, not an entry (trim + drop empty; the server still
|
||||
// rejects empties defensively, A4).
|
||||
const lines = ignoreTextarea.value.split("\n").map((l) => l.trim()).filter(Boolean);
|
||||
const t = ignoreTarget;
|
||||
const value = t.kind === "local" ? (t.path ?? t.url) : t.url;
|
||||
ignoreInFlight = true;
|
||||
if (ignoreErrorEl) {
|
||||
ignoreErrorEl.textContent = "";
|
||||
ignoreErrorEl.hidden = true;
|
||||
}
|
||||
if (ignoreCancelBtn) ignoreCancelBtn.disabled = true;
|
||||
if (ignoreSaveBtn) {
|
||||
ignoreSaveBtn.disabled = true;
|
||||
ignoreSaveBtn.textContent = "Saving…"; // §7.4 in-flight label
|
||||
}
|
||||
try {
|
||||
const r = await fetch(`/api/git-sources/${encodeURIComponent(t.id)}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ignore_paths: lines }),
|
||||
});
|
||||
if (r.ok) {
|
||||
// 200: the server replaced the row's list (A5).
|
||||
closeIgnoreEditor(); // focus returns to the row's button
|
||||
await loadSources(); // the "N ignored" count tag lands
|
||||
// The update confirmation is the LAST announcement: the
|
||||
// reload's "N sources listed." must not overwrite it (the
|
||||
// same order as the remove flow).
|
||||
announce(`Ignored paths updated for ${value}`);
|
||||
return;
|
||||
}
|
||||
// non-2xx: the in-dialog role="alert" line (the server detail)
|
||||
// — the dialog STAYS open and the textarea content is KEPT:
|
||||
// the fix is one edit + retry, not a re-type.
|
||||
if (ignoreErrorEl) {
|
||||
ignoreErrorEl.textContent = await apiDetail(r, "Could not save the ignored paths — try again.");
|
||||
ignoreErrorEl.hidden = false;
|
||||
}
|
||||
} catch {
|
||||
if (ignoreErrorEl) {
|
||||
ignoreErrorEl.textContent = "Could not save the ignored paths — is the app reachable?";
|
||||
ignoreErrorEl.hidden = false;
|
||||
}
|
||||
} finally {
|
||||
// Never stale (PLAN §7.4): the failure paths re-enable BOTH
|
||||
// buttons + relabel the save; the success path already closed
|
||||
// the dialog (which resets them) — the restore is a no-op
|
||||
// there.
|
||||
ignoreInFlight = false;
|
||||
if (ignoreCancelBtn) ignoreCancelBtn.disabled = false;
|
||||
if (ignoreSaveBtn) {
|
||||
ignoreSaveBtn.disabled = false;
|
||||
ignoreSaveBtn.textContent = "Save";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* The dialog's own buttons (static markup — wired once). */
|
||||
if (ignoreCancelBtn) ignoreCancelBtn.addEventListener("click", cancelIgnoreEditor);
|
||||
if (ignoreBackdrop) ignoreBackdrop.addEventListener("click", cancelIgnoreEditor);
|
||||
if (ignoreSaveBtn) ignoreSaveBtn.addEventListener("click", saveIgnorePaths);
|
||||
|
||||
/* ---------- add (POST /api/git-sources) — the git form ----------
|
||||
* wireAddForm gives the form the §7.4 never-stale lifecycle: while
|
||||
* the request is out the button disables + relabels "Adding…" and
|
||||
|
||||
@@ -2183,6 +2183,173 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
color: var(--bg);
|
||||
}
|
||||
|
||||
/* ---------- Per-source ignore-paths editor (phase 89, task 05) ----------
|
||||
The shell's Sources view: the page-local alertdialog that edits
|
||||
one source's ignore list (one path per line — the phase-89 A1
|
||||
prefix rule, stated in plain words in the helper copy). The EXACT
|
||||
#remove-confirm-dialog overlay contract (phase 69): a fixed
|
||||
full-viewport dim backdrop + a centered panel (z-index 1000, above
|
||||
the sticky header (20) + skip-link (100); NO blur — the phase-08
|
||||
no-blur perf anchor), scaled to the 46rem chat-column width or the
|
||||
viewport, whichever is narrower. The box: a VISIBLE block label
|
||||
(WCAG — never aria-label-only) over a mono textarea (the box is a
|
||||
data entry, not prose — the var(--mono) stack); the error line is
|
||||
the err pair (err-ink on err-bg 9.3:1, the err-line border);
|
||||
Cancel is the ghost ink-soft family (5.1:1 on --surface); Save is
|
||||
the solid brand family (--bg text on --brand 5.2:1 — the
|
||||
.new-chat-btn convention; the hover lightens the fill). Both
|
||||
buttons >=44px; :focus-visible via the global 3px outline rule (no
|
||||
local suppression — focus lands on Cancel at open and is visible);
|
||||
no animation (reduced-motion safe by construction). AA pairs: title
|
||||
/ copy / label are --ink (or ink-soft) on --surface (13.8:1 /
|
||||
5.1:1); the source value is --ink on --bg (16.7:1). Phase-08
|
||||
tokens only; system fonts; no CDN. */
|
||||
.ignore-editor {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex; /* the panel is the only in-flow child — margin: auto centers it */
|
||||
}
|
||||
/* Explicit (the global [hidden] rule already wins — the documented,
|
||||
testable contract for the skeleton). */
|
||||
.ignore-editor[hidden] { display: none; }
|
||||
|
||||
.ignore-editor-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
/* --bg at 82% — the doc-modal dim, no backdrop-filter (no-blur). */
|
||||
background: rgba(15, 10, 10, 0.82);
|
||||
}
|
||||
|
||||
.ignore-editor-panel {
|
||||
/* position:relative lifts the panel above the fixed backdrop
|
||||
(positioned elements paint over in-flow siblings otherwise). */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: auto;
|
||||
width: min(46rem, calc(100vw - 2rem));
|
||||
padding: 1.5rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.ignore-editor-title {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.3;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* The source's value (git URL or local path) — mono, wrapped (a long
|
||||
URL must not overflow the panel), --ink on --bg (16.7:1). */
|
||||
.ignore-editor-source {
|
||||
display: block;
|
||||
margin: 0 0 0.75rem;
|
||||
padding: 0.5rem 0.65rem;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
color: var(--ink);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.ignore-editor-copy {
|
||||
margin: 0 0 1rem;
|
||||
color: var(--ink); /* 13.8:1 on --surface */
|
||||
}
|
||||
.ignore-editor-copy code { font-family: var(--mono); font-size: 0.85em; }
|
||||
|
||||
/* Visible block label (WCAG — the textarea is NEVER labelled only via
|
||||
aria-label); ink-soft on --surface (5.1:1). */
|
||||
.ignore-editor-label {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* The box: full panel width, mono (the var(--mono) stack), theme-
|
||||
consistent border on --bg; the focus-visible ring is the global
|
||||
3px outline (no local suppression); vertical resize for long
|
||||
lists (up to 200 entries, A4). */
|
||||
.ignore-editor-textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 9rem;
|
||||
padding: 0.55rem 0.65rem;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
color: var(--ink);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* The in-dialog failure line (role=alert): the err pair (err-ink on
|
||||
err-bg 9.3:1, the err-line border) — the .remove-confirm-error
|
||||
language. */
|
||||
.ignore-editor-error {
|
||||
margin: 0.75rem 0 0;
|
||||
padding: 0.5rem 0.65rem;
|
||||
color: var(--err-ink);
|
||||
background: var(--err-bg);
|
||||
border: 1px solid var(--err-line);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.ignore-editor-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.6rem;
|
||||
margin-top: 1.1rem;
|
||||
}
|
||||
|
||||
/* The two dialog buttons: >=44px targets, the house 3px :focus-visible
|
||||
via the global outline rule (no local override). */
|
||||
.ignore-editor-btn {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 0.55rem 1.1rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ignore-editor-btn:disabled { opacity: 0.5; cursor: wait; }
|
||||
|
||||
/* Cancel — the ghost ink-soft family (5.1:1 on --surface), like the
|
||||
remove dialog's Cancel; the brand pair on hover (12.4:1 on
|
||||
brand-soft). */
|
||||
.ignore-editor-cancel {
|
||||
border: 1px solid var(--line);
|
||||
background: transparent;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.ignore-editor-cancel:hover:not(:disabled) {
|
||||
background: var(--brand-soft);
|
||||
color: var(--brand-ink);
|
||||
}
|
||||
|
||||
/* "Save" — the solid brand family (the .new-chat-btn convention):
|
||||
--bg text on --brand (5.2:1 — AA); the hover lightens the brand
|
||||
fill (the .new-chat-btn hover pair). */
|
||||
.ignore-editor-save {
|
||||
border: 0;
|
||||
background: var(--brand);
|
||||
color: var(--bg);
|
||||
}
|
||||
.ignore-editor-save:hover:not(:disabled) { background: #f55a72; }
|
||||
|
||||
/* The list: the Sources page's table pattern — full width in the
|
||||
72rem frame, surface card, horizontally scrollable wrapper (the
|
||||
URL column never wraps or ellipsizes: long URLs, credentials
|
||||
@@ -2266,6 +2433,54 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
.git-source-remove:hover:not(:disabled) { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
|
||||
.git-source-remove:disabled { opacity: 0.5; cursor: wait; }
|
||||
|
||||
/* Phase 89: the per-row "Ignore paths" trigger (makeRow — left of
|
||||
Remove): the .git-source-remove idiom (same size/spacing, >=44px
|
||||
target) in a NEUTRAL secondary fill — it must read distinct from
|
||||
the destructive Remove (text label included, never icon-only; the
|
||||
hover takes the brand-soft pair, 12.4:1 — Remove hovers to the err
|
||||
pair, 9.3:1). */
|
||||
.git-source-ignore {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
flex: 0 0 auto;
|
||||
padding: 0.35rem 0.7rem;
|
||||
margin-right: 0.4rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--ink-soft);
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
font-size: 0.82rem;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.git-source-ignore:hover:not(:disabled) {
|
||||
background: var(--brand-soft);
|
||||
color: var(--brand-ink);
|
||||
}
|
||||
|
||||
/* Phase 89: the "N ignored" count tag — next to the location <code>
|
||||
in the Source cell (TEXT + a distinct background, never color
|
||||
alone — WCAG 1.4.1): --ink on --bg (16.7:1) inside the --surface
|
||||
card; the line border keeps the chip legible when the row hover
|
||||
swaps the cell onto --bg too. */
|
||||
.git-source-ignore-count {
|
||||
display: inline-block;
|
||||
margin-left: 0.55rem;
|
||||
padding: 0.08rem 0.5rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
background: var(--bg);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Env-fallback rows carry no Remove (nothing is stored to remove) —
|
||||
the tag says where the row comes from (brand pair, 6.9:1). */
|
||||
.git-source-env-tag {
|
||||
|
||||
@@ -647,6 +647,44 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 89: the per-source ignore-paths editor — the EXACT
|
||||
#remove-confirm-dialog pattern (phase 69): page-local
|
||||
alertdialog, dim backdrop, focus on Cancel (the safe
|
||||
default), Escape/backdrop close as CANCEL, focus returns
|
||||
to the row's trigger button. The box itself: one path
|
||||
per line (prefix semantics — the phase-89 A1 rule,
|
||||
stated in plain words in the helper line); Save runs the
|
||||
§7.4 never-stale lifecycle (git-sources.js: "Saving…"
|
||||
while the PATCH is out). Stored rows only (A3) — the
|
||||
per-row button lives in git-sources.js's makeRow; this
|
||||
is the page-local dialog it opens. -->
|
||||
<div class="ignore-editor" id="ignore-editor-dialog" role="alertdialog"
|
||||
aria-modal="true" aria-labelledby="ignore-editor-title"
|
||||
aria-describedby="ignore-editor-copy" hidden>
|
||||
<div class="ignore-editor-backdrop" aria-hidden="true"></div>
|
||||
<div class="ignore-editor-panel">
|
||||
<h2 class="ignore-editor-title" id="ignore-editor-title">Ignored files and folders</h2>
|
||||
<code class="ignore-editor-source" id="ignore-editor-source"></code>
|
||||
<p class="ignore-editor-copy" id="ignore-editor-copy">
|
||||
One path per line. A file is ignored when its path in the
|
||||
source starts with any of these — <code>/my/files/</code>,
|
||||
<code>my/files/</code> and <code>my/files</code> mean the same
|
||||
thing. No wildcards, no matching in the middle of a path.
|
||||
</p>
|
||||
<label class="ignore-editor-label" for="ignore-editor-textarea">Ignored paths</label>
|
||||
<textarea class="ignore-editor-textarea" id="ignore-editor-textarea"
|
||||
rows="6" spellcheck="false"
|
||||
placeholder="my/files/"></textarea>
|
||||
<p class="ignore-editor-error" id="ignore-editor-error" role="alert" hidden></p>
|
||||
<div class="ignore-editor-actions">
|
||||
<button type="button" class="ignore-editor-btn ignore-editor-cancel"
|
||||
id="ignore-editor-cancel">Cancel</button>
|
||||
<button type="button" class="ignore-editor-btn ignore-editor-save"
|
||||
id="ignore-editor-save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Polite live region: the screen-reader confirmation for list
|
||||
loads, adds, and removals (git-sources.js owns the text). -->
|
||||
|
||||
Reference in New Issue
Block a user