mirror of
https://github.com/supabase/supabase.git
synced 2026-09-08 10:59:38 +08:00
Closes DOCS-1283 https://github.com/user-attachments/assets/6e55a27f-6f73-453b-b98f-e91d3c14a9e4 ## Problem Three defects in the docs code block: - The scroll container has no `tabindex`. On `/guides/database/tables`, 18 blocks, none focusable, 2 overflowing at 1280px. Tab skips the scroll region, so a keyboard-only user cannot scroll code that runs off the edge. - The container has `role="group"` with no accessible name, so it announces as bare "group". - The line-number gutter has no `aria-hidden`, so digits are read inline with the code. A block linearizes as `1import { createClient } from '@supabase/supabase-js'23const supabase = ...`, with lines 2 and 3 collapsing into "23". Four more surfaced while testing the fix: - The wrap and copy buttons were absolutely positioned inside the element that scrolls, so `right-2` measured against the scrollable content box. Scrolling dragged them out of the corner into the middle of the code. This one predates the PR. - The buttons preceded the code in the DOM, so a screen reader read two actions before naming what they act on. - `focus-within` only fired for the buttons, so focusing the block left the controls invisible. - Both buttons set an `aria-label` identical to their tooltip text, and Radix points `aria-describedby` at the tooltip on focus, producing "Copy code, button, Copy code". ## Solution Keyboard: - Split the scroll region out of the positioning container, so the controls stay pinned. - Give the scroll region a `tabIndex` and a focus ring. - Reveal the controls on `group-focus-within`. Screen reader: - Name the region `<language>, <n> lines`. Code content stays readable; the summary goes in the name so the group can be skipped or stepped into. - Map fence aliases to spoken names, so `ts` announces as TypeScript. Only the ambiguous ones; `bash`, `python`, `kotlin`, `dart`, `swift` already read fine. - `aria-hidden` the gutter. The numbers are already `select-none`, and copy takes its content from the source string rather than the DOM, so copy behavior is unchanged. - Order the controls after the code. - Announce the word wrap toggle through a live region, matching the copy button. - Opt both buttons out of Radix's generated description. Also moved the `data-wrapped` side effect out of the `setIsWrapped` updater, since React calls updaters twice under StrictMode. ## Manual testing 1. Open `/docs/guides/database/tables`. 2. Run `document.querySelectorAll('.code-scroll[tabindex="0"]').length` in the console. Expect `18`. 3. Run `[...document.querySelectorAll('.code-scroll')].map(b => b.getAttribute('aria-label'))`. Expect entries like `SQL, 11 lines` and `bash, 2 lines`, plus one bare `2 lines` for the fence with no language. 4. Tab to a code block. Expect a visible focus ring, and the wrap and copy buttons to appear. 5. Press ArrowRight on the block under "Basic data loading", which overflows. Expect it to scroll, and the buttons to stay in the top-right corner. 6. Press Enter on the wrap button. Expect the code to wrap and a screen reader to announce "Word wrap enabled". 7. With VoiceOver on, focus a code block. Expect "SQL, 11 lines, code block", then the code read without line numbers interleaved. Focus each button and expect its name once, not twice. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Accessibility** - Improved code block labels for screen readers, including programming language and line count. - Added announcements when word wrap is enabled or disabled. - Enhanced keyboard focus behavior for code block controls. - **Usability** - Kept code block controls visible while scrolling through code. - Improved wrapped-code overflow handling. - Removed redundant tooltip descriptions for copy and word-wrap controls. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
463 lines
9.8 KiB
CSS
463 lines
9.8 KiB
CSS
@import 'config/tailwind.config.css';
|
|
@import './../../../packages/ui/build/css/themes/faux-classic-dark.css';
|
|
|
|
@source '../app/**/*.{ts,tsx,mdx}';
|
|
@source '../components/**/*.tsx';
|
|
@source '../content/**/*.{ts,tsx,mdx}';
|
|
@source '../docs/**/*.{tsx,mdx}';
|
|
@source '../features/**/*.{ts,tsx,mdx}';
|
|
@source '../layouts/**/*.tsx';
|
|
@source '../pages/**/*.{tsx,mdx}';
|
|
@source './../../../packages/ui/src/**/*.{tsx,ts,js}';
|
|
@source './../../../packages/ui-patterns/src/**/*.{tsx,ts,js}';
|
|
|
|
@theme inline {
|
|
--font-sans:
|
|
var(--font-inter), Inter, Helvetica Neue, Helvetica, ui-sans-serif, system-ui, sans-serif;
|
|
--font-heading: var(--font-manrope, var(--font-sans));
|
|
--font-mono: 'Source Code Pro', ui-monospace, Menlo, monospace;
|
|
}
|
|
|
|
@theme {
|
|
--text-sm: 0.8125rem;
|
|
--text-base: 0.9375rem;
|
|
--text-lg: 1rem;
|
|
--text-xl: 1.125rem;
|
|
--text-2xl: 1.375rem;
|
|
--text-3xl: 1.75rem;
|
|
--text-4xl: 2.125rem;
|
|
--text-5xl: 2.875rem;
|
|
--text-6xl: 3.625rem;
|
|
--text-7xl: 4.375rem;
|
|
--text-8xl: 5.875rem;
|
|
--text-9xl: 7.875rem;
|
|
--font-weight-normal: 450;
|
|
}
|
|
|
|
@layer utilities {
|
|
.prose--remove-p-margin p {
|
|
margin: 0;
|
|
}
|
|
}
|
|
|
|
@layer base {
|
|
*,
|
|
::after,
|
|
::before,
|
|
::backdrop,
|
|
::file-selector-button {
|
|
border-color: var(--border-default, currentColor);
|
|
}
|
|
|
|
code,
|
|
.code-content,
|
|
pre,
|
|
kbd,
|
|
samp,
|
|
.font-mono {
|
|
--text-xs: 0.75rem;
|
|
--text-sm: 0.875rem;
|
|
--text-base: 1rem;
|
|
--text-lg: 1.125rem;
|
|
--text-xl: 1.25rem;
|
|
--text-2xl: 1.5rem;
|
|
--text-3xl: 1.875rem;
|
|
--text-4xl: 2.25rem;
|
|
--text-5xl: 3rem;
|
|
--text-6xl: 3.75rem;
|
|
--text-7xl: 4.5rem;
|
|
--text-8xl: 6rem;
|
|
--text-9xl: 8rem;
|
|
--font-weight-normal: 400;
|
|
}
|
|
}
|
|
|
|
:root {
|
|
--sidebar-width: 300px;
|
|
--header-height: 50px;
|
|
}
|
|
|
|
html {
|
|
scroll-behavior: smooth;
|
|
font-weight: 500;
|
|
}
|
|
|
|
html[data-theme*='dark'] {
|
|
font-weight: 450;
|
|
}
|
|
|
|
body {
|
|
@apply bg-background text-foreground;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
::selection {
|
|
background-color: #6ee7b7 !important;
|
|
color: #333 !important;
|
|
}
|
|
|
|
h1:not(.font-mono),
|
|
h2:not(.font-mono),
|
|
h3:not(.font-mono),
|
|
h4:not(.font-mono),
|
|
h5:not(.font-mono),
|
|
h6:not(.font-mono),
|
|
.h1:not(.font-mono),
|
|
.h2:not(.font-mono),
|
|
.h3:not(.font-mono),
|
|
.h4:not(.font-mono),
|
|
.h5:not(.font-mono),
|
|
.h6:not(.font-mono) {
|
|
@apply font-heading font-semibold;
|
|
}
|
|
|
|
.thin-scrollbar {
|
|
scrollbar-width: thin;
|
|
}
|
|
|
|
.sidebar-width {
|
|
width: var(--sidebar-width);
|
|
}
|
|
|
|
.docs-width {
|
|
max-width: calc(100% - var(--sidebar-width));
|
|
|
|
@media screen and (max-width: 1024px) {
|
|
max-width: none;
|
|
}
|
|
}
|
|
|
|
.width-full {
|
|
width: 100% !important;
|
|
}
|
|
|
|
.p:not(.prose *):not(.overwrite) {
|
|
@apply text-foreground-light;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.small:not(.prose *):not(.overwrite) {
|
|
@apply text-foreground-muted text-xs;
|
|
}
|
|
|
|
.prose :where(p):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
|
|
white-space: pre-line;
|
|
}
|
|
|
|
.prose :where(p, a, code, pre):not(:where([class~='not-prose'], [class~='not-prose'] *)),
|
|
.prose :where(ol > li):not(:where([class~='not-prose'], [class~='not-prose'] *))::marker {
|
|
font-weight: inherit;
|
|
}
|
|
|
|
/* Body text inherits a heavier base weight, so override the shared `strong: 500`
|
|
* to keep bold text visually distinct. */
|
|
.prose :where(strong, b):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
|
|
font-weight: 700;
|
|
}
|
|
|
|
.prose :where(h1, h2, h3, h4, h5, h6):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
|
|
@apply font-heading font-semibold;
|
|
}
|
|
|
|
.prose
|
|
:where(p, li, a, figcaption):not(:where([class~='not-prose'], [class~='not-prose'] *)):not(
|
|
:where(h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6) *
|
|
) {
|
|
@apply text-base leading-7;
|
|
}
|
|
|
|
.prose :where(h1, .h1):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
|
|
@apply text-4xl;
|
|
}
|
|
.prose :where(h2, .h2):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
|
|
@apply text-2xl;
|
|
}
|
|
.prose :where(h3, .h3):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
|
|
@apply text-xl leading-7;
|
|
}
|
|
|
|
code[class*='language-'],
|
|
pre[class*='language-'] {
|
|
text-shadow: none !important;
|
|
}
|
|
|
|
/* Spec doc specifics ported from docusaurus */
|
|
/* @TODO these should be converted to Tailwind classes */
|
|
|
|
.method-list-item {
|
|
@apply border-t border-gray-400;
|
|
.method-list-item-label {
|
|
@apply flex items-center gap-1;
|
|
}
|
|
.method-list-item-label-name {
|
|
@apply font-mono font-bold text-sm py-2;
|
|
}
|
|
.method-list-item-label-badge {
|
|
@apply font-mono text-xs px-1;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.method-list-item-validation {
|
|
@apply flex flex-wrap gap-2 items-center;
|
|
}
|
|
.method-list-item-description {
|
|
@apply text-sm space-y-2;
|
|
}
|
|
}
|
|
|
|
.method-list-title-isChild {
|
|
font-family: var(--font-mono);
|
|
& h5 {
|
|
font-weight: 200;
|
|
}
|
|
}
|
|
|
|
.method-list-item-validation {
|
|
code {
|
|
@apply text-xs bg-control rounded-sm;
|
|
padding: 1px 6px;
|
|
}
|
|
}
|
|
|
|
.method-list-group {
|
|
@apply p-0 m-0 list-none border-b;
|
|
}
|
|
|
|
.method-list-group .method-list-group {
|
|
@apply border border-overlay rounded-md;
|
|
margin-bottom: 20px;
|
|
max-width: 600px;
|
|
}
|
|
|
|
.method-list-group .method-list-group li {
|
|
margin-top: 0;
|
|
padding: 12px;
|
|
border-bottom: none;
|
|
}
|
|
.method-list-title {
|
|
margin: 0;
|
|
padding: 6px 12px;
|
|
}
|
|
.method-list-group .method-list-group li div,
|
|
.method-list-group .method-list-group li h4,
|
|
.method-list-group .method-list-group li p {
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
.method-list-group .method-list-group li h4 {
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
/* These should move to their own components */
|
|
/* wasn't able to get an import path working */
|
|
.parent-menu-toggle.active {
|
|
svg {
|
|
transform: rotate(90deg);
|
|
}
|
|
}
|
|
|
|
/* ToC styles */
|
|
/* .toc__menu-item--active { */
|
|
/* color: hsl(var(--brand-default)) !important; */
|
|
/* } */
|
|
|
|
.video-container {
|
|
position: relative;
|
|
width: 100%;
|
|
padding-bottom: 56.25%;
|
|
|
|
iframe {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
border: 0;
|
|
}
|
|
}
|
|
|
|
.admonition-content > p {
|
|
@apply m-0;
|
|
}
|
|
|
|
/* format <code> inside <p> */
|
|
h2 code,
|
|
h3 code,
|
|
h4 code {
|
|
word-break: keep-all !important;
|
|
white-space: nowrap !important;
|
|
padding: 0.1rem 0.35rem !important;
|
|
|
|
&:before {
|
|
display: none;
|
|
}
|
|
&::after {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* code inside admonitions */
|
|
.admonition-content p code {
|
|
@apply bg-control;
|
|
word-break: keep-all !important;
|
|
white-space: nowrap !important;
|
|
}
|
|
|
|
article p strong {
|
|
color: inherit !important;
|
|
}
|
|
|
|
/* fix box shadow when <code> is inside <a> */
|
|
a:has(code) {
|
|
box-shadow: none !important;
|
|
}
|
|
|
|
/* fix code line wrapping */
|
|
/* need to set this to happen from medium onwards, otherwise the would cause horizontal scroll */
|
|
article p code {
|
|
&::before,
|
|
&::after {
|
|
display: none !important;
|
|
}
|
|
}
|
|
|
|
.short-inline-codeblock {
|
|
word-wrap: break-word;
|
|
@media screen and (min-width: 769px) {
|
|
white-space: pre !important;
|
|
}
|
|
}
|
|
|
|
/* fix firefox issue with li wrapping */
|
|
.doc-content-container ul li div.relative {
|
|
display: inline-block;
|
|
}
|
|
|
|
/* fix ToC links when they have <code> inside */
|
|
.toc-menu li a code {
|
|
background: none;
|
|
border: none;
|
|
}
|
|
|
|
/*
|
|
* sets the image in @Next/Image components to respect the height of the content
|
|
*/
|
|
.next-image--dynamic-fill {
|
|
width: 100%;
|
|
grid-column: 1 / -1;
|
|
}
|
|
.next-image--dynamic-fill > span {
|
|
position: relative !important;
|
|
}
|
|
.next-image--dynamic-fill img {
|
|
object-fit: contain;
|
|
width: 100% !important;
|
|
position: relative !important;
|
|
height: unset !important;
|
|
}
|
|
|
|
#docs-content-container #introduction {
|
|
@apply scroll-mt-60;
|
|
}
|
|
|
|
/* dat.GUI */
|
|
|
|
.dg.ac {
|
|
z-index: 9999 !important;
|
|
}
|
|
|
|
/* tables */
|
|
|
|
th code {
|
|
@apply text-nowrap;
|
|
}
|
|
|
|
/*
|
|
* First column (e.g. variable names): wrap at word boundaries, not mid-token.
|
|
*/
|
|
.prose :where(td:first-child) code {
|
|
@apply break-words whitespace-normal;
|
|
}
|
|
|
|
/*
|
|
* Other columns: prevents very long code lines (checksums, URLs, etc.) from
|
|
* breaking table widths.
|
|
*/
|
|
.prose :where(td:not(:first-child)) code {
|
|
@apply break-all;
|
|
}
|
|
|
|
.prose table td:first-child,
|
|
.prose table th:first-child {
|
|
min-width: 12rem;
|
|
width: 30%;
|
|
}
|
|
|
|
/* Zoomable image */
|
|
|
|
[data-rmiz-modal]:focus,
|
|
[data-rmiz-modal-overlay]:focus {
|
|
outline: none !important;
|
|
}
|
|
|
|
[data-rmiz-modal-overlay],
|
|
[data-rmiz-modal-img] {
|
|
transition-timing-function: cubic-bezier(0.24, 0.25, 0.05, 1) !important;
|
|
}
|
|
[data-rmiz-modal-overlay='visible'] {
|
|
background-color: var(--background-default) !important;
|
|
opacity: 0.8;
|
|
}
|
|
[data-rmiz-modal-img] {
|
|
image-rendering: high-quality;
|
|
}
|
|
|
|
/* Code blocks need margin applied when in content container */
|
|
.prose :where(.shiki:not(.shiki-wrapper *), .shiki-wrapper) {
|
|
margin-block: 2rem;
|
|
}
|
|
|
|
/* Code block theme colors for use with Supabase Theme */
|
|
[data-theme='dark'],
|
|
.dark {
|
|
--code-token-keyword: #bda4ff;
|
|
--code-foreground: #ffffff;
|
|
--code-token-constant: #3ecf8e;
|
|
--code-token-string: #ffcda1;
|
|
--code-token-comment: #949494;
|
|
--code-token-parameter: #ffffff;
|
|
--code-token-function: #3ecf8e;
|
|
--code-token-string-expression: #ffcda1;
|
|
--code-token-punctuation: #ffffff;
|
|
--code-token-link: #ffffff;
|
|
--code-token-number: #ffffff;
|
|
--code-token-property: #3ecf8e;
|
|
--code-highlight-color: #232323;
|
|
}
|
|
[data-theme='light'],
|
|
.light {
|
|
--code-token-keyword: #5f2fc4;
|
|
--code-foreground: oklch(from var(--foreground-light) l c h / 1);
|
|
--code-token-constant: #15593b;
|
|
--code-token-string: #9a5200;
|
|
--code-token-comment: #6a6a6a;
|
|
--code-token-parameter: oklch(from var(--foreground-light) l c h / 1);
|
|
--code-token-function: #15593b;
|
|
--code-token-string-expression: #9a5200;
|
|
--code-token-punctuation: oklch(from var(--foreground-light) l c h / 1);
|
|
--code-token-link: oklch(from var(--foreground-light) l c h / 1);
|
|
--code-token-number: oklch(from var(--foreground-light) l c h / 1);
|
|
--code-token-property: #15593b;
|
|
--code-highlight-color: #1c1c1c;
|
|
}
|
|
|
|
/* Word wrap styles for code blocks */
|
|
.shiki[data-wrapped='true'] .code-scroll {
|
|
overflow-x: hidden !important;
|
|
}
|
|
|
|
.shiki[data-wrapped='true'] .code-content {
|
|
white-space: pre-wrap !important;
|
|
word-break: break-word !important;
|
|
}
|