diff --git a/.changeset/legal-pigs-tan.md b/.changeset/legal-pigs-tan.md
new file mode 100644
index 000000000000..36781344fc61
--- /dev/null
+++ b/.changeset/legal-pigs-tan.md
@@ -0,0 +1,19 @@
+---
+'@braid-design-system/docs-ui': minor
+---
+
+**TitleLink:** Added new component for rendering linkable headings with an optional copy-to-clipboard interaction. Should be wrapped in your required typographic component.
+
+**EXAMPLE:**
+```jsx
+
+ Getting started
+
+```
+
+With copy-to-clipboard:
+```jsx
+
+ Appearance
+
+```
diff --git a/.changeset/red-drinks-float.md b/.changeset/red-drinks-float.md
new file mode 100644
index 000000000000..5f34abb35b96
--- /dev/null
+++ b/.changeset/red-drinks-float.md
@@ -0,0 +1,16 @@
+---
+'braid-design-system': minor
+---
+
+---
+updated:
+ - vars
+---
+
+**vars:** Exposed `vars.transition`. Transition CSS variables are available in stylesheets and runtime styles.
+
+**EXAMPLE USAGE:**
+```ts
+export const myStyle = style({
+ transition: vars.transition.fast,
+});
\ No newline at end of file
diff --git a/.changeset/slick-ways-exist.md b/.changeset/slick-ways-exist.md
new file mode 100644
index 000000000000..501a59346918
--- /dev/null
+++ b/.changeset/slick-ways-exist.md
@@ -0,0 +1,10 @@
+---
+'@braid-design-system/docs-ui': minor
+---
+
+**CategoryHeading:** Added new component for rendering category-style navigation headings.
+
+**EXAMPLE USAGE:**
+```jsx
+Layout
+```
diff --git a/.changeset/some-states-battle.md b/.changeset/some-states-battle.md
new file mode 100644
index 000000000000..b14ee671b423
--- /dev/null
+++ b/.changeset/some-states-battle.md
@@ -0,0 +1,28 @@
+---
+'braid-design-system': minor
+---
+
+---
+updated:
+ - Badge
+---
+
+**Badge:** Added aria-hidden and aria-label props.
+
+`aria-hidden` allows a badge to be hidden from assistive technology
+
+`aria-label` allows visible badge text to be overridden with a more descriptive label for screen readers
+
+
+**EXAMPLE USAGE:**
+```jsx
+
+ Deprecated
+
+```
+
+```jsx
+
+ 2
+
+```
\ No newline at end of file
diff --git a/packages/braid-design-system/src/css.ts b/packages/braid-design-system/src/css.ts
index 8f966ee7e1da..e508e272af34 100644
--- a/packages/braid-design-system/src/css.ts
+++ b/packages/braid-design-system/src/css.ts
@@ -24,6 +24,7 @@ const {
borderRadius,
borderWidth,
shadow,
+ transition,
} = internalVars;
const vars = {
@@ -38,6 +39,7 @@ const vars = {
borderRadius,
borderWidth,
shadow,
+ transition,
};
function atoms(props: Omit) {
diff --git a/packages/braid-design-system/src/lib/components/Badge/Badge.tsx b/packages/braid-design-system/src/lib/components/Badge/Badge.tsx
index 241d8727e40f..8ce41cfb71aa 100644
--- a/packages/braid-design-system/src/lib/components/Badge/Badge.tsx
+++ b/packages/braid-design-system/src/lib/components/Badge/Badge.tsx
@@ -39,6 +39,8 @@ export interface BadgeProps {
data?: DataAttributeMap;
tabIndex?: BoxProps['tabIndex'];
'aria-describedby'?: string;
+ 'aria-hidden'?: boolean;
+ 'aria-label'?: string;
}
const lightModeBackgroundForTone = {
@@ -74,6 +76,8 @@ export const Badge = forwardRef(
data,
tabIndex,
'aria-describedby': ariaDescribedBy,
+ 'aria-hidden': ariaHidden,
+ 'aria-label': ariaLabel,
...restProps
},
ref,
@@ -112,6 +116,8 @@ export const Badge = forwardRef(
ref={ref}
tabIndex={tabIndex}
aria-describedby={ariaDescribedBy}
+ aria-hidden={ariaHidden}
+ aria-label={ariaLabel}
title={
title ??
(!ariaDescribedBy ? stringifyChildren(children) : undefined)
diff --git a/packages/braid-design-system/src/lib/css/vars.docs.tsx b/packages/braid-design-system/src/lib/css/vars.docs.tsx
index 878193339d75..d7b395b0ac1a 100644
--- a/packages/braid-design-system/src/lib/css/vars.docs.tsx
+++ b/packages/braid-design-system/src/lib/css/vars.docs.tsx
@@ -32,7 +32,7 @@ const Row = ({
hideCanvas?: boolean;
darkCanvas?: boolean;
}) => (
-
+
vars{group ? `.${group}` : null}.
@@ -49,16 +49,22 @@ const Row = ({
);
-const ContentWidthValue = ({ var: varName }: { var: string }) => {
- const [size, setSize] = useState(0);
+const CssVarValue = ({
+ var: varName,
+ property,
+}: {
+ var: string;
+ property: 'width' | 'transition';
+}) => {
+ const [value, setValue] = useState('');
const { themeKey } = useThemeSettings();
const ref = useRef(null);
useEffect(() => {
if (ref.current && themeKey) {
- setSize(ref.current.offsetWidth);
+ setValue(getComputedStyle(ref.current)[property]);
}
- }, [themeKey]);
+ }, [themeKey, property]);
return (
<>
@@ -68,9 +74,9 @@ const ContentWidthValue = ({ var: varName }: { var: string }) => {
pointerEvents="none"
opacity={0}
ref={ref}
- style={{ width: varName }}
+ style={{ [property]: varName } as any}
/>
- {size > 0 ? `${size}px` : ' '}
+ {value || '\u00a0'}
>
);
};
@@ -106,7 +112,7 @@ const varDocs: Record = {
{index > 0 ? : null}
-
+
@@ -283,6 +289,27 @@ const varDocs: Record = {
))}
),
+ transition: (
+
+ {Object.entries(vars.transition).map(
+ ([transitionName, transitionVar], index) => (
+
+ {index > 0 ? : null}
+
+
+
+
+
+
+ ),
+ )}
+
+ ),
} as const;
const docs: CssDoc = {
diff --git a/packages/docs-ui/src/components/CategoryHeading/CategoryHeading.tsx b/packages/docs-ui/src/components/CategoryHeading/CategoryHeading.tsx
new file mode 100644
index 000000000000..147637248ae7
--- /dev/null
+++ b/packages/docs-ui/src/components/CategoryHeading/CategoryHeading.tsx
@@ -0,0 +1,16 @@
+import { Box, Text } from 'braid-design-system';
+import type { ComponentProps, ReactNode } from 'react';
+
+type CategoryHeadingProps = {
+ children: ReactNode;
+ component?: ComponentProps['component'];
+};
+
+export const CategoryHeading = ({
+ children,
+ component,
+}: CategoryHeadingProps) => (
+
+ {children}
+
+);
diff --git a/packages/docs-ui/src/components/SideNavigation/SideNavigationSection.tsx b/packages/docs-ui/src/components/SideNavigation/SideNavigationSection.tsx
index ca10bb399488..dac282a04423 100644
--- a/packages/docs-ui/src/components/SideNavigation/SideNavigationSection.tsx
+++ b/packages/docs-ui/src/components/SideNavigation/SideNavigationSection.tsx
@@ -1,4 +1,6 @@
-import { Box, HiddenVisually, Stack, Text } from 'braid-design-system';
+import { Box, HiddenVisually, Stack } from 'braid-design-system';
+
+import { CategoryHeading } from '../CategoryHeading/CategoryHeading';
import { SideNavigationItem } from './SideNavigationItem';
@@ -11,9 +13,7 @@ interface SideNavigationSection {
}
const Title = ({ children }: { children: string }) => (
-
- {children}
-
+ {children}
);
const ItemList = ({ items }: { items: SideNavigationItem[] }) => (
diff --git a/packages/docs-ui/src/components/TitleLink/TitleLink.css.ts b/packages/docs-ui/src/components/TitleLink/TitleLink.css.ts
new file mode 100644
index 000000000000..dc68b097e96e
--- /dev/null
+++ b/packages/docs-ui/src/components/TitleLink/TitleLink.css.ts
@@ -0,0 +1,29 @@
+import { style } from '@vanilla-extract/css';
+import { atoms, vars } from 'braid-design-system/css';
+
+export const titleLink = style([
+ atoms({
+ display: 'flex',
+ borderRadius: 'small',
+ gap: 'xsmall',
+ alignItems: 'center',
+ }),
+ {
+ maxWidth: 'fit-content',
+ scrollMarginBlockStart: vars.space.small,
+ outlineOffset: vars.space.xsmall,
+ },
+]);
+
+export const isCopying = style({});
+
+export const showIcon = style({
+ selectors: {
+ [`${titleLink}:hover &, ${titleLink}:focus-visible &, ${isCopying}&`]: {
+ opacity: 1,
+ },
+ [`${titleLink}:hover &`]: {
+ transition: vars.transition.fast,
+ },
+ },
+});
diff --git a/packages/docs-ui/src/components/TitleLink/TitleLink.tsx b/packages/docs-ui/src/components/TitleLink/TitleLink.tsx
new file mode 100644
index 000000000000..0f535fd8f5b0
--- /dev/null
+++ b/packages/docs-ui/src/components/TitleLink/TitleLink.tsx
@@ -0,0 +1,97 @@
+import {
+ Box,
+ IconLink,
+ Link,
+ TooltipRenderer,
+ Text,
+ IconPositive,
+} from 'braid-design-system';
+import type { MouseEventHandler, ReactNode, RefCallback } from 'react';
+
+import { useCopy } from '../../utils/useCopy';
+
+import * as styles from './TitleLink.css';
+
+const slugify = (string: string) =>
+ string
+ .replace(/[\s?]/g, '-')
+ .replace('--', '-')
+ .replace(/-$/, '')
+ .toLowerCase();
+
+type TriggerProps = {
+ ref: RefCallback;
+ tabIndex: 0;
+ 'aria-describedby': string;
+};
+
+const TitleLinkAnchor = ({
+ slug,
+ onClick,
+ children,
+ triggerProps,
+ copying,
+}: {
+ slug: string;
+ onClick?: MouseEventHandler;
+ children: ReactNode;
+ triggerProps?: TriggerProps;
+ copying?: boolean;
+}) => (
+
+
+ {children}
+
+ {copying ? : }
+
+
+
+);
+
+type TitleLink = { copyable?: boolean } & (
+ | { children: string }
+ | { children: ReactNode; label: string }
+);
+
+export const TitleLink = ({ copyable = false, ...restProps }: TitleLink) => {
+ const label = 'label' in restProps ? restProps.label : restProps.children;
+ const slug = slugify(label);
+ const { copying, onCopyClick } = useCopy();
+
+ const handleClick: MouseEventHandler = (event) => {
+ if (event.metaKey) {
+ return;
+ }
+ event.preventDefault();
+ const anchor = event.currentTarget as HTMLAnchorElement;
+ onCopyClick(anchor.href);
+ };
+
+ if (!copyable) {
+ return {restProps.children};
+ }
+
+ return (
+ Copy to clipboard}>
+ {({ triggerProps }) => (
+
+ {restProps.children}
+
+ )}
+
+ );
+};
diff --git a/packages/docs-ui/src/index.ts b/packages/docs-ui/src/index.ts
index 4b13a6b97cc1..9a7bcda64168 100644
--- a/packages/docs-ui/src/index.ts
+++ b/packages/docs-ui/src/index.ts
@@ -6,3 +6,5 @@ export {
KeyboardShortcut,
KeyboardIcon,
} from './components/KeyboardShortcut/KeyboardShortcut';
+export { TitleLink } from './components/TitleLink/TitleLink';
+export { CategoryHeading } from './components/CategoryHeading/CategoryHeading';
diff --git a/packages/docs-ui/src/utils/useCopy.ts b/packages/docs-ui/src/utils/useCopy.ts
new file mode 100644
index 000000000000..572a105963a9
--- /dev/null
+++ b/packages/docs-ui/src/utils/useCopy.ts
@@ -0,0 +1,26 @@
+import { useEffect, useState } from 'react';
+
+export const useCopy = () => {
+ const [copying, setCopying] = useState(false);
+
+ useEffect(() => {
+ if (copying) {
+ const timer = setTimeout(() => {
+ setCopying(false);
+ }, 2000);
+ return () => clearTimeout(timer);
+ }
+ }, [copying]);
+
+ const onCopyClick = async (content: string) => {
+ if (!copying) {
+ setCopying(true);
+ await navigator.clipboard.writeText(content);
+ }
+ };
+
+ return {
+ copying,
+ onCopyClick,
+ };
+};
diff --git a/packages/generate-component-docs/src/__snapshots__/contract.test.ts.snap b/packages/generate-component-docs/src/__snapshots__/contract.test.ts.snap
index f1bd60932e8f..6ebc96d2ccf4 100644
--- a/packages/generate-component-docs/src/__snapshots__/contract.test.ts.snap
+++ b/packages/generate-component-docs/src/__snapshots__/contract.test.ts.snap
@@ -160,6 +160,8 @@ exports[`Badge 1`] = `
exportType: component,
props: {
aria-describedby?: string
+ aria-hidden?: boolean
+ aria-label?: string
bleedY?: boolean
children:
| (string | number)[]
diff --git a/site/src/App/CategoryHeading/CategoryHeading.tsx b/site/src/App/CategoryHeading/CategoryHeading.tsx
deleted file mode 100644
index 683337d85904..000000000000
--- a/site/src/App/CategoryHeading/CategoryHeading.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Box, Text } from 'braid-design-system';
-import type { ComponentProps } from 'react';
-
-export const CategoryHeading = ({
- children,
- component,
-}: {
- children: React.ReactNode;
- component?: ComponentProps['component'];
-}) => (
-
-
- {children}
-
-
-);
diff --git a/site/src/App/DocNavigation/DocDetails.tsx b/site/src/App/DocNavigation/DocDetails.tsx
index 19ffb03c7a83..e7f9077a1a80 100644
--- a/site/src/App/DocNavigation/DocDetails.tsx
+++ b/site/src/App/DocNavigation/DocDetails.tsx
@@ -1,4 +1,4 @@
-import { LinkableHeading } from '@braid-design-system/docs-ui';
+import { CategoryHeading, TitleLink } from '@braid-design-system/docs-ui';
import {
Box,
Stack,
@@ -6,6 +6,7 @@ import {
TextLink,
Secondary,
Text,
+ Heading,
} from 'braid-src/lib/components';
import { PlayroomStateProvider } from 'braid-src/lib/playroom/playroomState';
import { useContext, useMemo } from 'react';
@@ -196,7 +197,9 @@ export const DocDetails = () => {
{'accessibility' in docs && docs.accessibility ? (
- Accessibility
+
+ Accessibility
+
{docs.accessibility}
) : null}
@@ -206,46 +209,55 @@ export const DocDetails = () => {
.filter(([, docSectionChildren]) =>
docSectionChildren.some(hasContent),
)
- .map(([sectionKey, docSectionChildren]) => (
-
-
- {getSectionHeading(sectionKey)}
-
-
- {docSectionChildren.map(
- (example: { label?: string }, index: number) => (
-
- ),
- )}
- {sectionKey === 'bestPractices' &&
- 'alternatives' in docs &&
- docs.alternatives.length > 0 ? (
-
-
- Alternatives
-
-
- {docs.alternatives.map((alt) => (
-
-
- {alt.name}
- {' '}
- — {alt.description}
-
- ))}
-
-
- ) : null}
+ .map(([sectionKey, docSectionChildren]) => {
+ const heading = getSectionHeading(sectionKey);
+ return (
+
+
+
+ {heading}
+
+
+
+
+ {docSectionChildren.map(
+ (example: { label?: string }, index: number) => (
+
+ ),
+ )}
+ {sectionKey === 'bestPractices' &&
+ 'alternatives' in docs &&
+ docs.alternatives.length > 0 ? (
+
+
+
+ Alternatives
+
+
+
+
+ {docs.alternatives.map((alt) => (
+
+
+ {alt.name}
+ {' '}
+ — {alt.description}
+
+ ))}
+
+
+ ) : null}
+
-
- ))}
+ );
+ })}
{(docs.additional || []).map((example, index) => (
{
!hasBestPractices &&
docs.alternatives.length > 0 ? (
- Alternatives
+
+ Alternatives
+
{docs.alternatives.map((alt) => (
diff --git a/site/src/App/DocNavigation/DocProps.tsx b/site/src/App/DocNavigation/DocProps.tsx
index 50ed19327994..7d4a4b1fc33d 100644
--- a/site/src/App/DocNavigation/DocProps.tsx
+++ b/site/src/App/DocNavigation/DocProps.tsx
@@ -1,4 +1,4 @@
-import { LinkableHeading } from '@braid-design-system/docs-ui';
+import { TitleLink } from '@braid-design-system/docs-ui';
import type {
NormalisedPropType,
ExportDoc,
@@ -13,6 +13,7 @@ import {
TooltipRenderer,
TextLink,
IconInfo,
+ Heading,
} from 'braid-src/lib/components';
import partition from 'lodash.partition';
import { Fragment, useContext, useMemo } from 'react';
@@ -199,7 +200,10 @@ export const DocProps = () => {
{Array.isArray(propsToDocument) ? (
propsToDocument.map((c) => (
- {c}
+
+ {c}
+
+
))
@@ -208,9 +212,9 @@ export const DocProps = () => {
)}
-
- Further References
-
+
+ Further References
+
View Source
diff --git a/site/src/App/DocNavigation/DocReleases.tsx b/site/src/App/DocNavigation/DocReleases.tsx
index 75d7559fa70e..19d0b9374986 100644
--- a/site/src/App/DocNavigation/DocReleases.tsx
+++ b/site/src/App/DocNavigation/DocReleases.tsx
@@ -1,4 +1,4 @@
-import { LinkableHeading } from '@braid-design-system/docs-ui';
+import { TitleLink } from '@braid-design-system/docs-ui';
import {
Box,
Stack,
@@ -64,7 +64,10 @@ export const DocReleases = () => {
) : null}
- {`v${version}`}
+
+ {`v${version}`}
+
+
{historyItem.time ? (
(
-
- {section.label ? (
- Deprecated
- ) : undefined
- }
- >
- {section.label}
-
- ) : null}
- {section.description ?? null}
- {section.code || section.Example ? (
-
-
-
- ) : null}
-
-);
+}) => {
+ const { deprecated } = section;
+ return (
+
+ {deprecated ? (
+
+ Deprecated
+
+ ) : undefined}
+
+ {section.label ? (
+
+
+ {section.label}
+ {deprecated ? (
+ , deprecated
+ ) : null}
+
+
+ ) : null}
+ {section.description ?? null}
+ {section.code || section.Example ? (
+
+
+
+ ) : null}
+
+
+ );
+};
diff --git a/site/src/App/JumpToModal/SearchResults.tsx b/site/src/App/JumpToModal/SearchResults.tsx
index cbef4c05ea70..05fcbdf3eb61 100644
--- a/site/src/App/JumpToModal/SearchResults.tsx
+++ b/site/src/App/JumpToModal/SearchResults.tsx
@@ -1,4 +1,7 @@
-import { KeyboardShortcut } from '@braid-design-system/docs-ui';
+import {
+ KeyboardShortcut,
+ CategoryHeading,
+} from '@braid-design-system/docs-ui';
import {
Box,
Stack,
@@ -8,8 +11,6 @@ import {
Spread,
} from 'braid-src/lib/components';
-import { CategoryHeading } from '../CategoryHeading/CategoryHeading';
-
import type { GroupedResults, SearchItem } from './getSearchItems';
interface SearchResultsProps {