diff --git a/web/src/app.css b/web/src/app.css
index 44811ecd..7f374a6e 100644
--- a/web/src/app.css
+++ b/web/src/app.css
@@ -25,6 +25,9 @@
/* Tint strength for Chip/badge fills (color-mix percentage). */
--chip-alpha: 16%;
+ /* Chip TEXT darkening: 100% = raw accent (dark theme); light theme
+ drops to ~72% mixed toward black so small colored labels hold AA. */
+ --chip-text-mix: 100%;
/* Brand accent. --accent-blue is aliased to it because ~95% of its 462
call sites mean "brand accent", not "blue" (see PLAN-2290); the
@@ -660,6 +663,7 @@ dialog.attachment-image-lightbox .attachment-image-lightbox-close:hover {
--card-border: #d0d0dc;
--shadow-card: 0 1px 3px rgba(23, 23, 40, 0.1);
--chip-alpha: 12%;
+ --chip-text-mix: 72%;
--accent-primary: #7c3aed;
--accent-primary-soft: #8b5cf6;
--accent-primary-strong: #7c3aed;
@@ -698,6 +702,7 @@ dialog.attachment-image-lightbox .attachment-image-lightbox-close:hover {
--card-border: #d0d0dc;
--shadow-card: 0 1px 3px rgba(23, 23, 40, 0.1);
--chip-alpha: 12%;
+ --chip-text-mix: 72%;
--accent-primary: #7c3aed;
--accent-primary-soft: #8b5cf6;
--accent-primary-strong: #7c3aed;
diff --git a/web/src/lib/components/collections/BoardView.svelte b/web/src/lib/components/collections/BoardView.svelte
index 8e99f1fc..519cc4ea 100644
--- a/web/src/lib/components/collections/BoardView.svelte
+++ b/web/src/lib/components/collections/BoardView.svelte
@@ -4,6 +4,7 @@
import { itemComparator, type SortMode } from '$lib/collections/itemSort';
import { reorderGroup, disabledDirections, adjacentColumn, type ReorderDirection } from '$lib/collections/reorder';
import { bucketByColumn, UNCATEGORIZED } from '$lib/collections/boardColumns';
+ import { columnAccentClassFor } from '$lib/utils/fieldColors';
import { dndzone, TRIGGERS, SHADOW_ITEM_MARKER_PROPERTY_NAME } from 'svelte-dnd-action';
import type { DndEvent } from 'svelte-dnd-action';
import ItemCard from './ItemCard.svelte';
@@ -453,17 +454,13 @@
return disabled;
}
+ // Delegates to the shared accent mapper (shareView.ts) so the in-app
+ // board and the public-share fork can never drift again — this was the
+ // FIFTH parallel status-color-ish implementation (PR #1023 Codex catch).
+ // Bonus: terminal_options-aware, so custom "Shipped"-style lanes read
+ // as done here too, matching public boards.
function columnCssClass(value: string): string {
- switch (value) {
- case 'in_progress':
- return 'col-in-progress';
- case 'done':
- return 'col-done';
- case 'blocked':
- return 'col-blocked';
- default:
- return '';
- }
+ return columnAccentClassFor(field, value);
}
@@ -712,6 +709,10 @@
gap: var(--space-1);
}
+ .column-header.col-open {
+ border-bottom-color: var(--status-blue);
+ }
+
.column-header.col-in-progress {
border-bottom-color: var(--accent-amber);
}
diff --git a/web/src/lib/components/collections/ItemCard.svelte b/web/src/lib/components/collections/ItemCard.svelte
index 14acfd61..3e7a17b5 100644
--- a/web/src/lib/components/collections/ItemCard.svelte
+++ b/web/src/lib/components/collections/ItemCard.svelte
@@ -7,6 +7,7 @@
import { copyToClipboard } from '$lib/utils/clipboard';
import { relativeTime } from '$lib/utils/markdown';
import { statusColor, priorityColor, formatFieldLabel as formatLabel } from '$lib/utils/fieldColors';
+ import Chip from '$lib/components/common/Chip.svelte';
import ItemActionsMenu from './ItemActionsMenu.svelte';
import type { ReorderDirection } from '$lib/collections/reorder';
import { shouldOpenInPane } from './itemCardClick';
@@ -173,14 +174,6 @@
{/if}
-
{#if showCollection && item.collection_name}
{#if item.collection_icon}{item.collection_icon} {/if}{item.collection_name}
@@ -208,6 +201,14 @@
{copied ? `Copied ${itemRef}` : ''}
{/if}
+
{#if onReorderItem}
{#if statusField && fields.status}
{#if statusCyclable}
-
+ {formatLabel(fields.status)}
+
{:else}
-
- {formatLabel(fields.status).toUpperCase()}
-
+
+ {formatLabel(fields.status)}
+
{/if}
{/if}
{#if priorityField && fields.priority}
- {#if statusField && fields.status}·{/if}
-
+
{formatLabel(fields.priority)}
-
+
{/if}
{#if item.parent_title}
- ·
{@const parentLabel = item.parent_ref ? `${item.parent_ref}: ${item.parent_title}` : item.parent_title}
{parentLabel}
{/if}
@@ -294,9 +293,10 @@
display: flex;
flex-direction: column;
gap: var(--space-2);
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: var(--radius);
+ background: var(--card-bg, var(--bg-primary));
+ border: 1px solid var(--card-border, var(--border));
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-card, none);
padding: var(--space-4) var(--space-5);
text-decoration: none;
color: inherit;
@@ -338,15 +338,19 @@
transform: scale(0.95);
}
- .item-card:hover,
- .item-card.focused {
- background: var(--bg-hover);
+ .item-card:hover {
+ border-color: var(--border-strong, var(--border));
text-decoration: none;
}
+ /* Selected-in-pane ring (the mock's violet glow). E2E asserts the
+ .focused CLASS, not these styles — keep the class name stable. */
.item-card.focused {
- outline: 2px solid var(--accent-blue);
- outline-offset: -2px;
+ border-color: color-mix(in srgb, var(--accent-primary, var(--accent-blue)) 60%, transparent);
+ box-shadow:
+ 0 0 0 1px color-mix(in srgb, var(--accent-primary, var(--accent-blue)) 45%, transparent),
+ 0 4px 18px color-mix(in srgb, var(--accent-primary, var(--accent-blue)) 22%, transparent);
+ text-decoration: none;
}
.item-card.compact {
@@ -359,10 +363,11 @@
gap: var(--space-2);
}
- /* Reorder kebab sits at the far right of the top row. When a PR badge
- is present the `.has-pr` padding-right reservation keeps it clear of
- the absolutely-positioned badge (kebab lands just left of it). */
- .card-top-row :global(.item-actions-menu) {
+ /* The star + reorder kebab form the right cluster (mock anatomy: ref
+ left, star/kebab right). ONE auto margin — on the star — pushes the
+ cluster right; the kebab follows it. Two competing auto margins
+ would split the free space (see .meta-spacer note below). */
+ .card-top-row .star-btn {
margin-left: auto;
}
@@ -489,60 +494,14 @@
min-width: 0;
}
- .meta-status {
- font-size: 0.7em;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.02em;
- white-space: nowrap;
- }
-
- .meta-status-btn {
- border: none;
- background: none;
- cursor: pointer;
- padding: 0;
- font-family: inherit;
- line-height: inherit;
- transition: filter 0.1s, transform 0.1s;
- }
-
- .meta-status-btn:hover {
- filter: brightness(1.3);
- transform: scale(1.05);
- }
-
- .meta-status-btn:active {
- transform: scale(0.95);
- }
-
- .meta-status-btn.pulsing {
- animation: status-pulse 0.3s ease-out;
- }
-
- @keyframes status-pulse {
- 0% {
- text-shadow: 0 0 0 currentColor;
- }
- 70% {
- text-shadow: 0 0 8px currentColor;
- }
- 100% {
- text-shadow: 0 0 0 currentColor;
- }
- }
+ /* Status + priority render as Chip primitives now (tinted pills per
+ the refresh mock); the chip carries the click-cycle + pulse. */
.meta-sep {
font-size: 0.7em;
color: var(--text-muted);
}
- .meta-priority {
- font-size: 0.7em;
- font-weight: 600;
- white-space: nowrap;
- }
-
.meta-parent {
font-size: 0.7em;
font-weight: 500;
@@ -596,16 +555,18 @@
gap: var(--space-1, 0.25rem);
}
+ /* Tag pills use the Chip tint treatment (purple family per the mock). */
.card-tag {
display: inline-flex;
align-items: center;
- padding: 0.05em 0.45em;
+ padding: 0.1em 0.55em;
font-size: 0.68em;
line-height: 1.5;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: 999px;
- color: var(--text-secondary);
+ background: color-mix(in srgb, var(--accent-purple) var(--chip-alpha, 16%), transparent);
+ border: none;
+ border-radius: 6px;
+ color: color-mix(in srgb, var(--accent-purple) var(--chip-text-mix, 100%), #000);
+ font-weight: 500;
cursor: pointer;
max-width: 12rem;
overflow: hidden;
@@ -614,8 +575,7 @@
}
.card-tag:hover {
- color: var(--text-primary);
- border-color: var(--text-tertiary, var(--text-secondary));
+ filter: brightness(1.15);
}
.card-progress {
diff --git a/web/src/lib/components/common/Chip.svelte b/web/src/lib/components/common/Chip.svelte
index f3de37fa..630fc482 100644
--- a/web/src/lib/components/common/Chip.svelte
+++ b/web/src/lib/components/common/Chip.svelte
@@ -65,7 +65,9 @@
line-height: 1.5;
white-space: nowrap;
background: color-mix(in srgb, var(--chip-c) var(--chip-alpha, 16%), transparent);
- color: var(--chip-c);
+ /* --chip-text-mix darkens chip text in light mode (100% dark / ~75%
+ light) so small colored labels hold AA on white-ish surfaces. */
+ color: color-mix(in srgb, var(--chip-c) var(--chip-text-mix, 100%), #000);
}
.chip.md {
diff --git a/web/src/lib/components/share/PublicBoardView.svelte b/web/src/lib/components/share/PublicBoardView.svelte
index 398f1de1..7eb77f15 100644
--- a/web/src/lib/components/share/PublicBoardView.svelte
+++ b/web/src/lib/components/share/PublicBoardView.svelte
@@ -93,6 +93,10 @@
font-size: 0.9em;
}
+ .column-header.col-open {
+ border-bottom-color: var(--status-blue);
+ }
+
.column-header.col-in-progress {
border-bottom-color: var(--accent-amber);
}
diff --git a/web/src/lib/components/share/shareView.ts b/web/src/lib/components/share/shareView.ts
index 4a46b659..dc3c726e 100644
--- a/web/src/lib/components/share/shareView.ts
+++ b/web/src/lib/components/share/shareView.ts
@@ -327,21 +327,10 @@ export function fieldValueColor(field: FieldDef | undefined, value: string): str
return 'var(--text-muted)';
}
-/** Schema-driven board-column accent class. Literal status values map to the
- * in-app palette; a custom terminal option falls back to the `done` accent so
- * a "Shipped"/"Closed" column still reads as a finished lane. */
-export function columnAccentClassFor(field: FieldDef | undefined, value: string): string {
- switch (value) {
- case 'in_progress':
- return 'col-in-progress';
- case 'done':
- return 'col-done';
- case 'blocked':
- return 'col-blocked';
- }
- if (value && field?.terminal_options?.includes(value)) return 'col-done';
- return '';
-}
+/** Board-column accent class — canonical implementation lives in
+ * $lib/utils/fieldColors (neutral module); re-exported here so the
+ * public-share import surface is unchanged. */
+export { columnAccentClassFor } from '$lib/utils/fieldColors';
/** Group `items` by `groupField` value, in option order with any extra values
diff --git a/web/src/lib/utils/fieldColors.ts b/web/src/lib/utils/fieldColors.ts
index 7e4ab5b2..eafad065 100644
--- a/web/src/lib/utils/fieldColors.ts
+++ b/web/src/lib/utils/fieldColors.ts
@@ -103,3 +103,28 @@ export function hasCanonicalStatus(value: string): boolean {
export function formatFieldLabel(value: string): string {
return value.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
}
+
+/** Board-column accent class for a lane value. Literal statuses map to the
+ * canonical lane palette; a custom terminal option (e.g. "shipped") reads
+ * as a done lane. Shared by BoardView AND the public-share fork
+ * (shareView.ts re-exports) so lane accents can never drift between them. */
+export function columnAccentClassFor(
+ field: { terminal_options?: string[] } | undefined,
+ value: string
+): string {
+ switch (value) {
+ case 'open':
+ case 'new':
+ case 'todo':
+ case 'planned':
+ return 'col-open';
+ case 'in_progress':
+ return 'col-in-progress';
+ case 'done':
+ return 'col-done';
+ case 'blocked':
+ return 'col-blocked';
+ }
+ if (value && field?.terminal_options?.includes(value)) return 'col-done';
+ return '';
+}