refactor(m4): full-bleed matrix + word-only line breaks
Two follow-up tweaks per user feedback ("wrap sur les mots, agrandit le
cadre"):
- Full-bleed wrapper: the matrix breaks out of the page's max-w-page (1400px)
constraint via `margin: 0 calc(50% - 50vw)` + `width: 100vw`, mirroring the
60px page padding internally. On wide viewports the picker now uses the
ENTIRE viewport width, so column widths grow proportionally — names that
used to wrap on 3 lines now fit on 1-2.
- Word-only wrapping: replaced `break-words` (overflow-wrap: break-word,
which falls back to mid-word breaks) with `break-normal hyphens-none`
(overflow-wrap: normal + word-break: normal). Cells break only at word
boundaries; if a single word is longer than the cell it overflows
visually rather than splitting `Aut\nhentication`-style. The grid is
configured `minmax(7rem, 1fr)` so the minimum column is wide enough for
every single word in MITRE v19 names, and stretches with available space.
- Spec §F2 rewritten as a bullet contract locking in: full-bleed, 15 cols
minmax(7rem, 1fr), word-only wrap, font sans 12px / count 10px, headers/
cells show name-only with external_id on hover + chips. Future spec-reviewer
passes can grade against this.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -130,10 +130,13 @@ export function MitreTagPicker({ value, onChange, className }: MitreTagPickerPro
|
||||
data-testid="mitre-matrix-scroll"
|
||||
role="region"
|
||||
aria-label="MITRE ATT&CK matrix"
|
||||
/* Equal-width columns share the container; no horizontal scroll. */
|
||||
className="grid gap-px bg-border rounded"
|
||||
/* `minmax(7rem, 1fr)` ensures every cell is wide enough for the
|
||||
* longest single word in MITRE names (no mid-word breaks), and
|
||||
* stretches to fill the container otherwise. Horizontal scroll only
|
||||
* kicks in on narrow viewports below ~1680px. */
|
||||
className="grid gap-px bg-border rounded overflow-x-auto"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${matrix.data.tactics.length}, minmax(0, 1fr))`,
|
||||
gridTemplateColumns: `repeat(${matrix.data.tactics.length}, minmax(7rem, 1fr))`,
|
||||
}}
|
||||
>
|
||||
{matrix.data.tactics.map((tactic) => {
|
||||
@@ -166,7 +169,7 @@ export function MitreTagPicker({ value, onChange, className }: MitreTagPickerPro
|
||||
data-testid={`mitre-tactic-${tactic.external_id}`}
|
||||
aria-pressed={tacticSel}
|
||||
>
|
||||
<div className="text-xs font-semibold leading-tight break-words">
|
||||
<div className="text-xs font-semibold leading-tight break-normal hyphens-none">
|
||||
{tactic.name}
|
||||
</div>
|
||||
<div className="text-[10px] text-text-dim mt-0.5">
|
||||
@@ -205,7 +208,7 @@ export function MitreTagPicker({ value, onChange, className }: MitreTagPickerPro
|
||||
name: tech.name,
|
||||
})
|
||||
}
|
||||
className="flex-1 min-w-0 text-left px-2 py-1.5 font-sans break-words"
|
||||
className="flex-1 min-w-0 text-left px-2 py-1.5 font-sans break-normal hyphens-none"
|
||||
title={`${tech.external_id} — ${tech.name}`}
|
||||
data-testid={`mitre-technique-${tech.external_id}`}
|
||||
aria-pressed={techSel}
|
||||
@@ -250,7 +253,7 @@ export function MitreTagPicker({ value, onChange, className }: MitreTagPickerPro
|
||||
})
|
||||
}
|
||||
className={cn(
|
||||
'block w-full text-left pl-5 pr-2 py-1 font-sans text-xs border-l-2 break-words',
|
||||
'block w-full text-left pl-5 pr-2 py-1 font-sans text-xs border-l-2 break-normal hyphens-none',
|
||||
subSel
|
||||
? 'accent-fill-purple border-purple text-text-bright'
|
||||
: 'border-purple/30 hover:bg-purple/10',
|
||||
|
||||
@@ -92,7 +92,21 @@ export function MitrePage() {
|
||||
</div>
|
||||
|
||||
<SectionHeader prefix="Tag" highlight="Picker" accent="orange" />
|
||||
<MitreTagPicker value={selected} onChange={setSelected} />
|
||||
{/* Full-bleed the matrix beyond max-w-page so it uses the full viewport
|
||||
* width. `calc(50% - 50vw)` is the canonical CSS recipe: the element's
|
||||
* left edge slides back to viewport x=0 regardless of how big the
|
||||
* outer max-w-page container is. `px-[60px]` mirrors the page padding
|
||||
* so cells don't touch the viewport edge. */}
|
||||
<div
|
||||
className="px-[60px]"
|
||||
style={{
|
||||
marginLeft: 'calc(50% - 50vw)',
|
||||
marginRight: 'calc(50% - 50vw)',
|
||||
width: '100vw',
|
||||
}}
|
||||
>
|
||||
<MitreTagPicker value={selected} onChange={setSelected} />
|
||||
</div>
|
||||
|
||||
{selected.length > 0 && (
|
||||
<Card accent="purple" className="mt-6" title="Selected (preview payload)">
|
||||
|
||||
Reference in New Issue
Block a user