diff --git a/@theme/markdoc/components.tsx b/@theme/markdoc/components.tsx index f3a804f4c..4964fc142 100644 --- a/@theme/markdoc/components.tsx +++ b/@theme/markdoc/components.tsx @@ -7,4 +7,5 @@ export { Quiz } from '@redocly/marketing-pages/components/Quiz/Quiz.js'; export { TagBadge } from './components/TagBadge/TagBadge'; export { SplitView, LeftView, RightView } from './components/SplitView/SplitView'; export { GroupElements } from './components/GroupElements/GroupElements'; +export { Demo } from './components/Demo/Demo'; export * from '../../docs/realm/@theme/markdoc/components'; \ No newline at end of file diff --git a/@theme/markdoc/components/ColorControl/ColorControl.tsx b/@theme/markdoc/components/ColorControl/ColorControl.tsx new file mode 100644 index 000000000..c457b1da2 --- /dev/null +++ b/@theme/markdoc/components/ColorControl/ColorControl.tsx @@ -0,0 +1,106 @@ +import React, { useId } from 'react'; +import styled from 'styled-components'; + +import { CheckmarkIcon } from '@redocly/theme/icons/CheckmarkIcon/CheckmarkIcon'; + +export type ColorControlProps = { + options: readonly string[]; + value?: string; + onChange: (value: string) => void; + /** Groups the inputs. Generated when the caller passes nothing. */ + name?: string; + ariaLabel?: string; + className?: string; +}; + +/** + * Picks one of the palette color names. Each swatch carries the theme's own + * `.tag-{name}` class, so it takes the color from `--tag-color` and also covers + * the custom names a project defines in its stylesheet. + */ +export function ColorControl({ + options, + value, + onChange, + name, + ariaLabel, + className, +}: ColorControlProps) { + const generatedName = useId(); + + return ( + + {options.map((option) => ( + + onChange(option)} + /> + + {value === option && ( + + + + )} + + + ))} + + ); +} + +const ColorControlWrapper = styled.div` + display: flex; + align-items: center; + gap: var(--spacing-xxs); + min-height: var(--demo-control-height, 32px); +`; + +/* Shares the row with the other swatches, so a long palette never wraps. */ +const Swatch = styled.label` + position: relative; + display: flex; + flex: 1 1 auto; + min-width: 0; + max-width: 20px; + cursor: pointer; +`; + +/* Kept in the layout so it stays focusable with the keyboard. */ +const Radio = styled.input` + position: absolute; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +`; + +const SwatchFill = styled.span` + position: relative; + display: block; + width: 100%; + aspect-ratio: 1; + border-radius: 50%; + background: var(--tag-color, var(--text-color-secondary)); + + ${Radio}:focus-visible + & { + outline: 2px solid var(--color-primary-base); + outline-offset: 2px; + } +`; + +const CheckMark = styled.span` + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +`; diff --git a/@theme/markdoc/components/Demo/AttributeControl.tsx b/@theme/markdoc/components/Demo/AttributeControl.tsx new file mode 100644 index 000000000..d1dd3149a --- /dev/null +++ b/@theme/markdoc/components/Demo/AttributeControl.tsx @@ -0,0 +1,242 @@ +import React, { useId, useMemo } from 'react'; +import styled from 'styled-components'; + +import { Select } from '@redocly/theme/components/Select/Select'; +import { Switch } from '@redocly/theme/components/Switch/Switch'; +import { Tooltip } from '@redocly/theme/components/Tooltip/Tooltip'; +import { InformationIcon } from '@redocly/theme/icons/InformationIcon/InformationIcon'; + +import { ColorControl } from '../ColorControl/ColorControl'; +import { RadioGroup } from '../RadioGroup/RadioGroup'; +import { getControlKind, getEnumOptions } from './properties'; + +import type { AttributeControlKind, AttributeDescriptor, AttributeValue } from './properties'; + +export type AttributeControlProps = { + name: string; + /** Shown instead of the name, exactly as given. */ + label?: string; + descriptor: AttributeDescriptor; + value: AttributeValue; + onChange: (value: AttributeValue) => void; + className?: string; +}; + +export function AttributeControl({ + name, + label: labelText, + descriptor, + value, + onChange, + className, +}: AttributeControlProps) { + const controlId = useId(); + const kind = getControlKind(descriptor); + // Select and ColorControl run effects on every new options identity, so keep the array stable. + const options = useMemo(() => getEnumOptions(descriptor), [descriptor]); + const selectOptions = useMemo( + () => + options?.map((option) => ({ + value: option, + label: String(option), + element: String(option), + })), + [options], + ); + const colorOptions = useMemo(() => options?.map((option) => String(option)), [options]); + + const label = ( + + + {kind === 'color' && value !== undefined && {formatLabel(String(value))}} + {descriptor.description && ( + + + + + + )} + + ); + + // A switch keeps its label on one row, with the control at the end. + if (kind === 'switch') { + return ( + + {label} + + + ); + } + + return ( + + {label} + {kind === 'color' && colorOptions ? ( + + ) : kind === 'radio' && selectOptions ? ( + + ) : kind === 'select' && selectOptions ? ( + onChange(next as AttributeValue)} + /> + ) : kind === 'textarea' ? ( +