Skip to content
Open
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
48 changes: 47 additions & 1 deletion src/ui/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,52 @@ cd external/src/ui && pnpm type-check && pnpm lint && pnpm test --run
pnpm format
```

## Reasoning Integrity: Avoiding Systematic Failure Modes

These principles guard against how Claude tends to reason incorrectly about code changes.

### Fix at the source — never by convergence

When two things are inconsistent, identify which is *correct* and fix the other. Do not flatten both to a common (often weaker) state to create false consistency.

```text
❌ A strips time from a datetime-local input → downgrade input to type="date" for "consistency"
✅ A strips time from a datetime-local input → remove the stripping; the datetime was correct
```

**The pattern to catch:** "These two things are inconsistent, so I'll make them both match the simpler one." This is always wrong. One side is the bug; find it.

### Capability loss is a regression — always

Reducing precision, expressiveness, or user capability requires explicit user approval even when it creates consistency or simplifies the implementation. This includes:

- Input type downgrades (`datetime-local` → `date`, `number` → `text`)
- Data truncation (full ISO datetime → date-only string, float → int)
- Feature removal (range picker → single value, multi-select → single-select)
- API parameter removal or narrowing

If the rationale for a change involves "simpler" at the cost of capability, stop and ask.

### Apply the reversal test before citing evidence

Before using a fact to justify a change, ask: *"Does this same evidence equally support the opposite conclusion?"*

```text
Fact: backend accepts datetime.datetime
→ Wrong: "date-only strings work too, so use type='date'"
→ Right: "the backend supports full precision — use datetime-local and pass full timestamps"
```

If evidence supports both a conclusion and its opposite, it is not justifying the change — it is post-hoc rationalization. Recognizing this pattern should trigger a full re-examination of the decision.

### Design intent vs. implementation bug: assume capability when uncertain

When implementation looks inconsistent (e.g., `datetime-local` input but time is then stripped), one side is correct intent and the other is the bug. **Default assumption: the richer/more capable side is the intent, the lossy transformation is the bug.** If genuinely uncertain, ask the user — do not silently resolve the ambiguity by picking the simpler option.

### Post-hoc rationalization: when evidence confirms a prior decision, be suspicious

Evidence found *after* a decision that *perfectly supports* it is a red flag. When you notice you are building a case for something already decided, explicitly argue the opposite before proceeding.

## Development Commands

```bash
Expand Down Expand Up @@ -51,7 +97,7 @@ pnpm generate-api # Regenerate from backend OpenAPI spec

## Architecture: The Critical Layer Pattern

```
```text
Page → Headless Hook → Adapter Hook → Generated API → Backend
Themed Components
Expand Down
3 changes: 3 additions & 0 deletions src/ui/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ const nextConfig: NextConfig = {
// Dataset file proxy route - alias to production version (zero mock code)
"@/app/proxy/dataset/file/route.impl":
"@/app/proxy/dataset/file/route.impl.production",

// Auth server utilities - alias to production version (zero env fallbacks)
"@/lib/auth/server": "@/lib/auth/server.production",
}
: {},
},
Expand Down
219 changes: 219 additions & 0 deletions src/ui/src/components/filter-bar/filter-bar-date-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

/**
* FilterBarDatePicker - Date/range picker panel rendered inside the FilterBar dropdown.
*
* B1 "Split Rail" layout: preset list on the left with active indicator,
* stacked From/To date inputs on the right.
*
* Selecting a preset or applying custom dates calls onCommit(value) where value
* is either a preset label ("last 7 days"), a single ISO date ("2026-03-11"),
* or an ISO range ("2026-03-01..2026-03-11").
*/

"use client";

import { useState, useCallback, useMemo, memo, useRef, useEffect } from "react";
import { DATE_RANGE_PRESETS } from "@/lib/date-range-utils";
import { DATE_CUSTOM_FROM, DATE_CUSTOM_TO, DATE_CUSTOM_APPLY } from "@/components/filter-bar/lib/types";
import { MONTHS_SHORT } from "@/lib/format-date";

interface FilterBarDatePickerProps {
/** Called when a date or range is committed. Value is preset label, ISO date, or ISO range. */
onCommit: (value: string) => void;
/** Preset label currently highlighted via keyboard navigation (shows active indicator). */
highlightedLabel?: string;
/** Called when Tab/Shift-Tab should wrap the cycle (e.g. Tab past Apply, Shift-Tab on From). */
onCycleStep?: (direction: "forward" | "backward", fromValue: string) => void;
}

/** Format a UTC YYYY-MM-DD string as "Mar 4" or "Mar 4 '25" (if year differs from currentYear). */
function fmtUtcDate(isoDate: string, currentYear: number): string {
const d = new Date(`${isoDate}T00:00:00Z`);
const mon = MONTHS_SHORT[d.getUTCMonth()];
const day = d.getUTCDate();
const year = d.getUTCFullYear();
return year !== currentYear ? `${mon} ${day} '${String(year).slice(2)}` : `${mon} ${day}`;
}

/**
* Build hint text from the raw preset value (before next-midnight adjustment).
* Single date → "Mar 11"; range → "Mar 4 – Mar 11".
*/
function buildPresetHint(rawValue: string, currentYear: number): string {
if (rawValue.includes("..")) {
const sep = rawValue.indexOf("..");
return `${fmtUtcDate(rawValue.slice(0, sep), currentYear)} – ${fmtUtcDate(rawValue.slice(sep + 2), currentYear)}`;
}
return fmtUtcDate(rawValue, currentYear);
}

export const FilterBarDatePicker = memo(function FilterBarDatePicker({
onCommit,
highlightedLabel,
onCycleStep,
}: FilterBarDatePickerProps) {
const [fromDate, setFromDate] = useState("");
const [toDate, setToDate] = useState("");

const fromRef = useRef<HTMLInputElement>(null);
const toRef = useRef<HTMLInputElement>(null);
const applyRef = useRef<HTMLButtonElement>(null);

// When keyboard navigation highlights a custom input sentinel, move DOM focus there.
// For the To input, also open the calendar picker — programmatic focus() always lands
// at the first sub-field (MM), but entering backward should start at the calendar end.
useEffect(() => {
if (highlightedLabel === DATE_CUSTOM_FROM) {
fromRef.current?.focus();
} else if (highlightedLabel === DATE_CUSTOM_TO) {
toRef.current?.focus();
} else if (highlightedLabel === DATE_CUSTOM_APPLY) {
applyRef.current?.focus();
}
}, [highlightedLabel]);

// Compute once per render (client-only component, only mounted on interaction).
const currentYear = useMemo(() => new Date().getUTCFullYear(), []);

const presetHints = useMemo(
() => Object.fromEntries(DATE_RANGE_PRESETS.map((p) => [p.label, buildPresetHint(p.getValue(), currentYear)])),
[currentYear],
);

// toDate must be strictly after fromDate (same minute = zero-second window after +1min adjustment)
const rangeError = !!fromDate && !!toDate && toDate <= fromDate;

const handleApply = useCallback(() => {
if (!fromDate || rangeError) return;
if (toDate) {
onCommit(`${fromDate}..${toDate}`);
} else {
onCommit(fromDate);
}
}, [fromDate, toDate, rangeError, onCommit]);

const handleFromChange = useCallback((value: string) => {
setFromDate(value);
// Clear "to" if it's now at or before "from" (equal = invalid range after +1min adjustment)
setToDate((prev) => (prev && prev <= value ? "" : prev));
}, []);

return (
<div
className="fb-date-picker"
role="none"
onKeyDown={(e) => e.stopPropagation()}
>
Comment on lines +116 to +121
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how parent components handle keyboard events and whether Escape is expected to bubble
rg -n "onKeyDown|handleKeyDown" --type=ts --type=tsx -C3 src/ui/src/components/filter-bar/ | head -80

Repository: NVIDIA/OSMO

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Search for onKeyDown handlers in filter-bar components
rg -n "onKeyDown|handleKeyDown" -C3 src/ui/src/components/filter-bar/

Repository: NVIDIA/OSMO

Length of output: 10776


🏁 Script executed:

#!/bin/bash
# List files in filter-bar directory
find src/ui/src/components/filter-bar -type f -name "*.ts" -o -name "*.tsx" | head -20

Repository: NVIDIA/OSMO

Length of output: 1180


🏁 Script executed:

#!/bin/bash
# Read the filter-bar-date-picker.tsx file to understand the context
wc -l src/ui/src/components/filter-bar/filter-bar-date-picker.tsx

Repository: NVIDIA/OSMO

Length of output: 118


🏁 Script executed:

#!/bin/bash
# Read the date picker file
cat -n src/ui/src/components/filter-bar/filter-bar-date-picker.tsx | head -230

Repository: NVIDIA/OSMO

Length of output: 9873


🏁 Script executed:

#!/bin/bash
# Check the Escape key handling in use-filter-keyboard.ts
sed -n '169,202p' src/ui/src/components/filter-bar/hooks/use-filter-keyboard.ts

Repository: NVIDIA/OSMO

Length of output: 1020


🏁 Script executed:

#!/bin/bash
# Search for onEscape implementation in the keyboard hook
rg -n "onEscape" src/ui/src/components/filter-bar/hooks/use-filter-keyboard.ts -A 10

Repository: NVIDIA/OSMO

Length of output: 674


🏁 Script executed:

#!/bin/bash
# Look at FilterBarInput which seems to manage the date picker visibility
cat -n src/ui/src/components/filter-bar/filter-bar-input.tsx | sed -n '1,150p'

Repository: NVIDIA/OSMO

Length of output: 5741


🏁 Script executed:

#!/bin/bash
# Get the full onEscape function
sed -n '338,360p' src/ui/src/components/filter-bar/hooks/use-filter-keyboard.ts

Repository: NVIDIA/OSMO

Length of output: 567


🏁 Script executed:

#!/bin/bash
# Look for the date picker rendering in FilterBar
rg -n "FilterBarDatePicker\|showDropdown\|activeDateRangeField" src/ui/src/components/filter-bar/filter-bar.tsx -B 2 -A 2

Repository: NVIDIA/OSMO

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Get more of the onEscape function
sed -n '338,375p' src/ui/src/components/filter-bar/hooks/use-filter-keyboard.ts

Repository: NVIDIA/OSMO

Length of output: 830


🏁 Script executed:

#!/bin/bash
# Search for FilterBarDatePicker in FilterBar component
cat -n src/ui/src/components/filter-bar/filter-bar.tsx | sed -n '1,200p'

Repository: NVIDIA/OSMO

Length of output: 7204


🏁 Script executed:

#!/bin/bash
# Check what closeDropdown does
rg -n "closeDropdown" src/ui/src/components/filter-bar/hooks/use-filter-state.ts -B 2 -A 5

Repository: NVIDIA/OSMO

Length of output: 394


Remove stopPropagation() or allow Escape to bubble to parent's keyboard handler.

The onKeyDown={(e) => e.stopPropagation()} blocks all keyboard events, including Escape, from bubbling to the parent's keyboard navigation system. The parent's onEscape handler expects Escape to propagate and calls closeDropdown() to close the date picker. Since the date picker does not handle Escape itself, pressing Escape has no effect. Only Tab has custom handling on the Apply button (intentional), so consider replacing the blanket stopPropagation() with a selective check: if (e.key !== "Escape") e.stopPropagation().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/src/components/filter-bar/filter-bar-date-picker.tsx` around lines 116
- 121, The fb-date-picker element's onKeyDown currently calls
e.stopPropagation() unconditionally, which prevents the parent's
onEscape/closeDropdown from receiving Escape; update the onKeyDown handler in
filter-bar-date-picker.tsx (the element with className "fb-date-picker") to only
stop propagation for keys other than Escape so Escape can bubble up to the
parent's onEscape and allow closeDropdown() to run.

<div className="fb-date-split">
{/* Left rail: presets with right-aligned date hints */}
<div className="fb-date-rail">
<div className="fb-date-section-label">Presets</div>
{DATE_RANGE_PRESETS.map((preset) => (
<button
key={preset.label}
type="button"
tabIndex={-1}
className="fb-date-preset-row"
data-active={highlightedLabel === preset.label ? "" : undefined}
onClick={() => onCommit(preset.label)}
>
<span className="fb-date-preset-label">{preset.label}</span>
<span className="fb-date-preset-hint">{presetHints[preset.label]}</span>
</button>
))}
</div>

{/* Right: custom range */}
<div className="fb-date-custom">
<div className="fb-date-section-label">Custom range</div>
<div className="fb-date-field">
<label
className="fb-date-label"
htmlFor="fb-date-from"
>
From
</label>
<input
ref={fromRef}
id="fb-date-from"
type="datetime-local"
value={fromDate}
onChange={(e) => handleFromChange(e.target.value)}
className="fb-date-input"
/>
</div>
<div className="fb-date-field">
<label
className="fb-date-label"
htmlFor="fb-date-to"
>
To
</label>
<input
ref={toRef}
id="fb-date-to"
type="datetime-local"
value={toDate}
onChange={(e) => setToDate(e.target.value)}
min={fromDate || undefined}
className="fb-date-input"
data-error={rangeError ? "" : undefined}
aria-invalid={rangeError}
aria-describedby={rangeError ? "fb-date-range-error" : undefined}
/>
{rangeError && (
<span
id="fb-date-range-error"
className="fb-date-error"
role="alert"
>
&ldquo;To&rdquo; must be after &ldquo;From&rdquo;
</span>
)}
</div>
<button
ref={applyRef}
type="button"
onClick={handleApply}
disabled={!fromDate || rangeError}
onKeyDown={(e) => {
if (e.key === "Tab" && !e.shiftKey) {
e.preventDefault();
onCycleStep?.("forward", DATE_CUSTOM_APPLY);
}
}}
className="fb-date-apply"
>
Apply →
</button>
</div>
</div>
{/* Focus sentinel: the last focusable element inside the picker.
When Tab exits Apply (or To when Apply is disabled), the browser naturally
focuses this sentinel. onFocus immediately redirects back into the cycle,
keeping focus trapped inside the filter bar. It must live inside the picker
(inside the container) so handleBlur sees relatedTarget as within-container. */}
<span
tabIndex={0}
aria-hidden="true"
className="sr-only"
onFocus={() => onCycleStep?.("forward", DATE_CUSTOM_APPLY)}
/>
</div>
);
});
Loading
Loading