Skip to content
Merged
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
47 changes: 37 additions & 10 deletions apps/docs/features/docs/Reference.ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,14 @@ interface ApiOperationRequestBodyDetailsInternalProps extends HTMLAttributes<HTM
schema: ISchema
}

// Some specs have allOf/anyOf/oneOf as a single schema object instead of an
// array. Wrap it so it still renders instead of crashing or disappearing.
function asSchemaArray(value: unknown): Array<any> {
if (Array.isArray(value)) return value
if (value && typeof value === 'object') return [value]
return []
}

function ApiOperationRequestBodyDetailsInternal({
schema,
...props
Expand All @@ -389,7 +397,7 @@ function ApiOperationRequestBodyDetailsInternal({
return (
<>
<span className="font-mono text-sm font-medium text-foreground">All of the following:</span>
{schema.allOf.map((option, index) => (
{asSchemaArray(schema.allOf).map((option, index) => (
<ApiSchemaParamSubdetails key={index} schema={option} />
))}
</>
Expand All @@ -398,7 +406,7 @@ function ApiOperationRequestBodyDetailsInternal({
return (
<>
<span className="font-mono text-sm font-medium text-foreground">Any of the following:</span>
{schema.anyOf.map((option, index) => (
{asSchemaArray(schema.anyOf).map((option, index) => (
<ApiSchemaParamSubdetails key={index} schema={option} />
))}
</>
Expand All @@ -407,7 +415,7 @@ function ApiOperationRequestBodyDetailsInternal({
return (
<>
<span className="font-mono text-sm font-medium text-foreground">One of the following:</span>
{schema.oneOf.map((option, index) => (
{asSchemaArray(schema.oneOf).map((option, index) => (
<ApiSchemaParamSubdetails key={index} schema={option} />
))}
</>
Expand All @@ -431,10 +439,11 @@ function ApiOperationRequestBodyDetailsInternal({
return (
<>
<span className="font-mono text-sm font-medium text-foreground">{`Array of ${displayName}`}</span>
{!(
'type' in schema.items &&
['string', 'boolean', 'number', 'integer'].includes(schema.items.type)
) && <ApiSchemaParamSubdetails className="mt-4" schema={schema.items} />}
{schema.items &&
!(
'type' in schema.items &&
['string', 'boolean', 'number', 'integer'].includes(schema.items.type)
) && <ApiSchemaParamSubdetails className="mt-4" schema={schema.items} />}
</>
)
} else if (schema.type === 'object') {
Expand Down Expand Up @@ -469,13 +478,14 @@ export function ApiSchemaParamSubdetails({
(schema.type === 'string' &&
!('minLength' in schema || 'maxLength' in schema || 'pattern' in schema)) ||
(schema.type === 'array' &&
schema.items &&
'type' in schema.items &&
['boolean', 'number', 'integer', 'string', 'file'].includes(schema.items.type)))
) {
return null
}

const subContent =
const rawSubContent =
'enum' in schema
? schema.enum
: 'anyOf' in schema
Expand All @@ -492,6 +502,7 @@ export function ApiSchemaParamSubdetails({
value: schema[key],
}))
: []
const subContent = asSchemaArray(rawSubContent)

return (
<Collapsible>
Expand Down Expand Up @@ -530,8 +541,24 @@ export function ApiSchemaParamSubdetails({
</CollapsibleTrigger>
<CollapsibleContent>
{'type' in schema && schema.type === 'object' ? (
<div className={cn('border-b border-x border-fault', 'rounded-b-lg', 'p-5')}>
<ApiSchema schema={schema} />
<div className={cn('border-b border-x border-default', 'rounded-b-lg')}>
<div className="p-5 border-b border-default">
<ApiSchema schema={schema} />
</div>
<ApiOperationRequestBodyDetailsInternal schema={schema} className="px-5" />
</div>
) : 'type' in schema &&
schema.type === 'array' &&
'items' in schema &&
schema.items &&
typeof schema.items === 'object' &&
'type' in schema.items &&
schema.items.type === 'object' ? (
<div className={cn('border-b border-x border-default', 'rounded-b-lg')}>
<div className="p-5 border-b border-default">
<ApiSchema schema={schema} />
</div>
<ApiOperationRequestBodyDetailsInternal schema={schema.items} className="px-5" />
</div>
) : (
<ul className={cn('border-b border-x border-default', 'rounded-b-lg')}>
Expand Down
36 changes: 22 additions & 14 deletions apps/docs/features/ui/CodeBlock/CodeBlock.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ export function CodeCopyButton({ className, content }: { className?: string; con
className
)}
aria-label="Copy code"
// Tooltip repeats the label; the description would read the name twice
aria-describedby={undefined}
>
{copied ? (
<Check size={14} className="text-lighter" />
Expand All @@ -135,36 +137,42 @@ export function CodeCopyButton({ className, content }: { className?: string; con

export function CodeBlockControls({ content }: { content: string }) {
const [isWrapped, setIsWrapped] = useState(false)
// Empty until the first toggle, so nothing is announced on mount
const [wrapStatus, setWrapStatus] = useState('')
const wrapperRef = useRef<HTMLDivElement>(null)

const toggleWrap = useCallback(() => {
setIsWrapped((prev) => {
const newValue = !prev
// Find the parent code block and toggle the wrap data attribute
const codeBlock = wrapperRef.current?.closest('.shiki')
if (codeBlock) {
if (newValue) {
codeBlock.setAttribute('data-wrapped', 'true')
} else {
codeBlock.removeAttribute('data-wrapped')
}
const newValue = !isWrapped
setIsWrapped(newValue)
setWrapStatus(newValue ? 'Word wrap enabled' : 'Word wrap disabled')

const codeBlock = wrapperRef.current?.closest('.shiki')
if (codeBlock) {
if (newValue) {
codeBlock.setAttribute('data-wrapped', 'true')
} else {
codeBlock.removeAttribute('data-wrapped')
}
return newValue
})
}, [])
}
}, [isWrapped])

return (
<div
ref={wrapperRef}
className="opacity-0 flex group-hover:opacity-100 focus-within:opacity-100 absolute top-2 right-2 gap-1"
className="opacity-0 flex group-hover:opacity-100 group-focus-within:opacity-100 absolute top-2 right-2 gap-1"
>
<span className="sr-only" aria-live="polite">
{wrapStatus}
</span>
<Tooltip>
<TooltipTrigger asChild>
<button
tabIndex={0}
onClick={toggleWrap}
className={cn('cursor-pointer border rounded-md p-1', 'hover:bg-selection transition')}
aria-label={isWrapped ? 'Disable word wrap' : 'Enable word wrap'}
// Tooltip repeats the label; the description would read the name twice
aria-describedby={undefined}
>
{isWrapped ? (
<ArrowRightFromLine size={14} className="text-lighter" />
Expand Down
98 changes: 53 additions & 45 deletions apps/docs/features/ui/CodeBlock/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createTwoslasher, type ExtraFiles, type NodeHover } from 'twoslash'
import { cn } from 'ui'

import { AnnotatedSpan, CodeBlockControls } from './CodeBlock.client'
import { getFontStyle } from './CodeBlock.utils'
import { getCodeBlockLabel, getFontStyle } from './CodeBlock.utils'
import theme from './supabase-2.json' with { type: 'json' }
import denoTypes from './types/lib.deno.d.ts.include'

Expand Down Expand Up @@ -46,11 +46,7 @@ export async function CodeBlock({
twoslashed = annotationsByLine(hoverNodes)
code = editedCode
} catch (_err) {
// Silently ignore, if imports aren't defined type compilation fails
// Uncomment lines below to debug in dev
// console.log('\n==========CODE==========\n')
// console.log(code)
// console.error(_err.recommendation)
// Type compilation fails when imports aren't defined
}
}

Expand All @@ -66,52 +62,64 @@ export async function CodeBlock({
'group',
'relative',
'not-prose',
'w-full overflow-x-auto',
'w-full',
'border border-default rounded-lg',
'bg-200',
'text-sm',
className
)}
role="group"
aria-roledescription="code block"
>
<div
className={cn(
'code-scroll',
'w-full overflow-x-auto rounded-lg',
'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring'
)}
role="group"
aria-roledescription="code block"
aria-label={getCodeBlockLabel(lang, tokens.length)}
tabIndex={0}
>
<pre>
<code className={lineNumbers ? 'grid grid-cols-[auto_1fr]' : ''}>
{lineNumbers ? (
<>
{tokens.map((line, idx) => (
<Fragment key={idx}>
<div
aria-hidden="true"
className={cn(
'select-none text-right text-muted bg-control px-2 min-h-5 leading-5',
idx === 0 && 'pt-6',
idx === tokens.length - 1 && 'pb-6'
)}
>
{idx + 1}
</div>
<div
className={cn(
'code-content min-h-5 leading-5 pl-6 pr-6',
idx === 0 && 'pt-6',
idx === tokens.length - 1 && 'pb-6'
)}
>
<CodeLine tokens={line} twoslash={twoslashed?.get(idx)} />
</div>
</Fragment>
))}
</>
) : (
<div className="code-content p-6">
{tokens.map((line, idx) => (
<CodeLine key={idx} tokens={line} twoslash={twoslashed?.get(idx)} />
))}
</div>
)}
</code>
</pre>
</div>
{/* After the code so the block is named before its controls, and outside the scroller so they stay pinned */}
{!hideControls && <CodeBlockControls content={code.trim()} />}
<pre>
<code className={lineNumbers ? 'grid grid-cols-[auto_1fr]' : ''}>
{lineNumbers ? (
<>
{tokens.map((line, idx) => (
<Fragment key={idx}>
<div
className={cn(
'select-none text-right text-muted bg-control px-2 min-h-5 leading-5',
idx === 0 && 'pt-6',
idx === tokens.length - 1 && 'pb-6'
)}
>
{idx + 1}
</div>
<div
className={cn(
'code-content min-h-5 leading-5 pl-6 pr-6',
idx === 0 && 'pt-6',
idx === tokens.length - 1 && 'pb-6'
)}
>
<CodeLine tokens={line} twoslash={twoslashed?.get(idx)} />
</div>
</Fragment>
))}
</>
) : (
<div className="code-content p-6">
{tokens.map((line, idx) => (
<CodeLine key={idx} tokens={line} twoslash={twoslashed?.get(idx)} />
))}
</div>
)}
</code>
</pre>
</div>
)
}
Expand Down
27 changes: 24 additions & 3 deletions apps/docs/features/ui/CodeBlock/CodeBlock.utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { type CSSProperties } from 'react'

/*
* As defined in @shikijs/core/dist/chunk-tokens.d.mts
*/
// As defined in @shikijs/core/dist/chunk-tokens.d.mts
enum FontStyle {
NotSet = -1,
None = 0,
Expand All @@ -28,3 +26,26 @@ export function getFontStyle(styleFlags: number): CSSProperties {

return style
}

// Fence aliases a screen reader would otherwise read letter by letter
const LANGUAGE_LABELS: Record<string, string> = {
c: 'C',
html: 'HTML',
js: 'JavaScript',
json: 'JSON',
jsx: 'JavaScript',
py: 'Python',
sh: 'Shell',
shell: 'Shell',
sql: 'SQL',
toml: 'TOML',
ts: 'TypeScript',
tsx: 'TypeScript',
yaml: 'YAML',
}

export function getCodeBlockLabel(lang: string | null, lineCount: number): string {
const lines = `${lineCount} ${lineCount === 1 ? 'line' : 'lines'}`
if (!lang) return lines
return `${LANGUAGE_LABELS[lang] ?? lang}, ${lines}`
}
1 change: 1 addition & 0 deletions apps/docs/public/humans.txt
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ Shane E
Shaun Newman
Shardul Borhade
Shreekar Shetty
Simon Tomlinson
Sreyas Udayavarman
Stephanie Jackson (stejacks)
Stephen Morgan
Expand Down
Loading
Loading