/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * One row in the Raw STEP editor — a positional STEP argument with an * inline pen-icon editor. Mirrors the visual rhythm of the existing * AttributeEditorField (PropertiesPanel.tsx) but operates against * `bim.store.setPositionalAttribute` instead of the named-attribute path. */ import { useCallback, useState } from 'react'; import { PenLine, X, Check, AlertCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { useViewerStore } from '@/store'; import { isInlineEditableToken, parseRawStepInput } from './raw-step-format'; /** Match a bare `#N` STEP entity reference. */ const REF_TOKEN_RE = /^#(\d+)$/; interface RawStepRowProps { modelId: string; entityId: number; /** Zero-based positional index. */ index: number; /** Schema attribute name (or `Arg N` fallback). */ name: string; /** Current value as a STEP token (verbatim from source, or * serialized from an overlay override). */ displayToken: string; /** Whether this index has an active overlay override. */ isMutated: boolean; /** Set false to lock the row (e.g. native-metadata model). */ enableEditing: boolean; /** Drill into a `#N` reference. RawStepCard auto-skips trivial * single-ref wrappers and pushes the meaningful target onto the * navigation stack. */ onNavigate?: (refId: number) => void; } export function RawStepRow({ modelId, entityId, index, name, displayToken, isMutated, enableEditing, onNavigate, }: RawStepRowProps) { const setPositionalAttribute = useViewerStore((s) => s.setPositionalAttribute); const bumpMutationVersion = useViewerStore((s) => s.bumpMutationVersion); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(''); const [error, setError] = useState(null); const editable = enableEditing && isInlineEditableToken(displayToken); const display = displayToken; const refMatch = display.match(REF_TOKEN_RE); const refTargetId = refMatch ? Number.parseInt(refMatch[1], 10) : null; const inputRef = useCallback((node: HTMLInputElement | null) => { if (node) { node.focus(); node.select(); } }, []); const startEdit = useCallback(() => { if (!editable) return; setDraft(display); setError(null); setEditing(true); }, [editable, display]); const cancelEdit = useCallback(() => { setEditing(false); setError(null); }, []); const saveEdit = useCallback(() => { const parsed = parseRawStepInput(draft); if ('error' in parsed) { setError(parsed.error); return; } setPositionalAttribute(modelId, entityId, index, parsed.value); bumpMutationVersion(); setEditing(false); setError(null); }, [draft, modelId, entityId, index, setPositionalAttribute, bumpMutationVersion]); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); saveEdit(); } else if (e.key === 'Escape') { e.preventDefault(); cancelEdit(); } }, [saveEdit, cancelEdit], ); return (
{/* Positional index — displayed 1-based to match the buildingSMART * IFC attribute tables and STEP documentation (GlobalId = #1). * The `index` prop stays 0-based for store/overlay addressing. */} [{index + 1}] {/* Schema attribute name */} {name} {/* Value cell — display or input */} {editing ? (
{ setDraft(e.target.value); if (error) setError(null); }} onKeyDown={handleKeyDown} onBlur={(e) => { // Don't save when the blur is caused by clicking the // confirm/cancel buttons — those handle their own action. const next = e.relatedTarget as HTMLElement | null; if (next?.dataset.rawStepAction) return; saveEdit(); }} className={`flex-1 min-w-0 h-7 px-2 text-xs font-mono bg-white dark:bg-zinc-900 border outline-none focus:ring-1 ${ error ? 'border-red-400 dark:border-red-500 focus:ring-red-400' : 'border-purple-300 dark:border-purple-700 focus:ring-purple-400' }`} spellCheck={false} autoCapitalize="off" autoCorrect="off" aria-invalid={error ? true : undefined} aria-describedby={error ? `raw-step-err-${entityId}-${index}` : undefined} />
) : refTargetId !== null && onNavigate ? ( // Reference token — render as a navigable chip. Drilling into // a ref shouldn't share the same hover affordance as editing // a scalar; the emerald accent matches the Raw tab indicator // and visually separates "follow" from "edit". Editing a ref // (changing it to point at a different `#N`) goes through // the pen icon on the right. ) : ( )} {/* Action cluster */}
{isMutated && !editing && ( Overlay override )} {editing ? ( <> ) : editable ? ( ) : ( )}
{/* Validation error — full-width row beneath the value */} {error && (

{error}

)}
); }