From 0ffae14a4316d67eae25a0539e72345d0ece7118 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 30 Oct 2026 21:19:48 +0000 Subject: [PATCH] Add interactive controls and refresh inline visualization styles (#58436) ## What changed - Add tab switching with keyboard navigation and a `.viz-carousel` variant picker with previous/next controls and screen reader announcements. - Support touch tooltip toggling and tooltip triggers inside shadow roots. - Refresh typography, tables, buttons, and form controls; add widget and progress styles and larger touch targets. ## Testing Add a viewer snapshot test covering embedded tab and carousel scripts, generated control markup, and selected CSS rules. GitOrigin-RevId: 3b85ef816fe307e8ac9903f176a0b81430e48f3f --- .../assets/inline_visualization/visualize.css | 912 +++++++++++++++--- .../inline_visualization/visualize.html | 394 +++++++- .../tui/src/inline_visualization_tests.rs | 54 ++ ...visualization__tests__viewer_controls.snap | 34 + 4 files changed, 1236 insertions(+), 158 deletions(-) create mode 100644 codex-rs/tui/src/snapshots/codex_tui__inline_visualization__tests__viewer_controls.snap diff --git a/codex-rs/tui/assets/inline_visualization/visualize.css b/codex-rs/tui/assets/inline_visualization/visualize.css index 81626e4d7f..96a7488060 100644 --- a/codex-rs/tui/assets/inline_visualization/visualize.css +++ b/codex-rs/tui/assets/inline_visualization/visualize.css @@ -75,7 +75,7 @@ --color-ring-primary, light-dark(rgb(51 156 255), rgb(131 195 255 / 76%)) ); - --font-size-base: var(--font-text-md-size, 14px); + --font-size-base: var(--font-text-md-size, 16px); --blue: light-dark(rgb(51 156 255), rgb(51 156 255)); --orange: light-dark(rgb(226 85 7), rgb(251 106 34)); --green: light-dark(rgb(0 162 64), rgb(64 201 119)); @@ -90,7 +90,10 @@ --viz-series-6: light-dark(rgb(58 185 177), rgb(90 203 194)); /* Internal implementation variables; not part of the agent contract. */ - --font-sans: -apple-system, system-ui, "Segoe UI", sans-serif; + --font-sans: + -apple-system-body, ui-sans-serif, -apple-system, system-ui, "Segoe UI", + Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", + "Segoe UI Symbol"; --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; @@ -100,16 +103,18 @@ --font-size-h1: calc(var(--font-size-normal) * 1.7142857143); --font-size-h2: calc(var(--font-size-normal) * 1.4285714286); --font-size-h3: calc(var(--font-size-normal) * 1.2857142857); - --font-weight-normal: 430; + --font-weight-normal: 400; --font-weight-medium: 500; + --icon-size: 16px; --line-height-normal: calc(var(--font-size-normal) * 1.5); --line-height-tooltip: calc(var(--font-size-tooltip) * 1.4285714286); --line-height-small: calc(var(--font-size-small) + 4px); - --radius: var(--border-radius-lg, 12.5px); - --radius-sm: calc(var(--radius) * 0.6); - --radius-md: calc(var(--radius) * 0.8); + --corner-shape: round; + --radius: var(--border-radius-lg, 8px); + --radius-sm: calc(var(--radius) * 0.5); + --radius-md: calc(var(--radius) * 0.75); --radius-lg: var(--radius); - --radius-2xl: calc(var(--radius) * 1.6); + --radius-2xl: calc(var(--radius) * 2); --radius-full: 9999px; --shadow-sm: 0 1px 2px -1px rgb(0 0 0 / 8%); --checkmark-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 17 17'%3E%3Cpath d='M12.8961 3.64101C13.1297 3.41418 13.4984 3.37523 13.7779 3.56581C14.0571 3.75635 14.1554 4.11331 14.0299 4.41347L13.9615 4.53847L7.71151 13.7045C7.59411 13.8767 7.4063 13.9877 7.19881 14.0072C6.99136 14.0267 6.78564 13.9533 6.63826 13.806L2.88826 10.056L2.79842 9.9457C2.6192 9.67407 2.64927 9.30496 2.88826 9.06581C3.12738 8.82669 3.49647 8.79676 3.76815 8.97597L3.8785 9.06581L7.03084 12.2182L12.8053 3.74941L12.8961 3.64101Z'/%3E%3C/svg%3E"); @@ -155,21 +160,26 @@ html > body { ); margin: 0; - padding: 5px; + padding: var(--visualize-paint-gutter, 0px); color: var(--foreground); background: transparent !important; - font-family: var(--font-sans); - font-size: var(--font-size-normal); + font-family: var(--font-sans) !important; + -webkit-font-smoothing: antialiased; + font-size: var(--font-size-base); + line-height: calc(var(--font-size-base) * 1.625); font-weight: var(--font-weight-normal); - line-height: var(--line-height-normal); +} + +.cursor-interaction { + cursor: var(--cursor-interaction, pointer); } a { color: color-mix(in srgb, var(--viz-accent) 80%, var(--viz-text) 20%); - cursor: pointer; - font-weight: var(--font-weight-medium, 500); + cursor: var(--cursor-interaction, pointer); text-decoration: none; text-underline-offset: 2px; + font-weight: inherit; } a:is(:hover, :focus-visible) { @@ -187,39 +197,58 @@ h6, p { margin-block: 0; } - -h1 { - font-size: var(--font-size-h1); - font-weight: var(--font-weight-medium); - line-height: 1.25; -} - -h2 { - font-size: var(--font-size-h2); - font-weight: var(--font-weight-medium); - line-height: 1.25; -} - +p, +h1, +h2, h3, h4, h5, -h6 { - font-size: var(--font-size-h3); - font-weight: var(--font-weight-medium); - line-height: 1.3; +h6, +li, +.table { + font-family: var(--font-sans); } +h1 { + font-size: calc(var(--font-size-base) * 1.5); + line-height: 1.3333333333; +} + +h2 { + font-size: calc(var(--font-size-base) * 1.25); + line-height: 1.4; +} +h3 { + font-size: calc(var(--font-size-base) * 1.125); + line-height: 1.5555555556; +} +h4 { + font-size: var(--font-size-base); + line-height: 1.5; +} +h5, +h6 { + font-size: var(--font-size-base); + line-height: calc(var(--font-size-base) * 1.625); +} + +h1, +h2, +h3, +h4, +h5, +h6, b, strong, th { - font-weight: var(--font-weight-medium); + font-weight: 600; } code:not(pre code) { display: inline; padding: 1px 6px; border-radius: var(--radius-sm); - corner-shape: superellipse(1.5); + corner-shape: var(--corner-shape); background: var(--muted); font-family: var(--font-mono); font-size: 0.92em; @@ -228,6 +257,12 @@ code:not(pre code) { box-decoration-break: clone; word-break: break-word; } +pre, +pre code { + font-family: var(--font-mono); + font-size: 12px; + line-height: 20px; +} .table-responsive { width: 100%; @@ -237,10 +272,14 @@ code:not(pre code) { .table { width: 100%; - border-collapse: collapse; color: var(--foreground); font: inherit; text-align: start; + border-collapse: separate; + border-spacing: 0; + font-size: calc(var(--font-size-base) * 0.875); + line-height: calc(var(--font-size-base) * 1.625); + font-family: var(--font-sans); } .table :is(th, td) { @@ -250,6 +289,10 @@ code:not(pre code) { border-bottom: 1px solid var(--border); text-align: start; vertical-align: top; + border-bottom-color: color-mix(in srgb, var(--foreground) 5%, transparent); +} +.table :is(th, td):not(:last-child) { + padding-inline-end: calc(var(--font-size-base) * 1.5); } .table-responsive > .table :is(th, td) { @@ -264,17 +307,21 @@ code:not(pre code) { font-weight: 600; } -.table thead th { - padding-block: 8px; - border-bottom-color: color-mix(in srgb, var(--foreground) 16%, transparent); +.table thead :is(th, td) { + border-bottom-color: var(--border-strong, var(--input)); + padding-block: calc(var(--font-size-base) / 2); + line-height: var(--font-size-base); } .table tbody tr:last-child :is(th, td) { border-bottom: 0; } +.table tbody :is(th, td) { + padding-block: calc(var(--font-size-base) * 0.625); +} .table.table-sm :is(th, td) { - padding-block: 6px; + padding-block: 8px; } .table.table-sm :is(th, td):not(:last-child) { @@ -309,12 +356,23 @@ code:not(pre code) { overflow: hidden; overflow-wrap: break-word; border-radius: var(--radius-2xl); - corner-shape: superellipse(1.5); + corner-shape: var(--corner-shape); color: var(--card-foreground); background: var(--card); } -#widget > :not(.card) { +.widget { + min-width: 0; + overflow: hidden; + border: 0; + border-radius: var(--radius-lg); + corner-shape: var(--corner-shape); + box-shadow: 0 0 0 0.5px var(--border-strong, var(--border)); + color: var(--foreground); + background: var(--background); +} + +#widget > :not(.card, .widget, .progress) { width: 100% !important; max-width: none !important; margin: 0 !important; @@ -340,19 +398,29 @@ code:not(pre code) { var(--tooltip-available-height, calc(100vh - 10px)), calc(100vh - 10px) ); - padding: 4px 8px; border: 1px solid var(--border); - border-radius: var(--radius-lg); - corner-shape: superellipse(1.5); - color: var(--popover-foreground); - background: var(--popover); - box-shadow: none; - font-size: var(--font-size-tooltip); - line-height: var(--line-height-tooltip); + corner-shape: var(--corner-shape); overflow-wrap: break-word; white-space: normal; pointer-events: none; user-select: none; + font-family: var(--font-sans); + padding: 5px 12px; + border-color: color-mix( + in oklab, + light-dark(var(--primary-foreground), var(--foreground)) 5%, + transparent + ); + border-radius: var(--radius-2xl); + color: light-dark(var(--primary-foreground), var(--foreground)); + background: light-dark(var(--foreground), var(--popover)); + box-shadow: 0 8px 18px rgb(15 23 42 / 20%); + letter-spacing: -0.15px; + font-size: calc(var(--font-size-base) * 14 / 16); + font-size: round(nearest, calc(var(--font-size-base) * 14 / 16), 1px); + font-weight: 600; + line-height: 18px; + text-align: center; } .viz-grid { @@ -380,17 +448,58 @@ code:not(pre code) { gap: 10px; } -.viz-badge { - padding: 3px 8px; +hr { + width: 100%; + height: 1px; + margin-block: 6px; + border: 0; + background: var(--border); +} + +.nav { + display: flex; + min-width: 0; + align-items: center; + gap: 4px; +} + +.progress { + display: flex; + height: 8px; + margin-block: calc((var(--line-height-normal, 1.5em) - 8px) / 2); + overflow: hidden; + border: 0; border-radius: var(--radius-full); - color: var(--accent-foreground); - background: var(--accent); - font-weight: var(--font-weight-medium); + background: var(--muted); +} + +.progress-bar:only-child { + border-radius: var(--radius-full); +} + +.progress-bar { + height: 100%; + flex-shrink: 0; + border: 0; + background: var(--viz-series-1); +} + +.viz-badge { + display: inline-flex; + align-items: center; + padding: 4px 8px; + border-radius: var(--radius-sm); + color: color-mix(in srgb, var(--foreground) 40%, var(--muted-foreground)); + background: color-mix(in srgb, var(--foreground) 5%, transparent); + font-family: var(--font-sans); + font-size: calc(var(--font-size-base) * 14 / 16); + font-size: round(nearest, calc(var(--font-size-base) * 14 / 16), 1px); + font-weight: var(--font-weight-normal); + line-height: 1; } small, -.text-small, -.viz-badge { +.text-small { font-size: var(--font-size-small); line-height: var(--line-height-small); } @@ -399,6 +508,10 @@ small, color: var(--muted-foreground); } +.tabular-nums { + font-variant-numeric: tabular-nums; +} + .sr-only { position: absolute; width: 1px; @@ -432,6 +545,7 @@ small, } .btn, +.nav-link, .form-check-input, .form-control, .form-range, @@ -439,12 +553,23 @@ small, font: inherit; } -.btn { +/* Match inline alignment without overriding a containing flex or grid layout. */ +.btn, +.nav-link, +.form-control, +.form-select, +.form-range, +.form-check { + vertical-align: middle; +} + +.btn, +.nav-link { + --btn-size: calc(12px + 2 * var(--control-spacing, 12px)); appearance: button; display: inline-flex; inline-size: fit-content; max-inline-size: 100%; - min-height: 28px; align-items: center; justify-content: center; gap: 4px; @@ -452,16 +577,40 @@ small, padding: 0 8px; -webkit-app-region: no-drag; border: 1px solid var(--input); - border-radius: var(--radius-lg); - corner-shape: superellipse(1.5); - color: var(--secondary-foreground); - background: var(--secondary); + corner-shape: var(--corner-shape); cursor: var(--cursor-interaction, pointer); text-align: center; text-decoration: none; white-space: nowrap; user-select: none; - -webkit-font-smoothing: antialiased; + -webkit-font-smoothing: inherit; + font-family: var(--font-sans); + height: fit-content; + min-height: var(--btn-size); + padding-inline: var(--control-spacing, 12px); + border-color: transparent; + border-radius: var(--radius-full); + color: var(--foreground); + background: color-mix(in srgb, var(--foreground) 5%, transparent); + font-size: calc(var(--font-size-base) * 14 / 16); + font-size: round(nearest, calc(var(--font-size-base) * 14 / 16), 1px); + font-weight: var(--font-weight-normal); + line-height: 18px; +} + +.btn { + border-color: var(--border); + color: var(--secondary-foreground); + background: var(--secondary); +} + +/* Codex Button size="compact"; appearance stays with the button variant. */ +.btn-sm { + --btn-size: 24px; + padding-inline: 8px; + font-size: calc(var(--font-size-base) * 12 / 16); + font-size: round(nearest, calc(var(--font-size-base) * 12 / 16), 1px); + line-height: 16px; } .btn:is(.btn-block, .viz-tile) { @@ -475,19 +624,26 @@ small, } a.btn { - cursor: pointer; + cursor: var(--cursor-interaction, pointer); text-decoration: none; } .btn:not(:disabled):hover { - background: color-mix(in srgb, var(--foreground) 6%, var(--secondary)); + background: light-dark( + color-mix(in srgb, var(--foreground) 5.5%, transparent), + color-mix(in srgb, var(--foreground) 8%, transparent) + ); +} + +.nav-link:not(:disabled):not([aria-disabled="true"]):hover { + background: color-mix(in srgb, var(--foreground) 10%, transparent); } .btn-primary { - border-color: transparent; + border-color: var(--border); color: var(--primary-foreground); background: var(--foreground); - background-clip: padding-box; + background-clip: border-box; } .btn-primary .text-muted { @@ -496,7 +652,7 @@ a.btn { .btn-primary:not(:disabled):hover { background: color-mix(in srgb, var(--foreground) 80%, transparent); - background-clip: padding-box; + background-clip: border-box; } .btn-ghost { @@ -506,11 +662,222 @@ a.btn { } .btn-ghost:not(:disabled):hover { - color: var(--foreground); - background: color-mix(in srgb, var(--foreground) 6%, var(--secondary)); + color: var(--muted-foreground); + background: light-dark( + color-mix(in srgb, var(--foreground) 5.5%, transparent), + color-mix(in srgb, var(--foreground) 8%, transparent) + ); } -.btn:disabled { +.nav-pills .nav-link { + min-width: 0; + border-color: transparent; + color: var(--muted-foreground); + background: transparent; +} + +.nav-justified .nav-link { + flex: 1 1 0; + overflow-wrap: anywhere; + white-space: normal; +} + +.nav-pills .nav-link.active { + color: var(--foreground); + background: var(--muted); +} + +/* Shared preview chrome; product styles stay inside each variant. */ +.viz-dotted-background { + background-image: radial-gradient( + circle, + color-mix(in srgb, var(--foreground) 14%, transparent) 0.8px, + transparent 0.8px + ); + background-position: center; + background-size: 16px 16px; +} + +.viz-carousel { + position: relative; + min-width: 0; +} + +.viz-carousel:has(> .viz-carousel-controls) { + min-height: calc(60px + var(--viz-carousel-picker-height, 0px)); + padding-bottom: 60px; +} + +.viz-carousel > [data-variant][hidden] { + display: none !important; +} + +.viz-carousel > [data-variant] { + isolation: isolate; +} + +.viz-carousel-controls { + --viz-carousel-hover: light-dark( + color-mix(in srgb, var(--foreground) 5.4%, transparent), + color-mix(in srgb, var(--foreground) 7.8%, transparent) + ); + position: absolute; + z-index: 2; + bottom: 8px; + left: 50%; + display: flex; + width: max-content; + /* Leave room for the host's design-controls launcher at the trailing edge. */ + max-width: calc(100% - 96px); + align-items: center; + gap: 0; + padding: 0; + border: 1px solid var(--border); + border-radius: 16px; + color: var(--foreground); + background: var(--background); + font-family: var(--font-sans); + /* Match the settings dropdown chrome independently of the content font size. */ + font-size: 13px; + font-weight: var(--font-weight-normal); + line-height: 18px; + transform: translateX(-50%); + user-select: none; +} + +/* Keep authored mockup button styles out of the shared carousel chrome. */ +.viz-carousel-controls button, +.viz-carousel-picker > summary { + display: flex !important; + height: auto !important; + min-height: 30px; + align-items: center !important; + justify-content: center !important; + gap: 4px; + padding: 0 6px !important; + -webkit-app-region: no-drag; + border: 0 !important; + border-radius: 14px !important; + color: inherit !important; + background: transparent !important; + font: inherit !important; + -webkit-tap-highlight-color: transparent; +} + +.viz-carousel-controls > button, +.viz-carousel-picker > summary { + position: relative; + isolation: isolate; +} + +.viz-carousel-controls > button { + --icon-size: 16px; + width: 30px !important; + padding: 0 !important; +} + +.viz-carousel-picker > summary { + padding-inline: 12px !important; +} + +.viz-carousel-controls .viz-carousel-caret { + width: 14px; + height: 14px; + flex: none; + margin: 0; + color: inherit; + transform: rotate(180deg); +} + +.viz-carousel-controls > button > svg, +.viz-carousel-controls .viz-carousel-caret, +.viz-carousel-count { + opacity: 0.5; +} + +.viz-carousel-controls > button::before, +.viz-carousel-picker > summary::before { + position: absolute; + z-index: -1; + inset: 3px; + border-radius: 12px; + content: ""; + pointer-events: none; +} + +.viz-carousel-controls > button:hover::before, +.viz-carousel-picker > summary:hover::before { + background: var(--viz-carousel-hover); +} + +.viz-carousel-controls > button, +.viz-carousel-controls [data-lucide], +.viz-carousel-controls .lucide { + flex-shrink: 0; +} + +.viz-carousel-picker { + min-width: 0; +} + +.viz-carousel-picker > summary { + min-width: 0; + list-style: none; +} + +.viz-carousel-picker > summary::-webkit-details-marker { + display: none; +} + +.viz-carousel-count { + flex-shrink: 0; + white-space: nowrap; +} + +.viz-carousel-name { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.viz-carousel-options { + position: absolute; + right: 0; + bottom: calc(100% + 4px); + left: 0; + display: flex; + max-height: 240px; + flex-direction: column; + gap: 2px; + overflow: auto; + padding: 4px; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: var(--background); + box-shadow: 0 4px 12px light-dark(rgb(0 0 0 / 6%), rgb(0 0 0 / 25%)); +} + +.viz-carousel-options > button { + border-radius: max(0px, calc(var(--radius-lg) - 5px)) !important; + width: 100% !important; + min-height: 28px; + justify-content: flex-start !important; + padding: 4px 8px !important; + font-size: var(--font-size-small) !important; + text-align: start; + white-space: normal; + overflow-wrap: anywhere; +} + +.viz-carousel-options > [aria-current="true"], +.viz-carousel-options > button:hover { + background: var(--viz-carousel-hover) !important; +} + +.btn:disabled, +.nav-link:disabled, +.nav-link[aria-disabled="true"] { cursor: not-allowed; opacity: 0.4; } @@ -528,14 +895,42 @@ a.btn { .form-control { display: block; width: 100%; - min-height: 28px; padding: 0 8px; outline: none; border: 1px solid var(--input); - border-radius: var(--radius-lg); - corner-shape: superellipse(1.5); + corner-shape: var(--corner-shape); color: var(--foreground); - background: var(--secondary); + font-family: var(--font-sans); + height: calc(12px + 2 * var(--control-spacing, 12px)); + min-height: calc(12px + 2 * var(--control-spacing, 12px)); + padding-inline: calc(4px + var(--control-spacing, 12px) / 2); + border-color: var(--input); + border-radius: var(--radius-md); + background: color-mix(in srgb, var(--popover) 96%, transparent); + font-size: calc(var(--font-size-base) * 14 / 16); + font-size: round(nearest, calc(var(--font-size-base) * 14 / 16), 1px); + font-weight: var(--font-weight-normal); + line-height: 1.4285714286; +} +.form-control:is( + [type="file"], + [type="date"], + [type="datetime-local"], + [type="month"], + [type="week"], + [type="color"] + ) { + height: calc(12px + 2 * var(--control-spacing, 12px)); + min-height: calc(12px + 2 * var(--control-spacing, 12px)); + line-height: 18px; +} +.form-control:is( + [type="date"], + [type="datetime-local"], + [type="month"], + [type="week"] + ) { + padding-inline: var(--control-spacing, 12px); } .form-control::placeholder { @@ -543,14 +938,12 @@ a.btn { } .form-control[type="file"] { - padding: 0; overflow: hidden; cursor: var(--cursor-interaction, pointer); + padding: 0; } .form-control[type="file"]::file-selector-button { - min-height: 26px; - margin-right: 8px; padding: 0 8px; border: 0; border-right: 1px solid var(--input); @@ -558,6 +951,9 @@ a.btn { background: var(--secondary); cursor: inherit; font: inherit; + min-height: calc(10px + 2 * var(--control-spacing, 12px)); + padding-inline: var(--control-spacing, 12px); + margin-inline-end: var(--control-spacing, 12px); } .form-control[type="file"]:not(:disabled):hover::file-selector-button { @@ -566,7 +962,7 @@ a.btn { .form-control-color[type="color"] { width: 40px; - height: 28px; + height: calc(12px + 2 * var(--control-spacing, 12px)); padding: 3px; cursor: var(--cursor-interaction, pointer); } @@ -578,63 +974,103 @@ a.btn { .form-control-color[type="color"]::-webkit-color-swatch { border: 0; border-radius: calc(var(--radius-lg) - 4px); - corner-shape: superellipse(1.5); + corner-shape: var(--corner-shape); } textarea.form-control { height: auto; - min-height: 72px; - padding: 8px 10px; resize: vertical; + min-height: 96px; + padding: 8px 10px; } .form-control:focus-visible { border-color: var(--ring); - box-shadow: inset 0 0 0 1px var(--ring); + box-shadow: none; +} +.form-control:disabled { + color: var(--muted-foreground); + background: color-mix(in srgb, var(--foreground) 5%, transparent); + opacity: 1; +} +.form-control[aria-invalid="true"] { + border-color: var(--red); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 20%, transparent); } .form-control:disabled, .form-select:disabled { cursor: not-allowed; - opacity: 0.4; } .form-select { appearance: none; display: block; width: 100%; - min-height: 28px; + height: fit-content; + min-height: calc(12px + 2 * var(--control-spacing, 12px)); margin: 0; - padding: 0 32px 0 8px; + /* The secondary icon role is 14px at compact density and 16px at web density. */ + --icon-size: calc(10px + var(--control-spacing, 12px) / 2); + /* Settings dropdowns retain a 12px inset and a 4px text-to-chevron gap. */ + padding: 0 calc(16px + var(--icon-size)) 0 12px; outline: none; - border: 1px solid var(--input); - border-radius: var(--radius-lg); - corner-shape: superellipse(1.5); + border: 1px solid var(--border); + border-radius: var(--radius-full); + corner-shape: var(--corner-shape); color: var(--foreground); background-color: var(--secondary); background-image: - linear-gradient(45deg, transparent 50%, var(--muted-foreground) 50%), - linear-gradient(135deg, var(--muted-foreground) 50%, transparent 50%); + linear-gradient( + 45deg, + transparent calc(50% - 0.6px), + var(--muted-foreground) calc(50% - 0.6px) calc(50% + 0.6px), + transparent calc(50% + 0.6px) + ), + linear-gradient( + 135deg, + transparent calc(50% - 0.6px), + var(--muted-foreground) calc(50% - 0.6px) calc(50% + 0.6px), + transparent calc(50% + 0.6px) + ); background-position: - calc(100% - 14px) 50%, - calc(100% - 10px) 50%; + calc(100% - 12px - var(--icon-size) / 2) 50%, + calc(100% - 12px - var(--icon-size) * 0.18) 50%; background-repeat: no-repeat; - background-size: 4px 4px; + background-size: calc(var(--icon-size) * 0.32) calc(var(--icon-size) * 0.32); cursor: var(--cursor-interaction, default); + font-size: calc(var(--font-size-base) * 14 / 16); + font-size: round(nearest, calc(var(--font-size-base) * 14 / 16), 1px); + font-weight: var(--font-weight-normal); + line-height: 18px; +} + +.form-select :is(option, optgroup) { + color: var(--popover-foreground); + background-color: var(--popover); +} + +.form-select :is(option, optgroup):disabled { + color: var(--muted-foreground); } .form-select:not(:disabled):hover { - background-color: color-mix(in srgb, var(--foreground) 6%, var(--secondary)); + background-color: light-dark( + color-mix(in srgb, var(--foreground) 5.5%, transparent), + color-mix(in srgb, var(--foreground) 8%, transparent) + ); } .form-select:focus-visible { - border-color: var(--ring); - box-shadow: inset 0 0 0 1px var(--ring); + box-shadow: 0 0 0 2px var(--ring); +} +.form-select:disabled { + opacity: 0.4; } .form-check { display: flex; - min-height: 20px; + min-height: calc(12px + 2 * var(--control-spacing, 12px)); align-items: center; gap: 6px; } @@ -656,11 +1092,15 @@ textarea.form-control { } .form-check:not(.form-switch) .form-check-input[type="checkbox"] { - border-color: var(--input); - border-radius: var(--radius-sm); - corner-shape: superellipse(1.5); - background-color: var(--secondary); + corner-shape: round; box-shadow: var(--shadow-sm); + position: relative; + width: calc(10px + var(--control-spacing, 12px) / 2); + height: calc(10px + var(--control-spacing, 12px) / 2); + border-color: var(--border-strong, var(--input)); + border-radius: calc(var(--radius-sm) / 2); + background-color: transparent; + color: var(--foreground); } .form-check:not(.form-switch) @@ -668,9 +1108,10 @@ textarea.form-control { background-color: var(--card); } -.form-check:not(.form-switch) .form-check-input[type="checkbox"]:checked { - border-color: var(--primary); - background-color: var(--primary); +.form-check:not(.form-switch) + .form-check-input[type="checkbox"]:is(:checked, :indeterminate) { + border-color: var(--border-strong, var(--input)); + background-color: color-mix(in srgb, var(--popover) 96%, transparent); } .form-check:not(.form-switch) @@ -681,25 +1122,49 @@ textarea.form-control { background: var(--primary-foreground); content: ""; mask: var(--checkmark-image) center / 12px 12px no-repeat; + background-color: var(--foreground); +} +.form-check:not(.form-switch) + .form-check-input[type="checkbox"]:indeterminate::before { + position: absolute; + inset: 0; + display: block; + width: 65%; + height: 6.65%; + margin: auto; + border-radius: var(--radius-full); + background: var(--foreground); + mask: none; + content: ""; } .form-check-input[type="radio"] { - width: 14px; - height: 14px; border-radius: var(--radius-full); + width: 16px; + height: 16px; + border: 0; + box-shadow: inset 0 0 0 1px var(--input); } .form-check-input[type="radio"]:checked { - border: 2px solid var(--primary); - background: - radial-gradient(circle, var(--primary-foreground) 0 2.5px, transparent 3px), - var(--primary); + border: 0; + background: radial-gradient( + circle, + var(--primary-foreground) 0 3px, + var(--foreground) 3px + ); + box-shadow: none; } .form-check:not(.form-switch) .form-check-input:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; } +.form-check:not(.form-switch) .form-check-input[type="checkbox"]:focus-visible { + border-color: var(--ring); + outline: none; + box-shadow: 0 0 0 2px var(--ring); +} .form-check:not(.form-switch) .form-check-input:disabled { cursor: not-allowed; @@ -709,6 +1174,12 @@ textarea.form-control { .form-check:not(.form-switch) .form-check-input:disabled + .form-check-label { cursor: not-allowed; } +.form-check:not(.form-switch) .form-check-input[type="radio"]:disabled, +.form-check:not(.form-switch) + .form-check-input[type="radio"]:disabled + + .form-check-label { + opacity: 0.5; +} .form-switch .form-check-input:disabled, .form-switch .form-check-input:disabled + .form-check-label { @@ -741,15 +1212,16 @@ textarea.form-control { box-sizing: border-box; border: 1px solid light-dark(var(--primary-foreground), var(--foreground)); border-radius: var(--radius-full); - background: light-dark(var(--primary-foreground), var(--foreground)); box-shadow: var(--shadow-sm); content: ""; transform: translate(2px, -50%); transition: transform 200ms cubic-bezier(0, 0, 0.2, 1); + border-color: #fff; + background: #fff; } .form-switch .form-check-input:checked { - background: var(--primary); + background: var(--blue); } .form-switch .form-check-input:checked::before { @@ -758,49 +1230,54 @@ textarea.form-control { .form-switch .form-check-input:focus-visible { box-shadow: 0 0 0 2px var(--ring); + outline: none; } .form-range { appearance: none; display: block; width: 100%; - height: 28px; flex: 1; margin: 0; padding: 0; - outline: none; border: 0; accent-color: var(--primary); - background: linear-gradient( - color-mix(in srgb, var(--foreground) 7%, transparent), - color-mix(in srgb, var(--foreground) 7%, transparent) - ) - center / 100% 2px no-repeat; + height: calc(12px + 2 * var(--control-spacing, 12px)); + min-height: calc(12px + 2 * var(--control-spacing, 12px)); + outline: none; + border-radius: var(--radius-full); + color: var(--foreground); + background: transparent; } .form-range::-webkit-slider-runnable-track { - height: 28px; - background: transparent; + background: var(--form-range-track, var(--border)); + height: 2px; + border-radius: var(--radius-full); } .form-range::-webkit-slider-thumb { appearance: none; width: 20px; height: 20px; - margin-top: 4px; border: 1px solid var(--border); border-radius: var(--radius-full); - background: light-dark(var(--primary-foreground), var(--foreground)); + margin-top: -9px; + border-color: transparent; + background: currentColor; + box-shadow: var(--shadow-sm); } -.form-range:focus-visible::-webkit-slider-thumb { - border-color: var(--ring); - box-shadow: inset 0 0 0 1px var(--ring); +/* Outline the native track and thumb, preserving the larger input hit area. */ +.form-range:focus-visible::-webkit-slider-runnable-track { + outline: 2px auto Highlight; + outline-color: -webkit-focus-ring-color; } .form-range::-moz-range-track { - height: 28px; - background: transparent; + background: var(--form-range-track, var(--border)); + height: 2px; + border-radius: var(--radius-full); } .form-range::-moz-range-thumb { @@ -808,17 +1285,19 @@ textarea.form-control { height: 20px; border: 1px solid var(--border); border-radius: var(--radius-full); - background: light-dark(var(--primary-foreground), var(--foreground)); + border-color: transparent; + background: currentColor; + box-shadow: var(--shadow-sm); } .form-range:focus-visible::-moz-range-thumb { - border-color: var(--ring); - box-shadow: inset 0 0 0 1px var(--ring); + outline: 2px solid Highlight; + outline-offset: 2px; } .form-range:disabled { - cursor: not-allowed; - opacity: 0.4; + opacity: 1; + cursor: default; } .btn:not(.btn-primary, .viz-tile):is( @@ -826,9 +1305,9 @@ textarea.form-control { [aria-selected="true"], .is-selected ) { - border-color: var(--primary); + border-color: transparent; color: var(--primary-foreground); - background: var(--primary); + background: var(--blue); } .btn.viz-tile:is([aria-pressed="true"], [aria-selected="true"], .is-selected) { @@ -836,9 +1315,166 @@ textarea.form-control { box-shadow: inset 0 0 0 1px var(--primary); } -.btn:focus-visible { - outline: 2px solid var(--ring); +.btn:focus-visible, +.nav-link:focus-visible { outline-offset: 2px; + outline: none; + box-shadow: 0 0 0 2px var(--ring); +} + +.viz-carousel-controls :is(button, summary):focus-visible { + outline: 2px solid var(--ring) !important; + outline-offset: 2px !important; +} + +/* Web recipes are the default, including on renderers without inline style queries. */ +@supports (border-radius: if(style(--corner-shape: round): 1px; else: 2px)) { + /* Match the host's proportional text-sm scale at every text preference. */ + .tooltip, + .viz-badge, + .btn, + .nav-link, + .form-control, + .form-select { + font-size: if( + style(--corner-shape: round): round( + nearest, + calc(var(--font-size-base) * 14 / 16), + 1px + ) + ; else: round(nearest, calc(var(--font-size-base) * 13 / 14), 1px) + ); + } + + .btn-sm { + font-size: if( + style(--corner-shape: round): round( + nearest, + calc(var(--font-size-base) * 12 / 16), + 1px + ) + ; else: round(nearest, calc(var(--font-size-base) * 12 / 14), 1px) + ); + } + + .btn, + .nav-link, + .form-select { + border-radius: if( + style(--corner-shape: round): var(--radius-full) ; else: var(--radius-lg) + ); + } + + .form-check:not(.form-switch) .form-check-input[type="checkbox"] { + border-radius: if( + style(--corner-shape: round): calc(var(--radius-sm) / 2) ; + else: calc(var(--radius-lg) * 0.4) + ); + } + + .tooltip { + font-weight: if( + style(--corner-shape: round): 600; else: var(--font-weight-normal) + ); + } +} + +@media (pointer: coarse) { + .viz-carousel:has(> .viz-carousel-controls) { + min-height: calc(76px + var(--viz-carousel-picker-height, 0px)); + padding-bottom: 76px; + } + + .btn, + .nav-link, + .viz-carousel-controls button, + .viz-carousel-picker > summary, + .form-control, + .form-select { + --btn-size: 44px; + min-width: 44px; + min-height: 44px; + } + + /* Keep small-screen WebKit from zooming when a text field receives focus. */ + .form-control, + .form-select { + font-size: max(16px, calc(var(--font-size-base) * 14 / 16)); + font-size: max( + 16px, + round(nearest, calc(var(--font-size-base) * 14 / 16), 1px) + ); + } + .form-control:is( + [type="file"], + [type="date"], + [type="datetime-local"], + [type="month"], + [type="week"], + [type="time"], + [type="color"] + ) { + height: 44px; + min-height: 44px; + font-size: max(16px, calc(var(--font-size-base) * 14 / 16)); + font-size: max( + 16px, + round(nearest, calc(var(--font-size-base) * 14 / 16), 1px) + ); + } + + .form-control[type="file"]::file-selector-button { + min-width: 44px; + min-height: 44px; + } + + .form-control-color[type="color"] { + width: 44px; + height: 44px; + } + + .form-control:is( + [type="date"], + [type="datetime-local"], + [type="month"], + [type="time"], + [type="week"] + ) { + min-width: 0; + max-width: 100%; + padding-block: 8px; + } + + .form-check { + min-height: 44px; + padding-inline: 15px; + } + + .form-check-input { + position: relative; + } + + .form-check-input::after { + position: absolute; + top: 50%; + left: 50%; + width: 44px; + height: 44px; + content: ""; + transform: translate(-50%, -50%); + } + + .form-check-label { + display: inline-flex; + min-width: 44px; + min-height: 44px; + align-items: center; + } + + .form-range { + height: 44px; + touch-action: pan-y; + } } svg { @@ -854,8 +1490,8 @@ svg { /* Model-authored chart rules can otherwise stretch icons after these styles load. */ svg.lucide { display: block; - width: 16px !important; - height: 16px !important; + width: var(--icon-size) !important; + height: var(--icon-size) !important; flex: none; margin: 0 !important; stroke-width: 1.6; diff --git a/codex-rs/tui/assets/inline_visualization/visualize.html b/codex-rs/tui/assets/inline_visualization/visualize.html index a34c0d45df..50fa77473a 100644 --- a/codex-rs/tui/assets/inline_visualization/visualize.html +++ b/codex-rs/tui/assets/inline_visualization/visualize.html @@ -22,11 +22,18 @@ let autoUpdateCleanup = null; let positionRequest = 0; let skipDelayUntil = 0; + let lastPointerWasTouch = false; + let pinnedByTouch = false; - const getTrigger = (target) => - target?.nodeType === Node.ELEMENT_NODE - ? target.closest("[data-tooltip]") - : null; + const getTrigger = (event) => { + for (const target of event.composedPath()) { + if (target?.nodeType === Node.ELEMENT_NODE) { + const trigger = target.closest("[data-tooltip]"); + if (trigger) return trigger; + } + } + return null; + }; const containsTarget = (trigger, target) => target?.nodeType != null && trigger.contains(target); const getContent = (trigger) => @@ -70,6 +77,7 @@ }; const closeTooltip = (activateSkipDelay = true) => { clearOpenTimeout(); + pinnedByTouch = false; positionRequest += 1; autoUpdateCleanup?.(); autoUpdateCleanup = null; @@ -170,14 +178,47 @@ if (event.pointerType === "touch") { return; } - const trigger = getTrigger(event.target); + const trigger = getTrigger(event); if (trigger == null || containsTarget(trigger, event.relatedTarget)) { return; } requestOpen(trigger); }; + const handlePointer = (event) => { + lastPointerWasTouch = + event.type === "pointerdown" && event.pointerType === "touch"; + }; + const handleTouchClick = (event) => { + const isTouch = event.pointerType === "touch" || lastPointerWasTouch; + lastPointerWasTouch = false; + if (!isTouch || event.detail === 0) { + return; + } + const trigger = getTrigger(event); + if ( + trigger == null || + !trigger.isConnected || + getContent(trigger).length === 0 || + (pinnedByTouch && activeTrigger === trigger) + ) { + if (pinnedByTouch) { + closeTooltip(); + } + return; + } + openTooltip(trigger); + pinnedByTouch = activeTrigger === trigger; + window.setTimeout(() => { + if (activeTrigger === trigger && !trigger.isConnected) { + closeTooltip(false); + } + }, 0); + }; const handleLeave = (event) => { - const trigger = getTrigger(event.target); + if (pinnedByTouch) { + return; + } + const trigger = getTrigger(event); if (trigger == null || containsTarget(trigger, event.relatedTarget)) { return; } @@ -186,7 +227,7 @@ } }; const handleFocusIn = (event) => { - const trigger = getTrigger(event.target); + const trigger = getTrigger(event); if (trigger?.matches(":focus-visible")) { requestOpen(trigger, true); } @@ -197,31 +238,344 @@ } }; const handleContextMenu = (event) => { - const trigger = getTrigger(event.target); + lastPointerWasTouch = false; + const trigger = getTrigger(event); if (activeTrigger === trigger || pendingTrigger === trigger) { closeTooltip(); } }; + const handlers = { + pointerover: handlePointerOpen, + pointerdown: handlePointer, + pointercancel: handlePointer, + click: handleTouchClick, + pointerout: handleLeave, + focusin: handleFocusIn, + focusout: handleLeave, + keydown: handleKeyDown, + contextmenu: handleContextMenu, + }; + const roots = new Map(); + const handledEvents = new WeakSet(); + const handleEvent = (event) => { + if (handledEvents.has(event)) return; + handledEvents.add(event); + handlers[event.type](event); + }; + const discoverRoots = (node) => { + if (node.shadowRoot) listen(node.shadowRoot); + for (const element of node.querySelectorAll("*")) { + if (element.shadowRoot) listen(element.shadowRoot); + } + }; + const listen = (root) => { + if (roots.has(root)) return; + for (const type of Object.keys(handlers)) + root.addEventListener(type, handleEvent); + const observer = new window.MutationObserver((records) => { + for (const { addedNodes } of records) { + for (const node of addedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) discoverRoots(node); + } + } + for (const [root, cleanup] of roots) { + if (root.host && !root.host.isConnected) { + cleanup(); + roots.delete(root); + } + } + if (activeTrigger && !activeTrigger.isConnected) closeTooltip(false); + }); + observer.observe(root, { childList: true, subtree: true }); + roots.set(root, () => { + observer.disconnect(); + for (const type of Object.keys(handlers)) + root.removeEventListener(type, handleEvent); + }); + discoverRoots(root); + }; const destroy = () => { closeTooltip(false); - document.removeEventListener("pointerover", handlePointerOpen); - document.removeEventListener("pointerout", handleLeave); - document.removeEventListener("focusin", handleFocusIn); - document.removeEventListener("focusout", handleLeave); - document.removeEventListener("keydown", handleKeyDown); - document.removeEventListener("contextmenu", handleContextMenu); + lastPointerWasTouch = false; + for (const cleanup of roots.values()) cleanup(); + roots.clear(); tooltip = null; }; - document.addEventListener("pointerover", handlePointerOpen); - document.addEventListener("pointerout", handleLeave); - document.addEventListener("focusin", handleFocusIn); - document.addEventListener("focusout", handleLeave); - document.addEventListener("keydown", handleKeyDown); - document.addEventListener("contextmenu", handleContextMenu); + // Events between children of one shadow root never reach the document. + listen(document); window.addEventListener("pagehide", destroy, { once: true }); })(); + +