From 484a2e4b2966dfb3588ae0bf56adf85e522f2a79 Mon Sep 17 00:00:00 2001 From: Oleksii Horbachevskyi <17987710+ohorbachevskyi@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:23:44 +0000 Subject: [PATCH 1/2] Merge branch 'main' into docs/card-styling-updates remoteId: rem_01jwbtfqhbbam07z9j9m0etng6 remoteUpdateId: grupd_01m0yp3c953wmfnjz50ch4g9xt branchName: docs/card-styling-updates commitSha: 58efaf006b98b474ec66132dc20c14b1543da59e commitUrl: https://github.com/Redocly/redocly/commit/58efaf006b98b474ec66132dc20c14b1543da59e prId: 26402 --- docs/realm/content/markdoc-tags/cards.md | 283 +++++++++++++++++++++++ 1 file changed, 283 insertions(+) diff --git a/docs/realm/content/markdoc-tags/cards.md b/docs/realm/content/markdoc-tags/cards.md index af9cf2c5c..f361a9b65 100644 --- a/docs/realm/content/markdoc-tags/cards.md +++ b/docs/realm/content/markdoc-tags/cards.md @@ -71,6 +71,63 @@ A card only works as a child of `cards`. --- +- linkIcon +- string +- Icon displayed at the end of the card title. + Can be `chevron` or `arrow`. + The icon displays only on a cards that also have the `to` attribute. + Without this attribute, the title has no icon. + +--- + +- cta +- string +- Call-to-action text displayed under the card content, followed by a chevron icon. + The text displays only on cards that also have the `to` attribute. + Clicking anywhere on the card opens that link. + The text and the icon change color on mouse hover. + +--- + +- badge +- string +- Text of a badge that displays in the card title, after the link icon. + +--- + +- badgeColor +- string +- Color of the badge. + Supports predefined color names for consistent styling. + + **Supported color names:** + `red`, `green`, `blue`, `grey`, `turquoise`, `magenta`, `purple`, `carrot`, `raspberry`, `orange`, `grass`, `persian-green`, `sky`, `blueberry`. + + To use a color of your own, see [Custom colors](#custom-colors). + Default: `grey`. + +--- + +- badgeIcon +- string +- Icon to display inside the badge. +- Either: + - A [Font Awesome](https://fontawesome.com/icons) icon name. + Realm has the following icon packs built in: Classic Regular, Classic Solid, Duotone Solid, and Classic Brands. + The icons automatically adjust their colors when users change the color mode. + + To add an icon from the Classic Regular pack, you can provide the icon name only or prefix the name with `regular`. + To add an icon from other built-in packs, prefix the icon name with: `solid` (for Classic Solid), `duotone` (for Duotone Solid), or `brands` (for Classic Brands). + + **Examples:** `book`, `duotone book`, `brands github` + + Using other prefixes, including the `fa-` prefix, causes the icon to not render. + - Relative path to an icon image file. + + **Example:** `./images/config-icon.svg` + +--- + - variant - string - Visual style of the card. @@ -122,6 +179,18 @@ A card only works as a child of `cards`. --- +- iconColor +- string +- Color of the icon. + Supports predefined color names for consistent styling. + + **Supported color names:** + `red`, `green`, `blue`, `grey`, `turquoise`, `magenta`, `purple`, `carrot`, `raspberry`, `orange`, `grass`, `persian-green`, `sky`, `blueberry`. + + To use a color of your own, see [Custom colors](#custom-colors). + +--- + - lineClamp - string - Limits the number of lines of text displayed in the card content. @@ -283,6 +352,131 @@ Each card variant has unique hover styles for cards as links. +### Link icons + +Set the `linkIcon` attribute on a link card to add an icon at the end of the card title. +The icon needs the `to` attribute, and a card without `to` ignores it. + +{% cards columns=2 cardMinWidth=180 %} + + {% card title="Chevron" to="#link-icons" linkIcon="chevron" %} + Uses the `chevron` icon. + {% /card %} + + {% card title="Arrow" to="#link-icons" linkIcon="arrow" %} + Uses the `arrow` icon. + {% /card %} + +{% /cards %} + +
+ See link icon example syntax + + {% markdoc-example %} + ``` {% process=false %} + {% cards columns=2 cardMinWidth=180 %} + + {% card title="Chevron" to="#link-icons" linkIcon="chevron" %} + Uses the `chevron` icon. + {% /card %} + + {% card title="Arrow" to="#link-icons" linkIcon="arrow" %} + Uses the `arrow` icon. + {% /card %} + + {% /cards %} + ``` + {% /markdoc-example %} + +
+ +### Cards with a call to action + +Use the `cta` attribute to add call-to-action text under the card content. +A chevron icon follows the text, and both change color when a reader hovers over the card. +The call to action needs the `to` attribute, so that a click anywhere on the card opens that link. +The `align` attribute positions the call to action together with the rest of the card content. + +{% cards columns=2 cardMinWidth=180 %} + + {% card title="Quickstart" to="#cards-with-a-call-to-action" cta="Start building" %} + Publish your first project in five minutes. + {% /card %} + + {% card title="API reference" to="#cards-with-a-call-to-action" cta="Browse the endpoints" variant="outlined" %} + Every endpoint, parameter, and response schema. + {% /card %} + +{% /cards %} + +
+ See call to action example syntax + + {% markdoc-example %} + ``` {% process=false %} + {% cards columns=2 cardMinWidth=180 %} + + {% card title="Quickstart" to="#cards-with-a-call-to-action" cta="Start building" %} + Publish your first project in five minutes. + {% /card %} + + {% card title="API reference" to="#cards-with-a-call-to-action" cta="Browse the endpoints" variant="outlined" %} + Every endpoint, parameter, and response schema. + {% /card %} + + {% /cards %} + ``` + {% /markdoc-example %} + +
+ +### Cards with a badge + +Use the `badge` attribute to label a card. +The badge displays in the card title, after the link icon. +`badgeColor` picks a color from the badge palette, and `badgeIcon` puts an icon inside the badge. + +{% cards columns=3 cardMinWidth=200 %} + + {% card title="Webhooks" badge="Beta" badgeColor="blue" %} + A color name. + {% /card %} + + {% card title="Legacy tokens" badge="Deprecated" badgeColor="carrot" badgeIcon="triangle-exclamation" %} + A color name and an icon. + {% /card %} + + {% card title="Scorecards" badge="New" badgeColor="green" %} + A color name. + {% /card %} + +{% /cards %} + +
+ See badge example syntax + + {% markdoc-example %} + ``` {% process=false %} + {% cards columns=3 cardMinWidth=200 %} + + {% card title="Webhooks" badge="Beta" badgeColor="blue" %} + A color name. + {% /card %} + + {% card title="Legacy tokens" badge="Deprecated" badgeColor="carrot" badgeIcon="triangle-exclamation" %} + A color name and an icon. + {% /card %} + + {% card title="Scorecards" badge="New" badgeColor="green" %} + A color name. + {% /card %} + + {% /cards %} + ``` + {% /markdoc-example %} + +
+ ### Icon cards Use the `icon` attribute to add an icon to a card and choose its styles with `iconVariant`. @@ -336,6 +530,62 @@ Use the `icon` attribute to add an icon to a card and choose its styles with `ic +#### Icon colors + +Use the `iconColor` attribute to color the icon with a name from the palette. + +{% cards columns=4 cardMinWidth=180 %} + + {% card title="Red icon" icon="./images/cards-markdoc/ghost-icon.svg" iconColor="red" variant="outlined" %} + Uses `iconColor="red"`. + {% /card %} + + {% card title="Sky icon" icon="./images/cards-markdoc/ghost-icon.svg" iconColor="sky" variant="outlined" %} + Uses `iconColor="sky"`. + {% /card %} + + {% card title="Grass icon" icon="./images/cards-markdoc/ghost-icon.svg" iconColor="grass" variant="outlined" %} + Uses `iconColor="grass"`. + {% /card %} + + {% card title="Theme icon" icon="./images/cards-markdoc/ghost-icon.svg" variant="outlined" %} + No `iconColor`, so the icon keeps the theme color. + {% /card %} + +{% /cards %} + +An icon from a file takes the color only where the file sets no color of its own. +An icon that hardcodes a `fill` keeps the colors of the file. + +
+ See icon color example syntax + + {% markdoc-example %} + ``` {% process=false %} + {% cards columns=4 cardMinWidth=180 %} + + {% card title="Red icon" icon="images/ghost-icon.svg" iconColor="red" variant="outlined" %} + Uses `iconColor="red"`. + {% /card %} + + {% card title="Sky icon" icon="images/ghost-icon.svg" iconColor="sky" variant="outlined" %} + Uses `iconColor="sky"`. + {% /card %} + + {% card title="Grass icon" icon="images/ghost-icon.svg" iconColor="grass" variant="outlined" %} + Uses `iconColor="grass"`. + {% /card %} + + {% card title="Theme icon" icon="images/ghost-icon.svg" variant="outlined" %} + No `iconColor`, so the icon keeps the theme color. + {% /card %} + + {% /cards %} + ``` + {% /markdoc-example %} + +
+ #### Icon card positioning Use the `layout` and `align` attributes to control the positioning of elements in your icon card. @@ -450,6 +700,39 @@ Use the `layout` and `align` attributes to control the positioning of elements i +### Custom colors + +The palette covers the built-in names. +To use a color of your own, name it and define it in your [theme stylesheet](../../branding/customize-styles.md), then use that name in both attributes. + +Define a `.tag-{name}` class for the badge, and a `.card-icon-color-{name}` class for the icon: + +```css {% title="@theme/styles.css" %} +.tag-mycolor { + --tag-bg-color: #DCE8FF; + --tag-color: #1A3A6B; +} + +.card-icon-color-mycolor { + --card-icon-color: #1A3A6B; +} +``` + +The badge takes a pair of colors, because it needs a background and a text color that stays readable on it. +The icon takes one color. + +Use the name the same way as a built-in one: + +{% markdoc-example %} + ```markdoc {% process=false %} + {% card title="Webhooks" icon="images/ghost-icon.svg" iconColor="mycolor" badge="Beta" badgeColor="mycolor" %} + Uses a custom color. + {% /card %} + ``` +{% /markdoc-example %} + +To set different colors for dark mode, define the same classes again under `:root.dark`. + ### Image cards Add an image to a card using the `image` attribute. From 11f74bcea0b6e5f9ed13f16f0169b595779c2bc1 Mon Sep 17 00:00:00 2001 From: Oleksii Horbachevskyi Date: Wed, 26 Aug 2026 10:21:03 +0000 Subject: [PATCH 2/2] Add `demo` custom tag --- @theme/markdoc/components.tsx | 1 + .../components/ColorControl/ColorControl.tsx | 106 ++ .../components/Demo/AttributeControl.tsx | 242 ++++ .../markdoc/components/Demo/Demo-markdoc.ts | 41 + @theme/markdoc/components/Demo/Demo.tsx | 313 +++++ .../components/Demo/MarkdownContent.tsx | 76 ++ @theme/markdoc/components/Demo/properties.ts | 247 ++++ @theme/markdoc/components/Demo/tag-source.ts | 40 + .../components/RadioGroup/RadioGroup.tsx | 120 ++ @theme/markdoc/schema.ts | 3 + docs/realm/content/markdoc-tags/cards.md | 1104 ++++++----------- package.json | 6 +- 12 files changed, 1588 insertions(+), 711 deletions(-) create mode 100644 @theme/markdoc/components/ColorControl/ColorControl.tsx create mode 100644 @theme/markdoc/components/Demo/AttributeControl.tsx create mode 100644 @theme/markdoc/components/Demo/Demo-markdoc.ts create mode 100644 @theme/markdoc/components/Demo/Demo.tsx create mode 100644 @theme/markdoc/components/Demo/MarkdownContent.tsx create mode 100644 @theme/markdoc/components/Demo/properties.ts create mode 100644 @theme/markdoc/components/Demo/tag-source.ts create mode 100644 @theme/markdoc/components/RadioGroup/RadioGroup.tsx 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' ? ( +