Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions docs/design-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ Application rules:

- `--color-ops-base`: `#0a0f0d`
- `--color-ops-surface-base`: `#0e1411`
- `--color-ops-surface-1`: `#121814`
- `--color-ops-surface-2`: `#1a221e`
- `--color-ops-surface-1`: `#101713`
- `--color-ops-surface-2`: `#18221d`
- `--color-ops-surface-3`: `#222b27`
- `--color-ops-surface-raised`: `#1a221e`
- `--color-ops-surface-overlay`: `#222b27`
Expand Down Expand Up @@ -127,6 +127,7 @@ Application rules:
- `--ops-hover-2-bg`: `rgba(255, 255, 255, 0.025)`
- `--ops-elevation-1`: standard card elevation
- `--ops-elevation-2`: active or hovered panel elevation
- `--ops-elevation-3`: reserved high-emphasis shell elevation
- `--ops-motion-fast`: `90ms ease-out`
- `--ops-motion-standard`: `160ms ease-out`
- `--ops-motion-slow`: `240ms ease-out`
Expand Down Expand Up @@ -222,11 +223,11 @@ Shared notch sizes live in `src/styles/index.css`.
Use the outer value on structural shells and the inner value on the surface inset.
Use the chip value for badges, compact controls, and clipped action buttons.

- `--ops-notch-shell-outer`: `16px`
- `--ops-notch-shell-inner`: `15px`
- `--ops-notch-panel-outer`: `12px`
- `--ops-notch-shell-outer`: `18px`
- `--ops-notch-shell-inner`: `16px`
- `--ops-notch-panel-outer`: `13px`
- `--ops-notch-panel-inner`: `11px`
- `--ops-notch-chip`: `8px`
- `--ops-notch-chip`: `7px`

Shared polygon pattern:

Expand Down
27 changes: 14 additions & 13 deletions src/components/DomainCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function DomainCard({

const shellClassName = busy
? 'clip-notched ops-notch-panel-outer h-full bg-ops-border-strong p-px'
: 'ops-domain-card group clip-notched ops-notch-panel-outer h-full bg-ops-border-strong p-px transition-colors';
: 'ops-domain-card group clip-notched ops-notch-panel-outer h-full bg-ops-border-strong p-px transition-colors hover:bg-ops-accent/25 focus-within:bg-ops-accent/25';

useEffect(() => {
const pendingStatus = pendingKeyboardFocusStatusRef.current;
Expand Down Expand Up @@ -150,12 +150,12 @@ export function DomainCard({
<div className={shellClassName}>
<div
className={[
'clip-notched ops-notch-panel-inner ops-surface-raised-card flex h-full min-h-[15rem] flex-col justify-between p-[var(--spacing-ops-5)] text-left transition-colors duration-150 ease-out lg:p-[var(--spacing-ops-4)] xl:min-h-[16rem] xl:p-[var(--spacing-ops-5)]',
'clip-notched ops-notch-panel-inner ops-surface-raised-card flex h-full min-h-[15rem] flex-col justify-between p-[var(--spacing-ops-5)] text-left transition-colors duration-150 ease-out sm:p-[var(--spacing-ops-6)] xl:min-h-[16rem]',
spineClassName,
].join(' ')}
>
<div className="grid gap-3">
<div className="flex items-center gap-2.5">
<div className="flex items-start gap-3">
<span className="grid shrink-0 justify-items-center gap-2">
<span
className={[
'ops-domain-glyph inline-flex h-9 w-9 items-center justify-center rounded-[2px] border border-ops-border-soft bg-ops-surface-base',
Expand All @@ -168,21 +168,22 @@ export function DomainCard({
</span>
<span
className={[
'ops-coordinate-chip clip-notched ops-notch-chip ops-tracking-grid inline-flex min-h-7 items-center gap-1.5 px-2.5 text-[10px] font-semibold uppercase',
'ops-coordinate-chip clip-notched ops-notch-chip ops-tracking-grid inline-flex min-h-6 items-center px-2 text-[10px] font-semibold uppercase',
sectorTintClassName,
].join(' ')}
aria-hidden="true"
>
<span>{sectorSigil}</span>
<span>/</span>
<span>{sector.shortLabel}</span>
{sectorSigil}
</span>
</div>
</span>
<div className="min-w-0">
<h3 className="ops-tracking-caption mt-2 text-sm leading-5 font-semibold text-ops-text-primary uppercase">
<span className="ops-mono ops-tracking-eyebrow flex flex-wrap items-center gap-2 text-xs font-semibold text-ops-text-muted uppercase">
<span>{sector.shortLabel}</span>
</span>
<h3 className="ops-domain-title ops-tracking-caption mt-2 text-sm leading-5 font-semibold text-ops-text-primary uppercase">
{sector.label}
</h3>
<p className="mt-2 line-clamp-3 text-sm leading-5 text-ops-text-muted">
<p className="ops-domain-description mt-3 text-sm leading-6 text-ops-text-secondary">
{sector.description}
</p>
</div>
Expand All @@ -201,7 +202,7 @@ export function DomainCard({
role="radiogroup"
aria-label={`${sector.label} status`}
aria-describedby={describedBy}
className="mt-3 grid grid-cols-3 gap-1.5 xl:gap-2"
className="mt-3 grid grid-cols-3 gap-2"
>
{STATUS_OPTIONS.map((option, optionIndex) => {
const content = getStatusContent(option);
Expand Down Expand Up @@ -229,7 +230,7 @@ export function DomainCard({
}}
onKeyDown={(event) => handleRadioKeyDown(event, optionIndex)}
className={[
'ops-focus-ring-chip ops-radio-chip tactical-chip-panel ops-tracking-grid min-h-[var(--ops-chip-min-h)] border px-1.5 py-2 text-center text-[11px] font-semibold uppercase xl:px-2',
'ops-focus-ring-chip ops-radio-chip tactical-chip-panel ops-tracking-grid min-h-[var(--ops-chip-min-h)] border px-2 py-2 text-center text-[11px] font-semibold uppercase',
busy ? 'cursor-wait opacity-70' : '',
isSelected
? `${content.classes} shadow-[inset_0_1px_0_rgba(255,255,255,0.08)]`
Expand Down
4 changes: 2 additions & 2 deletions src/components/NotchedFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@ export const NotchedFrame = forwardRef<HTMLElement, NotchedFrameProps>(
>
<div
className={joinClasses(
'clip-notched p-px',
'ops-notch-frame-outer clip-notched p-px',
outerNotchClassName,
!outerClassName && `ops-frame-emphasis-${emphasis}`,
outerClassName,
)}
>
<div
className={joinClasses(
'clip-notched',
'ops-notch-frame-inner clip-notched',
innerNotchClassName,
innerClassName,
)}
Expand Down
10 changes: 5 additions & 5 deletions src/components/StatusLegend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ import { StatusBadge } from './StatusBadge';

export function StatusLegend() {
return (
<div className="ops-mono ops-flat-panel ops-tracking-grid grid gap-2 px-3 py-2 text-[11px] text-ops-text-muted uppercase">
<div className="ops-status-legend ops-mono ops-flat-panel ops-tracking-grid grid gap-2 px-3 py-2 text-[11px] text-ops-text-muted uppercase">
<p className="ops-eyebrow text-[10px] leading-none text-ops-text-muted">
Legend
</p>
<div className="grid grid-cols-3 gap-2">
<div className="grid justify-items-start gap-1 whitespace-nowrap">
<div className="grid grid-cols-3 gap-1.5">
<div className="ops-status-legend-item grid justify-items-start gap-1 whitespace-nowrap">
<StatusBadge status="nominal" size="sm" />
<span>Nominal</span>
</div>
<div className="grid justify-items-start gap-1 whitespace-nowrap">
<div className="ops-status-legend-item grid justify-items-start gap-1 whitespace-nowrap">
<StatusBadge status="degraded" size="sm" />
<span>Degraded</span>
</div>
<div className="grid justify-items-start gap-1 whitespace-nowrap">
<div className="ops-status-legend-item grid justify-items-start gap-1 whitespace-nowrap">
<StatusBadge status="unmarked" size="sm" />
<span>Unmarked</span>
</div>
Expand Down
56 changes: 29 additions & 27 deletions src/features/checkin/TodayPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ function DayCompletionRollup({
return (
<div
className={[
'ops-day-rollup tactical-subpanel-strong mb-4 grid gap-4 p-4 sm:p-5',
'ops-day-rollup tactical-subpanel-strong mb-4 grid gap-4 p-4 sm:p-5 lg:grid-cols-[auto_minmax(0,1fr)] lg:items-center',
isComplete ? 'ops-day-rollup-complete ops-complete-badge' : '',
].join(' ')}
>
<div className="min-w-[8rem]">
<div className="ops-rollup-count-plate min-w-[8rem]">
<p className="ops-numeric flex items-baseline gap-1.5 text-2xl leading-none font-semibold text-ops-text-primary">
<span>{markedCount}</span>
<span className="text-base text-ops-text-muted">/ {totalCount}</span>
Expand All @@ -83,33 +83,35 @@ function DayCompletionRollup({
{rollupText}
</p>
</div>
<div
className="ops-rollup-pip-grid grid grid-cols-5 gap-2 sm:gap-4 lg:gap-5"
aria-label={rollupLabel}
>
{SECTORS.map((sector, index) => {
const status = statuses[index] ?? 'unmarked';
<div className="flex flex-wrap items-center gap-3 lg:justify-end">
<div
className="ops-day-rollup-meter grid min-w-[13.5rem] grid-cols-5 gap-px bg-ops-panel-border-strong p-px"
aria-label={rollupLabel}
>
{SECTORS.map((sector, index) => {
const status = statuses[index] ?? 'unmarked';

return (
<span
key={sector.id}
className={[
'ops-rollup-pip-cell clip-notched ops-notch-chip inline-flex min-h-10 flex-col items-center justify-center border',
getRollupPipClassName(status),
].join(' ')}
title={sector.label}
>
return (
<span
className={getRollupIndicatorClassName(status)}
aria-hidden="true"
/>
<span className="sr-only">{sector.label}</span>
<span className="ops-rollup-pip-glyph text-xs leading-none">
<SectorGlyphMark sectorId={sector.id} />
key={sector.id}
className={[
'ops-rollup-pip-cell clip-notched ops-notch-chip inline-flex min-h-10 flex-col items-center justify-center border',
getRollupPipClassName(status),
].join(' ')}
title={sector.label}
>
<span
className={getRollupIndicatorClassName(status)}
aria-hidden="true"
/>
<span className="sr-only">{sector.label}</span>
<span className="ops-rollup-pip-glyph text-xs leading-none">
<SectorGlyphMark sectorId={sector.id} />
</span>
</span>
</span>
);
})}
);
})}
</div>
</div>
</div>
);
Expand Down Expand Up @@ -254,7 +256,7 @@ export function TodayPanel({
totalCount={completion.totalCount}
/>

<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-5 lg:gap-5">
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 lg:gap-5">
{SECTORS.map((sector, index) => (
<div key={sector.id}>
<DomainCard
Expand Down
7 changes: 4 additions & 3 deletions src/features/history/DesktopHistoryGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export function DesktopHistoryGrid({ model }: DesktopHistoryGridProps) {
isToday
? 'ops-history-today-header bg-[var(--ops-tint-1)] text-ops-accent-muted'
: isSelectedColumn
? 'bg-[var(--ops-tint-2)] text-ops-text-primary'
? 'ops-history-selected-column-header bg-[var(--ops-tint-2)] text-ops-text-primary'
: 'bg-ops-surface-1 text-ops-text-secondary',
].join(' ')}
scope="col"
Expand Down Expand Up @@ -280,8 +280,9 @@ export function DesktopHistoryGrid({ model }: DesktopHistoryGridProps) {
? 'ops-history-selected-cell bg-[var(--ops-tint-3)]'
: isToday
? 'ops-history-today-cell'
: dateKey === selectedCell.dateKey
? 'bg-[var(--ops-tint-1)]'
: dateKey === selectedCell.dateKey &&
hasInteractedWithHistory
? 'ops-history-selected-column bg-[var(--ops-tint-1)]'
: isOddWeek
? 'ops-history-week-band'
: '',
Expand Down
33 changes: 16 additions & 17 deletions src/features/history/MobileHistoryGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export function MobileHistoryGrid({ model }: MobileHistoryGridProps) {
role="status"
aria-live="polite"
aria-atomic="true"
className="ops-flat-panel ops-tracking-grid px-3 py-2 text-center text-xs uppercase text-ops-text-secondary max-[360px]:px-2"
className="ops-mobile-week-status ops-flat-panel ops-tracking-grid px-3 py-2 text-center text-xs uppercase text-ops-text-secondary max-[360px]:px-2"
>
<span className="block text-[10px] text-ops-text-muted">
Week {visibleWeekIndex + 1} of {weekGroups.length}
Expand Down Expand Up @@ -237,22 +237,21 @@ export function MobileHistoryGrid({ model }: MobileHistoryGridProps) {
id={weekHeadingId}
className="ops-tracking-grid text-xs font-semibold uppercase text-ops-text-secondary"
>
W{weekIndex + 1} / {formatDayLabel(weekStart)}-
W{weekIndex + 1}/{formatDayLabel(weekStart)}-
{formatDayLabel(weekEnd)}
</p>
</div>
{visibleWeekIndex === weekIndex ? (
<div
id={weekStateId}
className="ops-flat-panel ops-tracking-grid px-2.5 py-1 text-right text-[10px] font-semibold text-ops-accent-muted uppercase"
>
On deck
</div>
) : (
<span id={weekStateId} className="sr-only">
Week not active
</span>
)}
<div
id={weekStateId}
className={[
'ops-mobile-week-state-chip ops-flat-panel ops-tracking-grid px-2.5 py-1 text-right text-[10px] font-semibold uppercase',
visibleWeekIndex === weekIndex
? 'text-ops-accent-muted'
: 'text-ops-text-muted',
].join(' ')}
>
{visibleWeekIndex === weekIndex ? 'On deck' : 'Stand by'}
</div>
</div>

<div
Expand All @@ -278,7 +277,7 @@ export function MobileHistoryGrid({ model }: MobileHistoryGridProps) {
className={[
'ops-focus-ring-chip ops-mobile-day-button ops-notch-chip clip-notched min-h-14 min-w-10 border px-2 py-2.5 text-center font-semibold transition hover:border-ops-border-struct hover:bg-[var(--ops-tint-2)] hover:text-ops-text-primary',
isSelectedDay && hasInteractedWithHistory
? 'ops-history-selected-cell text-ops-accent-muted'
? 'ops-mobile-day-button-selected ops-history-selected-cell text-ops-accent-muted'
: 'border-ops-border-soft bg-ops-surface-2 text-ops-text-secondary',
isToday && !isSelectedDay
? 'ops-history-today-cell'
Expand Down Expand Up @@ -366,8 +365,8 @@ export function MobileHistoryGrid({ model }: MobileHistoryGridProps) {
className={[
'ops-mobile-week-pip clip-notched ops-notch-chip border',
visibleWeekIndex === weekIndex
? 'ops-mobile-week-pip-active border-ops-accent bg-ops-accent'
: 'border-ops-border-soft bg-ops-border-struct',
? 'border-ops-accent bg-ops-accent'
: 'border-ops-border-soft bg-transparent',
].join(' ')}
/>
))}
Expand Down
44 changes: 38 additions & 6 deletions src/features/history/useHistoryGridModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,30 @@ interface HistoryGridDayStatus {
status: UiStatus;
}

function scrollWeekIntoContainer(
scrollNode: HTMLDivElement | null,
weekNode: HTMLDivElement | null | undefined,
) {
if (!scrollNode || !weekNode) {
return;
}

const nextScrollLeft = Math.max(
weekNode.offsetLeft - scrollNode.offsetLeft,
0,
);

if (typeof scrollNode.scrollTo === 'function') {
scrollNode.scrollTo({
behavior: 'auto',
left: nextScrollLeft,
});
return;
}

scrollNode.scrollLeft = nextScrollLeft;
}

export interface HistoryGridModel {
dateKeys: string[];
todayKey: string;
Expand Down Expand Up @@ -193,7 +217,7 @@ export function useHistoryGridModel({
const hasAlignedInitialMobileWeekRef = useRef(false);

useEffect(() => {
if (isDesktopHistory) {
if (isDesktopHistory || !hasEntries) {
hasAlignedInitialMobileWeekRef.current = false;
return;
}
Expand All @@ -202,10 +226,16 @@ export function useHistoryGridModel({
return;
}

hasAlignedInitialMobileWeekRef.current = true;
const scrollNode = mobileScrollRef.current;
const weekNode = weekRefs.current.get(initialSelectedWeekIndex);
weekNode?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
}, [initialSelectedWeekIndex, isDesktopHistory]);

if (!scrollNode || !weekNode) {
return;
}

hasAlignedInitialMobileWeekRef.current = true;
scrollWeekIntoContainer(scrollNode, weekNode);
}, [hasEntries, initialSelectedWeekIndex, isDesktopHistory]);

useEffect(() => {
if (
Expand Down Expand Up @@ -355,8 +385,10 @@ export function useHistoryGridModel({
dateKey: targetDateKey,
}));

const weekNode = weekRefs.current.get(targetWeekIndex);
weekNode?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
scrollWeekIntoContainer(
mobileScrollRef.current,
weekRefs.current.get(targetWeekIndex),
);
},
[lastWeekIndex, selectedCell.dateKey, weekGroups],
);
Expand Down
Loading
Loading