diff --git a/package-lock.json b/package-lock.json index 4343684bdc..18b162e6b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27233,6 +27233,13 @@ "node": ">=8.0" } }, + "node_modules/tocbot": { + "version": "4.36.4", + "resolved": "https://registry.npmjs.org/tocbot/-/tocbot-4.36.4.tgz", + "integrity": "sha512-ffznkKnZ1NdghwR1y8hN6W7kjn4FwcXq32Z1mn35gA7jd8dt2cTVAwL3d0BXXZGPu0Hd0evverUvcYAb/7vn0g==", + "dev": true, + "license": "MIT" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -29948,11 +29955,14 @@ "js-beautify": "^1.15.1", "prettier-eslint": "^16.3.0", "prettier-eslint-cli": "^8.0.1", + "react": "^18", + "react-dom": "^18", "react-syntax-highlighter": "^15.6.1", "remark-gfm": "^4.0.0", "rollup": "^4.12.0", "storybook": "^9.0.5", "storybook-addon-pseudo-states": "^9.0.5", + "tocbot": "^4.21.0", "typescript": "~5.4.5", "vite": "^7.0.0" }, diff --git a/packages/storybook/.storybook/blocks/DocsGrid.tsx b/packages/storybook/.storybook/blocks/DocsGrid.tsx new file mode 100644 index 0000000000..d931b88c45 --- /dev/null +++ b/packages/storybook/.storybook/blocks/DocsGrid.tsx @@ -0,0 +1,61 @@ +/* eslint-disable */ +import React from 'react'; +import { styled } from 'storybook/theming'; + +type DocsGridProps = React.HTMLAttributes & { + columns?: number; + minColumnWidth?: number; + gap?: number; + children?: React.ReactNode; +}; + +// Generic responsive grid for docs content (flex-based, no media queries) +// - columns: desired columns per row at wide sizes (default 2). Rows will never exceed this count. +// - minColumnWidth: minimum column width before wrapping to fewer columns (default 200) +// - gap: spacing between items in px (default 16) +const Grid = ((styled as any)('div')(({ $gap = 16 }: { $gap?: number }) => ({ + display: 'flex', + flexWrap: 'wrap', + gap: `${$gap}px`, + alignItems: 'stretch', + width: '100%', + boxSizing: 'border-box', + paddingBlockEnd: '24px' +}))) as any; + +const Cell = ((styled as any)('div')( + ({ $columns = 2, $minCol = 200, $gap = 16 }: { $columns?: number; $minCol?: number; $gap?: number }) => ({ + // Target N columns per row at wide widths (e.g., 50% for 2 columns) + flex: `1 1 calc(${100 / Math.max(1, $columns)}% - ${($columns - 1) * $gap / $columns}px)`, + // Never shrink below the minimum width; items will wrap to the next line instead + minWidth: `${$minCol}px`, + boxSizing: 'border-box', + // Ensure contained component stretches to fill the available cell width + '& > *': { + width: '100%' + } + }) +)) as any; + +export function DocsGrid({ + children, + className, + columns = 2, + minColumnWidth = 200, + gap = 16, + style, + ...rest +}: DocsGridProps) { + const items = React.Children.toArray(children); + return ( + + {items.map((child, index) => ( + + {child} + + ))} + + ); +} + +export default DocsGrid; diff --git a/packages/storybook/.storybook/blocks/ExampleContainer.tsx b/packages/storybook/.storybook/blocks/ExampleContainer.tsx new file mode 100644 index 0000000000..099a578b14 --- /dev/null +++ b/packages/storybook/.storybook/blocks/ExampleContainer.tsx @@ -0,0 +1,39 @@ +/* eslint-disable */ +import React from 'react'; +import { styled } from 'storybook/theming'; + +type ExampleContainerProps = React.HTMLAttributes & { + background?: string; + children?: React.ReactNode; +}; + +// Use a transient prop to avoid passing to the DOM +const Frame = ((styled as any)('div')(({ $background }: { $background?: string }) => ({ + position: 'relative', + width: '90%', + overflow: 'hidden', + margin: '25px 0 40px', + padding: '32px 30px', + borderRadius: 5, + background: $background ?? 'rgba(255, 255, 255, 1)', + boxShadow: 'rgba(0, 0, 0, 0.1) 0px 1px 3px 0px', + // Use tokenized border width when available + border: 'var(--ni-nimble-border-width, 1px) solid rgb(211, 213, 214)' +}))) as any; + +export default function ExampleContainer({ + children, + background = 'var(--ni-nimble-application-background-color, rgb(252, 252, 252))', + style, + ...rest +}: ExampleContainerProps) { + const resolvedStyle = { + ...style, + } as React.CSSProperties; + + return ( + + {children} + + ); +} diff --git a/packages/storybook/.storybook/blocks/Guideline.tsx b/packages/storybook/.storybook/blocks/Guideline.tsx new file mode 100644 index 0000000000..0e05e670d1 --- /dev/null +++ b/packages/storybook/.storybook/blocks/Guideline.tsx @@ -0,0 +1,179 @@ +/* eslint-disable */ +import React, { useId } from 'react'; +import { styled } from 'storybook/theming'; + +const TEXT_DEFAULT = 'var(--ni-nimble-body-font-color, #161617)'; + +const CheckIcon: React.FC = () => ( + +); + +const CrossIcon: React.FC = () => ( + +); + +const VARIANTS = { + do: { + headerBg: 'var(--ni-nimble-success-color, #009921)', + bodyBg: 'color-mix(in srgb, var(--ni-nimble-success-color, #009921) 10%, transparent)', + icon: CheckIcon, + headerGap: 6 + }, + dont: { + headerBg: 'var(--ni-nimble-error-color, #C4000C)', + bodyBg: 'color-mix(in srgb, var(--ni-nimble-error-color, #C4000C) 10%, transparent)', + icon: CrossIcon, + headerGap: 8 + } +} as const; + +type VariantKey = keyof typeof VARIANTS; + +const Wrapper = ((styled as any)('section')(({ maxWidth }: { maxWidth?: number }) => ({ + width: '100%', + maxWidth: maxWidth ?? 900, + display: 'flex', + flexDirection: 'column', + alignItems: 'stretch', + boxSizing: 'border-box', +}))) as any; + +const Header = ((styled as any)('div')(({ bg, gap }: { bg: string; gap: number }) => ({ + backgroundColor: bg, + color: 'var(--ni-nimble-contrast-font-color, #fff)', + height: 22, + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-start', + gap, + padding: '0 24px', + borderTopLeftRadius: 4, + borderTopRightRadius: 4, +}))) as any; + +const HeaderIcon = styled.span(() => ({ + display: 'inline-flex', + width: 16, + height: 16, + alignItems: 'center', + justifyContent: 'center', +})); + +const HeaderText = styled.span(({ theme }) => ({ + fontFamily: `'Source Sans Pro', 'Source_Sans_Pro', ${theme.typography.fonts.base}`, + fontWeight: 700, + fontSize: 18, + lineHeight: '22px', + letterSpacing: 0, +})); + +const Body = ((styled as any)('div')(({ bg }: { bg: string }) => ({ + backgroundColor: bg, + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '10px 10px 12px 10px', + boxSizing: 'border-box', +}))) as any; + +const Frame = ((styled as any)('div')(() => ({ + width: '100%', + backgroundColor: 'var(--ni-nimble-application-background-color, #fff)', + minHeight: 180, + maxHeight: 280, + overflow: 'hidden', + padding: 32, + boxSizing: 'border-box', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}))) as any; + +const Copy = ((styled as any)('div')(() => ({ + width: '100%', + maxWidth: 750, + color: TEXT_DEFAULT, + padding: '16px 24px 8px 10px', + boxSizing: 'border-box', +}))) as any; + +const CopyTitle = styled('div')(({ theme }) => ({ + margin: 0, + fontFamily: `"Founders Grotesk", "Founders_Grotesk", ${theme.typography.fonts.base}`, + fontWeight: 500, + fontSize: 18, + lineHeight: '21px', + letterSpacing: '0.18px', + color: TEXT_DEFAULT, +})); + +const CopyText = styled('p')(({ theme }) => ({ + margin: '6px 0 0 0', + fontFamily: `'Source Sans Pro', 'Source_Sans_Pro', ${theme.typography.fonts.base}`, + fontWeight: 400, + fontSize: 15, + lineHeight: '21px', + color: TEXT_DEFAULT, +})); + +type GuidelineProps = React.HTMLAttributes & { + variant?: VariantKey; + title?: React.ReactNode; + children?: React.ReactNode; + figure?: React.ReactNode; + copy?: React.ReactNode; + headerText?: string; + maxWidth?: number; + id?: string; +}; + +export default function Guideline({ + variant = 'do', + title, + children, + figure, + copy, + headerText, + maxWidth, + id, +}: GuidelineProps) { + const v = VARIANTS[variant] ?? VARIANTS.do; + const Icon = v.icon; + const titleId = useId(); + const headingId = id || `guideline-${variant}-${titleId}`; + const computedHeaderText = headerText ?? (variant === 'do' ? 'DO' : 'DON’T'); + const childArray = React.Children.toArray(children); + const hasElementChild = childArray.some((c) => React.isValidElement(c)); + const frameContent = figure ?? (hasElementChild ? children : null); + const copyNode = copy ?? (!hasElementChild && children ? children : null); + return ( + +
+ + + + {computedHeaderText} +
+ + {frameContent} + + + {title} + {copyNode ? {copyNode} : null} + +
+ ); +} + +export function GuidelinesDo(props: Omit) { + return ; +} + +export function GuidelinesDont(props: Omit) { + return ; +} diff --git a/packages/storybook/.storybook/blocks/HyperlinkCard.tsx b/packages/storybook/.storybook/blocks/HyperlinkCard.tsx new file mode 100644 index 0000000000..e88e88a753 --- /dev/null +++ b/packages/storybook/.storybook/blocks/HyperlinkCard.tsx @@ -0,0 +1,113 @@ +/* eslint-disable */ +import React from 'react'; +import { styled } from 'storybook/theming'; + +const TEXT_DEFAULT = 'var(--ni-nimble-body-font-color, #161617)'; +const CARD_BG = 'var(--ni-nimble-application-background-color, #ffffff)'; +const CARD_BORDER = 'rgba(var(--ni-nimble-action-rgb-partial-color, 22,22,23), 0.1)'; + +const Card = (styled('a')(({ theme }) => ({ + display: 'block', + position: 'relative', + textDecoration: 'none', + color: TEXT_DEFAULT, + backgroundColor: CARD_BG, + borderRadius: 8, + boxSizing: 'border-box', + padding: '24px 20px 20px 24px', + border: `var(--ni-nimble-border-width, 2px) solid ${CARD_BORDER}`, + transition: 'border-color 120ms ease, box-shadow 120ms ease, transform 120ms ease', + outline: 'none', + + '&:hover': { + borderColor: CARD_BORDER, + boxShadow: '0 1px 0 0 rgba(var(--ni-nimble-action-rgb-partial-color, 22,22,23), 0.06), 0 2px 8px rgba(var(--ni-nimble-action-rgb-partial-color, 22,22,23), 0.06)' + }, + '&:focus-visible': { + borderColor: CARD_BORDER, + boxShadow: '0 0 0 2px color-mix(in srgb, var(--ni-nimble-accent-color, #00734b) 40%, transparent)' + } +})) as any); + +const Title = styled('div')(({ theme }) => ({ + fontFamily: `'Source Sans Pro', 'Source_Sans_Pro', ${theme.typography.fonts.base}`, + fontSize: 13, + lineHeight: '19px', + letterSpacing: '0.13px', + textTransform: 'uppercase', + fontWeight: 600, + width: '100%', +})); + +const Corner = ((styled as any)('div')(() => ({ + position: 'absolute', + top: 12, + right: 12, + width: 16, + height: 16, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' +}))) as any; + +const ExternalLinkIcon: React.FC = () => ( + +); + +export interface HyperlinkCardProps extends Omit, 'title'> { + href?: string; + title?: React.ReactNode; + children?: React.ReactNode; +} + +export default function HyperlinkCard({ href, title, children, ...anchorProps }: HyperlinkCardProps) { + const normalizeStorybookHref = (h?: string) => { + if (!h) return '#'; + if (/^\?(path|id)=/.test(h)) { + return `/${h}`; + } + if (/^\/?\?(path|id)=/.test(h)) { + return h.startsWith('/') ? h : `/${h}`; + } + if (/^\/?iframe\.html\?/.test(h)) { + const query = h.split('?')[1] || ''; + return query ? `/?${query}` : '/'; + } + return h; + }; + + const managerHref = normalizeStorybookHref(href); + + const onClick: React.MouseEventHandler = (e) => { + if (/^\/?\?(path|id)=/.test(managerHref)) { + e.preventDefault(); + try { + if (typeof window !== 'undefined' && window.top) { + window.top.location.assign(managerHref.startsWith('/') ? managerHref : `/${managerHref}`); + } + } catch (_) { + // noop + } + } + }; + + const isManager = /^\/?\?(path|id)=/.test(managerHref); + + return ( + + {title} + + + + {children} + + ); +} diff --git a/packages/storybook/.storybook/blocks/InlineGuideline.tsx b/packages/storybook/.storybook/blocks/InlineGuideline.tsx new file mode 100644 index 0000000000..dd066012e8 --- /dev/null +++ b/packages/storybook/.storybook/blocks/InlineGuideline.tsx @@ -0,0 +1,93 @@ +/* eslint-disable */ +import React from 'react'; +import { styled } from 'storybook/theming'; + +// Colors from design +const ORANGE = 'var(--ni-nimble-warning-color, #FF4B00)'; // Warning +const ORANGE_TINT_10 = 'color-mix(in srgb, var(--ni-nimble-warning-color, #FF4B00) 10%, transparent)'; +const TEXT = 'var(--ni-nimble-body-font-color, #161617)'; + +const Wrapper = ((styled as any)('div')(() => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'stretch', + gap: 'var(--ni-nimble-xsmall-padding, 4px)', + width: '100%', + padding: '0 0 24px 0' +}))) as any; + +const ColorBlock = ((styled as any)('div')(() => ({ + backgroundColor: ORANGE, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: 'var(--ni-nimble-small-padding, 6px) var(--ni-nimble-xsmall-padding, 2px)' +}))) as any; + +const IconFrame = ((styled as any)('div')(() => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 18, + height: 18, + padding: 4 +}))) as any; + +const Main = ((styled as any)('div')(() => ({ + flexGrow: 1, + backgroundColor: ORANGE_TINT_10, + display: 'flex', + alignItems: 'center', + padding: 10, + minWidth: 1, + minHeight: 1 +}))) as any; + +const TextEl = ((styled as any)('p')(({ theme }: any) => ({ + margin: 0, + maxWidth: 700, + fontFamily: `"Source Sans Pro", "Source_Sans_Pro", ${theme.typography.fonts.base}`, + fontWeight: 400, + fontSize: 16, + lineHeight: '21px', + color: TEXT +}))) as any; + +const Strong = ((styled as any)('span')(() => ({ + fontWeight: 600 +}))) as any; + +const TriangleFilled16: React.FC = () => ( + +); + +export interface InlineGuidelineProps extends React.HTMLAttributes { + prefix?: string; + children?: React.ReactNode; +} + +/** + * InlineGuideline - warning/info callout inline with content. + * Usage: Your message here. + */ +export const InlineGuideline: React.FC = ({ prefix = 'Note:', children, className }) => { + return ( + + + + + + +
+ + {prefix ? {prefix} : null} + {children} + +
+
+ ); +}; + +export default InlineGuideline; diff --git a/packages/storybook/.storybook/blocks/InlineTOC.tsx b/packages/storybook/.storybook/blocks/InlineTOC.tsx new file mode 100644 index 0000000000..3155a13a26 --- /dev/null +++ b/packages/storybook/.storybook/blocks/InlineTOC.tsx @@ -0,0 +1,329 @@ +/* eslint-disable indent, @typescript-eslint/indent */ +import React, { useEffect } from 'react'; +import { styled } from 'storybook/theming'; +import tocbot from 'tocbot'; + +// Inline container instead of an