From 412874b6323ccc1aed71d99df4bce3b40e848a60 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 21 Jul 2026 15:10:49 -0700 Subject: [PATCH 01/24] docs: ADR for Paragon Next Co-Authored-By: Claude --- .../0022-modernization-remove-bootstrap.rst | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 docs/decisions/0022-modernization-remove-bootstrap.rst diff --git a/docs/decisions/0022-modernization-remove-bootstrap.rst b/docs/decisions/0022-modernization-remove-bootstrap.rst new file mode 100644 index 00000000000..f99b3d78718 --- /dev/null +++ b/docs/decisions/0022-modernization-remove-bootstrap.rst @@ -0,0 +1,178 @@ +22. Modernization: Removing Bootstrap and Modernizing the Toolchain +------------------------------------------------------------------- + +Status +------ + +Proposed + +Context +------- + +Paragon is a mature, accessibility-focused React component library. Its public +surface has two stable, widely-depended-upon contracts: + +1. The **React component API** — ~99 exports whose props are consumed by dozens + of Open edX micro-frontends (MFEs). +2. The **design-tokens theming API** — a ``style-dictionary`` pipeline that + compiles DTCG-style token JSON into CSS custom properties, shipped as + ``core.css`` / ``light.css`` and overridden by ``@edx/brand`` packages. + +Underneath those contracts, however, Paragon rests on aging foundations: + +- **Bootstrap 4** provides both a *behavior* layer (34 components wrap + ``react-bootstrap`` sub-components) and a *style* layer (``core.scss`` imports + ``~bootstrap/scss/{reboot,root,mixins,transitions,grid,...}``). Bootstrap 4 is + no longer actively developed, is built on legacy SCSS, and blocks adoption of + modern CSS and tooling. ``react-bootstrap`` v1 targets Bootstrap 4 and is + effectively frozen for our purposes. +- The **build** relies on Babel + ``tsc`` + a hand-written ``Makefile``. +- **Tests** run on Jest with ``ts-jest`` / ``babel-jest``. +- The **documentation site** runs on Gatsby 5 with + ``gatsby-transformer-react-docgen`` and ``react-docgen`` v5, which parses + JavaScript rather than our TypeScript types. This makes props tables + unreliable and the doc pipeline difficult to maintain. + +Two properties of the current codebase make modernization tractable rather than +requiring a hard fork: + +- Paragon components are already **thin adapters**: Paragon owns the public prop + API and delegates internals to ``react-bootstrap``. Swapping the internal + engine does not, by itself, change the public API. +- The **theming/token pipeline is already Bootstrap-independent**. Components + reference token-derived CSS custom properties; the ``source: $scss-var`` field + on each token is only a back-compat bridge to Bootstrap SCSS variables. The + emitted ``core.css`` / ``light.css`` contract does not depend on Bootstrap. + +A TypeScript migration is also already ~half complete (roughly 31 ``index.tsx`` +vs. 27 ``index.jsx`` at time of writing). + +This ADR supersedes the direction of: + +- **ADR 0004** (Usage of Bootstrap) and **ADR 0009** (Usage of React-Bootstrap), + which established Bootstrap and ``react-bootstrap`` as foundations. + +It also revisits **ADR 0006** (Removal of CSS Module Support). ADR 0006 removed +CSS Modules for one specific reason: component SCSS files imported Bootstrap +*partials* (mixins/variables) that drifted between Bootstrap minor versions and +caused compile errors in consuming apps. Once Bootstrap SCSS is removed +entirely, that objection no longer applies — the CSS Modules reintroduced here +consume only Paragon's own token CSS custom properties, never Bootstrap +partials. + +Decision +-------- + +We will re-implement Paragon on modern, fully-typed foundations while preserving +both public contracts (the React component API and the design-tokens theming +API). The change is delivered **incrementally** — swapping engines behind stable +APIs — not as a big-bang rewrite. + +Concretely: + +1. **Behavior layer → React Aria Components.** Replace ``react-bootstrap`` with + `React Aria Components `_. It is + headless (leaving Paragon in full control of DOM and CSS), fully typed, and + provides best-in-class WAI-ARIA patterns plus built-in internationalization + and RTL support — a direct fit for Paragon's accessibility and ``react-intl`` + commitments. Each component's public props are preserved; only the internal + engine changes. This also retires ``uncontrollable``, ``tabbable``, and + ``react-focus-on``. + +2. **Style layer → CSS Modules driven by token CSS variables.** Remove Bootstrap + SCSS. Each component's styles become a co-located ``*.module.css`` file that + references the existing token-derived CSS custom properties. The small + Bootstrap surface actually in use (``reboot``/``root``, grid, utilities) is + re-implemented with modern native CSS (nesting, ``:where()`` for low + specificity, logical properties for RTL, container queries). Consumer usage + (``dependent-usage.json``) guides which utilities to retain. + +3. **Design-tokens theming API → preserved unchanged.** The ``style-dictionary`` + pipeline and the emitted ``core.css`` / ``light.css`` CSS-variable contract + stay byte-compatible so ``@edx/brand`` themes and consuming MFEs are + unaffected. Additionally, a new build target emits a typed ``tokens.ts`` so + tokens are first-class in TypeScript. The ``source: $scss-var`` back-compat + field is removed only after Bootstrap SCSS is gone. + +4. **Supporting dependencies:** + + - ``react-table`` v7 → **TanStack Table v8** (same author, fully typed). + - ``@popperjs`` / ``react-popper`` → **Floating UI** or React Aria overlays. + - ``axios`` → native ``fetch``. + - ``classnames`` → ``clsx`` (or retain). + - Retain: ``style-dictionary``, ``chroma-js``, ``react-intl``, + ``react-colorful``, ``react-dropzone``, ``react-imask``, + ``react-loading-skeleton``. + +5. **Build → Vite (library mode) + ``vite-plugin-dts``** (or ``tsup``), + replacing Babel + ``tsc`` + ``Makefile``. Emits ESM + ``.d.ts`` and preserves + ``sideEffects``/tree-shaking. + +6. **Tests → Vitest**, keeping ``@testing-library/react`` and the existing + accessibility assertions. + +7. **Documentation → Storybook 8 (Vite builder)**, retiring Gatsby, + ``gatsby-transformer-react-docgen``, ``react-docgen`` v5, ``react-live``, and + Playroom. Props tables are generated from TypeScript types + (``react-docgen-typescript``); existing per-component ``README.md`` / ``.mdx`` + map onto Storybook Docs pages; interactive controls replace Playroom; the a11y + addon and interaction/visual testing reinforce the accessibility mission. + +8. **Complete the TypeScript migration** so the library is 100% typed. + +Migration sequence +~~~~~~~~~~~~~~~~~~~ + +The work ships continuously behind stable public APIs: + +1. Switch build to Vite, tests to Vitest, finish remaining ``.jsx → .tsx``, and + emit a typed ``tokens.ts``. No consumer-visible change. +2. Replace Bootstrap SCSS imports with a token-built reset/grid/utility layer. + Snapshot the full set of emitted CSS custom properties to prove the theming + contract held. +3. Swap the behavior engine component-by-component onto React Aria (and DataTable + onto TanStack Table), leaf components first, each shipping independently. +4. Stand up Storybook 8 alongside Gatsby, port component pages, then retire + Gatsby. +5. Remove the ``source:`` SCSS back-compat from tokens and drop the legacy + dependencies (``bootstrap``, ``react-bootstrap``, ``axios``, + ``uncontrollable``, ``tabbable``, ``react-focus-on``, ``sass``). + +Consequences +------------ + +- **Public contracts are preserved.** Because components are adapters and the + token pipeline is Bootstrap-independent, the React component API and the CDN + ``core.css`` / ``light.css`` theming contract remain stable. ``@edx/brand`` + themes and consuming MFEs upgrade without breaking, likely across one or two + major versions rather than a fork. +- **Accessibility improves**, since React Aria's ARIA/keyboard/focus/i18n + behavior is more rigorous and better maintained than our Bootstrap 4 baseline. +- **The internal styling model changes** from global Bootstrap classes to + component-scoped CSS Modules over token variables. This reintroduces CSS + Modules (cf. ADR 0006) but without the Bootstrap-partial version-drift problem + that motivated their removal. +- **Consumers relying on raw Bootstrap 4 class names** (e.g. ``btn``, + ``col-*``, Bootstrap utility classes) rather than Paragon components or tokens + will be affected. Paragon will provide a compatibility utility layer for the + classes real consumers depend on and communicate removals via major-version + releases. +- **Guardrails required before removal steps:** snapshot the emitted CSS + custom-property set (theming regression net), keep per-component + ``@testing-library`` + a11y assertions as the public-API contract test, and use + ``dependent-usage.json`` to confirm which utility classes and props consumers + actually use before pruning. +- **Toolchain is unified** on Vite/Vitest/Storybook, reducing bespoke build code + (Makefile, Babel config, Gatsby plugins) and making the docs site maintainable. + +References +---------- + +* React Aria Components: https://react-spectrum.adobe.com/react-aria/ +* TanStack Table: https://tanstack.com/table/latest +* Floating UI: https://floating-ui.com/ +* Vite library mode: https://vitejs.dev/guide/build.html#library-mode +* Storybook: https://storybook.js.org/ +* ADR 0004 (Usage of Bootstrap), ADR 0006 (Removal of CSS Module Support), + ADR 0009 (Usage of React-Bootstrap), ADR 0018 (Design Tokens / + ``style-dictionary``), ADR 0019 (Scaling Styles with Design Tokens) From 664dc7c267bf4de7fb64cdae6580a2b0b40a5ac9 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 21 Jul 2026 15:33:58 -0700 Subject: [PATCH 02/24] feat: + ))} + + ), +}; + +export const OutlineAndInverse: Story = { + render: () => ( +
+
+ {BASE_VARIANTS.filter((v) => v !== 'link').map((v) => ( + + ))} +
+
+ {BASE_VARIANTS.filter((v) => v !== 'link').map((v) => ( + + ))} +
+
+ ), +}; + +export const Sizes: Story = { + render: () => ( +
+ + + + + Text with an + {' '} + + {' '} + button. + +
+ ), +}; + +export const WithIcons: Story = { + render: () => ( +
+ + + +
+ ), +}; + +export const Disabled: Story = { + args: { disabled: true }, +}; + +/** Demonstrates polymorphism: rendered as an anchor, still keyboard-accessible + * and correctly disabled via React Aria (no `pointer-events` hack needed). */ +export const AsAnchor: Story = { + render: () => ( +
+ + +
+ ), +}; + +export const Groups: Story = { + render: () => ( + + + + + + + + + + + + + ), +}; diff --git a/prototype/src/Button/Button.test.tsx b/prototype/src/Button/Button.test.tsx new file mode 100644 index 00000000000..ac6db43954d --- /dev/null +++ b/prototype/src/Button/Button.test.tsx @@ -0,0 +1,91 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, it, expect, vi } from 'vitest'; + +import { Button } from './Button'; +import { ButtonGroup } from './ButtonGroup'; +import { ButtonToolbar } from './ButtonToolbar'; + +describe('Button', () => { + it('renders a native button with its children', () => { + render(); + const btn = screen.getByRole('button', { name: 'Save' }); + expect(btn.tagName).toBe('BUTTON'); + }); + + it('applies the variant as a data attribute so themes/tests can target it', () => { + render(); + expect(screen.getByRole('button', { name: 'Delete' })).toHaveAttribute('data-variant', 'danger'); + }); + + it('forwards the legacy onClick handler', async () => { + const onClick = vi.fn(); + const user = userEvent.setup(); + render(); + await user.click(screen.getByRole('button', { name: 'Click' })); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('fires React Aria onPress for keyboard activation', async () => { + const onPress = vi.fn(); + const user = userEvent.setup(); + render(); + await user.tab(); + await user.keyboard('{Enter}'); + expect(onPress).toHaveBeenCalled(); + }); + + it('does not fire handlers when disabled', async () => { + const onClick = vi.fn(); + // Bypass user-event's pointer-events guard; the CSS disables pointer events, + // but we want to assert the handler itself never fires. + const user = userEvent.setup({ pointerEventsCheck: 0 }); + render(); + await user.click(screen.getByRole('button', { name: 'Nope' })); + expect(onClick).not.toHaveBeenCalled(); + }); + + it('renders as a custom element via `as` while staying an accessible button', () => { + render(); + const el = screen.getByRole('button', { name: 'Docs' }); + expect(el.tagName).toBe('A'); + expect(el).toHaveAttribute('href', 'https://openedx.org'); + }); + + it('marks a disabled anchor as aria-disabled (no native disabled on
)', () => { + render(); + expect(screen.getByText('Docs')).toHaveAttribute('aria-disabled', 'true'); + }); + + it('forwards a ref to the underlying element', () => { + const ref = { current: null as HTMLElement | null }; + render(); + expect(ref.current).toBeInstanceOf(HTMLButtonElement); + }); +}); + +describe('ButtonGroup / ButtonToolbar', () => { + it('exposes group and toolbar roles', () => { + render( + + + + + + , + ); + expect(screen.getByRole('toolbar', { name: 'tools' })).toBeInTheDocument(); + expect(screen.getByRole('group', { name: 'group' })).toBeInTheDocument(); + }); + + it('propagates its size to child buttons via context', () => { + render( + + + , + ); + // The size class is applied from context; assert the button rendered. + // (Class hashing is environment-specific, so we assert presence + role.) + expect(screen.getByRole('button', { name: 'Small via group' })).toBeInTheDocument(); + }); +}); diff --git a/prototype/src/Button/Button.tsx b/prototype/src/Button/Button.tsx new file mode 100644 index 00000000000..bfec1810559 --- /dev/null +++ b/prototype/src/Button/Button.tsx @@ -0,0 +1,131 @@ +import React, { useContext } from 'react'; +import { + useButton, useFocusRing, useObjectRef, mergeProps, +} from 'react-aria'; +import clsx from 'clsx'; + +import type { ButtonProps, ButtonVariant } from './types'; +import { ButtonGroupContext } from './ButtonGroupContext'; +import styles from './Button.module.css'; + +/** + * Maps the generic `--pgn-btn-*` custom properties consumed by the base `.btn` + * rule to the variant-specific token variables. This is a 1:1 port of the SCSS + * `button-variant` mixin — the mixin only ever re-pointed these variables, so + * doing it inline keeps the styling token-driven while avoiding 44 near-identical + * CSS blocks. (A static-class alternative is noted in the prototype README.) + */ +function variantVars(variant: ButtonVariant): React.CSSProperties { + const v = variant; + return { + '--pgn-btn-color': `var(--pgn-color-btn-text-${v})`, + '--pgn-btn-bg': `var(--pgn-color-btn-bg-${v})`, + '--pgn-btn-border-color': `var(--pgn-color-btn-border-${v})`, + '--pgn-btn-hover-color': `var(--pgn-color-btn-hover-text-${v})`, + '--pgn-btn-hover-bg': `var(--pgn-color-btn-hover-bg-${v})`, + '--pgn-btn-hover-border-color': `var(--pgn-color-btn-hover-border-${v})`, + '--pgn-btn-disabled-color': `var(--pgn-color-btn-disabled-text-${v})`, + '--pgn-btn-disabled-bg': `var(--pgn-color-btn-disabled-bg-${v})`, + '--pgn-btn-disabled-border-color': `var(--pgn-color-btn-disabled-border-${v})`, + '--pgn-btn-active-color': `var(--pgn-color-btn-active-text-${v})`, + '--pgn-btn-active-bg': `var(--pgn-color-btn-active-bg-${v})`, + '--pgn-btn-active-border-color': `var(--pgn-color-btn-active-border-${v})`, + '--pgn-btn-focus-outline-color': `var(--pgn-color-btn-focus-outline-${v})`, + '--pgn-btn-focus-color': `var(--pgn-color-btn-focus-text-${v})`, + '--pgn-btn-focus-border-color': `var(--pgn-color-btn-focus-border-${v})`, + '--pgn-btn-focus-bg': `var(--pgn-color-btn-focus-bg-${v})`, + } as React.CSSProperties; +} + +const SIZE_CLASS = { + sm: styles.sm, + md: undefined, + lg: styles.lg, + inline: styles.inline, +} as const; + +/** + * Accessible, themeable Button. + * + * Behaviour comes from React Aria's `useButton` (cross-device press handling, + * correct `disabled` semantics even when rendered as an `` via `as`, and + * focus-ring state) rather than Bootstrap. The public prop API is unchanged + * from `@openedx/paragon`'s Button, and all styling is driven by the existing + * `--pgn-*` design tokens. + */ +export const Button = React.forwardRef(( + { + as, + variant = 'primary', + size, + iconBefore: IconBefore, + iconAfter: IconAfter, + disabled = false, + block = false, + className, + children, + onClick, + onPress, + type = 'button', + ...rest + }, + forwardedRef, +) => { + const ref = useObjectRef(forwardedRef as React.ForwardedRef); + const ElementType: React.ElementType = as ?? 'button'; + const groupSize = useContext(ButtonGroupContext); + const resolvedSize = size ?? groupSize ?? 'md'; + + const { buttonProps, isPressed } = useButton( + { + elementType: ElementType, + isDisabled: disabled, + onPress, + type, + 'aria-label': rest['aria-label'], + }, + ref, + ); + const { isFocusVisible, focusProps } = useFocusRing(); + + const isLink = variant === 'link'; + + return ( + + {IconBefore && ( + + + + )} + {children} + {IconAfter && ( + + + + )} + + ); +}); + +Button.displayName = 'Button'; + +export default Button; diff --git a/prototype/src/Button/ButtonGroup.tsx b/prototype/src/Button/ButtonGroup.tsx new file mode 100644 index 00000000000..9a1c24c577c --- /dev/null +++ b/prototype/src/Button/ButtonGroup.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import clsx from 'clsx'; + +import type { ButtonSize } from './types'; +import { ButtonGroupContext } from './ButtonGroupContext'; +import styles from './Button.module.css'; + +export interface ButtonGroupProps { + /** Specifies element type for this component (default: `div`). */ + as?: React.ElementType; + /** An ARIA role describing the button group (default: `group`). */ + role?: React.AriaRole; + /** Sets the size for all Buttons in the group (default: `md`). */ + size?: ButtonSize; + /** Stack the Buttons vertically (default: `false`). */ + vertical?: boolean; + className?: string; + children: React.ReactNode; + 'aria-label'?: string; +} + +/** + * Groups related Buttons and propagates a shared `size` to them via context, + * replacing Bootstrap's `.btn-group-*` global-class mechanism. + */ +export const ButtonGroup = React.forwardRef(( + { + as, + role = 'group', + size = 'md', + vertical = false, + className, + children, + ...rest + }, + ref, +) => { + const ElementType: React.ElementType = as ?? 'div'; + return ( + + + {children} + + + ); +}); + +ButtonGroup.displayName = 'ButtonGroup'; + +export default ButtonGroup; diff --git a/prototype/src/Button/ButtonGroupContext.ts b/prototype/src/Button/ButtonGroupContext.ts new file mode 100644 index 00000000000..859d78b2b24 --- /dev/null +++ b/prototype/src/Button/ButtonGroupContext.ts @@ -0,0 +1,9 @@ +import { createContext } from 'react'; +import type { ButtonSize } from './types'; + +/** + * Lets a `ButtonGroup` set the size of all descendant `Button`s, replicating + * Bootstrap's `.btn-group-sm` / `.btn-group-lg` behaviour without global + * class-name coupling. `undefined` means "no group override". + */ +export const ButtonGroupContext = createContext(undefined); diff --git a/prototype/src/Button/ButtonToolbar.tsx b/prototype/src/Button/ButtonToolbar.tsx new file mode 100644 index 00000000000..5a6329872b7 --- /dev/null +++ b/prototype/src/Button/ButtonToolbar.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import clsx from 'clsx'; + +import styles from './Button.module.css'; + +export interface ButtonToolbarProps { + /** Specifies element type for this component (default: `div`). */ + as?: React.ElementType; + /** An ARIA role describing the toolbar (default: `toolbar`). */ + role?: React.AriaRole; + className?: string; + children: React.ReactNode; + 'aria-label'?: string; +} + +/** A flex container that lays out one or more ButtonGroups. */ +export const ButtonToolbar = React.forwardRef(( + { + as, role = 'toolbar', className, children, ...rest + }, + ref, +) => { + const ElementType: React.ElementType = as ?? 'div'; + return ( + + {children} + + ); +}); + +ButtonToolbar.displayName = 'ButtonToolbar'; + +export default ButtonToolbar; diff --git a/prototype/src/Button/index.ts b/prototype/src/Button/index.ts new file mode 100644 index 00000000000..8ddda877fed --- /dev/null +++ b/prototype/src/Button/index.ts @@ -0,0 +1,6 @@ +export { default, Button } from './Button'; +export { ButtonGroup } from './ButtonGroup'; +export { ButtonToolbar } from './ButtonToolbar'; +export type { ButtonProps, ButtonVariant, ButtonSize, BaseVariant } from './types'; +export type { ButtonGroupProps } from './ButtonGroup'; +export type { ButtonToolbarProps } from './ButtonToolbar'; diff --git a/prototype/src/Button/types.ts b/prototype/src/Button/types.ts new file mode 100644 index 00000000000..d771018b024 --- /dev/null +++ b/prototype/src/Button/types.ts @@ -0,0 +1,83 @@ +import type React from 'react'; +import type { PressEvent } from 'react-aria'; + +/** + * The eleven base button variants Paragon ships. Preserved exactly from the + * current `@openedx/paragon` Button API. + */ +export type BaseVariant = ( + | 'primary' + | 'secondary' + | 'tertiary' + | 'brand' + | 'success' + | 'danger' + | 'warning' + | 'info' + | 'dark' + | 'light' + | 'link' +); + +/** + * Every valid `variant` value: each base variant plus its `outline-`, + * `inverse-` and `inverse-outline-` forms. This is the same union the current + * TypeScript Button exposes, so consumers see no change. + */ +export type ButtonVariant = + | BaseVariant + | `inverse-${BaseVariant}` + | `outline-${BaseVariant}` + | `inverse-outline-${BaseVariant}`; + +export type ButtonSize = 'sm' | 'md' | 'lg' | 'inline'; + +export interface ButtonProps { + /** Set a custom element for this component (default: `button`, with `type="button"`). */ + as?: React.ElementType; + /** Specifies variant to use (default: `primary`). */ + variant?: ButtonVariant; + /** Button size. Note `md` and `inline` are Paragon extensions over Bootstrap. */ + size?: ButtonSize; + /** + * An icon component to render before the label. Example: + * ``` + * import { Close } from '@openedx/paragon/icons'; + * + * ``` + */ + iconBefore?: React.ComponentType; + /** An icon component to render after the label. */ + iconAfter?: React.ComponentType; + /** Disables the Button, even when rendered as a non-` - - - - -`} /> +Related buttons can be combined with `ButtonGroup` and `ButtonToolbar` — see the +[ButtonGroup](?path=/docs/buttonlike-buttongroup--docs) page. ## Accessibility notes diff --git a/prototype/src/Button/Button.stories.tsx b/prototype/src/Button/Button.stories.tsx index 3ab0459d774..c6e65114336 100644 --- a/prototype/src/Button/Button.stories.tsx +++ b/prototype/src/Button/Button.stories.tsx @@ -21,8 +21,14 @@ function DotIcon() { } const meta: Meta = { - title: 'Prototype/Button', + // Kept under a separate, fully-hidden root so it does NOT share a title with + // the `Buttonlike/Button` docs page — that keeps the MDX an unattached, single + // sidebar leaf (no "Docs" sub-node). Stories are the raw material for that + // page, hidden from the sidebar via `!dev` but still referenceable through + // `` / ``. + title: 'Internal/Button', component: Button, + tags: ['!dev'], parameters: { layout: 'padded' }, args: { children: 'Button', diff --git a/prototype/src/Button/ButtonGroup.mdx b/prototype/src/Button/ButtonGroup.mdx new file mode 100644 index 00000000000..80c368ec68e --- /dev/null +++ b/prototype/src/Button/ButtonGroup.mdx @@ -0,0 +1,87 @@ +import { Meta, Title, Subtitle, Story, Controls } from '@storybook/addon-docs/blocks'; + +import * as ButtonGroupStories from './ButtonGroup.stories'; +import { LiveExample } from '../docs/LiveExample'; + + + +Button Group + +Group related buttons so they read as a single control; ButtonToolbar arranges multiple groups. + +`ButtonGroup` visually and semantically groups related `Button`s (ARIA +`role="group"`). It replaces Bootstrap's `.btn-group-*` global classes: a group +propagates its `size` to child buttons via React context, and inner border radii +are collapsed with logical properties so the group works in both LTR and RTL. + +`ButtonToolbar` (ARIA `role="toolbar"`) lays out one or more groups. + +## Basic usage + +```tsx +import { Button, ButtonGroup } from '@openedx/paragon'; + + + + + + +``` + + + +## Props + + + +## Sizes + +A group sets the size of every button it contains — set it once on the group +rather than on each `Button`. + + + + + + + + + + + +`} /> + +## Vertical + + + + + + +`} /> + +## ButtonToolbar + +Compose several groups inside a `ButtonToolbar`. + + + + + + + + + + + + +`} /> + +## Accessibility notes + +- Always provide an `aria-label` on each `ButtonGroup` and `ButtonToolbar`. +- Use `ButtonToolbar` when you have more than one group, so assistive tech + announces the toolbar and its constituent groups correctly. diff --git a/prototype/src/Button/ButtonGroup.stories.tsx b/prototype/src/Button/ButtonGroup.stories.tsx new file mode 100644 index 00000000000..6a8488d3649 --- /dev/null +++ b/prototype/src/Button/ButtonGroup.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { Button } from './Button'; +import { ButtonGroup } from './ButtonGroup'; + +const meta: Meta = { + // Separate hidden root — see the note in Button.stories.tsx. + title: 'Internal/ButtonGroup', + component: ButtonGroup, + tags: ['!dev'], + parameters: { layout: 'padded' }, + args: { size: 'md', vertical: false }, + argTypes: { + size: { control: 'inline-radio', options: ['sm', 'md', 'lg', 'inline'] }, + vertical: { control: 'boolean' }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ( + + + + + + ), +}; From 1806ca59184de8afcf08ece9d985a3d0345ee345 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 21 Jul 2026 17:15:19 -0700 Subject: [PATCH 06/24] docs: fix fonts - don't inherit storybook styling onto our components --- prototype/.storybook/preview.tsx | 3 +++ prototype/src/base.css | 18 ++++++++++++++++++ prototype/src/docs/LiveExample.tsx | 5 ++++- prototype/src/docs/LivePlayground.tsx | 3 ++- prototype/src/docs/live.module.css | 5 +++++ prototype/src/index.ts | 1 + 6 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 prototype/src/base.css diff --git a/prototype/.storybook/preview.tsx b/prototype/.storybook/preview.tsx index c2911a8d537..6f0264871bb 100644 --- a/prototype/.storybook/preview.tsx +++ b/prototype/.storybook/preview.tsx @@ -4,6 +4,9 @@ import type { Preview } from '@storybook/react-vite'; // app would. This is the *same* compiled token output the real library ships — // proof that the CSS Modules approach preserves the theming contract. import '../src/tokens.css'; +// The reboot-replacement base layer, so story/docs frames inherit Paragon's +// root typography just like a consuming app would. +import '../src/base.css'; const preview: Preview = { parameters: { diff --git a/prototype/src/base.css b/prototype/src/base.css new file mode 100644 index 00000000000..1e948a3a3c7 --- /dev/null +++ b/prototype/src/base.css @@ -0,0 +1,18 @@ +/* + * Foundational base layer — the modern replacement for Bootstrap's `reboot`. + * + * Paragon's button (and form) font tokens resolve to `inherit`, on the + * assumption that a base layer establishes the root typography that components + * inherit. Bootstrap's reboot used to do this; here we set it explicitly from + * the same `--pgn-*` design tokens. Without this, buttons fall back to the + * user-agent serif default. + * + * This is imported both by the library entry (so consumers get it) and by the + * Storybook preview (so docs/story frames render like a real consuming app). + */ +:where(html) { + font-family: var(--pgn-typography-font-family-base); + font-size: var(--pgn-typography-font-size-base); + line-height: var(--pgn-typography-line-height-base); + color: var(--pgn-color-body-base); +} diff --git a/prototype/src/docs/LiveExample.tsx b/prototype/src/docs/LiveExample.tsx index dadc3366484..9d9202c9b6f 100644 --- a/prototype/src/docs/LiveExample.tsx +++ b/prototype/src/docs/LiveExample.tsx @@ -19,7 +19,10 @@ export interface LiveExampleProps { */ export function LiveExample({ code }: LiveExampleProps) { return ( -
+ // `sb-unstyled` opts the preview out of Storybook's docs typography reset + // (which otherwise forces "Nunito Sans" onto every div in the docs prose), + // so components render in the Paragon base font like a real consuming app. +
Editable — try changing the code
diff --git a/prototype/src/docs/LivePlayground.tsx b/prototype/src/docs/LivePlayground.tsx index 25d148eee39..4506bc32480 100644 --- a/prototype/src/docs/LivePlayground.tsx +++ b/prototype/src/docs/LivePlayground.tsx @@ -31,7 +31,8 @@ export function generateCode(args: Partial): string { export function LivePlayground(args: Partial) { const code = generateCode(args); return ( -
+ // `sb-unstyled`: opt out of Storybook's docs font reset (see LiveExample). +
{/* key={code}: a new generated string (from a changed control) forces a remount so the editor content is replaced with the regenerated JSX. */} diff --git a/prototype/src/docs/live.module.css b/prototype/src/docs/live.module.css index 3daa7f9f2b9..93668aa000e 100644 --- a/prototype/src/docs/live.module.css +++ b/prototype/src/docs/live.module.css @@ -14,6 +14,11 @@ flex-wrap: wrap; gap: .5rem; align-items: center; + /* Anchor the Paragon base typography so previews render like a consuming app, + independent of Storybook's own docs font (which would otherwise be inherited + by inline examples). */ + font-family: var(--pgn-typography-font-family-base); + color: var(--pgn-color-body-base); } .editor { diff --git a/prototype/src/index.ts b/prototype/src/index.ts index 31c5c565753..60ef7478763 100644 --- a/prototype/src/index.ts +++ b/prototype/src/index.ts @@ -1,4 +1,5 @@ import './tokens.css'; +import './base.css'; export { Button, ButtonGroup, ButtonToolbar, From 3a6092f8f03a15bde365bfd4efdbf66fdc5e98b5 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 21 Jul 2026 17:31:48 -0700 Subject: [PATCH 07/24] docs: re-organize - - - - - - - -
+ + + + + + + `} /> -## Sizes +## Core Buttons (Inverse Pallete) -`sm`, `md` (default), `lg`, and Paragon's `inline` size for buttons that sit -within a line of text. + + + + + + + +`} /> + +## Utility Buttons + + + + + + + + + + + + + + + + +`} /> + +## Size + + + + + + + + + + + + + + + +`} /> + +### When to use the inline size + +Use inline size buttons for when a button sits with a line of text. - - - -
+

+ 2 items selected. + + +

`} /> -## Icons +## Block Buttons + + + + + +`} /> -Pass any icon component to \`iconBefore\` and/or \`iconAfter\`. +### Disabled - - - -
+ + + + + `} /> -## Polymorphism (`as`) +### With empty href -Render as any element while keeping accessible button behavior. A disabled -anchor is handled via `aria-disabled` rather than the legacy -`pointer-events: none` hack. +For link to be `disabled`, it must have href defined with some value. - - -
+ + + + `} /> +### With Icons before or after + + + + + + + + +`} /> + +## Stateful buttons + +To implement a loading state, Paragon provides the `StatefulButton` component +(not part of this prototype). It manages and displays loading/complete states +for a more efficient, user-friendly experience. + +{/* --- Additions beyond the original README --- */} + ## Grouping Related buttons can be combined with `ButtonGroup` and `ButtonToolbar` — see the diff --git a/prototype/src/docs/scope.tsx b/prototype/src/docs/scope.tsx index def2fbe0c09..56707801b5e 100644 --- a/prototype/src/docs/scope.tsx +++ b/prototype/src/docs/scope.tsx @@ -1,3 +1,5 @@ +import type React from 'react'; + import { Button, ButtonGroup, ButtonToolbar } from '../Button'; /** A tiny inline icon so live examples can demonstrate icon slots dependency-free. */ @@ -9,6 +11,59 @@ export function DotIcon() { ); } +// Minimal stand-ins for Paragon icons so the mirrored README examples read the +// same (`iconBefore={ArrowBack}` etc.). Sized in `em` so they scale with the button. +const icon = (path: string) => function Icon() { + return ( + + + + ); +}; + +export const Add = icon('M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'); +export const Remove = icon('M19 13H5v-2h14v2z'); +export const ArrowBack = icon('M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z'); +export const ArrowDropDown = icon('M7 10l5 5 5-5z'); +export const Highlight = icon('M6 14l3 3v5h6v-5l3-3V9H6v5zM11 2h2v3h-2V2zM3.5 5.87l1.41-1.41 2.12 2.12L5.62 8 3.5 5.87zM16.96 6.58l2.12-2.12 1.41 1.41L18.38 8l-1.42-1.42z'); + +const SPACERS = [0, 0.25, 0.5, 1, 1.5, 3]; // Bootstrap/Paragon spacer scale (rem) + +export interface StackProps { + children: React.ReactNode; + /** Spacing between items, on the Paragon spacer scale (0–5). */ + gap?: number; + /** Layout direction (default: `vertical`). */ + direction?: 'horizontal' | 'vertical'; + className?: string; + style?: React.CSSProperties; +} + +/** + * A minimal stand-in for Paragon's `Stack` so the mirrored README examples keep + * their original markup. Wraps on small screens instead of the original's + * `useMediaQuery` direction switch. + */ +export function Stack({ + children, gap = 0, direction = 'vertical', className, style, +}: StackProps) { + return ( +
+ {children} +
+ ); +} + /** * Everything a react-live snippet may reference must be in `scope` — react-live * evaluates a single JSX expression with no `import` statements. This mirrors how @@ -18,5 +73,11 @@ export const scope = { Button, ButtonGroup, ButtonToolbar, + Stack, DotIcon, + Add, + Remove, + ArrowBack, + ArrowDropDown, + Highlight, }; From 0500ce01ec57aa9c437d5922741348a97d71973d Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 21 Jul 2026 18:03:40 -0700 Subject: [PATCH 08/24] feat: prototype --- prototype/src/Stack/Stack.mdx | 85 +++++++++++++++++++++++++++ prototype/src/Stack/Stack.module.css | 35 +++++++++++ prototype/src/Stack/Stack.stories.tsx | 38 ++++++++++++ prototype/src/Stack/Stack.test.tsx | 56 ++++++++++++++++++ prototype/src/Stack/Stack.tsx | 68 +++++++++++++++++++++ prototype/src/Stack/index.ts | 2 + prototype/src/docs/live.module.css | 9 +-- prototype/src/docs/scope.tsx | 40 +------------ prototype/src/index.ts | 2 + 9 files changed, 292 insertions(+), 43 deletions(-) create mode 100644 prototype/src/Stack/Stack.mdx create mode 100644 prototype/src/Stack/Stack.module.css create mode 100644 prototype/src/Stack/Stack.stories.tsx create mode 100644 prototype/src/Stack/Stack.test.tsx create mode 100644 prototype/src/Stack/Stack.tsx create mode 100644 prototype/src/Stack/index.ts diff --git a/prototype/src/Stack/Stack.mdx b/prototype/src/Stack/Stack.mdx new file mode 100644 index 00000000000..f0b30ef4d77 --- /dev/null +++ b/prototype/src/Stack/Stack.mdx @@ -0,0 +1,85 @@ +import { Meta, Title, Subtitle, Story, Controls } from '@storybook/addon-docs/blocks'; + +import * as StackStories from './Stack.stories'; +import { LiveExample } from '../docs/LiveExample'; + + + +Stack + +Flexbox layout helper — space children vertically or horizontally using Paragon's spacer tokens. + +Shorthand helpers that build on top of our flexbox utilities to make component +layout faster and easier than ever. Similar to the +[Bootstrap Stack](https://react-bootstrap.github.io/docs/layout/stack/) component. + +`Stack` is the prototype re-implementation of Paragon's `Stack` on the modern +stack proposed in [ADR 0022](https://github.com/openedx/paragon). Its public +prop API (`direction`, `gap`, `reversed`) is unchanged from `@openedx/paragon`; +internally the global `pgn__vstack` / `pgn__hstack` classes become locally-scoped +CSS Module classes, and the `gap` is resolved to the matching +`--pgn-spacing-spacer-*` design token, so spacing stays token-driven and +themeable. + +## Playground + + + + + +{/* + The sections below mirror the existing Paragon docs (src/Stack/README.md) + 1:1 and in the same order, so the two docs are easy to compare. The JSX is + kept close to the original; the README's `className="border p-2"` boxes are + expressed with inline styles that read the `--pgn-color-border` and + `--pgn-spacing-spacer-*` tokens, since the prototype does not ship Bootstrap's + border/padding utility classes. +*/} + +## Basic Usage + +## Vertical direction + +Stacks are vertical by default and stacked items are full-width by default. Watch +this pull request to see more details about the +[auto stretching behavior](https://github.com/openedx/paragon/pull/1188). + + + + + + +`} /> + +## Horizontal direction + + +
first block
+
second block
+
third block
+ +`} /> + +## Reversed props + +**Vertical** with the `reversed` prop: + + + + + + +`} /> + +**Horizontal** with the `reversed` prop: + + +
first block
+
second block
+
third block
+ +`} /> diff --git a/prototype/src/Stack/Stack.module.css b/prototype/src/Stack/Stack.module.css new file mode 100644 index 00000000000..49280ee24f5 --- /dev/null +++ b/prototype/src/Stack/Stack.module.css @@ -0,0 +1,35 @@ +/* + * Stack styles — a CSS Modules port of src/Stack/index.scss. + * + * Faithful to the original: vertical stacks stretch and flex-grow; horizontal + * stacks center their items. `reversed` flips the flow direction. The gap is + * still driven entirely by the existing spacer design tokens — the component + * sets `--pgn-stack-gap` to the matching `--pgn-spacing-spacer-*` token (see + * Stack.tsx), so nothing is hard-coded and theming is unaffected. + */ + +.vstack, +.hstack { + display: flex; + align-self: stretch; + gap: var(--pgn-stack-gap, var(--pgn-size-stack-gap)); +} + +.vstack { + flex: 1 1 auto; + flex-direction: column; +} + +.vstack.reversed { + flex-direction: column-reverse; +} + +.hstack { + flex-direction: row; + align-items: center; +} + +.hstack.reversed { + flex-direction: row-reverse; + justify-content: flex-end; +} diff --git a/prototype/src/Stack/Stack.stories.tsx b/prototype/src/Stack/Stack.stories.tsx new file mode 100644 index 00000000000..616b3018c13 --- /dev/null +++ b/prototype/src/Stack/Stack.stories.tsx @@ -0,0 +1,38 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { Stack } from './Stack'; +import { Button } from '../Button'; + +const meta: Meta = { + // Hidden `Internal/` root + `!dev` so the sidebar only shows the standalone + // "Layout/Stack" MDX docs page (same pattern as Button.stories.tsx). + title: 'Internal/Stack', + component: Stack, + tags: ['!dev'], + parameters: { layout: 'padded' }, + args: { + direction: 'vertical', gap: 3, reversed: false, + }, + argTypes: { + direction: { control: 'inline-radio', options: ['vertical', 'horizontal'] }, + gap: { + control: { + type: 'number', min: 0, max: 6, step: 0.5, + }, + }, + reversed: { control: 'boolean' }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Playground: Story = { + render: (args) => ( + + + + + + ), +}; diff --git a/prototype/src/Stack/Stack.test.tsx b/prototype/src/Stack/Stack.test.tsx new file mode 100644 index 00000000000..0af81ac9305 --- /dev/null +++ b/prototype/src/Stack/Stack.test.tsx @@ -0,0 +1,56 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; + +import { Stack } from './Stack'; + +describe('Stack', () => { + it('renders a vertical stack by default', () => { + render(Content); + const stack = screen.getByTestId('stack'); + expect(stack.tagName).toBe('DIV'); + expect(stack.className).toMatch(/vstack/); + }); + + it('renders a horizontal stack when direction is horizontal', () => { + render(Content); + expect(screen.getByTestId('stack').className).toMatch(/hstack/); + }); + + it('resolves the gap to the matching spacer token', () => { + render(Content); + // Gap is token-driven: the component sets --pgn-stack-gap to the spacer var. + expect(screen.getByTestId('stack').style.getPropertyValue('--pgn-stack-gap')) + .toBe('var(--pgn-spacing-spacer-3)'); + }); + + it('maps half-step gaps onto the spacer scale (1.5 -> 1-5)', () => { + render(Content); + expect(screen.getByTestId('stack').style.getPropertyValue('--pgn-stack-gap')) + .toBe('var(--pgn-spacing-spacer-1-5)'); + }); + + it('applies the reversed modifier', () => { + render(Content); + expect(screen.getByTestId('stack').className).toMatch(/reversed/); + }); + + it('forwards className and DOM attributes', () => { + render(Content); + const stack = screen.getByTestId('stack'); + expect(stack.className).toContain('extra'); + expect(stack).toHaveAttribute('aria-label', 'things'); + }); + + it('merges a caller-supplied style with the gap variable', () => { + render(Content); + const stack = screen.getByTestId('stack'); + expect(stack.style.marginTop).toBe('1rem'); + expect(stack.style.getPropertyValue('--pgn-stack-gap')).toBe('var(--pgn-spacing-spacer-2)'); + }); + + it('forwards a ref to the underlying element', () => { + const ref = { current: null as HTMLDivElement | null }; + render(Content); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); +}); diff --git a/prototype/src/Stack/Stack.tsx b/prototype/src/Stack/Stack.tsx new file mode 100644 index 00000000000..4aef3703ed1 --- /dev/null +++ b/prototype/src/Stack/Stack.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import clsx from 'clsx'; + +import styles from './Stack.module.css'; + +export interface StackProps extends React.HTMLAttributes { + /** Specifies the content of the `Stack`. */ + children: React.ReactNode; + /** Specifies direction of the children blocks (default: `vertical`). */ + direction?: 'horizontal' | 'vertical'; + /** + * Inner space between children, on the Paragon spacer scale. + * + * Valid values match the `--pgn-spacing-spacer-*` tokens: + * `0, 1, 2, 3, 4, 5, 6` and the half-steps `1.5, 2.5, 3.5, 4.5, 5.5`. + */ + gap?: number; + /** Reverse the order of the children (default: `false`). */ + reversed?: boolean; + /** Specifies an additional `className` to add to the base element. */ + className?: string; +} + +/** + * Flexbox layout helper. A vertical or horizontal `Stack` that spaces its + * children using Paragon's spacer design tokens. + * + * This is a 1:1 port of `@openedx/paragon`'s `Stack`: the public API + * (`direction`, `gap`, `reversed`, `className`, DOM passthrough) is unchanged. + * The only internal difference is that the global `pgn__*stack` classes become + * locally-scoped CSS Module classes, and the gap is resolved to the matching + * `--pgn-spacing-spacer-*` token via the `--pgn-stack-gap` custom property + * rather than the SCSS `@each` loop. + */ +export const Stack = React.forwardRef(( + { + direction = 'vertical', + gap = 0, + reversed = false, + children, + className, + style, + ...rest + }, + ref, +) => ( +
`1-5`), + // keeping spacing token-driven and themeable. + '--pgn-stack-gap': `var(--pgn-spacing-spacer-${String(gap).replace('.', '-')})`, + ...style, + } as React.CSSProperties} + {...rest} + > + {children} +
+)); + +Stack.displayName = 'Stack'; + +export default Stack; diff --git a/prototype/src/Stack/index.ts b/prototype/src/Stack/index.ts new file mode 100644 index 00000000000..50ad3676f96 --- /dev/null +++ b/prototype/src/Stack/index.ts @@ -0,0 +1,2 @@ +export { default, Stack } from './Stack'; +export type { StackProps } from './Stack'; diff --git a/prototype/src/docs/live.module.css b/prototype/src/docs/live.module.css index 93668aa000e..3619a610014 100644 --- a/prototype/src/docs/live.module.css +++ b/prototype/src/docs/live.module.css @@ -10,10 +10,11 @@ .preview { padding: 1.5rem; background: #fff; - display: flex; - flex-wrap: wrap; - gap: .5rem; - align-items: center; + /* A plain block formatting context (not a flex row): each example lays out its + own children — usually via — so block-level/flex-container siblings + (e.g. multiple horizontal Stacks, or `block` buttons) stack vertically and + fill the width, matching how the current Paragon docs render them. */ + display: flow-root; /* Anchor the Paragon base typography so previews render like a consuming app, independent of Storybook's own docs font (which would otherwise be inherited by inline examples). */ diff --git a/prototype/src/docs/scope.tsx b/prototype/src/docs/scope.tsx index 56707801b5e..4ee7684d1ee 100644 --- a/prototype/src/docs/scope.tsx +++ b/prototype/src/docs/scope.tsx @@ -1,6 +1,5 @@ -import type React from 'react'; - import { Button, ButtonGroup, ButtonToolbar } from '../Button'; +import { Stack } from '../Stack'; /** A tiny inline icon so live examples can demonstrate icon slots dependency-free. */ export function DotIcon() { @@ -27,43 +26,6 @@ export const ArrowBack = icon('M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.8 export const ArrowDropDown = icon('M7 10l5 5 5-5z'); export const Highlight = icon('M6 14l3 3v5h6v-5l3-3V9H6v5zM11 2h2v3h-2V2zM3.5 5.87l1.41-1.41 2.12 2.12L5.62 8 3.5 5.87zM16.96 6.58l2.12-2.12 1.41 1.41L18.38 8l-1.42-1.42z'); -const SPACERS = [0, 0.25, 0.5, 1, 1.5, 3]; // Bootstrap/Paragon spacer scale (rem) - -export interface StackProps { - children: React.ReactNode; - /** Spacing between items, on the Paragon spacer scale (0–5). */ - gap?: number; - /** Layout direction (default: `vertical`). */ - direction?: 'horizontal' | 'vertical'; - className?: string; - style?: React.CSSProperties; -} - -/** - * A minimal stand-in for Paragon's `Stack` so the mirrored README examples keep - * their original markup. Wraps on small screens instead of the original's - * `useMediaQuery` direction switch. - */ -export function Stack({ - children, gap = 0, direction = 'vertical', className, style, -}: StackProps) { - return ( -
- {children} -
- ); -} - /** * Everything a react-live snippet may reference must be in `scope` — react-live * evaluates a single JSX expression with no `import` statements. This mirrors how diff --git a/prototype/src/index.ts b/prototype/src/index.ts index 60ef7478763..f47fd164346 100644 --- a/prototype/src/index.ts +++ b/prototype/src/index.ts @@ -7,3 +7,5 @@ export { export type { ButtonProps, ButtonGroupProps, ButtonToolbarProps, ButtonVariant, ButtonSize, BaseVariant, } from './Button'; +export { Stack } from './Stack'; +export type { StackProps } from './Stack'; From 1e0f2ef86f789ceb4f0ecec9e6ce66cbcb40981d Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 21 Jul 2026 18:18:01 -0700 Subject: [PATCH 09/24] fix: font sizes, demo background colors --- prototype/src/base.css | 11 ++++++++++- prototype/src/docs/live.module.css | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/prototype/src/base.css b/prototype/src/base.css index 1e948a3a3c7..50558c2efde 100644 --- a/prototype/src/base.css +++ b/prototype/src/base.css @@ -10,7 +10,16 @@ * This is imported both by the library entry (so consumers get it) and by the * Storybook preview (so docs/story frames render like a real consuming app). */ -:where(html) { +/* + * Apply the base typography to `body`, NOT `html` — exactly as Bootstrap's + * reboot does. `--pgn-typography-font-size-base` is `1.125rem`, so setting it on + * the root (`html`) would inflate the `rem` reference from 16px to 18px and make + * every `rem`-based component token compound (e.g. the button's own `1.125rem` + * font-size would render at 20.25px instead of 18px). Keeping `html` at the + * browser default preserves `rem` = 16px, so `1.125rem` resolves to 18px + * everywhere, matching the current library. + */ +:where(body) { font-family: var(--pgn-typography-font-family-base); font-size: var(--pgn-typography-font-size-base); line-height: var(--pgn-typography-line-height-base); diff --git a/prototype/src/docs/live.module.css b/prototype/src/docs/live.module.css index 3619a610014..16dbf6d2ec4 100644 --- a/prototype/src/docs/live.module.css +++ b/prototype/src/docs/live.module.css @@ -9,7 +9,7 @@ .preview { padding: 1.5rem; - background: #fff; + background-color: var(--pgn-color-light-200); /* A plain block formatting context (not a flex row): each example lays out its own children — usually via — so block-level/flex-container siblings (e.g. multiple horizontal Stacks, or `block` buttons) stack vertically and From 760cc8eb42eb9cdb12a39ae334a02aebfe23513e Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 21 Jul 2026 18:27:54 -0700 Subject: [PATCH 10/24] temp: deploy storybook to Netlify --- Makefile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 741bc7bfbb8..765305b6111 100644 --- a/Makefile +++ b/Makefile @@ -33,9 +33,15 @@ requirements: ## install ci requirements # Instead of having this directly in the build-docs script # in the top-level package.json, put it here so we can have # a single source of truth and get proper error codes in CI +# +# ✨ For our prototype, we're overriding the docs build so we can see the new storybook on Netlify .PHONY: build-docs build-docs: - npm run build --workspace=www + cd prototype && npm install + cd prototype && npm run build + cd prototype && npm run build-storybook + rm -rf www/public + cp -r prototype/storybook-static www/public # npm swallows errors # see https://github.com/openedx/paragon/issues/3329 From 582128e589c7d37e8bcb7075ec5f71a7e59d2e5d Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 21 Jul 2026 19:47:53 -0700 Subject: [PATCH 11/24] refactor: Remove inline styles used to propagate design token vars --- prototype/README.md | 15 +- prototype/src/Button/Button.module.css | 743 ++++++++++++++++++++++++- prototype/src/Button/Button.tsx | 35 +- prototype/src/Stack/Stack.module.css | 26 +- prototype/src/Stack/Stack.test.tsx | 17 +- prototype/src/Stack/Stack.tsx | 10 +- 6 files changed, 788 insertions(+), 58 deletions(-) diff --git a/prototype/README.md b/prototype/README.md index 637a267f724..423f298d9aa 100644 --- a/prototype/README.md +++ b/prototype/README.md @@ -49,11 +49,16 @@ npm run build # ESM + .d.ts into dist/ working and expose `onPress` alongside it. **Decision needed:** whether to deprecate `onClick` over a major version or keep both indefinitely. -3. **Variant theming is inline token-variable remapping** (`variantVars` in - `Button.tsx`), a 1:1 port of the SCSS `button-variant` mixin. This avoids 44 - near-identical CSS rules. The alternative — static per-variant classes or - `[data-variant]` blocks — keeps all styling in CSS at the cost of verbosity. - Worth a WG preference call before rolling out to 60+ components. +3. **Variant theming is per-variant CSS**, not inline styles. Each variant has a + `.btn[data-variant="…"]` block in `Button.module.css` that re-points the + generic `--pgn-btn-*` custom properties to that variant's tokens — a 1:1 port + of the SCSS `button-variant` mixin. The component only sets `data-variant`; + there are no inline styles. (Stack's `gap` works the same way, via `.gap-*` + classes.) This keeps all styling in CSS at the cost of verbosity, so the + blocks are generated from the token set rather than hand-maintained. An + earlier revision did this remapping inline in `Button.tsx` (`variantVars`); + moving it to CSS keeps rendered elements attribute-only and lets the whole + variant surface be themed/inspected in one place. 4. **Focus uses `[data-focus-visible]` from React Aria** instead of `:focus`, so the focus ring shows only for keyboard users. diff --git a/prototype/src/Button/Button.module.css b/prototype/src/Button/Button.module.css index c2602680c2d..ca905d4d20e 100644 --- a/prototype/src/Button/Button.module.css +++ b/prototype/src/Button/Button.module.css @@ -4,8 +4,10 @@ * Every value still comes from the same `--pgn-*` design tokens; nothing is * hard-coded. Differences from the Bootstrap-era SCSS: * - Class names are locally scoped (no global `.btn-primary` leakage). - * - Variant token remapping happens inline in Button.tsx (see `variantVars`), - * so there are no per-variant rules here. + * - Variant token remapping lives in the `[data-variant="…"]` blocks below + * (a 1:1 port of Bootstrap's `button-variant` mixin). Each variant only + * re-points the generic `--pgn-btn-*` custom properties the rules above + * consume; the component just sets `data-variant` — no inline styles. * - Focus is driven by React Aria's `[data-focus-visible]` instead of `:focus`, * which correctly distinguishes keyboard focus from pointer focus. * - Logical properties (margin-inline-*) give RTL support without `[dir=rtl]` @@ -74,6 +76,743 @@ pointer-events: none; } +/* + * Variant theming. + * + * Each block re-points the generic `--pgn-btn-*` custom properties (consumed by + * the base `.btn` rules above) to that variant's design tokens. This is the + * all-CSS equivalent of the former `variantVars()` inline-style helper and of + * Paragon's SCSS `button-variant` mixin. Variants whose tokens are absent (e.g. + * `outline-tertiary`) intentionally have no block: the generic vars stay unset + * and the base rules fall back, exactly as before. + * + * Only the variant suffix changes between blocks, so this section is generated + * from the token set rather than hand-maintained (see the prototype README). + * `link` is styled by its own `.link` class below, not by this remap. + */ + +.btn[data-variant="brand"] { + --pgn-btn-color: var(--pgn-color-btn-text-brand); + --pgn-btn-bg: var(--pgn-color-btn-bg-brand); + --pgn-btn-border-color: var(--pgn-color-btn-border-brand); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-brand); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-brand); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-brand); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-brand); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-brand); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-brand); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-brand); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-brand); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-brand); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-brand); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-brand); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-brand); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-brand); +} + +.btn[data-variant="danger"] { + --pgn-btn-color: var(--pgn-color-btn-text-danger); + --pgn-btn-bg: var(--pgn-color-btn-bg-danger); + --pgn-btn-border-color: var(--pgn-color-btn-border-danger); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-danger); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-danger); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-danger); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-danger); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-danger); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-danger); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-danger); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-danger); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-danger); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-danger); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-danger); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-danger); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-danger); +} + +.btn[data-variant="dark"] { + --pgn-btn-color: var(--pgn-color-btn-text-dark); + --pgn-btn-bg: var(--pgn-color-btn-bg-dark); + --pgn-btn-border-color: var(--pgn-color-btn-border-dark); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-dark); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-dark); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-dark); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-dark); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-dark); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-dark); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-dark); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-dark); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-dark); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-dark); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-dark); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-dark); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-dark); +} + +.btn[data-variant="info"] { + --pgn-btn-color: var(--pgn-color-btn-text-info); + --pgn-btn-bg: var(--pgn-color-btn-bg-info); + --pgn-btn-border-color: var(--pgn-color-btn-border-info); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-info); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-info); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-info); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-info); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-info); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-info); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-info); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-info); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-info); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-info); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-info); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-info); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-info); +} + +.btn[data-variant="inverse-brand"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-brand); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-brand); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-brand); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-brand); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-brand); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-brand); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-brand); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-brand); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-brand); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-brand); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-brand); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-brand); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-brand); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-brand); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-brand); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-brand); +} + +.btn[data-variant="inverse-danger"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-danger); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-danger); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-danger); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-danger); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-danger); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-danger); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-danger); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-danger); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-danger); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-danger); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-danger); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-danger); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-danger); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-danger); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-danger); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-danger); +} + +.btn[data-variant="inverse-dark"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-dark); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-dark); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-dark); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-dark); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-dark); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-dark); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-dark); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-dark); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-dark); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-dark); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-dark); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-dark); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-dark); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-dark); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-dark); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-dark); +} + +.btn[data-variant="inverse-info"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-info); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-info); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-info); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-info); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-info); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-info); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-info); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-info); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-info); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-info); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-info); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-info); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-info); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-info); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-info); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-info); +} + +.btn[data-variant="inverse-light"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-light); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-light); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-light); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-light); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-light); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-light); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-light); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-light); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-light); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-light); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-light); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-light); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-light); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-light); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-light); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-light); +} + +.btn[data-variant="inverse-outline-brand"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-brand); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-brand); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-brand); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-outline-brand); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-outline-brand); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-outline-brand); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-outline-brand); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-outline-brand); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-outline-brand); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-outline-brand); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-outline-brand); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-outline-brand); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-outline-brand); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-outline-brand); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-outline-brand); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-brand); +} + +.btn[data-variant="inverse-outline-danger"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-danger); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-danger); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-danger); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-outline-danger); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-outline-danger); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-outline-danger); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-outline-danger); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-outline-danger); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-outline-danger); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-outline-danger); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-outline-danger); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-outline-danger); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-outline-danger); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-outline-danger); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-outline-danger); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-danger); +} + +.btn[data-variant="inverse-outline-dark"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-dark); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-dark); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-dark); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-outline-dark); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-outline-dark); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-outline-dark); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-outline-dark); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-outline-dark); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-outline-dark); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-outline-dark); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-outline-dark); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-outline-dark); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-outline-dark); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-outline-dark); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-outline-dark); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-dark); +} + +.btn[data-variant="inverse-outline-info"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-info); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-info); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-info); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-outline-info); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-outline-info); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-outline-info); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-outline-info); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-outline-info); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-outline-info); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-outline-info); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-outline-info); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-outline-info); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-outline-info); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-outline-info); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-outline-info); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-info); +} + +.btn[data-variant="inverse-outline-light"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-light); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-light); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-light); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-outline-light); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-outline-light); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-outline-light); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-outline-light); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-outline-light); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-outline-light); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-outline-light); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-outline-light); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-outline-light); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-outline-light); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-outline-light); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-outline-light); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-light); +} + +.btn[data-variant="inverse-outline-primary"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-primary); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-primary); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-primary); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-outline-primary); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-outline-primary); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-outline-primary); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-outline-primary); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-outline-primary); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-outline-primary); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-outline-primary); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-outline-primary); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-outline-primary); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-outline-primary); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-outline-primary); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-outline-primary); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-primary); +} + +.btn[data-variant="inverse-outline-secondary"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-secondary); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-secondary); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-secondary); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-outline-secondary); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-outline-secondary); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-outline-secondary); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-outline-secondary); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-outline-secondary); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-outline-secondary); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-outline-secondary); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-outline-secondary); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-outline-secondary); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-outline-secondary); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-outline-secondary); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-outline-secondary); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-secondary); +} + +.btn[data-variant="inverse-outline-success"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-success); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-success); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-success); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-outline-success); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-outline-success); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-outline-success); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-outline-success); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-outline-success); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-outline-success); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-outline-success); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-outline-success); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-outline-success); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-outline-success); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-outline-success); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-outline-success); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-success); +} + +.btn[data-variant="inverse-outline-warning"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-warning); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-warning); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-warning); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-outline-warning); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-outline-warning); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-outline-warning); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-outline-warning); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-outline-warning); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-outline-warning); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-outline-warning); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-outline-warning); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-outline-warning); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-outline-warning); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-outline-warning); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-outline-warning); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-warning); +} + +.btn[data-variant="inverse-primary"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-primary); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-primary); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-primary); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-primary); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-primary); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-primary); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-primary); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-primary); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-primary); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-primary); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-primary); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-primary); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-primary); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-primary); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-primary); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-primary); +} + +.btn[data-variant="inverse-secondary"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-secondary); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-secondary); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-secondary); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-secondary); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-secondary); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-secondary); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-secondary); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-secondary); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-secondary); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-secondary); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-secondary); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-secondary); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-secondary); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-secondary); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-secondary); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-secondary); +} + +.btn[data-variant="inverse-success"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-success); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-success); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-success); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-success); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-success); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-success); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-success); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-success); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-success); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-success); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-success); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-success); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-success); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-success); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-success); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-success); +} + +.btn[data-variant="inverse-tertiary"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-tertiary); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-tertiary); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-tertiary); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-tertiary); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-tertiary); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-tertiary); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-tertiary); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-tertiary); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-tertiary); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-tertiary); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-tertiary); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-tertiary); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-tertiary); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-tertiary); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-tertiary); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-tertiary); +} + +.btn[data-variant="inverse-warning"] { + --pgn-btn-color: var(--pgn-color-btn-text-inverse-warning); + --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-warning); + --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-warning); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-inverse-warning); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-inverse-warning); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-inverse-warning); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-inverse-warning); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-inverse-warning); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-inverse-warning); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-inverse-warning); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-inverse-warning); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-inverse-warning); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-inverse-warning); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-inverse-warning); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-inverse-warning); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-warning); +} + +.btn[data-variant="light"] { + --pgn-btn-color: var(--pgn-color-btn-text-light); + --pgn-btn-bg: var(--pgn-color-btn-bg-light); + --pgn-btn-border-color: var(--pgn-color-btn-border-light); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-light); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-light); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-light); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-light); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-light); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-light); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-light); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-light); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-light); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-light); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-light); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-light); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-light); +} + +.btn[data-variant="outline-brand"] { + --pgn-btn-color: var(--pgn-color-btn-text-outline-brand); + --pgn-btn-bg: var(--pgn-color-btn-bg-outline-brand); + --pgn-btn-border-color: var(--pgn-color-btn-border-outline-brand); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-outline-brand); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-outline-brand); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-outline-brand); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-outline-brand); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-outline-brand); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-outline-brand); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-outline-brand); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-outline-brand); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-outline-brand); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-outline-brand); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-outline-brand); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-outline-brand); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-brand); +} + +.btn[data-variant="outline-danger"] { + --pgn-btn-color: var(--pgn-color-btn-text-outline-danger); + --pgn-btn-bg: var(--pgn-color-btn-bg-outline-danger); + --pgn-btn-border-color: var(--pgn-color-btn-border-outline-danger); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-outline-danger); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-outline-danger); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-outline-danger); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-outline-danger); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-outline-danger); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-outline-danger); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-outline-danger); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-outline-danger); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-outline-danger); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-outline-danger); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-outline-danger); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-outline-danger); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-danger); +} + +.btn[data-variant="outline-dark"] { + --pgn-btn-color: var(--pgn-color-btn-text-outline-dark); + --pgn-btn-bg: var(--pgn-color-btn-bg-outline-dark); + --pgn-btn-border-color: var(--pgn-color-btn-border-outline-dark); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-outline-dark); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-outline-dark); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-outline-dark); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-outline-dark); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-outline-dark); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-outline-dark); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-outline-dark); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-outline-dark); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-outline-dark); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-outline-dark); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-outline-dark); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-outline-dark); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-dark); +} + +.btn[data-variant="outline-info"] { + --pgn-btn-color: var(--pgn-color-btn-text-outline-info); + --pgn-btn-bg: var(--pgn-color-btn-bg-outline-info); + --pgn-btn-border-color: var(--pgn-color-btn-border-outline-info); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-outline-info); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-outline-info); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-outline-info); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-outline-info); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-outline-info); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-outline-info); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-outline-info); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-outline-info); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-outline-info); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-outline-info); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-outline-info); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-outline-info); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-info); +} + +.btn[data-variant="outline-light"] { + --pgn-btn-color: var(--pgn-color-btn-text-outline-light); + --pgn-btn-bg: var(--pgn-color-btn-bg-outline-light); + --pgn-btn-border-color: var(--pgn-color-btn-border-outline-light); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-outline-light); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-outline-light); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-outline-light); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-outline-light); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-outline-light); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-outline-light); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-outline-light); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-outline-light); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-outline-light); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-outline-light); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-outline-light); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-outline-light); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-light); +} + +.btn[data-variant="outline-primary"] { + --pgn-btn-color: var(--pgn-color-btn-text-outline-primary); + --pgn-btn-bg: var(--pgn-color-btn-bg-outline-primary); + --pgn-btn-border-color: var(--pgn-color-btn-border-outline-primary); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-outline-primary); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-outline-primary); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-outline-primary); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-outline-primary); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-outline-primary); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-outline-primary); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-outline-primary); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-outline-primary); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-outline-primary); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-outline-primary); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-outline-primary); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-outline-primary); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-primary); +} + +.btn[data-variant="outline-secondary"] { + --pgn-btn-color: var(--pgn-color-btn-text-outline-secondary); + --pgn-btn-bg: var(--pgn-color-btn-bg-outline-secondary); + --pgn-btn-border-color: var(--pgn-color-btn-border-outline-secondary); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-outline-secondary); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-outline-secondary); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-outline-secondary); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-outline-secondary); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-outline-secondary); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-outline-secondary); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-outline-secondary); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-outline-secondary); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-outline-secondary); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-outline-secondary); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-outline-secondary); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-outline-secondary); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-secondary); +} + +.btn[data-variant="outline-success"] { + --pgn-btn-color: var(--pgn-color-btn-text-outline-success); + --pgn-btn-bg: var(--pgn-color-btn-bg-outline-success); + --pgn-btn-border-color: var(--pgn-color-btn-border-outline-success); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-outline-success); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-outline-success); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-outline-success); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-outline-success); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-outline-success); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-outline-success); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-outline-success); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-outline-success); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-outline-success); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-outline-success); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-outline-success); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-outline-success); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-success); +} + +.btn[data-variant="outline-warning"] { + --pgn-btn-color: var(--pgn-color-btn-text-outline-warning); + --pgn-btn-bg: var(--pgn-color-btn-bg-outline-warning); + --pgn-btn-border-color: var(--pgn-color-btn-border-outline-warning); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-outline-warning); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-outline-warning); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-outline-warning); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-outline-warning); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-outline-warning); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-outline-warning); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-outline-warning); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-outline-warning); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-outline-warning); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-outline-warning); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-outline-warning); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-outline-warning); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-warning); +} + +.btn[data-variant="primary"] { + --pgn-btn-color: var(--pgn-color-btn-text-primary); + --pgn-btn-bg: var(--pgn-color-btn-bg-primary); + --pgn-btn-border-color: var(--pgn-color-btn-border-primary); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-primary); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-primary); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-primary); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-primary); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-primary); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-primary); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-primary); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-primary); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-primary); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-primary); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-primary); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-primary); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-primary); +} + +.btn[data-variant="secondary"] { + --pgn-btn-color: var(--pgn-color-btn-text-secondary); + --pgn-btn-bg: var(--pgn-color-btn-bg-secondary); + --pgn-btn-border-color: var(--pgn-color-btn-border-secondary); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-secondary); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-secondary); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-secondary); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-secondary); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-secondary); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-secondary); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-secondary); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-secondary); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-secondary); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-secondary); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-secondary); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-secondary); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-secondary); +} + +.btn[data-variant="success"] { + --pgn-btn-color: var(--pgn-color-btn-text-success); + --pgn-btn-bg: var(--pgn-color-btn-bg-success); + --pgn-btn-border-color: var(--pgn-color-btn-border-success); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-success); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-success); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-success); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-success); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-success); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-success); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-success); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-success); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-success); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-success); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-success); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-success); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-success); +} + +.btn[data-variant="tertiary"] { + --pgn-btn-color: var(--pgn-color-btn-text-tertiary); + --pgn-btn-bg: var(--pgn-color-btn-bg-tertiary); + --pgn-btn-border-color: var(--pgn-color-btn-border-tertiary); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-tertiary); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-tertiary); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-tertiary); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-tertiary); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-tertiary); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-tertiary); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-tertiary); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-tertiary); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-tertiary); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-tertiary); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-tertiary); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-tertiary); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-tertiary); +} + +.btn[data-variant="warning"] { + --pgn-btn-color: var(--pgn-color-btn-text-warning); + --pgn-btn-bg: var(--pgn-color-btn-bg-warning); + --pgn-btn-border-color: var(--pgn-color-btn-border-warning); + --pgn-btn-hover-color: var(--pgn-color-btn-hover-text-warning); + --pgn-btn-hover-bg: var(--pgn-color-btn-hover-bg-warning); + --pgn-btn-hover-border-color: var(--pgn-color-btn-hover-border-warning); + --pgn-btn-disabled-color: var(--pgn-color-btn-disabled-text-warning); + --pgn-btn-disabled-bg: var(--pgn-color-btn-disabled-bg-warning); + --pgn-btn-disabled-border-color: var(--pgn-color-btn-disabled-border-warning); + --pgn-btn-active-color: var(--pgn-color-btn-active-text-warning); + --pgn-btn-active-bg: var(--pgn-color-btn-active-bg-warning); + --pgn-btn-active-border-color: var(--pgn-color-btn-active-border-warning); + --pgn-btn-focus-outline-color: var(--pgn-color-btn-focus-outline-warning); + --pgn-btn-focus-color: var(--pgn-color-btn-focus-text-warning); + --pgn-btn-focus-border-color: var(--pgn-color-btn-focus-border-warning); + --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-warning); +} + /* Icon slots */ .iconBefore { display: inline-flex; diff --git a/prototype/src/Button/Button.tsx b/prototype/src/Button/Button.tsx index bfec1810559..37d146dac6e 100644 --- a/prototype/src/Button/Button.tsx +++ b/prototype/src/Button/Button.tsx @@ -4,39 +4,10 @@ import { } from 'react-aria'; import clsx from 'clsx'; -import type { ButtonProps, ButtonVariant } from './types'; +import type { ButtonProps } from './types'; import { ButtonGroupContext } from './ButtonGroupContext'; import styles from './Button.module.css'; -/** - * Maps the generic `--pgn-btn-*` custom properties consumed by the base `.btn` - * rule to the variant-specific token variables. This is a 1:1 port of the SCSS - * `button-variant` mixin — the mixin only ever re-pointed these variables, so - * doing it inline keeps the styling token-driven while avoiding 44 near-identical - * CSS blocks. (A static-class alternative is noted in the prototype README.) - */ -function variantVars(variant: ButtonVariant): React.CSSProperties { - const v = variant; - return { - '--pgn-btn-color': `var(--pgn-color-btn-text-${v})`, - '--pgn-btn-bg': `var(--pgn-color-btn-bg-${v})`, - '--pgn-btn-border-color': `var(--pgn-color-btn-border-${v})`, - '--pgn-btn-hover-color': `var(--pgn-color-btn-hover-text-${v})`, - '--pgn-btn-hover-bg': `var(--pgn-color-btn-hover-bg-${v})`, - '--pgn-btn-hover-border-color': `var(--pgn-color-btn-hover-border-${v})`, - '--pgn-btn-disabled-color': `var(--pgn-color-btn-disabled-text-${v})`, - '--pgn-btn-disabled-bg': `var(--pgn-color-btn-disabled-bg-${v})`, - '--pgn-btn-disabled-border-color': `var(--pgn-color-btn-disabled-border-${v})`, - '--pgn-btn-active-color': `var(--pgn-color-btn-active-text-${v})`, - '--pgn-btn-active-bg': `var(--pgn-color-btn-active-bg-${v})`, - '--pgn-btn-active-border-color': `var(--pgn-color-btn-active-border-${v})`, - '--pgn-btn-focus-outline-color': `var(--pgn-color-btn-focus-outline-${v})`, - '--pgn-btn-focus-color': `var(--pgn-color-btn-focus-text-${v})`, - '--pgn-btn-focus-border-color': `var(--pgn-color-btn-focus-border-${v})`, - '--pgn-btn-focus-bg': `var(--pgn-color-btn-focus-bg-${v})`, - } as React.CSSProperties; -} - const SIZE_CLASS = { sm: styles.sm, md: undefined, @@ -99,7 +70,9 @@ export const Button = React.forwardRef(( // link elements, which is why `rest` must be spread last. {...rest} ref={ref} - style={isLink ? undefined : variantVars(variant)} + // Styling is entirely token-driven CSS: `data-variant` selects the + // `.btn[data-variant="…"]` block in Button.module.css, which re-points the + // generic `--pgn-btn-*` custom properties. No inline styles. data-variant={variant} data-pressed={isPressed || undefined} data-focus-visible={isFocusVisible || undefined} diff --git a/prototype/src/Stack/Stack.module.css b/prototype/src/Stack/Stack.module.css index 49280ee24f5..3af7a0e4d78 100644 --- a/prototype/src/Stack/Stack.module.css +++ b/prototype/src/Stack/Stack.module.css @@ -3,16 +3,15 @@ * * Faithful to the original: vertical stacks stretch and flex-grow; horizontal * stacks center their items. `reversed` flips the flow direction. The gap is - * still driven entirely by the existing spacer design tokens — the component - * sets `--pgn-stack-gap` to the matching `--pgn-spacing-spacer-*` token (see - * Stack.tsx), so nothing is hard-coded and theming is unaffected. + * selected by a `.gap-*` class (a port of the SCSS `@each $spacers` loop), so + * spacing is fully token-driven with no inline styles on the component. */ .vstack, .hstack { display: flex; align-self: stretch; - gap: var(--pgn-stack-gap, var(--pgn-size-stack-gap)); + gap: var(--pgn-size-stack-gap); } .vstack { @@ -33,3 +32,22 @@ flex-direction: row-reverse; justify-content: flex-end; } + +/* + * Gap classes on the Paragon spacer scale — a 1:1 port of the SCSS + * `@each $level, $space in $spacers` loop. The component adds the class matching + * its `gap` prop (e.g. `gap={1.5}` -> `.gap-1-5`); values outside the scale get + * no class and fall back to `--pgn-size-stack-gap`, exactly as the original did. + */ +.gap-0 { gap: var(--pgn-spacing-spacer-0); } +.gap-1 { gap: var(--pgn-spacing-spacer-1); } +.gap-2 { gap: var(--pgn-spacing-spacer-2); } +.gap-3 { gap: var(--pgn-spacing-spacer-3); } +.gap-4 { gap: var(--pgn-spacing-spacer-4); } +.gap-5 { gap: var(--pgn-spacing-spacer-5); } +.gap-6 { gap: var(--pgn-spacing-spacer-6); } +.gap-1-5 { gap: var(--pgn-spacing-spacer-1-5); } +.gap-2-5 { gap: var(--pgn-spacing-spacer-2-5); } +.gap-3-5 { gap: var(--pgn-spacing-spacer-3-5); } +.gap-4-5 { gap: var(--pgn-spacing-spacer-4-5); } +.gap-5-5 { gap: var(--pgn-spacing-spacer-5-5); } diff --git a/prototype/src/Stack/Stack.test.tsx b/prototype/src/Stack/Stack.test.tsx index 0af81ac9305..b92365dc2e8 100644 --- a/prototype/src/Stack/Stack.test.tsx +++ b/prototype/src/Stack/Stack.test.tsx @@ -16,17 +16,16 @@ describe('Stack', () => { expect(screen.getByTestId('stack').className).toMatch(/hstack/); }); - it('resolves the gap to the matching spacer token', () => { + it('applies the gap class for the matching spacer level', () => { render(Content); - // Gap is token-driven: the component sets --pgn-stack-gap to the spacer var. - expect(screen.getByTestId('stack').style.getPropertyValue('--pgn-stack-gap')) - .toBe('var(--pgn-spacing-spacer-3)'); + // Gap is token-driven via a CSS Module class, not an inline style. + expect(screen.getByTestId('stack').className).toMatch(/gap-3/); + expect(screen.getByTestId('stack').getAttribute('style')).toBeNull(); }); - it('maps half-step gaps onto the spacer scale (1.5 -> 1-5)', () => { + it('maps half-step gaps onto the spacer scale (1.5 -> gap-1-5)', () => { render(Content); - expect(screen.getByTestId('stack').style.getPropertyValue('--pgn-stack-gap')) - .toBe('var(--pgn-spacing-spacer-1-5)'); + expect(screen.getByTestId('stack').className).toMatch(/gap-1-5/); }); it('applies the reversed modifier', () => { @@ -41,11 +40,11 @@ describe('Stack', () => { expect(stack).toHaveAttribute('aria-label', 'things'); }); - it('merges a caller-supplied style with the gap variable', () => { + it('forwards a caller-supplied style alongside the gap class', () => { render(Content); const stack = screen.getByTestId('stack'); expect(stack.style.marginTop).toBe('1rem'); - expect(stack.style.getPropertyValue('--pgn-stack-gap')).toBe('var(--pgn-spacing-spacer-2)'); + expect(stack.className).toMatch(/gap-2/); }); it('forwards a ref to the underlying element', () => { diff --git a/prototype/src/Stack/Stack.tsx b/prototype/src/Stack/Stack.tsx index 4aef3703ed1..06d13db3bfd 100644 --- a/prototype/src/Stack/Stack.tsx +++ b/prototype/src/Stack/Stack.tsx @@ -39,7 +39,6 @@ export const Stack = React.forwardRef(( reversed = false, children, className, - style, ...rest }, ref, @@ -49,14 +48,11 @@ export const Stack = React.forwardRef(( className={clsx( direction === 'horizontal' ? styles.hstack : styles.vstack, reversed && styles.reversed, + // Select the spacer-scale gap class (e.g. 1.5 -> `gap-1-5`). All styling + // lives in the CSS Module; the component sets no inline styles. + styles[`gap-${String(gap).replace('.', '-')}`], className, )} - style={{ - // Map the numeric gap onto the spacer token scale (e.g. 1.5 -> `1-5`), - // keeping spacing token-driven and themeable. - '--pgn-stack-gap': `var(--pgn-spacing-spacer-${String(gap).replace('.', '-')})`, - ...style, - } as React.CSSProperties} {...rest} > {children} From f49529bf75515e2c51ada7ae65ce754677687974 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Tue, 21 Jul 2026 20:29:33 -0700 Subject: [PATCH 12/24] feat: Type check examples in the MDX docs --- prototype/src/Button/Button.mdx | 111 ++-------- prototype/src/Button/Button.stories.tsx | 212 +++++++++++-------- prototype/src/Button/ButtonGroup.mdx | 41 +--- prototype/src/Button/ButtonGroup.stories.tsx | 52 ++++- prototype/src/Stack/Stack.mdx | 34 +-- prototype/src/Stack/Stack.stories.tsx | 46 ++++ prototype/src/docs/extractExamples.ts | 34 +++ prototype/src/raw.d.ts | 6 + 8 files changed, 287 insertions(+), 249 deletions(-) create mode 100644 prototype/src/docs/extractExamples.ts create mode 100644 prototype/src/raw.d.ts diff --git a/prototype/src/Button/Button.mdx b/prototype/src/Button/Button.mdx index af45ce89f49..40230ebc5d4 100644 --- a/prototype/src/Button/Button.mdx +++ b/prototype/src/Button/Button.mdx @@ -3,6 +3,8 @@ import { Meta, Title, Subtitle, Story, Controls } from '@storybook/addon-docs/bl import * as ButtonStories from './Button.stories'; import { LiveExample } from '../docs/LiveExample'; +export const examples = ButtonStories.examples; + Button @@ -34,133 +36,52 @@ directly in the editor to experiment. Changing a control overwrites the editor. {/* The sections below mirror the existing Paragon docs (src/Button/README.md) - 1:1 and in the same order, so the two docs are easy to compare. The JSX is - kept close to the original; `Stack` and the named icons are provided via the - react-live scope, and the original Bootstrap utility classes - (`bg-dark-700`, `p-4`, `mb-2`, `mr-1`) are expressed as inline styles since - the prototype does not ship Bootstrap's utility classes. + 1:1 and in the same order, so the two docs are easy to compare. The examples + are authored as real TSX in `Button.examples.tsx` (so TypeScript typechecks + every prop) and their source is rendered here, editable, via react-live. The + original Bootstrap utility classes (`bg-dark-700`, `p-4`, `mb-2`, `mr-1`) are + expressed as inline styles since the prototype does not ship them. */} ## Core Buttons - - - - - - - -`} /> + ## Core Buttons (Inverse Pallete) - - - - - - -
-`} /> + ## Utility Buttons - - - - - - - - - - - - - - - -`} /> + ## Size - - - - - - - - - - - - - - -`} /> + ### When to use the inline size Use inline size buttons for when a button sits with a line of text. - - 2 items selected. - - -

-`} /> + ## Block Buttons - - - - -`} /> + ### Disabled - - - - - -`} /> + ### With empty href For link to be `disabled`, it must have href defined with some value. - - - - -`} /> + ### With Icons before or after - - - - - - - -`} /> + ## Stateful buttons diff --git a/prototype/src/Button/Button.stories.tsx b/prototype/src/Button/Button.stories.tsx index c6e65114336..d03d88bd957 100644 --- a/prototype/src/Button/Button.stories.tsx +++ b/prototype/src/Button/Button.stories.tsx @@ -1,34 +1,33 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { Button } from './Button'; -import { ButtonGroup } from './ButtonGroup'; -import { ButtonToolbar } from './ButtonToolbar'; +import { Stack } from '../Stack'; import type { BaseVariant } from './types'; import { LivePlayground } from '../docs/LivePlayground'; +import { + Add, Remove, ArrowBack, ArrowDropDown, Highlight, +} from '../docs/scope'; +import { extractExamples } from '../docs/extractExamples'; +import raw from './Button.stories.tsx?raw'; const BASE_VARIANTS: BaseVariant[] = [ 'primary', 'secondary', 'tertiary', 'brand', 'success', 'danger', 'warning', 'info', 'dark', 'light', 'link', ]; -// A tiny inline icon so stories stay dependency-free. -function DotIcon() { - return ( - - - - ); -} - const meta: Meta = { // Kept under a separate, fully-hidden root so it does NOT share a title with // the `Buttonlike/Button` docs page — that keeps the MDX an unattached, single - // sidebar leaf (no "Docs" sub-node). Stories are the raw material for that + // sidebar leaf (no "Docs" sub-node). The story is the raw material for that // page, hidden from the sidebar via `!dev` but still referenceable through // `` / ``. title: 'Internal/Button', component: Button, tags: ['!dev'], + // Only PascalCase exports are Storybook stories. The camelCase exports at the + // bottom of this file (`coreButtons`, … and the `examples` source map) are the + // docs' live-example source — real TSX so `tsc` typechecks them — not stories. + includeStories: /^[A-Z]/, parameters: { layout: 'padded' }, args: { children: 'Button', @@ -62,88 +61,119 @@ export const Playground: Story = { render: (args) => , }; -export const Variants: Story = { - render: () => ( -
- {BASE_VARIANTS.map((v) => ( - - ))} -
- ), -}; +/* + * --------------------------------------------------------------------------- + * Docs live examples. + * + * Authored as real TSX so TypeScript typechecks every prop against the Button + * types (a bad `variant`, a removed prop, or a mistyped icon fails + * `npm run type-check`). Their verbatim source is extracted (see + * `extractExamples`) and fed to react-live, so the docs stay editable. + * + * These are camelCase on purpose — `includeStories` above keeps Storybook from + * treating them as stories. Each must be a single JSX expression wrapped in + * `( … )` (react-live evaluates one; the extractor keys off that shape). + * --------------------------------------------------------------------------- + */ -export const OutlineAndInverse: Story = { - render: () => ( -
-
- {BASE_VARIANTS.filter((v) => v !== 'link').map((v) => ( - - ))} -
-
- {BASE_VARIANTS.filter((v) => v !== 'link').map((v) => ( - - ))} -
-
- ), -}; +export const coreButtons = ( + + + + + + + +); -export const Sizes: Story = { - render: () => ( -
- - - - - Text with an - {' '} - - {' '} - button. - -
- ), -}; +export const coreButtonsInverse = ( + + + + + + + +); -export const WithIcons: Story = { - render: () => ( -
- - - -
- ), -}; +export const utilityButtons = ( + <> + + + + + + + + + + + + + + +); -export const Disabled: Story = { - args: { disabled: true }, -}; +export const sizes = ( + <> + + + + + + + + + + + + + +); -/** Demonstrates polymorphism: rendered as an anchor, still keyboard-accessible - * and correctly disabled via React Aria (no `pointer-events` hack needed). */ -export const AsAnchor: Story = { - render: () => ( -
- - -
- ), -}; +export const inlineSize = ( +

+ 2 items selected. + + +

+); -export const Groups: Story = { - render: () => ( - - - - - - - - - - - - - ), -}; +export const blockButtons = ( + <> + + + +); + +export const disabled = ( + + + + + +); + +export const emptyHref = ( + + + + +); + +export const withIcons = ( + + + + + + + +); + +/** Editable source strings for react-live, extracted verbatim from this file. */ +export const examples = extractExamples(raw); diff --git a/prototype/src/Button/ButtonGroup.mdx b/prototype/src/Button/ButtonGroup.mdx index 80c368ec68e..aa09ced9de6 100644 --- a/prototype/src/Button/ButtonGroup.mdx +++ b/prototype/src/Button/ButtonGroup.mdx @@ -3,6 +3,8 @@ import { Meta, Title, Subtitle, Story, Controls } from '@storybook/addon-docs/bl import * as ButtonGroupStories from './ButtonGroup.stories'; import { LiveExample } from '../docs/LiveExample'; +export const examples = ButtonGroupStories.examples; + Button Group @@ -28,57 +30,28 @@ import { Button, ButtonGroup } from '@openedx/paragon'; ``` - + ## Props - + ## Sizes A group sets the size of every button it contains — set it once on the group rather than on each `Button`. - - - - - - - - - - -`} /> + ## Vertical - - - - - -`} /> + ## ButtonToolbar Compose several groups inside a `ButtonToolbar`. - - - - - - - - - - - -`} /> + ## Accessibility notes diff --git a/prototype/src/Button/ButtonGroup.stories.tsx b/prototype/src/Button/ButtonGroup.stories.tsx index 6a8488d3649..85a2ff6bc97 100644 --- a/prototype/src/Button/ButtonGroup.stories.tsx +++ b/prototype/src/Button/ButtonGroup.stories.tsx @@ -2,12 +2,18 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { Button } from './Button'; import { ButtonGroup } from './ButtonGroup'; +import { ButtonToolbar } from './ButtonToolbar'; +import { extractExamples } from '../docs/extractExamples'; +import raw from './ButtonGroup.stories.tsx?raw'; const meta: Meta = { // Separate hidden root — see the note in Button.stories.tsx. title: 'Internal/ButtonGroup', component: ButtonGroup, tags: ['!dev'], + // PascalCase exports are stories; camelCase exports below are typechecked + // docs example source (see Button.stories.tsx for the full rationale). + includeStories: /^[A-Z]/, parameters: { layout: 'padded' }, args: { size: 'md', vertical: false }, argTypes: { @@ -19,7 +25,7 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Default: Story = { +export const Playground: Story = { render: (args) => ( @@ -28,3 +34,47 @@ export const Default: Story = { ), }; + +/* + * Docs live examples — typechecked TSX, source-extracted for react-live. + * camelCase so `includeStories` excludes them from Storybook. See + * Button.stories.tsx for the full rationale. + */ + +export const sizes = ( +
+ + + + + + + + +
+); + +export const vertical = ( + + + + + +); + +export const toolbar = ( + + + + + + + + + + + +); + +/** Editable source strings for react-live, extracted verbatim from this file. */ +export const examples = extractExamples(raw); diff --git a/prototype/src/Stack/Stack.mdx b/prototype/src/Stack/Stack.mdx index f0b30ef4d77..69fd835b129 100644 --- a/prototype/src/Stack/Stack.mdx +++ b/prototype/src/Stack/Stack.mdx @@ -3,6 +3,8 @@ import { Meta, Title, Subtitle, Story, Controls } from '@storybook/addon-docs/bl import * as StackStories from './Stack.stories'; import { LiveExample } from '../docs/LiveExample'; +export const examples = StackStories.examples; + Stack @@ -44,42 +46,18 @@ Stacks are vertical by default and stacked items are full-width by default. Watc this pull request to see more details about the [auto stretching behavior](https://github.com/openedx/paragon/pull/1188). - - - - - -`} /> + ## Horizontal direction - -
first block
-
second block
-
third block
- -`} /> + ## Reversed props **Vertical** with the `reversed` prop: - - - - - -`} /> + **Horizontal** with the `reversed` prop: - -
first block
-
second block
-
third block
- -`} /> + diff --git a/prototype/src/Stack/Stack.stories.tsx b/prototype/src/Stack/Stack.stories.tsx index 616b3018c13..c614ad92636 100644 --- a/prototype/src/Stack/Stack.stories.tsx +++ b/prototype/src/Stack/Stack.stories.tsx @@ -2,6 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { Stack } from './Stack'; import { Button } from '../Button'; +import { extractExamples } from '../docs/extractExamples'; +import raw from './Stack.stories.tsx?raw'; const meta: Meta = { // Hidden `Internal/` root + `!dev` so the sidebar only shows the standalone @@ -9,6 +11,9 @@ const meta: Meta = { title: 'Internal/Stack', component: Stack, tags: ['!dev'], + // PascalCase exports are stories; camelCase exports below are typechecked + // docs example source (see Button.stories.tsx for the full rationale). + includeStories: /^[A-Z]/, parameters: { layout: 'padded' }, args: { direction: 'vertical', gap: 3, reversed: false, @@ -36,3 +41,44 @@ export const Playground: Story = { ), }; + +/* + * Docs live examples — typechecked TSX, source-extracted for react-live. + * camelCase so `includeStories` excludes them from Storybook. See + * Button.stories.tsx for the full rationale. + */ + +export const vertical = ( + + + + + +); + +export const horizontal = ( + +
first block
+
second block
+
third block
+
+); + +export const reversedVertical = ( + + + + + +); + +export const reversedHorizontal = ( + +
first block
+
second block
+
third block
+
+); + +/** Editable source strings for react-live, extracted verbatim from this file. */ +export const examples = extractExamples(raw); diff --git a/prototype/src/docs/extractExamples.ts b/prototype/src/docs/extractExamples.ts new file mode 100644 index 00000000000..f0d4cf954d3 --- /dev/null +++ b/prototype/src/docs/extractExamples.ts @@ -0,0 +1,34 @@ +/** + * Extracts the verbatim source of each `export const NAME = ( … );` block from a + * module's raw text (imported via Vite's `?raw`). + * + * This is what lets the docs examples be both **typechecked** and **editable**: + * the examples are authored as real JSX in a co-located `*.examples.tsx` file + * (so `tsc` validates every prop against the component types), and their exact + * source is pulled back out here and handed to react-live's editor. + * + * Convention (matches Prettier's stable formatting): each example is a single + * JSX expression wrapped in `(` … `)`, with the opening `(` at end-of-line and + * the closing `);` in column 0. Any other export (e.g. the `examples` map + * itself) is a single line and is ignored. + */ +export function extractExamples(raw: string): Record { + const out: Record = {}; + const re = /^export const (\w+) = \(\n([\s\S]*?)\n\);$/gm; + let m: RegExpExecArray | null; + // eslint-disable-next-line no-cond-assign + while ((m = re.exec(raw)) !== null) { + out[m[1]] = dedent(m[2]); + } + return out; +} + +/** Removes the common leading indentation so the snippet reads flush-left. */ +function dedent(block: string): string { + const lines = block.split('\n'); + const indents = lines + .filter((l) => l.trim().length) + .map((l) => l.match(/^ */)![0].length); + const min = indents.length ? Math.min(...indents) : 0; + return lines.map((l) => l.slice(min)).join('\n'); +} diff --git a/prototype/src/raw.d.ts b/prototype/src/raw.d.ts new file mode 100644 index 00000000000..f116029ccf4 --- /dev/null +++ b/prototype/src/raw.d.ts @@ -0,0 +1,6 @@ +// Vite's `?raw` import returns a module's source text as a string. Used to feed +// react-live the verbatim JSX authored in the co-located `*.examples.tsx` files. +declare module '*?raw' { + const src: string; + export default src; +} From 3cbbf989bcb5ff4920c4f5d9a12124b54f9f71a7 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 22 Jul 2026 09:13:02 -0700 Subject: [PATCH 13/24] chore: version bumps --- prototype/README.md | 13 + prototype/package-lock.json | 2239 ++++++++++++++------------------ prototype/package.json | 10 +- prototype/src/css-modules.d.ts | 4 + 4 files changed, 973 insertions(+), 1293 deletions(-) diff --git a/prototype/README.md b/prototype/README.md index 423f298d9aa..1f4eeff0526 100644 --- a/prototype/README.md +++ b/prototype/README.md @@ -35,6 +35,19 @@ npm run storybook # interactive workbench at http://localhost:6007 npm run build # ESM + .d.ts into dist/ ``` +## Toolchain versions + +The prototype tracks TypeScript 6 and Vite 8 (Vite 8 bundles with Rolldown). + +**We cannot move to TypeScript 7 yet.** Storybook's prop tables are generated by +`@joshwooding/vite-plugin-react-docgen-typescript`, which parses component types +with `react-docgen-typescript`. Both rely on TypeScript's JavaScript Compiler +API. TypeScript 7 is the native (Go) port and does not yet expose that API, so +the plugin does not support it — the docgen step (and therefore Storybook's +auto-generated props docs) breaks under TS 7. Once +`@joshwooding/vite-plugin-react-docgen-typescript` ships TypeScript 7 support we +can bump `typescript` accordingly. + ## Key migration decisions surfaced here (for WG ratification) 1. **Polymorphic `as` via `useButton`, not `
); - expect(screen.getByRole('button', { name: 'Delete' })).toHaveAttribute('data-variant', 'danger'); + const btn = screen.getByRole('button', { name: 'Delete' }); + // Global, stable class names (not hashed) — the same public API a raw + // `` relies on. + expect(btn).toHaveClass('btn', 'btn-danger'); }); it('forwards the legacy onClick handler', async () => { diff --git a/prototype/src/Button/Button.tsx b/prototype/src/Button/Button.tsx index 37d146dac6e..970ecc93cd7 100644 --- a/prototype/src/Button/Button.tsx +++ b/prototype/src/Button/Button.tsx @@ -6,13 +6,13 @@ import clsx from 'clsx'; import type { ButtonProps } from './types'; import { ButtonGroupContext } from './ButtonGroupContext'; -import styles from './Button.module.css'; +import '../styles/button.css'; const SIZE_CLASS = { - sm: styles.sm, + sm: 'btn-sm', md: undefined, - lg: styles.lg, - inline: styles.inline, + lg: 'btn-lg', + inline: 'btn-inline', } as const; /** @@ -59,8 +59,6 @@ export const Button = React.forwardRef(( ); const { isFocusVisible, focusProps } = useFocusRing(); - const isLink = variant === 'link'; - return ( (( // link elements, which is why `rest` must be spread last. {...rest} ref={ref} - // Styling is entirely token-driven CSS: `data-variant` selects the - // `.btn[data-variant="…"]` block in Button.module.css, which re-points the - // generic `--pgn-btn-*` custom properties. No inline styles. - data-variant={variant} data-pressed={isPressed || undefined} data-focus-visible={isFocusVisible || undefined} + // Styling comes from the global, public `btn` class layer (src/styles/ + // button.css): `btn` + `btn-` (+ size/block). These are the same + // Bootstrap-compatible class names Paragon has always shipped, so a raw + // `` renders identically to this component. className={clsx( - styles.btn, - isLink && styles.link, + 'btn', + `btn-${variant}`, SIZE_CLASS[resolvedSize], - block && styles.block, + block && 'btn-block', className, )} > {IconBefore && ( - + )} {children} {IconAfter && ( - + )} diff --git a/prototype/src/Button/ButtonGroup.mdx b/prototype/src/Button/ButtonGroup.mdx index aa09ced9de6..767f2b24b29 100644 --- a/prototype/src/Button/ButtonGroup.mdx +++ b/prototype/src/Button/ButtonGroup.mdx @@ -12,9 +12,11 @@ export const examples = ButtonGroupStories.examples; Group related buttons so they read as a single control; ButtonToolbar arranges multiple groups. `ButtonGroup` visually and semantically groups related `Button`s (ARIA -`role="group"`). It replaces Bootstrap's `.btn-group-*` global classes: a group -propagates its `size` to child buttons via React context, and inner border radii -are collapsed with logical properties so the group works in both LTR and RTL. +`role="group"`). It renders the public `btn-group` class (part of Paragon's +global class layer) and additionally propagates its `size` to child buttons via +React context — replacing Bootstrap's `.btn-group-sm` / `.btn-group-lg` sizing +classes — while inner border radii are collapsed with logical properties so the +group works in both LTR and RTL. `ButtonToolbar` (ARIA `role="toolbar"`) lays out one or more groups. diff --git a/prototype/src/Button/ButtonGroup.tsx b/prototype/src/Button/ButtonGroup.tsx index 9a1c24c577c..2760728e94f 100644 --- a/prototype/src/Button/ButtonGroup.tsx +++ b/prototype/src/Button/ButtonGroup.tsx @@ -3,7 +3,7 @@ import clsx from 'clsx'; import type { ButtonSize } from './types'; import { ButtonGroupContext } from './ButtonGroupContext'; -import styles from './Button.module.css'; +import '../styles/button.css'; export interface ButtonGroupProps { /** Specifies element type for this component (default: `div`). */ @@ -42,7 +42,7 @@ export const ButtonGroup = React.forwardRef(( {...rest} ref={ref} role={role} - className={clsx(styles.btnGroup, vertical && styles.btnGroupVertical, className)} + className={clsx('btn-group', vertical && 'btn-group-vertical', className)} > {children} diff --git a/prototype/src/Button/ButtonToolbar.tsx b/prototype/src/Button/ButtonToolbar.tsx index 5a6329872b7..93025e35b43 100644 --- a/prototype/src/Button/ButtonToolbar.tsx +++ b/prototype/src/Button/ButtonToolbar.tsx @@ -1,7 +1,7 @@ import React from 'react'; import clsx from 'clsx'; -import styles from './Button.module.css'; +import '../styles/button.css'; export interface ButtonToolbarProps { /** Specifies element type for this component (default: `div`). */ @@ -26,7 +26,7 @@ export const ButtonToolbar = React.forwardRef(( {...rest} ref={ref} role={role} - className={clsx(styles.btnToolbar, className)} + className={clsx('btn-toolbar', className)} > {children} diff --git a/prototype/src/Collapsible/Collapsible.mdx b/prototype/src/Collapsible/Collapsible.mdx index 0408a955429..150fad7348a 100644 --- a/prototype/src/Collapsible/Collapsible.mdx +++ b/prototype/src/Collapsible/Collapsible.mdx @@ -45,10 +45,10 @@ When to use: The sections below mirror the existing Paragon docs (src/Collapsible/README.md) 1:1 and in the same order. The examples are authored as real TSX in Collapsible.stories.tsx (so TypeScript typechecks every prop) and their source - is rendered here, editable, via react-live. The component's own style classes - (collapsible-card / -trigger / -body) map to this module's classes, exposed to - the live scope as `collapsibleStyles`; the Bootstrap utilities become inline - styles / the `collapsibleStyles.closeButton` helper. + is rendered here, editable, via react-live. The component style classes + (collapsible-card / -trigger / -body) and the utility / button classes + (d-flex, flex-grow-1, btn btn-outline-primary) are all part of Paragon's global + public class layer (src/styles/*.css), so these snippets are verbatim README. */} ## Basic Usage diff --git a/prototype/src/Collapsible/Collapsible.stories.tsx b/prototype/src/Collapsible/Collapsible.stories.tsx index c74082d899f..fb77894a5d9 100644 --- a/prototype/src/Collapsible/Collapsible.stories.tsx +++ b/prototype/src/Collapsible/Collapsible.stories.tsx @@ -4,7 +4,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { Collapsible } from './Collapsible'; import type { CollapsibleStyling } from './Collapsible'; import { ExamplePropsForm } from '../docs/ExamplePropsForm'; -import collapsibleStyles from './Collapsible.module.css'; import { extractExamples } from '../docs/extractExamples'; import raw from './Collapsible.stories.tsx?raw'; @@ -52,11 +51,10 @@ export const Playground: Story = { * stories. Each is a single expression wrapped in `( … )`. * * These mirror src/Collapsible/README.md 1:1 and in the same order. The - * original component style classes (`collapsible-card`, `collapsible-trigger`, - * `collapsible-body`) map to this module's classes, exposed to react-live as - * `collapsibleStyles`; the Bootstrap utilities (`d-flex`, `flex-grow-1`, `btn - * btn-outline-primary`) become inline styles / the `collapsibleStyles.closeButton` - * helper, since the prototype does not ship them. + * component style classes (`collapsible-card`, `collapsible-trigger`, + * `collapsible-body`) and the utility / button classes (`d-flex`, `flex-grow-1`, + * `btn btn-outline-primary`) are all part of Paragon's global public class layer + * (src/styles/*.css), so these snippets are verbatim copies of the README. * --------------------------------------------------------------------------- */ @@ -135,31 +133,31 @@ export const advancedBareMinimum = ( ); export const advancedCard = ( - - - This is the title + + + This is the title + - - + The content ); export const advancedWithCloseButton = ( - - - This is the title + + + This is the title + - - +

The content

- + Close
@@ -168,13 +166,13 @@ export const advancedWithCloseButton = ( export const advancedCallbacks = ( console.log('Collapsible toggled and open is: ', isOpen)} onOpen={() => console.log('Collapsible opened.')} onClose={() => console.log('Collapsible closed.')} > - -

I'm a heading

+ +

I'm a heading

+ @@ -185,10 +183,10 @@ export const advancedCallbacks = (
- +

Your stuff goes here.

- + Close
@@ -203,10 +201,10 @@ export const advancedControlled = ( setCollapseOpen(isOpen)} - className={collapsibleStyles.card} + className="collapsible-card" > - -

I'm a heading

+ +

I'm a heading

+ @@ -217,10 +215,10 @@ export const advancedControlled = (
- +

Your stuff goes here.

- + Close
diff --git a/prototype/src/Collapsible/Collapsible.tsx b/prototype/src/Collapsible/Collapsible.tsx index 0be74c05311..34959df9c0b 100644 --- a/prototype/src/Collapsible/Collapsible.tsx +++ b/prototype/src/Collapsible/Collapsible.tsx @@ -10,7 +10,13 @@ import CollapsibleAdvanced, { import CollapsibleTrigger from './CollapsibleTrigger'; import CollapsibleBody from './CollapsibleBody'; import CollapsibleVisible from './CollapsibleVisible'; -import styles from './Collapsible.module.css'; +import '../styles/collapsible.css'; + +const STYLING_CLASS: Record = { + basic: 'collapsible-basic', + card: 'collapsible-card', + 'card-lg': 'collapsible-card-lg', +}; /** Style variant of the convenience `Collapsible`. */ export type CollapsibleStyling = 'basic' | 'card' | 'card-lg'; @@ -78,21 +84,17 @@ export const Collapsible = React.forwardRef const openIcon = iconWhenOpen ?? ; const titleElement = React.isValidElement(title) ? title : {title}; - const stylingClass = styling === 'basic' - ? styles.basic - : clsx(styles.card, styling === 'card-lg' && styles.cardLg); - return ( - - + + {titleElement} - + {closedIcon} {openIcon} - {children} + {children} ); }) as CollapsibleComponent; diff --git a/prototype/src/Collapsible/CollapsibleAdvanced.tsx b/prototype/src/Collapsible/CollapsibleAdvanced.tsx index abf2fc1e9f4..a428d8e6864 100644 --- a/prototype/src/Collapsible/CollapsibleAdvanced.tsx +++ b/prototype/src/Collapsible/CollapsibleAdvanced.tsx @@ -5,7 +5,7 @@ import { useDisclosure, type AriaButtonProps } from 'react-aria'; import { useDisclosureState } from 'react-stately'; import clsx from 'clsx'; -import styles from './Collapsible.module.css'; +import '../styles/collapsible.css'; /** * The value shared by `CollapsibleAdvanced` with its `Trigger`, `Body` and @@ -146,7 +146,7 @@ export const CollapsibleAdvanced = React.forwardRef diff --git a/prototype/src/Collapsible/CollapsibleBody.tsx b/prototype/src/Collapsible/CollapsibleBody.tsx index b3a255fe63b..deb770fcb0d 100644 --- a/prototype/src/Collapsible/CollapsibleBody.tsx +++ b/prototype/src/Collapsible/CollapsibleBody.tsx @@ -3,7 +3,7 @@ import { mergeProps } from 'react-aria'; import clsx from 'clsx'; import { useCollapsibleContext } from './CollapsibleAdvanced'; -import styles from './Collapsible.module.css'; +import '../styles/collapsible.css'; export interface CollapsibleBodyProps extends React.HTMLAttributes { @@ -35,7 +35,7 @@ export const CollapsibleBody = ({ {children} diff --git a/prototype/src/Collapsible/CollapsibleTrigger.tsx b/prototype/src/Collapsible/CollapsibleTrigger.tsx index adb0b22e0a2..2cc8acaea5b 100644 --- a/prototype/src/Collapsible/CollapsibleTrigger.tsx +++ b/prototype/src/Collapsible/CollapsibleTrigger.tsx @@ -5,7 +5,7 @@ import { import clsx from 'clsx'; import { useCollapsibleContext } from './CollapsibleAdvanced'; -import styles from './Collapsible.module.css'; +import '../styles/collapsible.css'; export interface CollapsibleTriggerProps extends React.HTMLAttributes { @@ -82,7 +82,7 @@ export const CollapsibleTrigger = React.forwardRef {children} diff --git a/prototype/src/docs/live.test.tsx b/prototype/src/docs/live.test.tsx index 226fcc34060..db4a92367cc 100644 --- a/prototype/src/docs/live.test.tsx +++ b/prototype/src/docs/live.test.tsx @@ -23,7 +23,7 @@ describe('LivePlayground', () => { it('generates JSX from args and renders the matching Button', async () => { render(Go); const btn = await screen.findByRole('button', { name: 'Go' }); - expect(btn).toHaveAttribute('data-variant', 'danger'); + expect(btn).toHaveClass('btn', 'btn-danger'); }); }); diff --git a/prototype/src/docs/scope.tsx b/prototype/src/docs/scope.tsx index 292d19691d9..fea63481af3 100644 --- a/prototype/src/docs/scope.tsx +++ b/prototype/src/docs/scope.tsx @@ -3,7 +3,6 @@ import React, { useState } from 'react'; import { Button, ButtonGroup, ButtonToolbar } from '../Button'; import { Stack } from '../Stack'; import { Collapsible } from '../Collapsible'; -import collapsibleStyles from '../Collapsible/Collapsible.module.css'; import { ExamplePropsForm } from './ExamplePropsForm'; /** A tiny inline icon so live examples can demonstrate icon slots dependency-free. */ @@ -45,9 +44,6 @@ export const scope = { ButtonToolbar, Stack, Collapsible, - // The Collapsible CSS Module, so `Collapsible.Advanced` examples can apply the - // card/basic style presets by name (e.g. `className={collapsibleStyles.card}`). - collapsibleStyles, ExamplePropsForm, DotIcon, Add, diff --git a/prototype/src/index.ts b/prototype/src/index.ts index 2dab61d1d70..087067ff78c 100644 --- a/prototype/src/index.ts +++ b/prototype/src/index.ts @@ -1,5 +1,8 @@ import './tokens.css'; import './base.css'; +// The global, public, Bootstrap-compatible class layer (btn/utility/component +// classes). Shipped for consumers who apply these classes to their own markup. +import './styles/index.css'; export { Button, ButtonGroup, ButtonToolbar, diff --git a/prototype/src/Button/Button.module.css b/prototype/src/styles/button.css similarity index 94% rename from prototype/src/Button/Button.module.css rename to prototype/src/styles/button.css index ca905d4d20e..950ef572bb8 100644 --- a/prototype/src/Button/Button.module.css +++ b/prototype/src/styles/button.css @@ -1,17 +1,21 @@ /* - * Button styles — a CSS Modules port of src/Button/index.scss. + * Button class layer - global, public, Bootstrap-compatible classes. * - * Every value still comes from the same `--pgn-*` design tokens; nothing is - * hard-coded. Differences from the Bootstrap-era SCSS: - * - Class names are locally scoped (no global `.btn-primary` leakage). - * - Variant token remapping lives in the `[data-variant="…"]` blocks below - * (a 1:1 port of Bootstrap's `button-variant` mixin). Each variant only - * re-points the generic `--pgn-btn-*` custom properties the rules above - * consume; the component just sets `data-variant` — no inline styles. - * - Focus is driven by React Aria's `[data-focus-visible]` instead of `:focus`, - * which correctly distinguishes keyboard focus from pointer focus. - * - Logical properties (margin-inline-*) give RTL support without `[dir=rtl]` - * overrides. + * These class names (btn, btn-primary, btn-outline-danger, btn-lg, btn-block, + * btn-group, ...) are part of Paragon's PUBLIC API: consuming MFEs apply them to + * their own markup (e.g.
), so they must stay global + * and stable - see ADR 0022. This file is the single source of truth for button + * appearance; the Button React component just applies these classes and adds + * React Aria behaviour on top. + * + * Ported from Bootstrap SCSS to plain CSS over the same --pgn-* design tokens: + * - Variant remapping lives in the .btn- blocks (a 1:1 port of + * Bootstrap's button-variant mixin), each re-pointing the generic + * --pgn-btn-* custom properties the base .btn rules consume. + * - Focus is driven by React Aria's [data-focus-visible] instead of :focus. + * - Logical properties (margin-inline-*) give RTL support without [dir=rtl]. + * + * The variant blocks are generated from the token set rather than hand-maintained. */ .btn { @@ -58,7 +62,7 @@ color: var(--pgn-btn-focus-color, var(--pgn-color-body-base)); } -.btn[data-focus-visible]:not(:disabled, .link)::before { +.btn[data-focus-visible]:not(:disabled, .btn-link)::before { content: ""; position: absolute; inset: calc(var(--pgn-spacing-btn-focus-distance-to-border) * -1); @@ -88,10 +92,10 @@ * * Only the variant suffix changes between blocks, so this section is generated * from the token set rather than hand-maintained (see the prototype README). - * `link` is styled by its own `.link` class below, not by this remap. + * `link` is styled by its own `.btn-link` class below, not by this remap. */ -.btn[data-variant="brand"] { +.btn-brand { --pgn-btn-color: var(--pgn-color-btn-text-brand); --pgn-btn-bg: var(--pgn-color-btn-bg-brand); --pgn-btn-border-color: var(--pgn-color-btn-border-brand); @@ -110,7 +114,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-brand); } -.btn[data-variant="danger"] { +.btn-danger { --pgn-btn-color: var(--pgn-color-btn-text-danger); --pgn-btn-bg: var(--pgn-color-btn-bg-danger); --pgn-btn-border-color: var(--pgn-color-btn-border-danger); @@ -129,7 +133,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-danger); } -.btn[data-variant="dark"] { +.btn-dark { --pgn-btn-color: var(--pgn-color-btn-text-dark); --pgn-btn-bg: var(--pgn-color-btn-bg-dark); --pgn-btn-border-color: var(--pgn-color-btn-border-dark); @@ -148,7 +152,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-dark); } -.btn[data-variant="info"] { +.btn-info { --pgn-btn-color: var(--pgn-color-btn-text-info); --pgn-btn-bg: var(--pgn-color-btn-bg-info); --pgn-btn-border-color: var(--pgn-color-btn-border-info); @@ -167,7 +171,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-info); } -.btn[data-variant="inverse-brand"] { +.btn-inverse-brand { --pgn-btn-color: var(--pgn-color-btn-text-inverse-brand); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-brand); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-brand); @@ -186,7 +190,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-brand); } -.btn[data-variant="inverse-danger"] { +.btn-inverse-danger { --pgn-btn-color: var(--pgn-color-btn-text-inverse-danger); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-danger); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-danger); @@ -205,7 +209,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-danger); } -.btn[data-variant="inverse-dark"] { +.btn-inverse-dark { --pgn-btn-color: var(--pgn-color-btn-text-inverse-dark); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-dark); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-dark); @@ -224,7 +228,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-dark); } -.btn[data-variant="inverse-info"] { +.btn-inverse-info { --pgn-btn-color: var(--pgn-color-btn-text-inverse-info); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-info); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-info); @@ -243,7 +247,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-info); } -.btn[data-variant="inverse-light"] { +.btn-inverse-light { --pgn-btn-color: var(--pgn-color-btn-text-inverse-light); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-light); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-light); @@ -262,7 +266,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-light); } -.btn[data-variant="inverse-outline-brand"] { +.btn-inverse-outline-brand { --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-brand); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-brand); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-brand); @@ -281,7 +285,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-brand); } -.btn[data-variant="inverse-outline-danger"] { +.btn-inverse-outline-danger { --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-danger); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-danger); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-danger); @@ -300,7 +304,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-danger); } -.btn[data-variant="inverse-outline-dark"] { +.btn-inverse-outline-dark { --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-dark); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-dark); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-dark); @@ -319,7 +323,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-dark); } -.btn[data-variant="inverse-outline-info"] { +.btn-inverse-outline-info { --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-info); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-info); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-info); @@ -338,7 +342,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-info); } -.btn[data-variant="inverse-outline-light"] { +.btn-inverse-outline-light { --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-light); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-light); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-light); @@ -357,7 +361,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-light); } -.btn[data-variant="inverse-outline-primary"] { +.btn-inverse-outline-primary { --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-primary); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-primary); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-primary); @@ -376,7 +380,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-primary); } -.btn[data-variant="inverse-outline-secondary"] { +.btn-inverse-outline-secondary { --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-secondary); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-secondary); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-secondary); @@ -395,7 +399,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-secondary); } -.btn[data-variant="inverse-outline-success"] { +.btn-inverse-outline-success { --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-success); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-success); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-success); @@ -414,7 +418,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-success); } -.btn[data-variant="inverse-outline-warning"] { +.btn-inverse-outline-warning { --pgn-btn-color: var(--pgn-color-btn-text-inverse-outline-warning); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-outline-warning); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-outline-warning); @@ -433,7 +437,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-outline-warning); } -.btn[data-variant="inverse-primary"] { +.btn-inverse-primary { --pgn-btn-color: var(--pgn-color-btn-text-inverse-primary); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-primary); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-primary); @@ -452,7 +456,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-primary); } -.btn[data-variant="inverse-secondary"] { +.btn-inverse-secondary { --pgn-btn-color: var(--pgn-color-btn-text-inverse-secondary); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-secondary); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-secondary); @@ -471,7 +475,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-secondary); } -.btn[data-variant="inverse-success"] { +.btn-inverse-success { --pgn-btn-color: var(--pgn-color-btn-text-inverse-success); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-success); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-success); @@ -490,7 +494,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-success); } -.btn[data-variant="inverse-tertiary"] { +.btn-inverse-tertiary { --pgn-btn-color: var(--pgn-color-btn-text-inverse-tertiary); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-tertiary); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-tertiary); @@ -509,7 +513,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-tertiary); } -.btn[data-variant="inverse-warning"] { +.btn-inverse-warning { --pgn-btn-color: var(--pgn-color-btn-text-inverse-warning); --pgn-btn-bg: var(--pgn-color-btn-bg-inverse-warning); --pgn-btn-border-color: var(--pgn-color-btn-border-inverse-warning); @@ -528,7 +532,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-inverse-warning); } -.btn[data-variant="light"] { +.btn-light { --pgn-btn-color: var(--pgn-color-btn-text-light); --pgn-btn-bg: var(--pgn-color-btn-bg-light); --pgn-btn-border-color: var(--pgn-color-btn-border-light); @@ -547,7 +551,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-light); } -.btn[data-variant="outline-brand"] { +.btn-outline-brand { --pgn-btn-color: var(--pgn-color-btn-text-outline-brand); --pgn-btn-bg: var(--pgn-color-btn-bg-outline-brand); --pgn-btn-border-color: var(--pgn-color-btn-border-outline-brand); @@ -566,7 +570,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-brand); } -.btn[data-variant="outline-danger"] { +.btn-outline-danger { --pgn-btn-color: var(--pgn-color-btn-text-outline-danger); --pgn-btn-bg: var(--pgn-color-btn-bg-outline-danger); --pgn-btn-border-color: var(--pgn-color-btn-border-outline-danger); @@ -585,7 +589,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-danger); } -.btn[data-variant="outline-dark"] { +.btn-outline-dark { --pgn-btn-color: var(--pgn-color-btn-text-outline-dark); --pgn-btn-bg: var(--pgn-color-btn-bg-outline-dark); --pgn-btn-border-color: var(--pgn-color-btn-border-outline-dark); @@ -604,7 +608,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-dark); } -.btn[data-variant="outline-info"] { +.btn-outline-info { --pgn-btn-color: var(--pgn-color-btn-text-outline-info); --pgn-btn-bg: var(--pgn-color-btn-bg-outline-info); --pgn-btn-border-color: var(--pgn-color-btn-border-outline-info); @@ -623,7 +627,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-info); } -.btn[data-variant="outline-light"] { +.btn-outline-light { --pgn-btn-color: var(--pgn-color-btn-text-outline-light); --pgn-btn-bg: var(--pgn-color-btn-bg-outline-light); --pgn-btn-border-color: var(--pgn-color-btn-border-outline-light); @@ -642,7 +646,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-light); } -.btn[data-variant="outline-primary"] { +.btn-outline-primary { --pgn-btn-color: var(--pgn-color-btn-text-outline-primary); --pgn-btn-bg: var(--pgn-color-btn-bg-outline-primary); --pgn-btn-border-color: var(--pgn-color-btn-border-outline-primary); @@ -661,7 +665,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-primary); } -.btn[data-variant="outline-secondary"] { +.btn-outline-secondary { --pgn-btn-color: var(--pgn-color-btn-text-outline-secondary); --pgn-btn-bg: var(--pgn-color-btn-bg-outline-secondary); --pgn-btn-border-color: var(--pgn-color-btn-border-outline-secondary); @@ -680,7 +684,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-secondary); } -.btn[data-variant="outline-success"] { +.btn-outline-success { --pgn-btn-color: var(--pgn-color-btn-text-outline-success); --pgn-btn-bg: var(--pgn-color-btn-bg-outline-success); --pgn-btn-border-color: var(--pgn-color-btn-border-outline-success); @@ -699,7 +703,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-success); } -.btn[data-variant="outline-warning"] { +.btn-outline-warning { --pgn-btn-color: var(--pgn-color-btn-text-outline-warning); --pgn-btn-bg: var(--pgn-color-btn-bg-outline-warning); --pgn-btn-border-color: var(--pgn-color-btn-border-outline-warning); @@ -718,7 +722,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-outline-warning); } -.btn[data-variant="primary"] { +.btn-primary { --pgn-btn-color: var(--pgn-color-btn-text-primary); --pgn-btn-bg: var(--pgn-color-btn-bg-primary); --pgn-btn-border-color: var(--pgn-color-btn-border-primary); @@ -737,7 +741,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-primary); } -.btn[data-variant="secondary"] { +.btn-secondary { --pgn-btn-color: var(--pgn-color-btn-text-secondary); --pgn-btn-bg: var(--pgn-color-btn-bg-secondary); --pgn-btn-border-color: var(--pgn-color-btn-border-secondary); @@ -756,7 +760,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-secondary); } -.btn[data-variant="success"] { +.btn-success { --pgn-btn-color: var(--pgn-color-btn-text-success); --pgn-btn-bg: var(--pgn-color-btn-bg-success); --pgn-btn-border-color: var(--pgn-color-btn-border-success); @@ -775,7 +779,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-success); } -.btn[data-variant="tertiary"] { +.btn-tertiary { --pgn-btn-color: var(--pgn-color-btn-text-tertiary); --pgn-btn-bg: var(--pgn-color-btn-bg-tertiary); --pgn-btn-border-color: var(--pgn-color-btn-border-tertiary); @@ -794,7 +798,7 @@ --pgn-btn-focus-bg: var(--pgn-color-btn-focus-bg-tertiary); } -.btn[data-variant="warning"] { +.btn-warning { --pgn-btn-color: var(--pgn-color-btn-text-warning); --pgn-btn-bg: var(--pgn-color-btn-bg-warning); --pgn-btn-border-color: var(--pgn-color-btn-border-warning); @@ -814,20 +818,20 @@ } /* Icon slots */ -.iconBefore { +.btn-icon-before { display: inline-flex; margin-inline-end: .5rem; margin-inline-start: -.25em; } -.iconAfter { +.btn-icon-after { display: inline-flex; margin-inline-start: .5rem; margin-inline-end: -.25em; } /* Link-styled button */ -.link { +.btn-link { font-weight: var(--pgn-typography-font-weight-normal); color: var(--pgn-color-link-base); background-color: transparent; @@ -835,112 +839,112 @@ text-decoration: var(--pgn-typography-link-decoration-base); } -.link:hover:not(:disabled) { +.btn-link:hover:not(:disabled) { color: var(--pgn-color-link-hover); background-color: transparent; border-color: transparent; text-decoration: var(--pgn-typography-link-decoration-hover); } -.link:disabled, -.link[aria-disabled="true"] { +.btn-link:disabled, +.btn-link[aria-disabled="true"] { color: var(--pgn-color-btn-disabled-link); } /* Sizes */ -.lg { +.btn-lg { padding: var(--pgn-spacing-btn-padding-y-lg) var(--pgn-spacing-btn-padding-x-lg); font-size: var(--pgn-typography-btn-font-size-lg); line-height: var(--pgn-typography-btn-line-height-lg); border-radius: var(--pgn-size-btn-border-radius-lg); } -.lg[data-focus-visible]::before { +.btn-lg[data-focus-visible]::before { border-radius: var(--pgn-size-btn-focus-border-radius-lg); } -.sm { +.btn-sm { padding: var(--pgn-spacing-btn-padding-y-sm) var(--pgn-spacing-btn-padding-x-sm); font-size: var(--pgn-typography-btn-font-size-sm); line-height: var(--pgn-typography-btn-line-height-sm); border-radius: var(--pgn-size-btn-border-radius-sm); } -.sm[data-focus-visible]::before { +.btn-sm[data-focus-visible]::before { border-radius: var(--pgn-size-btn-focus-border-radius-sm); } -.inline { +.btn-inline { line-height: calc(var(--pgn-typography-line-height-base) * 1em - 2 * var(--pgn-size-btn-border-width)); font-size: inherit; vertical-align: bottom; padding: 0 .25em; } -.block { +.btn-block { display: flex; width: 100%; } -.block + .block { +.btn-block + .btn-block { margin-top: var(--pgn-spacing-btn-block-spacing-y); } /* Button group & toolbar */ -.btnGroup { +.btn-group { position: relative; display: inline-flex; vertical-align: middle; } -.btnGroup > .btn { +.btn-group > .btn { position: relative; flex: 1 1 auto; } -.btnGroup > .btn:not(:first-child) { +.btn-group > .btn:not(:first-child) { margin-inline-start: calc(var(--pgn-size-btn-border-width) * -1); } /* Collapse inner border radii so grouped buttons read as one control. */ -.btnGroup > .btn:not(:first-child) { +.btn-group > .btn:not(:first-child) { border-start-start-radius: 0; border-end-start-radius: 0; } -.btnGroup > .btn:not(:last-child) { +.btn-group > .btn:not(:last-child) { border-start-end-radius: 0; border-end-end-radius: 0; } -.btnGroup > .btn:hover, -.btnGroup > .btn[data-focus-visible], -.btnGroup > .btn[data-pressed] { +.btn-group > .btn:hover, +.btn-group > .btn[data-focus-visible], +.btn-group > .btn[data-pressed] { z-index: 1; } -.btnGroupVertical { +.btn-group-vertical { flex-direction: column; align-items: flex-start; } -.btnGroupVertical > .btn { +.btn-group-vertical > .btn { width: 100%; } -.btnGroupVertical > .btn:not(:first-child) { +.btn-group-vertical > .btn:not(:first-child) { margin-inline-start: 0; margin-top: calc(var(--pgn-size-btn-border-width) * -1); border-start-start-radius: 0; border-start-end-radius: 0; } -.btnGroupVertical > .btn:not(:last-child) { +.btn-group-vertical > .btn:not(:last-child) { border-end-start-radius: 0; border-end-end-radius: 0; } -.btnToolbar { +.btn-toolbar { display: flex; flex-wrap: wrap; justify-content: flex-start; diff --git a/prototype/src/Collapsible/Collapsible.module.css b/prototype/src/styles/collapsible.css similarity index 54% rename from prototype/src/Collapsible/Collapsible.module.css rename to prototype/src/styles/collapsible.css index 815b38f4d46..b9f2d8ade1c 100644 --- a/prototype/src/Collapsible/Collapsible.module.css +++ b/prototype/src/styles/collapsible.css @@ -1,61 +1,61 @@ /* - * Collapsible styles — a CSS Modules port of src/Collapsible/index.scss. + * Collapsible class layer - global, public classes. * - * Every value still comes from the same `--pgn-*` design tokens; nothing is - * hard-coded. Differences from the Bootstrap-era SCSS: - * - Class names are locally scoped (no global `.collapsible-card` leakage); - * the structural `.panel`/`.focusRing` classes are applied by the - * components, and the `.card`/`.basic` style presets are applied by the - * convenience `Collapsible`. - * - `.card`/`.card-body` no longer `@extend` Bootstrap's `.card`; the handful - * of card properties actually used are inlined from the same card tokens. - * - The body's expand/collapse animation is driven by React Aria's - * `--disclosure-panel-height` custom property instead of the JS-based - * `Collapse` / `TransitionReplace` components. - * - Focus is driven by React Aria's `[data-focus-visible]` instead of `:focus`. - * - Logical properties (`margin-inline-start`) give RTL support without a - * `[dir=rtl]` override. + * The `collapsible-card`, `collapsible-card-lg`, `collapsible-basic`, + * `collapsible-trigger`, `collapsible-body` and `collapsible-icon` class names + * are part of Paragon's PUBLIC API - `Collapsible.Advanced` consumers apply them + * directly to their own markup - so they stay global and stable (ADR 0022). + * The `pgn__collapsible-*` classes are prototype-internal (namespaced) and are + * applied by the components; they are not a consumer contract. + * + * Ported from src/Collapsible/index.scss to plain CSS over the same `--pgn-*` + * tokens. `.collapsible-card`/`.collapsible-card-lg` no longer `@extend` + * Bootstrap's `.card`; the handful of card properties actually used are inlined + * from the same card tokens. The expand/collapse animation is driven by React + * Aria's `--disclosure-panel-height` custom property (see CollapsibleBody) + * instead of the JS-based `Collapse` / `TransitionReplace` components. */ -.collapsible { - /* The composable root; unstyled by default like Paragon's CollapsibleAdvanced. */ +.pgn_collapsible { text-align: start; } /* - * The disclosure panel. React Aria's `useDisclosure` sets + * The disclosure panel (internal). React Aria's `useDisclosure` sets * `--disclosure-panel-height` on this element (a pixel value while animating, * then `auto`) and adds `hidden="until-found"` once fully collapsed. We animate * `height` from that property; `box-sizing: border-box` keeps the body's own * padding inside the animated height so it collapses cleanly to zero. */ -.panel { +.pgn__collapsible-panel { box-sizing: border-box; overflow: hidden; height: var(--disclosure-panel-height, auto); } @media (prefers-reduced-motion: no-preference) { - .panel { + .pgn__collapsible-panel { transition: height 300ms ease; } } /* Keyboard-only focus ring for the (div-based) trigger, mirroring Button. */ -.focusRing[data-focus-visible] { +.pgn__collapsible-focus[data-focus-visible] { outline: var(--pgn-size-btn-focus-width, 2px) solid var(--pgn-color-primary-base); outline-offset: 2px; } -/* ------------------------------------------------------------------ card --- */ +/* --------------------------------------------------------- card / card-lg --- */ -.card { +.collapsible-card, +.collapsible-card-lg { background-color: var(--pgn-color-card-bg-base); border: var(--pgn-size-card-border-width) solid var(--pgn-color-card-border-base); border-radius: 0; } -.card .trigger { +.collapsible-card .collapsible-trigger, +.collapsible-card-lg .collapsible-trigger { padding: var(--pgn-spacing-collapsible-card-spacer-y-base) var(--pgn-spacing-collapsible-card-spacer-x-base); border-radius: var(--pgn-size-card-border-radius-inner); border-bottom: var(--pgn-size-card-border-width) solid transparent; @@ -67,40 +67,43 @@ text-align: start; } -.card .trigger > * { +.collapsible-card .collapsible-trigger > *, +.collapsible-card-lg .collapsible-trigger > * { margin-top: 0; margin-bottom: 0; } -.card .trigger[aria-expanded="true"] { +.collapsible-card .collapsible-trigger[aria-expanded="true"], +.collapsible-card-lg .collapsible-trigger[aria-expanded="true"] { border-radius: var(--pgn-size-card-border-radius-inner) var(--pgn-size-card-border-radius-inner) 0 0; border-color: var(--pgn-color-card-border-base); transition: none; } -.card .body { +.collapsible-card .collapsible-body, +.collapsible-card-lg .collapsible-body { padding: var(--pgn-spacing-collapsible-card-spacer-y-base) var(--pgn-spacing-collapsible-card-spacer-x-base); padding-left: var(--pgn-spacing-collapsible-card-spacer-left-body); text-align: start; } -.card .body > *:last-child { +.collapsible-card .collapsible-body > *:last-child, +.collapsible-card-lg .collapsible-body > *:last-child { margin-bottom: 0; } -/* --------------------------------------------------------------- card-lg --- */ - -.cardLg .trigger { +/* card-lg overrides only the paddings (Paragon's `@extend .collapsible-card`). */ +.collapsible-card-lg .collapsible-trigger { padding: var(--pgn-spacing-collapsible-card-spacer-y-lg) var(--pgn-spacing-collapsible-card-spacer-x-lg); } -.cardLg .body { +.collapsible-card-lg .collapsible-body { padding: var(--pgn-spacing-collapsible-card-spacer-y-lg) var(--pgn-spacing-collapsible-card-spacer-x-lg); } -/* ----------------------------------------------------------------- basic --- */ +/* ------------------------------------------------------------------ basic --- */ -.basic .trigger { +.collapsible-basic .collapsible-trigger { display: flex; cursor: pointer; align-items: center; @@ -109,35 +112,19 @@ padding: var(--pgn-spacing-collapsible-card-spacer-basic-y) var(--pgn-spacing-collapsible-card-spacer-basic-x); } -.basic .body { +.collapsible-basic .collapsible-body { padding: var(--pgn-spacing-collapsible-card-spacer-basic-y) var(--pgn-spacing-collapsible-card-spacer-basic-x); text-align: start; } /* ------------------------------------------------------------------ icon --- */ -.icon { +.collapsible-icon { display: inline-flex; align-items: center; margin-inline-start: var(--pgn-spacing-collapsible-card-spacer-icon); } -.basic .icon { +.collapsible-basic .collapsible-icon { margin-inline-start: var(--pgn-spacing-collapsible-card-spacer-basic-icon); } - -/* - * A convenience "outline-primary" button look for the docs' close-button - * example — a token-driven stand-in for the `btn btn-outline-primary` utility - * classes the original README uses, exposed to live examples via `collapsibleStyles`. - */ -.closeButton { - margin-top: 1rem; - padding: var(--pgn-spacing-btn-padding-y-base) var(--pgn-spacing-btn-padding-x-base); - border: var(--pgn-size-btn-border-width) solid var(--pgn-color-btn-border-outline-primary); - border-radius: var(--pgn-size-btn-border-radius-base); - background-color: transparent; - color: var(--pgn-color-btn-text-outline-primary); - font: inherit; - cursor: pointer; -} diff --git a/prototype/src/styles/index.css b/prototype/src/styles/index.css new file mode 100644 index 00000000000..a89e1b27e3f --- /dev/null +++ b/prototype/src/styles/index.css @@ -0,0 +1,9 @@ +/* + * The global, public class layer: Bootstrap-compatible component and utility + * classes that consuming MFEs apply to their own markup (see ADR 0022). Kept + * separate from the token CSS and from each component's internal CSS Modules so + * the public class contract is one importable stylesheet. + */ +@import "./utilities.css"; +@import "./button.css"; +@import "./collapsible.css"; diff --git a/prototype/src/styles/utilities.css b/prototype/src/styles/utilities.css new file mode 100644 index 00000000000..c5b0c67a2ac --- /dev/null +++ b/prototype/src/styles/utilities.css @@ -0,0 +1,23 @@ +/* + * Utility class layer - global, public, Bootstrap-compatible classes. + * + * Like the button classes, Paragon's layout/spacing utilities (d-flex, + * flex-grow-1, ...) are part of the PUBLIC API: consuming MFEs apply them to + * their own markup, so they must stay global and stable (ADR 0022). Ported from + * Bootstrap to plain CSS; `:where()` keeps their specificity at zero so a + * component's own rules always win, matching Bootstrap's utility behaviour. + * + * This prototype ships only the small, representative subset the migrated + * components' docs exercise. The full set Paragon ships is generated from the + * same token scale and pruned against `dependent-usage.json` (see ADR 0022). + */ + +:where(.d-flex) { display: flex; } +:where(.d-inline-flex) { display: inline-flex; } +:where(.d-block) { display: block; } +:where(.d-none) { display: none; } + +:where(.flex-grow-1) { flex-grow: 1; } +:where(.flex-column) { flex-direction: column; } +:where(.align-items-center) { align-items: center; } +:where(.justify-content-between) { justify-content: space-between; } From 322f49b8935e2ad27418f395e863b898d68a9a07 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 22 Jul 2026 11:22:27 -0700 Subject: [PATCH 16/24] docs: make all component docs use consistently --- prototype/src/Button/Button.stories.tsx | 6 +- prototype/src/Button/ButtonGroup.mdx | 15 +-- prototype/src/Button/ButtonGroup.stories.tsx | 16 +++- prototype/src/Collapsible/Collapsible.mdx | 8 +- .../src/Collapsible/Collapsible.stories.tsx | 12 ++- prototype/src/Stack/Stack.stories.tsx | 16 +++- prototype/src/docs/LivePlayground.tsx | 94 +++++++++++++++---- prototype/src/docs/live.test.tsx | 38 +++++--- 8 files changed, 142 insertions(+), 63 deletions(-) diff --git a/prototype/src/Button/Button.stories.tsx b/prototype/src/Button/Button.stories.tsx index d03d88bd957..b0648e37c43 100644 --- a/prototype/src/Button/Button.stories.tsx +++ b/prototype/src/Button/Button.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { Button } from './Button'; import { Stack } from '../Stack'; import type { BaseVariant } from './types'; -import { LivePlayground } from '../docs/LivePlayground'; +import { LivePlayground, generateCode } from '../docs/LivePlayground'; import { Add, Remove, ArrowBack, ArrowDropDown, Highlight, } from '../docs/scope'; @@ -58,7 +58,9 @@ type Story = StoryObj; * a control rewrites the editor; you can also edit the JSX directly. */ export const Playground: Story = { - render: (args) => , + render: (args) => ( + + ), }; /* diff --git a/prototype/src/Button/ButtonGroup.mdx b/prototype/src/Button/ButtonGroup.mdx index 767f2b24b29..8ba2f94f7a7 100644 --- a/prototype/src/Button/ButtonGroup.mdx +++ b/prototype/src/Button/ButtonGroup.mdx @@ -20,22 +20,13 @@ group works in both LTR and RTL. `ButtonToolbar` (ARIA `role="toolbar"`) lays out one or more groups. -## Basic usage +## Playground -```tsx -import { Button, ButtonGroup } from '@openedx/paragon'; - - - - - - -``` +Edit the props with the controls and the JSX below regenerates; you can also type +directly in the editor to experiment. Changing a control overwrites the editor. -## Props - ## Sizes diff --git a/prototype/src/Button/ButtonGroup.stories.tsx b/prototype/src/Button/ButtonGroup.stories.tsx index 85a2ff6bc97..6182980072b 100644 --- a/prototype/src/Button/ButtonGroup.stories.tsx +++ b/prototype/src/Button/ButtonGroup.stories.tsx @@ -3,6 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { Button } from './Button'; import { ButtonGroup } from './ButtonGroup'; import { ButtonToolbar } from './ButtonToolbar'; +import { LivePlayground, generateCode } from '../docs/LivePlayground'; import { extractExamples } from '../docs/extractExamples'; import raw from './ButtonGroup.stories.tsx?raw'; @@ -27,11 +28,16 @@ type Story = StoryObj; export const Playground: Story = { render: (args) => ( - - - - - + Bold', + '', + '', + ].join('\n'), + })} + /> ), }; diff --git a/prototype/src/Collapsible/Collapsible.mdx b/prototype/src/Collapsible/Collapsible.mdx index 150fad7348a..0408a955429 100644 --- a/prototype/src/Collapsible/Collapsible.mdx +++ b/prototype/src/Collapsible/Collapsible.mdx @@ -45,10 +45,10 @@ When to use: The sections below mirror the existing Paragon docs (src/Collapsible/README.md) 1:1 and in the same order. The examples are authored as real TSX in Collapsible.stories.tsx (so TypeScript typechecks every prop) and their source - is rendered here, editable, via react-live. The component style classes - (collapsible-card / -trigger / -body) and the utility / button classes - (d-flex, flex-grow-1, btn btn-outline-primary) are all part of Paragon's global - public class layer (src/styles/*.css), so these snippets are verbatim README. + is rendered here, editable, via react-live. The component's own style classes + (collapsible-card / -trigger / -body) map to this module's classes, exposed to + the live scope as `collapsibleStyles`; the Bootstrap utilities become inline + styles / the `collapsibleStyles.closeButton` helper. */} ## Basic Usage diff --git a/prototype/src/Collapsible/Collapsible.stories.tsx b/prototype/src/Collapsible/Collapsible.stories.tsx index fb77894a5d9..a82a612cdea 100644 --- a/prototype/src/Collapsible/Collapsible.stories.tsx +++ b/prototype/src/Collapsible/Collapsible.stories.tsx @@ -4,6 +4,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { Collapsible } from './Collapsible'; import type { CollapsibleStyling } from './Collapsible'; import { ExamplePropsForm } from '../docs/ExamplePropsForm'; +import { LivePlayground, generateCode } from '../docs/LivePlayground'; import { extractExamples } from '../docs/extractExamples'; import raw from './Collapsible.stories.tsx?raw'; @@ -19,7 +20,7 @@ const meta: Meta = { parameters: { layout: 'padded' }, args: { title: 'Toggle Collapsible', - styling: 'card', + styling: 'basic', defaultOpen: false, }, argTypes: { @@ -34,9 +35,12 @@ type Story = StoryObj; export const Playground: Story = { render: (args) => ( - -

Your stuff goes here.

-
+ Your stuff goes here.

', + })} + /> ), }; diff --git a/prototype/src/Stack/Stack.stories.tsx b/prototype/src/Stack/Stack.stories.tsx index c614ad92636..b4e8ff7e016 100644 --- a/prototype/src/Stack/Stack.stories.tsx +++ b/prototype/src/Stack/Stack.stories.tsx @@ -2,6 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { Stack } from './Stack'; import { Button } from '../Button'; +import { LivePlayground, generateCode } from '../docs/LivePlayground'; import { extractExamples } from '../docs/extractExamples'; import raw from './Stack.stories.tsx?raw'; @@ -34,11 +35,16 @@ type Story = StoryObj; export const Playground: Story = { render: (args) => ( - - - - - + first button', + '', + '', + ].join('\n'), + })} + /> ), }; diff --git a/prototype/src/docs/LivePlayground.tsx b/prototype/src/docs/LivePlayground.tsx index 4506bc32480..e2673ed1bab 100644 --- a/prototype/src/docs/LivePlayground.tsx +++ b/prototype/src/docs/LivePlayground.tsx @@ -2,34 +2,90 @@ import { LiveProvider, LiveEditor, LivePreview, LiveError, } from 'react-live'; -import type { ButtonProps } from '../Button/types'; import { scope } from './scope'; import styles from './live.module.css'; -/** Serializes the current Button args into a JSX string shown in the editor. */ -export function generateCode(args: Partial): string { - const attrs: string[] = [`variant="${args.variant ?? 'primary'}"`]; - if (args.size && args.size !== 'md') attrs.push(`size="${args.size}"`); - if (args.disabled) attrs.push('disabled'); - if (args.block) attrs.push('block'); +export interface GenerateCodeOptions { + /** + * Prop values equal to these are treated as the component's default and are + * dropped from the generated JSX (e.g. Button's `size="md"`, Stack's + * `direction="vertical"`), keeping the snippet minimal. + */ + defaults?: Record; + /** + * Literal JSX to place between the open and close tags. When it spans multiple + * lines the element is formatted as an indented block. If omitted, a string + * `children` prop is used as the text body (the Button case); with neither the + * element is self-closed. + */ + children?: string; +} + +/** + * Serializes a component name + its story args into an editable JSX string. + * + * This is deliberately component-agnostic: every prototype component's + * playground feeds its own args, defaults and children template through here, so + * there is no per-component code-generation logic hard-coded in the docs layer. + * + * Serialization rules (matching how the examples are hand-authored): + * - a prop whose value equals a supplied default is omitted; + * - `undefined` / `null` / `false` props are omitted; + * - `true` renders as a bare attribute (`disabled`); + * - strings render as `name="value"`, everything else as `name={value}`. + */ +export function generateCode( + component: string, + // `object`, not `Record`, so a component's concrete args type + // (which has no index signature) can be passed without a cast at the call site. + props: object, + options: GenerateCodeOptions = {}, +): string { + const { defaults = {}, children } = options; + const record = props as Record; + + const attrs: string[] = []; + for (const [name, value] of Object.entries(record)) { + if (name === 'children') continue; + if (name in defaults && defaults[name] === value) continue; + if (value === undefined || value === null || value === false) continue; + if (value === true) { + attrs.push(name); + } else if (typeof value === 'string') { + attrs.push(`${name}="${value}"`); + } else { + attrs.push(`${name}={${JSON.stringify(value)}}`); + } + } + const attrStr = attrs.length ? ` ${attrs.join(' ')}` : ''; - const label = typeof args.children === 'string' && args.children.length - ? args.children - : 'Button'; + const body = children ?? (typeof record.children === 'string' ? record.children : ''); + if (!body) return `<${component}${attrStr} />`; + if (body.includes('\n')) { + const indented = body.split('\n').map((l) => (l ? ` ${l}` : l)).join('\n'); + return `<${component}${attrStr}>\n${indented}\n`; + } + return `<${component}${attrStr}>${body}`; +} - const attrStr = attrs.join(' '); - return ``; +export interface LivePlaygroundProps { + /** The generated JSX snippet — rendered live and shown in the editor. */ + code: string; } /** - * The Playground example: a live editor whose JSX is generated from the story's - * args. Because Storybook's `` block edits those same args, changing a - * control regenerates the code and — via the `key` below — remounts the editor, - * overwriting whatever was typed. Direct edits in the editor persist until the - * next control change. + * The Playground example: a live editor whose JSX is generated (by + * `generateCode`) from a story's args. Because Storybook's `` block + * edits those same args, changing a control regenerates the code and — via the + * `key` below — remounts the editor, overwriting whatever was typed. Direct edits + * in the editor persist until the next control change. + * + * It is intentionally component-agnostic: it just renders whatever `code` string + * it is handed, so the same playground drives Button, ButtonGroup, Collapsible, + * Stack, and any future prototype component (everything the snippet references + * must be in `scope`). */ -export function LivePlayground(args: Partial) { - const code = generateCode(args); +export function LivePlayground({ code }: LivePlaygroundProps) { return ( // `sb-unstyled`: opt out of Storybook's docs font reset (see LiveExample).
diff --git a/prototype/src/docs/live.test.tsx b/prototype/src/docs/live.test.tsx index db4a92367cc..90eecf5a5ba 100644 --- a/prototype/src/docs/live.test.tsx +++ b/prototype/src/docs/live.test.tsx @@ -20,27 +20,41 @@ describe('LiveExample', () => { }); describe('LivePlayground', () => { - it('generates JSX from args and renders the matching Button', async () => { - render(Go); + it('renders the provided code string as a live preview', async () => { + render(Go'} />); const btn = await screen.findByRole('button', { name: 'Go' }); expect(btn).toHaveClass('btn', 'btn-danger'); }); - }); describe('generateCode (Controls → JSX)', () => { - it('serializes variant plus a non-default size and label', () => { - expect(generateCode({ variant: 'success', size: 'sm', children: 'Save' })) - .toBe(''); + it('serializes props, omitting defaults, with a string body from children', () => { + expect(generateCode('Button', { variant: 'success', size: 'sm', children: 'Save' }, { + defaults: { size: 'md' }, + })).toBe(''); + }); + + it('omits a prop whose value matches the supplied default', () => { + expect(generateCode('Button', { variant: 'primary', size: 'md', children: 'Default' }, { + defaults: { size: 'md' }, + })).toBe(''); + }); + + it('renders booleans bare and serializes numbers with braces', () => { + expect(generateCode('Stack', { direction: 'vertical', gap: 3, reversed: true }, { + defaults: { direction: 'vertical' }, + children: '', + })).toBe(''); }); - it('omits the size attribute for the default md size', () => { - expect(generateCode({ variant: 'primary', size: 'md', children: 'Default' })) - .toBe(''); + it('formats multi-line children as an indented block', () => { + expect(generateCode('ButtonGroup', { 'aria-label': 'X' }, { + children: '\n', + })).toBe('\n \n \n'); }); - it('includes boolean flags and falls back to a default label', () => { - expect(generateCode({ variant: 'danger', disabled: true, block: true })) - .toBe(''); + it('self-closes when there is no body', () => { + expect(generateCode('Button', { variant: 'danger', disabled: true })) + .toBe('', '', '', - ].join('\n'), + ].join('\n')), })} /> ), diff --git a/prototype/src/Collapsible/Collapsible.stories.tsx b/prototype/src/Collapsible/Collapsible.stories.tsx index 683b83e2fb9..0f3e46cbcb2 100644 --- a/prototype/src/Collapsible/Collapsible.stories.tsx +++ b/prototype/src/Collapsible/Collapsible.stories.tsx @@ -4,7 +4,9 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { Collapsible } from './Collapsible'; import type { CollapsibleStyling } from './Collapsible'; import { ExamplePropsForm } from '../docs/ExamplePropsForm'; -import { LivePlayground, generateCode, raw as rawExpr } from '../docs/LivePlayground'; +import { + LivePlayground, generateCode, raw as rawExpr, playgroundChildren, +} from '../docs/LivePlayground'; import { extractExamples } from '../docs/extractExamples'; import raw from './Collapsible.stories.tsx?raw'; @@ -29,6 +31,7 @@ const meta: Meta = { title: { control: 'text' }, styling: { control: 'inline-radio', options: ['basic', 'card', 'card-lg'] }, defaultOpen: { control: 'boolean' }, + children: { control: 'text' }, // The icon slots take JSX, but the control only carries a choice string that // the Playground maps to the JSX shown in the generated snippet (see below). iconWhenClosed: { @@ -68,7 +71,7 @@ export const Playground: Story = { iconWhenOpen: iconProp(args.iconWhenOpen as IconChoice, '', 'Close'), }, { defaults: { styling: 'card', defaultOpen: false }, - children: '

Your stuff goes here.

', + children: playgroundChildren(args.children, '

Your stuff goes here.

'), })} /> ), diff --git a/prototype/src/Stack/Stack.stories.tsx b/prototype/src/Stack/Stack.stories.tsx index b4e8ff7e016..3a4b6e2d5f9 100644 --- a/prototype/src/Stack/Stack.stories.tsx +++ b/prototype/src/Stack/Stack.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { Stack } from './Stack'; import { Button } from '../Button'; -import { LivePlayground, generateCode } from '../docs/LivePlayground'; +import { LivePlayground, generateCode, playgroundChildren } from '../docs/LivePlayground'; import { extractExamples } from '../docs/extractExamples'; import raw from './Stack.stories.tsx?raw'; @@ -27,6 +27,9 @@ const meta: Meta = { }, }, reversed: { control: 'boolean' }, + // Plain-text children make no sense here (a Stack lays out elements); hide the + // control rather than let Storybook auto-generate a ReactNode "Set object" one. + children: { table: { disable: true } }, }, }; @@ -38,11 +41,11 @@ export const Playground: Story = { first button', '', '', - ].join('\n'), + ].join('\n')), })} /> ), diff --git a/prototype/src/docs/LivePlayground.tsx b/prototype/src/docs/LivePlayground.tsx index 71608e4489a..6ae4557be4c 100644 --- a/prototype/src/docs/LivePlayground.tsx +++ b/prototype/src/docs/LivePlayground.tsx @@ -89,6 +89,16 @@ export function generateCode( return `<${component}${attrStr}>${body}`; } +/** + * Resolves a Playground's optional `children` text arg into the JSX-source string + * for `generateCode`'s `children` option: when the control has been set to a + * non-empty string it is used verbatim (so a consumer can type text or JSX); + * otherwise the component's default example template is kept. + */ +export function playgroundChildren(children: unknown, defaultTemplate: string): string { + return typeof children === 'string' && children.trim() ? children : defaultTemplate; +} + export interface LivePlaygroundProps { /** The generated JSX snippet — rendered live and shown in the editor. */ code: string; diff --git a/prototype/src/docs/live.test.tsx b/prototype/src/docs/live.test.tsx index bca231f9fdc..890102e0684 100644 --- a/prototype/src/docs/live.test.tsx +++ b/prototype/src/docs/live.test.tsx @@ -2,7 +2,9 @@ import { render, screen } from '@testing-library/react'; import { describe, it, expect } from 'vitest'; import { LiveExample } from './LiveExample'; -import { LivePlayground, generateCode, raw } from './LivePlayground'; +import { + LivePlayground, generateCode, raw, playgroundChildren, +} from './LivePlayground'; // react-live computes the preview element in an effect, so the rendered Button // appears after the initial commit — hence the async `findBy*` queries. @@ -64,3 +66,15 @@ describe('generateCode (Controls → JSX)', () => { })).toBe('}>

Body

'); }); }); + +describe('playgroundChildren (optional children override)', () => { + it('uses a non-empty string value verbatim', () => { + expect(playgroundChildren('', '

Default

')) + .toBe(''); + }); + + it('falls back to the default template when unset or blank', () => { + expect(playgroundChildren(undefined, '

Default

')).toBe('

Default

'); + expect(playgroundChildren(' ', '

Default

')).toBe('

Default

'); + }); +}); From 6b8d8320766dc27e026d6446d49dd58b9dc7c014 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 22 Jul 2026 11:56:19 -0700 Subject: [PATCH 19/24] docs: we don't need "ExamplePropsForm" --- .../src/Collapsible/Collapsible.stories.tsx | 43 ++--------- prototype/src/docs/ExamplePropsForm.tsx | 72 ------------------- prototype/src/docs/exampleForm.module.css | 47 ------------ prototype/src/docs/scope.tsx | 2 - 4 files changed, 7 insertions(+), 157 deletions(-) delete mode 100644 prototype/src/docs/ExamplePropsForm.tsx delete mode 100644 prototype/src/docs/exampleForm.module.css diff --git a/prototype/src/Collapsible/Collapsible.stories.tsx b/prototype/src/Collapsible/Collapsible.stories.tsx index 0f3e46cbcb2..7f2ea41e281 100644 --- a/prototype/src/Collapsible/Collapsible.stories.tsx +++ b/prototype/src/Collapsible/Collapsible.stories.tsx @@ -1,9 +1,7 @@ -import React, { useState } from 'react'; +import React from 'react'; import type { Meta, StoryObj } from '@storybook/react-vite'; import { Collapsible } from './Collapsible'; -import type { CollapsibleStyling } from './Collapsible'; -import { ExamplePropsForm } from '../docs/ExamplePropsForm'; import { LivePlayground, generateCode, raw as rawExpr, playgroundChildren, } from '../docs/LivePlayground'; @@ -105,39 +103,12 @@ export const basicStyle = ( ); export const cardStyle = ( - () => { - const [styling, setStyling] = useState('card'); - const [withIcon, setWithIcon] = useState(false); - const iconProps = { - iconWhenOpen: CLOSE SESAME, - iconWhenClosed: OPEN SESAME, - }; - - return ( - <> - {/* start example form block */} - setStyling(value as CollapsibleStyling), - options: ['card', 'card-lg'], - name: 'styling', - }, - { value: withIcon, setValue: setWithIcon, name: 'with icon' }, - ]} - /> - {/* end example form block */} - Toggle Collapsible

} - {...withIcon ? iconProps : {}} - > -

Your stuff goes here.

-
- - ); - } + Toggle Collapsible

} + > +

Your stuff goes here.

+
); export const defaultOpen = ( diff --git a/prototype/src/docs/ExamplePropsForm.tsx b/prototype/src/docs/ExamplePropsForm.tsx deleted file mode 100644 index a94f7fcc92b..00000000000 --- a/prototype/src/docs/ExamplePropsForm.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import styles from './exampleForm.module.css'; - -interface RadioInput { - name: string; - value: string; - setValue: (value: string) => void; - options: string[]; -} - -interface SwitchInput { - name: string; - value: boolean; - setValue: (value: boolean) => void; - options?: undefined; -} - -export type ExampleInput = RadioInput | SwitchInput; - -export interface ExamplePropsFormProps { - inputs: ExampleInput[]; -} - -/** - * A tiny props panel for live docs examples — a dependency-free stand-in for the - * `ExamplePropsForm` used by Paragon's current Gatsby docs (which is built from - * the `Form` components). Each input is either a radio set (`options`) or a - * boolean switch, driven by the example's own `useState`. - */ -export function ExamplePropsForm({ inputs }: ExamplePropsFormProps) { - return ( -
-

Props panel

- {inputs.map((input) => { - if (input.options) { - return ( -
- {input.name} -
- {input.options.map((option) => ( - - ))} -
-
- ); - } - return ( - - ); - })} -
- ); -} - -export default ExamplePropsForm; diff --git a/prototype/src/docs/exampleForm.module.css b/prototype/src/docs/exampleForm.module.css deleted file mode 100644 index ab7d53d100d..00000000000 --- a/prototype/src/docs/exampleForm.module.css +++ /dev/null @@ -1,47 +0,0 @@ -.form { - margin-bottom: 1rem; - padding: 1rem; - border: 1px solid var(--pgn-color-border, #d0d0d0); - border-radius: 6px; - background-color: var(--pgn-color-light-100, #fafafa); -} - -.heading { - margin: 0 0 .75rem; - font-size: .875rem; - font-weight: 700; -} - -.group { - margin: 0 0 .5rem; - padding: 0; - border: 0; -} - -.radioRow { - display: flex; - flex-wrap: wrap; - gap: 1rem; - margin-top: .25rem; -} - -.radioLabel, -.switchLabel { - display: inline-flex; - align-items: center; - gap: .375rem; - cursor: pointer; -} - -.switchLabel { - margin-top: .25rem; -} - -.badge { - display: inline-block; - padding: .125rem .5rem; - border-radius: 4px; - background-color: var(--pgn-color-light-300, #e9e9e9); - font-size: .8125rem; - font-weight: 600; -} diff --git a/prototype/src/docs/scope.tsx b/prototype/src/docs/scope.tsx index fea63481af3..54923e13b6a 100644 --- a/prototype/src/docs/scope.tsx +++ b/prototype/src/docs/scope.tsx @@ -3,7 +3,6 @@ import React, { useState } from 'react'; import { Button, ButtonGroup, ButtonToolbar } from '../Button'; import { Stack } from '../Stack'; import { Collapsible } from '../Collapsible'; -import { ExamplePropsForm } from './ExamplePropsForm'; /** A tiny inline icon so live examples can demonstrate icon slots dependency-free. */ export function DotIcon() { @@ -44,7 +43,6 @@ export const scope = { ButtonToolbar, Stack, Collapsible, - ExamplePropsForm, DotIcon, Add, Remove, From c4e9625d50bb67892acceadf480477e857e0d227 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 22 Jul 2026 12:07:29 -0700 Subject: [PATCH 20/24] docs: example of paragon theme values editor --- prototype/src/Button/Button.mdx | 20 ++ prototype/src/docs/ThemeVariables.tsx | 320 +++++++++++++++++++ prototype/src/docs/themeVariables.module.css | 181 +++++++++++ 3 files changed, 521 insertions(+) create mode 100644 prototype/src/docs/ThemeVariables.tsx create mode 100644 prototype/src/docs/themeVariables.module.css diff --git a/prototype/src/Button/Button.mdx b/prototype/src/Button/Button.mdx index a715e1f029d..15afa8e6d22 100644 --- a/prototype/src/Button/Button.mdx +++ b/prototype/src/Button/Button.mdx @@ -2,6 +2,7 @@ import { Meta, Title, Subtitle, Story, Controls } from '@storybook/addon-docs/bl import * as ButtonStories from './Button.stories'; import { LiveExample } from '../docs/LiveExample'; +import { ThemeVariables } from '../docs/ThemeVariables'; export const examples = ButtonStories.examples; @@ -94,6 +95,25 @@ for a more efficient, user-friendly experience. {/* --- Additions beyond the original README --- */} +## Theme Variables + +Button appearance is authored entirely over Paragon's `--pgn-*` design tokens — +the same set the current docs list under +[Theme variables](https://paragon-openedx.netlify.app/components/button/). Edit +any value below and the preview buttons update instantly, because every `.btn` +rule reads these custom properties at render time. This is exactly how +`@edx/brand` themes Paragon: it re-points these tokens, and the button module +follows along without any component changes. + +The upstream table auto-generates its list by scraping `var(--pgn-…)` references +from Button's SCSS, which means it silently omits every **per-variant colour** +(`--pgn-color-btn-bg-primary`, `-brand`, `-secondary`, …) — in the SCSS those are +written with interpolation (`--pgn-color-btn-bg-#{$variant}`) the scraper's regex +can't match, even though they are the tokens that actually colour each button. +They are included below. + + + ## Grouping Related buttons can be combined with `ButtonGroup` and `ButtonToolbar` — see the diff --git a/prototype/src/docs/ThemeVariables.tsx b/prototype/src/docs/ThemeVariables.tsx new file mode 100644 index 00000000000..57b5c0df185 --- /dev/null +++ b/prototype/src/docs/ThemeVariables.tsx @@ -0,0 +1,320 @@ +import { + useLayoutEffect, useMemo, useRef, useState, + type CSSProperties, +} from 'react'; + +import { Button } from '../Button'; +import styles from './themeVariables.module.css'; + +/** + * The Button module's theme variables, grouped for readability. + * + * This is the same `--pgn-*` custom-property set the current Paragon docs list + * under "Theme Variables" for Button — there, the list is scraped from the + * component's SCSS (every `var(--pgn-…)` reference) and rendered read-only with + * its computed value (see www/src/components/ComponentVariablesTable.tsx). Here + * the identical set is authored explicitly (the prototype has no SCSS to scrape) + * and made editable, so changing a token updates the live preview below. + */ +interface VarGroup { + label: string; + variables: string[]; +} + +export const BUTTON_THEME_VARIABLES: VarGroup[] = [ + { + label: 'Sizing', + variables: [ + '--pgn-size-btn-border-width', + '--pgn-size-btn-border-radius-base', + '--pgn-size-btn-border-radius-sm', + '--pgn-size-btn-border-radius-lg', + '--pgn-size-btn-focus-width', + '--pgn-size-btn-focus-border-radius-base', + '--pgn-size-btn-focus-border-radius-sm', + '--pgn-size-btn-focus-border-radius-lg', + ], + }, + { + label: 'Spacing', + variables: [ + '--pgn-spacing-btn-padding-y-base', + '--pgn-spacing-btn-padding-x-base', + '--pgn-spacing-btn-padding-y-sm', + '--pgn-spacing-btn-padding-x-sm', + '--pgn-spacing-btn-padding-y-lg', + '--pgn-spacing-btn-padding-x-lg', + '--pgn-spacing-btn-block-spacing-y', + '--pgn-spacing-btn-focus-distance-to-border', + ], + }, + { + label: 'Typography', + variables: [ + '--pgn-typography-btn-font-family', + '--pgn-typography-btn-font-weight', + '--pgn-typography-btn-font-size-base', + '--pgn-typography-btn-font-size-sm', + '--pgn-typography-btn-font-size-lg', + '--pgn-typography-btn-line-height-base', + '--pgn-typography-btn-line-height-sm', + '--pgn-typography-btn-line-height-lg', + ], + }, + { + label: 'Base colors', + variables: [ + '--pgn-color-body-base', + '--pgn-color-link-base', + '--pgn-color-link-hover', + ], + }, + // Per-variant colours. The upstream docs' "Theme variables" table omits these: + // it is generated by scraping `var(--pgn-…)` out of Button's SCSS, but there + // each variant's tokens are written with interpolation + // (`--pgn-color-btn-bg-#{$variant}`), which the scraper's regex never matches. + // They are the tokens that actually colour each variant, so they are listed + // here. Note: editing the *generic* `--pgn-btn-bg` would not work — every + // `.btn-` re-declares it locally from the source tokens below, so the + // source token is what has to be overridden. + { + label: 'Brand variant colors', + variables: [ + '--pgn-color-btn-bg-brand', + '--pgn-color-btn-text-brand', + '--pgn-color-btn-border-brand', + ], + }, + { + label: 'Primary variant colors', + variables: [ + '--pgn-color-btn-bg-primary', + '--pgn-color-btn-text-primary', + '--pgn-color-btn-border-primary', + ], + }, + { + label: 'Secondary variant colors', + variables: [ + '--pgn-color-btn-bg-secondary', + '--pgn-color-btn-text-secondary', + '--pgn-color-btn-border-secondary', + ], + }, + { + label: 'Tertiary variant colors', + variables: [ + '--pgn-color-btn-bg-tertiary', + '--pgn-color-btn-text-tertiary', + '--pgn-color-btn-border-tertiary', + ], + }, + { + label: 'Success variant colors', + variables: [ + '--pgn-color-btn-bg-success', + '--pgn-color-btn-text-success', + '--pgn-color-btn-border-success', + ], + }, + { + label: 'Danger variant colors', + variables: [ + '--pgn-color-btn-bg-danger', + '--pgn-color-btn-text-danger', + '--pgn-color-btn-border-danger', + ], + }, + { + label: 'Other', + variables: [ + '--pgn-transition-btn', + '--pgn-other-btn-disabled-opacity', + '--pgn-typography-link-decoration-base', + '--pgn-typography-link-decoration-hover', + ], + }, +]; + +const ALL_VARIABLES = BUTTON_THEME_VARIABLES.flatMap((g) => g.variables); + +/** + * True when a value looks like a colour we can offer a native colour picker for. + * Covers #rgb, #rgba, #rrggbb, #rrggbbaa and rgb()/rgba() — the forms the + * compiled Paragon colour tokens resolve to. + */ +function isColorValue(value: string): boolean { + const v = value.trim(); + return /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(v) || /^rgba?\(/i.test(v); +} + +/** + * Normalises any CSS colour to the `#rrggbb` a native `` + * needs. Alpha (from #rgba / #rrggbbaa / rgba()) is dropped for the swatch — the + * adjacent text field keeps the full, alpha-preserving value. + */ +function toHex(value: string): string { + const v = value.trim(); + const expand = (c: string) => c + c; + if (/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(v)) return v.slice(0, 7); + if (/^#[0-9a-f]{3,4}$/i.test(v)) { + return `#${v.slice(1, 4).split('').map(expand).join('')}`; + } + const m = v.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/i); + if (m) { + const hex = m.slice(1, 4).map((n) => Number(n).toString(16).padStart(2, '0')).join(''); + return `#${hex}`; + } + return '#000000'; +} + +/** + * An interactive version of Paragon's "Theme Variables" docs table for Button. + * + * Reads the initial (computed) value of every Button `--pgn-*` token from the + * live preview container, lists them in an editable table, and writes any edit + * back onto that container as a custom-property override. Because `.btn` styling + * is authored entirely over these tokens, the preview buttons re-render with the + * new value instantly — the same mechanism `@edx/brand` uses to theme Paragon. + */ +export function ThemeVariables() { + const previewRef = useRef(null); + // Computed token defaults, read once from the DOM after mount. + const [defaults, setDefaults] = useState>({}); + // Only the tokens the user has actually edited (so "Reset" can restore them). + const [overrides, setOverrides] = useState>({}); + + useLayoutEffect(() => { + if (!previewRef.current) return; + const computed = getComputedStyle(previewRef.current); + const initial: Record = {}; + for (const name of ALL_VARIABLES) { + initial[name] = computed.getPropertyValue(name).trim(); + } + setDefaults(initial); + }, []); + + // The custom-property overrides applied to the preview container. + const previewStyle = useMemo( + () => overrides as unknown as CSSProperties, + [overrides], + ); + + const hasOverrides = Object.keys(overrides).length > 0; + + const setValue = (name: string, value: string) => { + setOverrides((prev) => ({ ...prev, [name]: value })); + }; + + const resetValue = (name: string) => { + setOverrides((prev) => { + const next = { ...prev }; + delete next[name]; + return next; + }); + }; + + const valueOf = (name: string) => overrides[name] ?? defaults[name] ?? ''; + + return ( +
+ {/* Live preview: the element the edited tokens are applied to. */} +
+
+ + + + + + +
+
+ + + +
+
+ + + +
+
+ +
+ + Edit a token to see it applied to the buttons above + + +
+ +
+ + + + + + + + + {BUTTON_THEME_VARIABLES.map((group) => [ + + + , + ...group.variables.map((name) => { + const value = valueOf(name); + const overridden = name in overrides; + return ( + + + + + + ); + }), + ])} + +
CSS VariableValue +
{group.label}
{name} +
+ {isColorValue(value) && ( + setValue(name, e.target.value)} + /> + )} + setValue(name, e.target.value)} + /> +
+
+ +
+
+
+ ); +} + +export default ThemeVariables; diff --git a/prototype/src/docs/themeVariables.module.css b/prototype/src/docs/themeVariables.module.css new file mode 100644 index 00000000000..494e2434b29 --- /dev/null +++ b/prototype/src/docs/themeVariables.module.css @@ -0,0 +1,181 @@ +/* + * Styling for the interactive Theme Variables table (see ThemeVariables.tsx). + * + * Mirrors the read-only "Theme Variables" table the current Paragon docs render + * (www/src/components/ComponentVariablesTable.tsx) but adds an editable value + * column: edits are written as `--pgn-*` custom properties on the live preview, + * so consumers can see a token change ripple through the buttons immediately. + */ + +.root { + border: 1px solid #d0d0d0; + border-radius: 6px; + overflow: hidden; + margin: 1rem 0; +} + +/* Live preview area — the container these custom-property overrides are applied + to, so every descendant .btn re-reads the edited tokens. */ +.preview { + padding: 1.5rem; + background-color: var(--pgn-color-light-200); + display: flex; + flex-direction: column; + gap: 0.75rem; + font-family: var(--pgn-typography-font-family-base); + color: var(--pgn-color-body-base); +} + +.previewRow { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; +} + +.toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.5rem 1rem; + background: #f4f4f6; + border-top: 1px solid #d0d0d0; + border-bottom: 1px solid #d0d0d0; +} + +.toolbarLabel { + font: 600 11px/1 system-ui, sans-serif; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #6b6b7b; +} + +.resetAll { + font: 600 12px/1 system-ui, sans-serif; + color: #3a3a4a; + background: #fff; + border: 1px solid #c4c4cc; + border-radius: 4px; + padding: 0.35rem 0.6rem; + cursor: pointer; +} + +.resetAll:disabled { + opacity: 0.4; + cursor: default; +} + +.tableWrap { + overflow-x: auto; +} + +.table { + width: 100%; + border-collapse: collapse; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + font-size: 12.5px; +} + +.table th { + text-align: left; + font: 600 11px/1 system-ui, sans-serif; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #6b6b7b; + padding: 0.6rem 1rem; + background: #fafafa; + border-bottom: 1px solid #e2e2e6; + position: sticky; + top: 0; +} + +.groupRow td { + padding: 0.6rem 1rem 0.3rem; + background: #fff; + border-bottom: 1px solid #f0f0f2; +} + +.groupLabel { + font: 700 11px/1 system-ui, sans-serif; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #9a9aa6; +} + +.table td { + padding: 0.4rem 1rem; + border-bottom: 1px solid #f0f0f2; + vertical-align: middle; +} + +.varName { + color: #2d2d38; + white-space: nowrap; +} + +.overridden .varName::after { + content: "edited"; + margin-inline-start: 0.5rem; + font: 600 9px/1 system-ui, sans-serif; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #7a5cff; + background: #efeaff; + border-radius: 3px; + padding: 2px 4px; + vertical-align: middle; +} + +.valueCell { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.swatch { + inline-size: 1.4rem; + block-size: 1.4rem; + padding: 0; + border: 1px solid #c4c4cc; + border-radius: 4px; + background: none; + cursor: pointer; + flex: none; +} + +.valueInput { + flex: 1 1 auto; + min-width: 8rem; + font-family: inherit; + font-size: 12.5px; + padding: 0.3rem 0.45rem; + border: 1px solid #c4c4cc; + border-radius: 4px; + background: #fff; + color: #2d2d38; +} + +.valueInput:focus { + outline: 2px solid #7a5cff; + outline-offset: -1px; + border-color: #7a5cff; +} + +.resetRow { + font: 600 15px/1 system-ui, sans-serif; + color: #9a9aa6; + background: none; + border: none; + cursor: pointer; + padding: 0 0.25rem; + visibility: hidden; +} + +.overridden .resetRow { + visibility: visible; +} + +.resetRow:hover { + color: #3a3a4a; +} From d1749ba51548a76bf50f5b43f6a212f51c058068 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 22 Jul 2026 14:57:19 -0700 Subject: [PATCH 21/24] docs: design tokens playground --- prototype/package-lock.json | 2124 ++++++++++++++++- prototype/package.json | 5 +- prototype/src/Button/Button.mdx | 23 +- prototype/src/docs/ThemeVariables.tsx | 320 --- prototype/src/docs/themeVariables.module.css | 181 -- prototype/src/theming/DesignTokens.mdx | 62 + .../src/theming/DesignTokensPlayground.tsx | 357 +++ prototype/src/theming/compileTokens.test.ts | 41 + prototype/src/theming/compileTokens.ts | 147 ++ prototype/src/theming/designTokens.module.css | 328 +++ prototype/src/theming/tokenTree.ts | 360 +++ 11 files changed, 3402 insertions(+), 546 deletions(-) delete mode 100644 prototype/src/docs/ThemeVariables.tsx delete mode 100644 prototype/src/docs/themeVariables.module.css create mode 100644 prototype/src/theming/DesignTokens.mdx create mode 100644 prototype/src/theming/DesignTokensPlayground.tsx create mode 100644 prototype/src/theming/compileTokens.test.ts create mode 100644 prototype/src/theming/compileTokens.ts create mode 100644 prototype/src/theming/designTokens.module.css create mode 100644 prototype/src/theming/tokenTree.ts diff --git a/prototype/package-lock.json b/prototype/package-lock.json index 7a6db048609..d4f6a4118b5 100644 --- a/prototype/package-lock.json +++ b/prototype/package-lock.json @@ -8,11 +8,13 @@ "name": "@openedx/paragon-prototype", "version": "0.0.0", "dependencies": { + "chroma-js": "^3.2.0", "clsx": "^2.1.1", "react-aria": "^3.35.0", "react-aria-components": "^1.5.0", "react-live": "^4.1.8", - "react-stately": "^3.33.0" + "react-stately": "^3.33.0", + "style-dictionary": "^4.4.0" }, "devDependencies": { "@storybook/addon-a11y": "^10.5.3", @@ -22,6 +24,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", + "@types/chroma-js": "^3.1.2", "@types/react": "^18", "@types/react-dom": "^18", "@vitejs/plugin-react": "^6.0.4", @@ -317,6 +320,90 @@ "node": ">=6.9.0" } }, + "node_modules/@bundled-es-modules/deepmerge": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/deepmerge/-/deepmerge-4.3.2.tgz", + "integrity": "sha512-q8doe7ndrY2IolUOFIn0A0++JBX38pMhN7kFhTF4cnjIcILf6X6H2yWczInyv8ZFdR0lrE8088X8XS5efxXz8A==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1" + } + }, + "node_modules/@bundled-es-modules/glob": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/glob/-/glob-10.4.2.tgz", + "integrity": "sha512-740y5ofkzydsFao5EXJrGilcIL6EFEw/cmPf2uhTw9J6G1YOhiIFjNFCHdpgEiiH5VlU3G0SARSjlFlimRRSMA==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "buffer": "^6.0.3", + "events": "^3.3.0", + "glob": "^10.4.2", + "patch-package": "^8.0.0", + "path": "^0.12.7", + "stream": "^0.0.3", + "string_decoder": "^1.3.0", + "url": "^0.11.3" + } + }, + "node_modules/@bundled-es-modules/glob/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@bundled-es-modules/glob/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/@bundled-es-modules/glob/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@bundled-es-modules/memfs": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/memfs/-/memfs-4.17.0.tgz", + "integrity": "sha512-ykdrkEmQr9BV804yd37ikXfNnvxrwYfY9Z2/EtMHFEFadEjsQXJ1zL9bVZrKNLDtm91UdUOEHso6Aweg93K6xQ==", + "license": "Apache-2.0", + "dependencies": { + "assert": "^2.1.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "memfs": "^4.17.0", + "path": "^0.12.7", + "stream": "^0.0.3", + "util": "^0.12.5" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -935,6 +1022,23 @@ "@swc/helpers": "^0.5.0" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.7.0.tgz", @@ -1001,6 +1105,416 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.64.0.tgz", + "integrity": "sha512-zs2TAq7Six5jgMuoMNjpspAvOP3mhtgq/k1UyQodEzCtQi/N83y2/y+zcvnZSGp/Rxq96DBN+bValOBQAyn/ew==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.64.0.tgz", + "integrity": "sha512-nMWOVbkLFyEgmXZih3wyvxA9XpgyyqyfrINMHvEFqhi7uqfRl7c9ERJt6yX7vgMPrB9Uo+OJO+Spa0cFzPD01w==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.64.0.tgz", + "integrity": "sha512-dO+NNkODbUli4uV42bcNrrLvq5rE7SNpdZ5TNd0dtbLsAaNK3MDiIC9lUi+brboGoIjW6vd2fB1qao60nrk5xA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "@jsonjoy.com/fs-print": "4.64.0", + "@jsonjoy.com/fs-snapshot": "4.64.0", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.64.0.tgz", + "integrity": "sha512-/o7WRFhUWaM/fOrslwLZGnzn4RmRILykn+lAL+mNObqqRNw+CQSiij6hpCeZ+C7buhdoVo7go/OYqzaSUfDYmA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.64.0.tgz", + "integrity": "sha512-WDD9WVs0hb7UAEKTgZW2f66WDrbj7gIIWwpP3spbLyXa0rghtUaFTB8L4gdR3ZCWwiKIsj38/CNijpVmpnuPUw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.64.0.tgz", + "integrity": "sha512-k5Indsx9hWW9xSF7Y6oSKKwtCUNhzZxadub3owhIlitc+iMRVlPPdX2duTKQWBL3qNWpXya8jykgaaWpheeS4w==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "glob-to-regex.js": "^1.0.1" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.64.0.tgz", + "integrity": "sha512-PHZFccchvkhWrwPWHjmVAhbC3vSHCtyZvlZfJJ3ho2bnzl450hXri6/8e6pbkWdH+SkmLXNml0sV8e5HDAfxKw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.64.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.64.0.tgz", + "integrity": "sha512-oM7UDeL83q6NBzzsfKAsYKXKVXlykKFqqOLh4xZZKAzzROTlInkPbc6LTDGThEOnPiFiUzA7tYziHG9xavd76Q==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@mdx-js/react": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", @@ -1743,6 +2257,16 @@ "win32" ] }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@react-types/shared": { "version": "3.36.0", "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.36.0.tgz", @@ -2484,6 +3008,13 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/chroma-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-3.1.2.tgz", + "integrity": "sha512-YBTQqArPN8A0niHXCwrO1z5x++a+6l0mLBykncUpr23oIPW7L4h39s6gokdK/bDrPmSh8+TjMmrhBPnyiaWPmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -2798,6 +3329,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@zip.js/zip.js": { + "version": "2.8.34", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.34.tgz", + "integrity": "sha512-+6a3lyqq69rpseLbvDPiVIWsZ/HdTGAAD6afFtug6ECPDGttb2dHnPC6cJgdPofYkzL9OvXizegq+DQVfL2rnA==", + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=18.0.0" + } + }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -2825,7 +3373,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2872,6 +3419,19 @@ "dequal": "^2.0.3" } }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2902,6 +3462,21 @@ "dev": true, "license": "MIT" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/axe-core": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", @@ -2912,6 +3487,32 @@ "node": ">=4" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.0.tgz", @@ -2925,6 +3526,27 @@ "node": ">=6.0.0" } }, + "node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.7", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", @@ -2959,6 +3581,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -2975,11 +3621,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2989,6 +3652,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001806", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", @@ -3027,6 +3706,24 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "license": "MIT" + }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -3037,6 +3734,27 @@ "node": ">= 16" } }, + "node_modules/chroma-js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.2.0.tgz", + "integrity": "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==", + "license": "(BSD-3-Clause AND Apache-2.0)" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -3052,6 +3770,24 @@ "node": ">=6" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3081,6 +3817,18 @@ "dev": true, "license": "MIT" }, + "node_modules/component-emitter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-2.0.0.tgz", + "integrity": "sha512-4m5s3Me2xxlVKG9PkZpQqHQR7bgpnN7joDMJ4yvVkVXngjoITG76IaZmzmywSeRTeTpc6N6r3H3+KyUurV8OYw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/confbox": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", @@ -3095,6 +3843,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -3179,6 +3941,15 @@ "node": ">=6" } }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/default-browser": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", @@ -3209,6 +3980,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -3222,6 +4010,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3276,7 +4081,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -3287,6 +4091,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.395", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz", @@ -3294,6 +4104,12 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/empathic": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", @@ -3321,7 +4137,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3331,7 +4146,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3348,7 +4162,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3456,6 +4269,15 @@ "node": ">=0.10.0" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -3490,6 +4312,58 @@ } } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -3507,6 +4381,20 @@ "node": ">= 6" } }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3526,12 +4414,20 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3546,7 +4442,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3571,7 +4466,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3599,6 +4493,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/glob/node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -3642,7 +4552,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3651,11 +4560,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3668,7 +4603,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -3684,7 +4618,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3734,6 +4667,15 @@ "node": ">= 14" } }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -3747,6 +4689,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -3757,6 +4719,40 @@ "node": ">=8" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", @@ -3789,6 +4785,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -3808,6 +4832,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3815,6 +4876,39 @@ "dev": true, "license": "MIT" }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-wsl": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", @@ -3831,6 +4925,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3891,11 +5012,29 @@ "node": ">=6" } }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -3911,6 +5050,36 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/kolorist": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", @@ -4268,12 +5437,65 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/memfs": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.64.0.tgz", + "integrity": "sha512-Kw72fgY7Wn+sD8KmtNWSafl1dz0UvAsE/PHs3YVfLiaZuA3HxNm9sRLqAu0ATiBGJvME1PxZXbBZPv5GycDeAw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.64.0", + "@jsonjoy.com/fs-fsa": "4.64.0", + "@jsonjoy.com/fs-node": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-to-fsa": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "@jsonjoy.com/fs-print": "4.64.0", + "@jsonjoy.com/fs-snapshot": "4.64.0", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -4307,11 +5529,25 @@ "node": ">=4" } }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4321,7 +5557,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -4422,6 +5657,63 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obug": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", @@ -4524,6 +5816,12 @@ "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -4537,6 +5835,131 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -4544,6 +5967,15 @@ "dev": true, "license": "MIT" }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -4578,6 +6010,27 @@ "node": "20 || >=22" } }, + "node_modules/path-unified": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/path-unified/-/path-unified-0.2.0.tgz", + "integrity": "sha512-MNKqvrKbbbb5p7XHXV6ZAsf/1f/yJQa13S/fcX0uua8ew58Tgc6jXV+16JyAbnR/clgCH+euKDxrF2STxMHdrg==", + "license": "MIT" + }, + "node_modules/path/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/path/node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -4635,6 +6088,15 @@ "pathe": "^2.0.3" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.21", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.21.tgz", @@ -4664,6 +6126,21 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -4692,6 +6169,15 @@ "react": ">=16.0.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4702,6 +6188,22 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/quansync": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", @@ -4988,6 +6490,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -5017,14 +6556,124 @@ "loose-envify": "^1.1.0" } }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/siginfo": { @@ -5034,6 +6683,27 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5130,6 +6800,114 @@ "node": ">=10" } }, + "node_modules/stream": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.3.tgz", + "integrity": "sha512-aMsbn7VKrl4A2T7QAQQbzgN7NVc70vgF5INQrBXqn4dCXN1zy3L9HGgLO5s7PExmdrzTJ8uR/27aviW8or8/+A==", + "license": "MIT", + "dependencies": { + "component-emitter": "^2.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -5153,6 +6931,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-dictionary": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/style-dictionary/-/style-dictionary-4.4.0.tgz", + "integrity": "sha512-+xU0IA1StzqAqFs/QtXkK+XJa7wpS4X5H+JQccRKsRCElgeLGocFU1U/UMvMUylKFw6vwGV+Y/a2wb2pm5rFFQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@bundled-es-modules/deepmerge": "^4.3.1", + "@bundled-es-modules/glob": "^10.4.2", + "@bundled-es-modules/memfs": "^4.9.4", + "@zip.js/zip.js": "^2.7.44", + "chalk": "^5.3.0", + "change-case": "^5.3.0", + "commander": "^12.1.0", + "is-plain-obj": "^4.1.0", + "json5": "^2.2.2", + "patch-package": "^8.0.0", + "path-unified": "^0.2.0", + "prettier": "^3.3.3", + "tinycolor2": "^1.6.0" + }, + "bin": { + "style-dictionary": "bin/style-dictionary.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/style-dictionary/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -5175,6 +6990,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -5216,6 +7043,22 @@ "node": ">=0.8" } }, + "node_modules/thingies": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", + "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==", + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -5230,6 +7073,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", @@ -5286,6 +7135,27 @@ "dev": true, "license": "MIT" }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tough-cookie": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", @@ -5312,6 +7182,22 @@ "node": ">=18" } }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/ts-dedent": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", @@ -5370,6 +7256,15 @@ "dev": true, "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unplugin": { "version": "2.3.11", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", @@ -5471,6 +7366,25 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, "node_modules/use-editable": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/use-editable/-/use-editable-2.3.3.tgz", @@ -5489,6 +7403,19 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/vite": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", @@ -5796,6 +7723,42 @@ "node": ">=18" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -5813,6 +7776,100 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/ws": { "version": "8.21.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", @@ -5874,6 +7931,21 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } } } } diff --git a/prototype/package.json b/prototype/package.json index f38a4c5b63f..aba7d18443b 100644 --- a/prototype/package.json +++ b/prototype/package.json @@ -27,11 +27,13 @@ "react-dom": "^18" }, "dependencies": { + "chroma-js": "^3.2.0", "clsx": "^2.1.1", "react-aria": "^3.35.0", "react-aria-components": "^1.5.0", "react-live": "^4.1.8", - "react-stately": "^3.33.0" + "react-stately": "^3.33.0", + "style-dictionary": "^4.4.0" }, "devDependencies": { "@storybook/addon-a11y": "^10.5.3", @@ -41,6 +43,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", + "@types/chroma-js": "^3.1.2", "@types/react": "^18", "@types/react-dom": "^18", "@vitejs/plugin-react": "^6.0.4", diff --git a/prototype/src/Button/Button.mdx b/prototype/src/Button/Button.mdx index 15afa8e6d22..e9390297a3c 100644 --- a/prototype/src/Button/Button.mdx +++ b/prototype/src/Button/Button.mdx @@ -2,7 +2,6 @@ import { Meta, Title, Subtitle, Story, Controls } from '@storybook/addon-docs/bl import * as ButtonStories from './Button.stories'; import { LiveExample } from '../docs/LiveExample'; -import { ThemeVariables } from '../docs/ThemeVariables'; export const examples = ButtonStories.examples; @@ -95,24 +94,12 @@ for a more efficient, user-friendly experience. {/* --- Additions beyond the original README --- */} -## Theme Variables +## Theming -Button appearance is authored entirely over Paragon's `--pgn-*` design tokens — -the same set the current docs list under -[Theme variables](https://paragon-openedx.netlify.app/components/button/). Edit -any value below and the preview buttons update instantly, because every `.btn` -rule reads these custom properties at render time. This is exactly how -`@edx/brand` themes Paragon: it re-points these tokens, and the button module -follows along without any component changes. - -The upstream table auto-generates its list by scraping `var(--pgn-…)` references -from Button's SCSS, which means it silently omits every **per-variant colour** -(`--pgn-color-btn-bg-primary`, `-brand`, `-secondary`, …) — in the SCSS those are -written with interpolation (`--pgn-color-btn-bg-#{$variant}`) the scraper's regex -can't match, even though they are the tokens that actually colour each button. -They are included below. - - +Button appearance is authored entirely over Paragon's `--pgn-*` design tokens, so +theming happens without any component changes. To experiment with token values +live — set a source colour and watch Style Dictionary re-derive the button +tokens — see the [Design Tokens](?path=/docs/theming-design-tokens--docs) page. ## Grouping diff --git a/prototype/src/docs/ThemeVariables.tsx b/prototype/src/docs/ThemeVariables.tsx deleted file mode 100644 index 57b5c0df185..00000000000 --- a/prototype/src/docs/ThemeVariables.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import { - useLayoutEffect, useMemo, useRef, useState, - type CSSProperties, -} from 'react'; - -import { Button } from '../Button'; -import styles from './themeVariables.module.css'; - -/** - * The Button module's theme variables, grouped for readability. - * - * This is the same `--pgn-*` custom-property set the current Paragon docs list - * under "Theme Variables" for Button — there, the list is scraped from the - * component's SCSS (every `var(--pgn-…)` reference) and rendered read-only with - * its computed value (see www/src/components/ComponentVariablesTable.tsx). Here - * the identical set is authored explicitly (the prototype has no SCSS to scrape) - * and made editable, so changing a token updates the live preview below. - */ -interface VarGroup { - label: string; - variables: string[]; -} - -export const BUTTON_THEME_VARIABLES: VarGroup[] = [ - { - label: 'Sizing', - variables: [ - '--pgn-size-btn-border-width', - '--pgn-size-btn-border-radius-base', - '--pgn-size-btn-border-radius-sm', - '--pgn-size-btn-border-radius-lg', - '--pgn-size-btn-focus-width', - '--pgn-size-btn-focus-border-radius-base', - '--pgn-size-btn-focus-border-radius-sm', - '--pgn-size-btn-focus-border-radius-lg', - ], - }, - { - label: 'Spacing', - variables: [ - '--pgn-spacing-btn-padding-y-base', - '--pgn-spacing-btn-padding-x-base', - '--pgn-spacing-btn-padding-y-sm', - '--pgn-spacing-btn-padding-x-sm', - '--pgn-spacing-btn-padding-y-lg', - '--pgn-spacing-btn-padding-x-lg', - '--pgn-spacing-btn-block-spacing-y', - '--pgn-spacing-btn-focus-distance-to-border', - ], - }, - { - label: 'Typography', - variables: [ - '--pgn-typography-btn-font-family', - '--pgn-typography-btn-font-weight', - '--pgn-typography-btn-font-size-base', - '--pgn-typography-btn-font-size-sm', - '--pgn-typography-btn-font-size-lg', - '--pgn-typography-btn-line-height-base', - '--pgn-typography-btn-line-height-sm', - '--pgn-typography-btn-line-height-lg', - ], - }, - { - label: 'Base colors', - variables: [ - '--pgn-color-body-base', - '--pgn-color-link-base', - '--pgn-color-link-hover', - ], - }, - // Per-variant colours. The upstream docs' "Theme variables" table omits these: - // it is generated by scraping `var(--pgn-…)` out of Button's SCSS, but there - // each variant's tokens are written with interpolation - // (`--pgn-color-btn-bg-#{$variant}`), which the scraper's regex never matches. - // They are the tokens that actually colour each variant, so they are listed - // here. Note: editing the *generic* `--pgn-btn-bg` would not work — every - // `.btn-` re-declares it locally from the source tokens below, so the - // source token is what has to be overridden. - { - label: 'Brand variant colors', - variables: [ - '--pgn-color-btn-bg-brand', - '--pgn-color-btn-text-brand', - '--pgn-color-btn-border-brand', - ], - }, - { - label: 'Primary variant colors', - variables: [ - '--pgn-color-btn-bg-primary', - '--pgn-color-btn-text-primary', - '--pgn-color-btn-border-primary', - ], - }, - { - label: 'Secondary variant colors', - variables: [ - '--pgn-color-btn-bg-secondary', - '--pgn-color-btn-text-secondary', - '--pgn-color-btn-border-secondary', - ], - }, - { - label: 'Tertiary variant colors', - variables: [ - '--pgn-color-btn-bg-tertiary', - '--pgn-color-btn-text-tertiary', - '--pgn-color-btn-border-tertiary', - ], - }, - { - label: 'Success variant colors', - variables: [ - '--pgn-color-btn-bg-success', - '--pgn-color-btn-text-success', - '--pgn-color-btn-border-success', - ], - }, - { - label: 'Danger variant colors', - variables: [ - '--pgn-color-btn-bg-danger', - '--pgn-color-btn-text-danger', - '--pgn-color-btn-border-danger', - ], - }, - { - label: 'Other', - variables: [ - '--pgn-transition-btn', - '--pgn-other-btn-disabled-opacity', - '--pgn-typography-link-decoration-base', - '--pgn-typography-link-decoration-hover', - ], - }, -]; - -const ALL_VARIABLES = BUTTON_THEME_VARIABLES.flatMap((g) => g.variables); - -/** - * True when a value looks like a colour we can offer a native colour picker for. - * Covers #rgb, #rgba, #rrggbb, #rrggbbaa and rgb()/rgba() — the forms the - * compiled Paragon colour tokens resolve to. - */ -function isColorValue(value: string): boolean { - const v = value.trim(); - return /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(v) || /^rgba?\(/i.test(v); -} - -/** - * Normalises any CSS colour to the `#rrggbb` a native `` - * needs. Alpha (from #rgba / #rrggbbaa / rgba()) is dropped for the swatch — the - * adjacent text field keeps the full, alpha-preserving value. - */ -function toHex(value: string): string { - const v = value.trim(); - const expand = (c: string) => c + c; - if (/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(v)) return v.slice(0, 7); - if (/^#[0-9a-f]{3,4}$/i.test(v)) { - return `#${v.slice(1, 4).split('').map(expand).join('')}`; - } - const m = v.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/i); - if (m) { - const hex = m.slice(1, 4).map((n) => Number(n).toString(16).padStart(2, '0')).join(''); - return `#${hex}`; - } - return '#000000'; -} - -/** - * An interactive version of Paragon's "Theme Variables" docs table for Button. - * - * Reads the initial (computed) value of every Button `--pgn-*` token from the - * live preview container, lists them in an editable table, and writes any edit - * back onto that container as a custom-property override. Because `.btn` styling - * is authored entirely over these tokens, the preview buttons re-render with the - * new value instantly — the same mechanism `@edx/brand` uses to theme Paragon. - */ -export function ThemeVariables() { - const previewRef = useRef(null); - // Computed token defaults, read once from the DOM after mount. - const [defaults, setDefaults] = useState>({}); - // Only the tokens the user has actually edited (so "Reset" can restore them). - const [overrides, setOverrides] = useState>({}); - - useLayoutEffect(() => { - if (!previewRef.current) return; - const computed = getComputedStyle(previewRef.current); - const initial: Record = {}; - for (const name of ALL_VARIABLES) { - initial[name] = computed.getPropertyValue(name).trim(); - } - setDefaults(initial); - }, []); - - // The custom-property overrides applied to the preview container. - const previewStyle = useMemo( - () => overrides as unknown as CSSProperties, - [overrides], - ); - - const hasOverrides = Object.keys(overrides).length > 0; - - const setValue = (name: string, value: string) => { - setOverrides((prev) => ({ ...prev, [name]: value })); - }; - - const resetValue = (name: string) => { - setOverrides((prev) => { - const next = { ...prev }; - delete next[name]; - return next; - }); - }; - - const valueOf = (name: string) => overrides[name] ?? defaults[name] ?? ''; - - return ( -
- {/* Live preview: the element the edited tokens are applied to. */} -
-
- - - - - - -
-
- - - -
-
- - - -
-
- -
- - Edit a token to see it applied to the buttons above - - -
- -
- - - - - - - - - {BUTTON_THEME_VARIABLES.map((group) => [ - - - , - ...group.variables.map((name) => { - const value = valueOf(name); - const overridden = name in overrides; - return ( - - - - - - ); - }), - ])} - -
CSS VariableValue -
{group.label}
{name} -
- {isColorValue(value) && ( - setValue(name, e.target.value)} - /> - )} - setValue(name, e.target.value)} - /> -
-
- -
-
-
- ); -} - -export default ThemeVariables; diff --git a/prototype/src/docs/themeVariables.module.css b/prototype/src/docs/themeVariables.module.css deleted file mode 100644 index 494e2434b29..00000000000 --- a/prototype/src/docs/themeVariables.module.css +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Styling for the interactive Theme Variables table (see ThemeVariables.tsx). - * - * Mirrors the read-only "Theme Variables" table the current Paragon docs render - * (www/src/components/ComponentVariablesTable.tsx) but adds an editable value - * column: edits are written as `--pgn-*` custom properties on the live preview, - * so consumers can see a token change ripple through the buttons immediately. - */ - -.root { - border: 1px solid #d0d0d0; - border-radius: 6px; - overflow: hidden; - margin: 1rem 0; -} - -/* Live preview area — the container these custom-property overrides are applied - to, so every descendant .btn re-reads the edited tokens. */ -.preview { - padding: 1.5rem; - background-color: var(--pgn-color-light-200); - display: flex; - flex-direction: column; - gap: 0.75rem; - font-family: var(--pgn-typography-font-family-base); - color: var(--pgn-color-body-base); -} - -.previewRow { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0.5rem; -} - -.toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 0.5rem 1rem; - background: #f4f4f6; - border-top: 1px solid #d0d0d0; - border-bottom: 1px solid #d0d0d0; -} - -.toolbarLabel { - font: 600 11px/1 system-ui, sans-serif; - letter-spacing: 0.04em; - text-transform: uppercase; - color: #6b6b7b; -} - -.resetAll { - font: 600 12px/1 system-ui, sans-serif; - color: #3a3a4a; - background: #fff; - border: 1px solid #c4c4cc; - border-radius: 4px; - padding: 0.35rem 0.6rem; - cursor: pointer; -} - -.resetAll:disabled { - opacity: 0.4; - cursor: default; -} - -.tableWrap { - overflow-x: auto; -} - -.table { - width: 100%; - border-collapse: collapse; - font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; - font-size: 12.5px; -} - -.table th { - text-align: left; - font: 600 11px/1 system-ui, sans-serif; - letter-spacing: 0.04em; - text-transform: uppercase; - color: #6b6b7b; - padding: 0.6rem 1rem; - background: #fafafa; - border-bottom: 1px solid #e2e2e6; - position: sticky; - top: 0; -} - -.groupRow td { - padding: 0.6rem 1rem 0.3rem; - background: #fff; - border-bottom: 1px solid #f0f0f2; -} - -.groupLabel { - font: 700 11px/1 system-ui, sans-serif; - letter-spacing: 0.06em; - text-transform: uppercase; - color: #9a9aa6; -} - -.table td { - padding: 0.4rem 1rem; - border-bottom: 1px solid #f0f0f2; - vertical-align: middle; -} - -.varName { - color: #2d2d38; - white-space: nowrap; -} - -.overridden .varName::after { - content: "edited"; - margin-inline-start: 0.5rem; - font: 600 9px/1 system-ui, sans-serif; - letter-spacing: 0.04em; - text-transform: uppercase; - color: #7a5cff; - background: #efeaff; - border-radius: 3px; - padding: 2px 4px; - vertical-align: middle; -} - -.valueCell { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.swatch { - inline-size: 1.4rem; - block-size: 1.4rem; - padding: 0; - border: 1px solid #c4c4cc; - border-radius: 4px; - background: none; - cursor: pointer; - flex: none; -} - -.valueInput { - flex: 1 1 auto; - min-width: 8rem; - font-family: inherit; - font-size: 12.5px; - padding: 0.3rem 0.45rem; - border: 1px solid #c4c4cc; - border-radius: 4px; - background: #fff; - color: #2d2d38; -} - -.valueInput:focus { - outline: 2px solid #7a5cff; - outline-offset: -1px; - border-color: #7a5cff; -} - -.resetRow { - font: 600 15px/1 system-ui, sans-serif; - color: #9a9aa6; - background: none; - border: none; - cursor: pointer; - padding: 0 0.25rem; - visibility: hidden; -} - -.overridden .resetRow { - visibility: visible; -} - -.resetRow:hover { - color: #3a3a4a; -} diff --git a/prototype/src/theming/DesignTokens.mdx b/prototype/src/theming/DesignTokens.mdx new file mode 100644 index 00000000000..8903f655134 --- /dev/null +++ b/prototype/src/theming/DesignTokens.mdx @@ -0,0 +1,62 @@ +import { Meta, Title, Subtitle } from '@storybook/addon-docs/blocks'; + +import { DesignTokensPlayground } from './DesignTokensPlayground'; + + + +Design Tokens + +Set a source colour and watch Style Dictionary re-derive the whole palette — compiled live, in your browser. + +Paragon's theming is driven by **design tokens** in [`tokens/src/`](https://github.com/openedx/paragon/tree/master/tokens/src). +A small number of _source_ tokens (like `$brand` and `$primary`) fan out, through +[Style Dictionary](https://styledictionary.com/), into hundreds of _derived_ +tokens: a 100–900 tint/shade **palette scale** (`mix` with white/black), a set of +**theme aliases** (`color.theme.*`), and finally the per-variant **button tokens** +(`color.btn.*`) that the `.btn-` classes consume. Button label colours are +`color-yiq` of their background — Bootstrap's contrast picker — so a light source +colour automatically flips the text dark. + +The playground below runs a **real Style Dictionary instance in the browser**. The +colour maths (`mix`, `color-yiq`, `darken`) is ported from the repo's +`tokens/style-dictionary.js`, and the in-memory token tree mirrors `tokens/src/` +one-to-one. Editing a source colour recompiles the entire graph and applies the +resulting `--pgn-*` custom properties to the preview live. + + + +## How it works + +1. **Source tokens.** `$brand` (`color.brand.base`) and `$primary` + (`color.primary.base`) are the only hand-set colours. +2. **Palette scale.** `color..100…900` are `mix()`es of the base with + white (tints, 100–400) or black (shades, 600–900); `500` _is_ the base. +3. **Theme aliases.** `color.theme.{bg,border,focus,hover,active}.` point + at fixed scale levels (100 / 200 / 500 / 700 / 900). +4. **Button tokens.** `color.btn.*.` reference the theme aliases; text + tokens are `color-yiq` of their background for guaranteed contrast. + +Expand **Derived tokens** under either source to see — and override — any token in +that chain. Pinning a derived token to a literal value cascades to everything +downstream of it, exactly as an `@edx/brand` override would. + +## Beyond colour + +The **Typography, sizing & spacing** controls theme every variant at once: + +- **Font family** (`typography.font.family.base`) — the buttons inherit it. +- **Button font size** (`typography.btn.font.size.base`, with `-sm` / `-lg`). +- **Border radius** (`size.border.radius.base`) — fans out, through a real + `calc()`, into `size.btn.border.radius.base` and the focus-ring radius + `size.btn.focus.border-radius.base`. +- **Button padding** (`spacing.btn.padding.{x,y}.base`). + +These are non-colour tokens (`dimension`, `fontFamily`), so they demonstrate that +the same in-browser Style Dictionary pipeline handles references and `calc()` +chains, not just colour maths. + +## Scope + +This iteration models **brand** and **primary** (solid + `outline-`) colours plus +the global typography/sizing/spacing tokens above. The same mechanism extends to +the other variants (secondary, success, danger, …) and further token groups. diff --git a/prototype/src/theming/DesignTokensPlayground.tsx b/prototype/src/theming/DesignTokensPlayground.tsx new file mode 100644 index 00000000000..79d852eab66 --- /dev/null +++ b/prototype/src/theming/DesignTokensPlayground.tsx @@ -0,0 +1,357 @@ +import { + useEffect, useMemo, useRef, useState, + type CSSProperties, +} from 'react'; + +import { Button, ButtonGroup } from '../Button'; +import { + BASE_COLORS, VARIANTS, BASE_TOKEN_PATH, GLOBAL_CONTROLS, type Variant, +} from './tokenTree'; +import { compileTokens, type CompileResult } from './compileTokens'; +import styles from './designTokens.module.css'; + +const SOURCE_LABEL: Record = { brand: '$brand', primary: '$primary' }; + +function isColorValue(value: string): boolean { + const v = value.trim(); + return /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(v) || /^rgba?\(/i.test(v); +} + +function toHex(value: string): string { + const v = value.trim(); + if (/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(v)) return v.slice(0, 7); + if (/^#[0-9a-f]{3,4}$/i.test(v)) { + return `#${v.slice(1, 4).split('').map((c) => c + c).join('')}`; + } + return '#000000'; +} + +/** Splits a variant's derived tokens into ordered, labelled groups for the tree. */ +function groupsForVariant(paths: string[], v: Variant) { + const isForVariant = (p: string) => p.endsWith(`.${v}`) || p.endsWith(`.outline-${v}`); + const scale = paths + .filter((p) => p.startsWith(`color.${v}.`) && !p.endsWith('.base')) + .sort(); + const theme = paths + .filter((p) => p.startsWith('color.theme.') && p.endsWith(`.${v}`)) + .sort(); + const button = paths + .filter((p) => p.startsWith('color.btn.') && isForVariant(p)) + .sort(); + return [ + { label: 'Palette scale', paths: scale }, + { label: 'Theme aliases', paths: theme }, + { label: 'Button tokens', paths: button }, + ].filter((g) => g.paths.length); +} + +/** + * Design-tokens / theming playground. + * + * The two source colours (`$brand`, `$primary`) are the prominent controls; every + * other token is *derived* from them by Style Dictionary, which recompiles in the + * browser on every edit (see compileTokens.ts). The derived tokens are listed in a + * collapsible tree and are themselves editable — pinning one to a literal value + * cascades to everything downstream. The compiled `--pgn-*` variables are applied + * to the preview so the buttons and button groups re-theme live. + */ +export function DesignTokensPlayground() { + const [baseOverrides, setBaseOverrides] = useState>>({}); + const [tokenOverrides, setTokenOverrides] = useState>({}); + const [result, setResult] = useState(null); + const [open, setOpen] = useState>({}); + const runId = useRef(0); + + useEffect(() => { + const id = ++runId.current; + compileTokens(baseOverrides, tokenOverrides).then((res) => { + // Ignore results from superseded compiles (keeps the latest edit winning). + if (id === runId.current) setResult(res); + }); + }, [baseOverrides, tokenOverrides]); + + const valueByPath = useMemo(() => { + const map: Record = {}; + for (const t of result?.tokens ?? []) map[t.path] = t.value; + return map; + }, [result]); + + const previewStyle = (result?.vars ?? {}) as unknown as CSSProperties; + const hasOverrides = Object.keys(baseOverrides).length > 0 || Object.keys(tokenOverrides).length > 0; + + const setToken = (path: string, value: string) => setTokenOverrides((p) => ({ ...p, [path]: value })); + const resetToken = (path: string) => setTokenOverrides((p) => { + const next = { ...p }; + delete next[path]; + return next; + }); + const resetAll = () => { setBaseOverrides({}); setTokenOverrides({}); }; + + const derivedPaths = result?.tokens.map((t) => t.path) ?? []; + + /** One editable derived/sibling token row (colour swatch when the value is a colour). */ + const renderTokenRow = (path: string, label?: string) => { + const value = tokenOverrides[path] ?? valueByPath[path] ?? ''; + const overridden = path in tokenOverrides; + const colorish = isColorValue(value); + return ( +
+ {colorish ? ( + setToken(path, e.target.value)} + /> + ) : ( + + )} + {label ?? path.replace(/^color\./, '')} + setToken(path, e.target.value)} + /> + +
+ ); + }; + + return ( +
+ {/* Live preview — the compiled tokens are applied here. `sb-unstyled` opts + out of Storybook's docs typography reset, which otherwise forces its own + font-family onto the buttons and defeats the `font-family: inherit` chain + the font-family token relies on. */} +
+
+

Body text

+ {/* Uses the base font family & size tokens directly, so changing the + Font family control is immediately visible here. */} +
+

Themeable by design

+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. This heading and + paragraph render in typography.font.family.base — the same source + token the buttons inherit — so switching it to Serif or Monospace re-typefaces + everything at once. +

+
+
+
+

Buttons

+
+ + + + +
+
+
+

Sizes & states

+
+ + + + +
+
+
+

Button group

+
+ + + + + +
+
+
+ + {/* Controls. */} +
+
+

Design tokens

+ +
+

+ Set a source colour below; Style Dictionary re-derives the palette scale, + theme aliases and button tokens live. Expand a source to edit its derived + tokens directly. +

+ + {VARIANTS.map((v) => { + const baseValue = baseOverrides[v] ?? BASE_COLORS[v]; + const groups = groupsForVariant(derivedPaths, v); + const isOpen = !!open[v]; + const derivedCount = groups.reduce((n, g) => n + g.paths.length, 0); + return ( +
+
+ setBaseOverrides((p) => ({ ...p, [v]: e.target.value }))} + /> + + {SOURCE_LABEL[v]} + {BASE_TOKEN_PATH[v]} + + setBaseOverrides((p) => ({ ...p, [v]: e.target.value }))} + /> +
+ + + + {isOpen && ( +
+ {groups.map((group) => ( +
+
{group.label}
+ {group.paths.map((path) => renderTokenRow(path))} +
+ ))} +
+ )} +
+ ); + })} + +

Typography, sizing & spacing

+

+ Global tokens that theme every variant. Border radius fans out — through + a real calc() — into the focus-ring radius. +

+ + {GLOBAL_CONTROLS.map((control) => { + const value = tokenOverrides[control.path] ?? valueByPath[control.path] ?? ''; + const overridden = control.path in tokenOverrides; + const isOpen = !!open[control.path]; + const derived = control.derived ?? []; + const related = control.related ?? []; + const detailCount = derived.length + related.length; + return ( +
+
+ {control.kind === 'font' && ( + Aa + )} + + {control.label} + {control.path} + + {overridden && ( + + )} +
+ +
+ setToken(control.path, e.target.value)} + /> +
+ + {control.presets && ( +
+ {control.presets.map((preset) => ( + + ))} +
+ )} + + {detailCount > 0 && ( + <> + + {isOpen && ( +
+ {derived.length > 0 && ( +
+
Derived — track the value above
+ {derived.map((path) => renderTokenRow(path))} +
+ )} + {related.length > 0 && ( +
+
+ {control.relatedLabel ?? 'Related — set independently'} +
+ {control.relatedNote && ( +

{control.relatedNote}

+ )} + {related.map((path) => renderTokenRow(path))} +
+ )} +
+ )} + + )} +
+ ); + })} +
+
+ ); +} + +export default DesignTokensPlayground; diff --git a/prototype/src/theming/compileTokens.test.ts b/prototype/src/theming/compileTokens.test.ts new file mode 100644 index 00000000000..f1fda55d405 --- /dev/null +++ b/prototype/src/theming/compileTokens.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest'; +import { compileTokens } from './compileTokens'; + +describe('compileTokens (browser Style Dictionary)', () => { + it('reproduces the shipped brand & primary button tokens', async () => { + const { vars } = await compileTokens(); + // Brand base #9D0054 -> white text (color-yiq), border == bg. + expect(vars['--pgn-color-btn-bg-brand']).toBe('#9D0054FF'); + expect(vars['--pgn-color-btn-text-brand']).toBe('#FFFFFFFF'); + expect(vars['--pgn-color-btn-border-brand']).toBe('#9D0054FF'); + // Primary base #0A3055 -> white text. + expect(vars['--pgn-color-btn-bg-primary']).toBe('#0A3055FF'); + expect(vars['--pgn-color-btn-text-primary']).toBe('#FFFFFFFF'); + // Scale midpoints resolve to the base. + expect(vars['--pgn-color-brand-500']).toBe('#9D0054FF'); + }); + + it('re-derives text colour when the base flips light', async () => { + const { vars } = await compileTokens({ brand: '#FFEE88' }); + expect(vars['--pgn-color-btn-bg-brand']).toBe('#FFEE88FF'); + // Light background -> dark text. + expect(vars['--pgn-color-btn-text-brand']?.toUpperCase()).toContain('454545'); + }); +}); + +describe('non-colour global tokens', () => { + it('passes through and resolves references inside calc()', async () => { + const { vars } = await compileTokens(); + expect(vars['--pgn-typography-font-family-base']).toContain('apple-system'); + expect(vars['--pgn-typography-btn-font-family']).toBe('inherit'); + expect(vars['--pgn-typography-btn-font-size-base']).toBe('1.125rem'); + expect(vars['--pgn-size-btn-border-radius-base']).toBe('.375rem'); + expect(vars['--pgn-size-btn-focus-border-radius-base']).toBe('calc(.375rem + calc(2px + 2px))'); + }); + + it('re-derives the focus radius when the base border radius changes', async () => { + const { vars } = await compileTokens({}, { 'size.border.radius.base': '1rem' }); + expect(vars['--pgn-size-btn-border-radius-base']).toBe('1rem'); + expect(vars['--pgn-size-btn-focus-border-radius-base']).toBe('calc(1rem + calc(2px + 2px))'); + }); +}); diff --git a/prototype/src/theming/compileTokens.ts b/prototype/src/theming/compileTokens.ts new file mode 100644 index 00000000000..1c5df44f99f --- /dev/null +++ b/prototype/src/theming/compileTokens.ts @@ -0,0 +1,147 @@ +/** + * Runs a real Style Dictionary instance **in the browser** over the in-memory + * brand/primary token tree (`tokenTree.ts`) and returns the compiled CSS custom + * properties, so the Theming playground can recompile the whole derivation graph + * live as the user edits a token. + * + * The colour maths (`mix`, `color-yiq`, `darken`, `lighten`) is ported verbatim + * from the repo's `tokens/style-dictionary.js` + `tokens/sass-helpers.js` (chroma + * -js), with the `color-yiq` constants (threshold / light / dark) inlined from + * the repo's `other.json` token files. Style Dictionary v4 is browser-safe: its core only + * touches the filesystem through the swappable `style-dictionary/fs` entry, which + * defaults to an in-memory implementation, and we build entirely from an + * in-memory `tokens` object with no file sources or file output. + */ +import StyleDictionary from 'style-dictionary'; +import type { DesignTokens } from 'style-dictionary/types'; +import chroma from 'chroma-js'; + +import { buildTokenTree, type Variant } from './tokenTree'; + +// color-yiq constants, inlined from tokens/src/**/global/other.json (light theme). +const YIQ_THRESHOLD = 128; +const YIQ_TEXT_DARK = '#454545'; +const YIQ_TEXT_LIGHT = '#FFFFFF'; + +const RESERVED = ['inherit', 'initial', 'revert', 'unset', 'currentColor', 'none', 'transparent']; + +/** Bootstrap's `color-yiq`: pick the dark or light text colour for a background. */ +function colorYiq(background: chroma.Color): chroma.Color { + const [r, g, b] = background.rgb(); + const yiq = ((r * 299) + (g * 587) + (b * 114)) * 0.001; + let result = yiq >= YIQ_THRESHOLD ? chroma(YIQ_TEXT_DARK) : chroma(YIQ_TEXT_LIGHT); + + // Nudge toward the required 4.5:1 contrast, exactly as sass-helpers.js does. + const maxAttempts = 10; + const brightening = yiq < YIQ_THRESHOLD; + let attempts = 1; + while (chroma.contrast(background, result) < 4.5 && attempts <= maxAttempts) { + result = brightening ? result.brighten(0.1) : result.darken(0.1); + attempts += 1; + } + return result; +} + +// SASS-compatible darken/lighten (operate on HSL lightness), from sass-helpers.js. +const lighten = (color: chroma.Color, amount: number) => color.set('hsl.l', (color.get('hsl.l') as number) + amount); +const darken = (color: chroma.Color, amount: number) => lighten(color, -amount); + +interface ModifyStep { type: string; amount?: number; otherColor?: string; } + +/** Applies a token's `modify` chain and returns an 8-digit hex, like colorTransform. */ +function colorTransform(value: string, originalValue: string, modify?: ModifyStep[]): string { + if (RESERVED.includes(originalValue)) return originalValue; + if (RESERVED.includes(value)) return value; + if (!chroma.valid(value)) return value; + + let color = chroma(value); + for (const step of modify ?? []) { + switch (step.type) { + case 'mix': + color = chroma.mix(color, step.otherColor as string, step.amount, 'rgb'); + break; + case 'color-yiq': + color = colorYiq(color); + break; + case 'darken': + color = darken(color, step.amount as number); + break; + case 'lighten': + color = lighten(color, step.amount as number); + break; + default: + break; + } + } + return color.hex('rgba').toUpperCase(); +} + +let registered = false; +function ensureRegistered() { + if (registered) return; + StyleDictionary.registerTransform({ + name: 'color/pgn-modify', + type: 'value', + transitive: true, // re-run after references resolve, so color-yiq sees the resolved bg + filter: (token) => token.$type === 'color' || String(token.$value).startsWith('#'), + transform: (token) => colorTransform( + String(token.$value), + String(token.original?.$value ?? token.$value), + (token as { modify?: ModifyStep[] }).modify, + ), + }); + registered = true; +} + +export interface CompiledToken { + /** Dotted DTCG path, e.g. `color.btn.bg.brand`. */ + path: string; + /** CSS custom property name incl. prefix, e.g. `--pgn-color-btn-bg-brand`. */ + cssVar: string; + /** Final computed value, e.g. `#9D0054FF`. */ + value: string; +} + +export interface CompileResult { + /** `--pgn-*` → value, ready to apply to an element's style. */ + vars: Record; + /** Every compiled token, for driving the derived-tokens tree. */ + tokens: CompiledToken[]; +} + +/** + * Compiles the token tree with the given overrides. + * + * @param baseOverrides new base colour per variant (the `$brand` / `$primary` pickers) + * @param tokenOverrides literal pins for individually-edited derived tokens + */ +export async function compileTokens( + baseOverrides: Partial> = {}, + tokenOverrides: Record = {}, +): Promise { + ensureRegistered(); + const tokens = buildTokenTree(baseOverrides, tokenOverrides); + + const sd = new StyleDictionary({ + tokens: tokens as unknown as DesignTokens, + platforms: { + css: { + transforms: ['attribute/cti', 'name/kebab', 'color/pgn-modify'], + prefix: 'pgn', + }, + }, + log: { verbosity: 'silent', warnings: 'disabled' }, + }); + + const dictionary = await sd.getPlatformTokens('css', { cache: false }); + + const vars: Record = {}; + const compiled: CompiledToken[] = []; + for (const token of dictionary.allTokens) { + const cssVar = `--${token.name}`; + const value = String(token.$value); + vars[cssVar] = value; + compiled.push({ path: token.path.join('.'), cssVar, value }); + } + return { vars, tokens: compiled }; +} diff --git a/prototype/src/theming/designTokens.module.css b/prototype/src/theming/designTokens.module.css new file mode 100644 index 00000000000..95aeb752b32 --- /dev/null +++ b/prototype/src/theming/designTokens.module.css @@ -0,0 +1,328 @@ +/* Styling for the Design Tokens / Theming playground (DesignTokensPlayground.tsx). */ + +.root { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 22rem); + gap: 1.5rem; + align-items: start; + margin: 1rem 0; + font-family: var(--pgn-typography-font-family-base); + color: var(--pgn-color-body-base); +} + +@media (max-width: 48rem) { + .root { grid-template-columns: minmax(0, 1fr); } +} + +/* --- Live preview (left) --- */ +.preview { + border: 1px solid #d0d0d0; + border-radius: 8px; + padding: 1.5rem; + background-color: var(--pgn-color-light-200); + display: flex; + flex-direction: column; + gap: 1.25rem; + position: sticky; + top: 1rem; +} + +.previewSection > h4 { + font: 600 11px/1 system-ui, sans-serif; + letter-spacing: 0.05em; + text-transform: uppercase; + color: #6b6b7b; + margin: 0 0 0.6rem; +} + +.previewRow { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; +} + +/* Sample prose that renders in the base font tokens, so the Font family control + has something obvious (beyond the buttons) to affect. */ +.proseSample { + font-family: var(--pgn-typography-font-family-base); + color: var(--pgn-color-body-base); +} + +.proseSample h3 { + font-size: 1.375rem; + font-weight: 700; + margin: 0 0 0.4rem; +} + +.proseSample p { + font-size: var(--pgn-typography-font-size-base, 1.125rem); + line-height: 1.5; + margin: 0; + max-width: 42rem; +} + +.proseSample code { + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 0.85em; + background: rgba(0, 0, 0, 0.06); + border-radius: 3px; + padding: 1px 4px; +} + +/* --- Controls (right) --- */ +.panel { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.panelHeader { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; +} + +.panelTitle { + font: 700 13px/1.2 system-ui, sans-serif; + margin: 0; +} + +.resetAll { + font: 600 12px/1 system-ui, sans-serif; + color: #3a3a4a; + background: #fff; + border: 1px solid #c4c4cc; + border-radius: 4px; + padding: 0.35rem 0.6rem; + cursor: pointer; +} + +.resetAll:disabled { opacity: 0.4; cursor: default; } + +.card { + border: 1px solid #d9d9df; + border-radius: 8px; + overflow: hidden; +} + +/* The prominent top-level source-token control. */ +.baseControl { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.9rem 1rem; + background: #fafafa; +} + +.baseSwatch { + inline-size: 2.75rem; + block-size: 2.75rem; + border: 1px solid #c4c4cc; + border-radius: 6px; + padding: 0; + background: none; + cursor: pointer; + flex: none; +} + +.baseMeta { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; } + +.baseName { + font: 700 14px/1 system-ui, sans-serif; +} + +.baseToken { + font: 12px/1 "SFMono-Regular", Consolas, monospace; + color: #8a8a96; +} + +.baseValueInput { + margin-inline-start: auto; + inline-size: 6.5rem; + font: 12.5px/1 "SFMono-Regular", Consolas, monospace; + padding: 0.35rem 0.45rem; + border: 1px solid #c4c4cc; + border-radius: 4px; + text-transform: uppercase; +} + +/* Disclosure for the derived tokens. */ +.disclosure { + width: 100%; + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.55rem 1rem; + background: #fff; + border: none; + border-top: 1px solid #ececf0; + font: 600 12px/1 system-ui, sans-serif; + color: #4a4a58; + cursor: pointer; + text-align: start; +} + +.disclosure:hover { background: #fbfbfd; } + +.caret { + display: inline-block; + transition: transform 0.12s ease; + color: #9a9aa6; +} + +.caretOpen { transform: rotate(90deg); } + +.count { + margin-inline-start: auto; + font: 600 11px/1 system-ui, sans-serif; + color: #9a9aa6; +} + +.derived { + border-top: 1px solid #ececf0; + background: #fcfcfd; +} + +.groupHeader { + font: 700 10px/1 system-ui, sans-serif; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #9a9aa6; + padding: 0.6rem 1rem 0.3rem; +} + +.groupNote { + font: 11px/1.4 system-ui, sans-serif; + color: #9a9aa6; + margin: 0; + padding: 0 1rem 0.35rem; +} + +.tokenRow { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.3rem 1rem; +} + +.tokenRow.overridden { + background: #f6f2ff; +} + +.tokenSwatch { + inline-size: 1.25rem; + block-size: 1.25rem; + border: 1px solid #c4c4cc; + border-radius: 4px; + padding: 0; + background: none; + cursor: pointer; + flex: none; +} + +.tokenSwatchStatic { + inline-size: 1.25rem; + block-size: 1.25rem; + border: 1px solid #c4c4cc; + border-radius: 4px; + flex: none; +} + +.tokenName { + font: 11.5px/1.3 "SFMono-Regular", Consolas, monospace; + color: #3a3a48; + flex: 1 1 auto; + min-width: 0; + overflow-wrap: anywhere; +} + +.tokenValue { + inline-size: 6.25rem; + font: 11px/1 "SFMono-Regular", Consolas, monospace; + padding: 0.25rem 0.4rem; + border: 1px solid #d0d0d6; + border-radius: 4px; + flex: none; +} + +.tokenValueReadonly { + color: #8a8a96; + background: #f4f4f6; +} + +.resetRow { + font: 600 14px/1 system-ui, sans-serif; + color: #9a9aa6; + background: none; + border: none; + cursor: pointer; + padding: 0 0.15rem; + flex: none; +} + +.resetRow:hover { color: #3a3a4a; } + +.hint { + font: 12px/1.5 system-ui, sans-serif; + color: #6b6b7b; + margin: 0; +} + +.hint code { + font: 11px/1 "SFMono-Regular", Consolas, monospace; + background: #eeeef2; + border-radius: 3px; + padding: 1px 4px; +} + +/* Wide value input used by the non-colour global controls. */ +.valueInputWide { + flex: 1 1 auto; + min-width: 0; + font: 12.5px/1.3 "SFMono-Regular", Consolas, monospace; + padding: 0.4rem 0.5rem; + border: 1px solid #c4c4cc; + border-radius: 4px; +} + +.valueInputWide:focus { + outline: 2px solid #7a5cff; + outline-offset: -1px; + border-color: #7a5cff; +} + +.fontPreview { + inline-size: 2.75rem; + block-size: 2.75rem; + border: 1px solid #c4c4cc; + border-radius: 6px; + display: grid; + place-items: center; + font-size: 1.1rem; + font-weight: 700; + color: #3a3a48; + flex: none; +} + +.presets { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + padding: 0 1rem 0.75rem; +} + +.preset { + font: 600 11px/1 system-ui, sans-serif; + color: #4a4a58; + background: #f4f4f6; + border: 1px solid #d9d9df; + border-radius: 999px; + padding: 0.3rem 0.65rem; + cursor: pointer; +} + +.preset:hover { + background: #ececf0; +} diff --git a/prototype/src/theming/tokenTree.ts b/prototype/src/theming/tokenTree.ts new file mode 100644 index 00000000000..8714e377b38 --- /dev/null +++ b/prototype/src/theming/tokenTree.ts @@ -0,0 +1,360 @@ +/** + * An in-memory DTCG token subset covering Paragon's brand & primary button + * chains, authored to match `tokens/src/` exactly. + * + * This is the same derivation graph Style Dictionary builds from the repo's JSON + * — a single "source" colour (`$brand` / `$primary`) fans out into a 100–900 + * tint/shade scale (`mix` with white/black), a set of theme-level aliases + * (`color.theme.*`), and finally the per-variant button tokens + * (`color.btn.*.{brand,primary}`) the `.btn-` CSS consumes. Button text + * colours are `color-yiq` of their background (Bootstrap's contrast picker), so + * flipping the base colour light/dark flips the label automatically. + * + * Only brand & primary (solid + outline) are modelled — see the Theming page. + * The compiler in `compileTokens.ts` runs a real Style Dictionary instance over + * this tree in the browser, so editing any node recompiles the whole graph. + */ + +/** A colour modification, matching `tokens/style-dictionary.js`'s `modify` API. */ +export type ColorModify = + | { type: 'mix'; otherColor: 'white' | 'black'; amount: number } + | { type: 'color-yiq' } + | { type: 'darken'; amount: number } + | { type: 'lighten'; amount: number }; + +export interface ColorToken { + $type: 'color'; + $value: string; + modify?: ColorModify[]; + /** Human label for the derived-tokens tree (not used by Style Dictionary). */ + $description?: string; +} + +/** Any DTCG leaf token (colour, dimension, fontFamily, …). */ +export interface TokenLeaf { + $type: string; + $value: string; + modify?: ColorModify[]; +} + +/** The two base ("source") colours the whole tree derives from. */ +export const BASE_COLORS = { + brand: '#9D0054', + primary: '#0A3055', +} as const; + +export type Variant = keyof typeof BASE_COLORS; +export const VARIANTS: Variant[] = ['brand', 'primary']; + +/** Dotted token path → its source ($…) token in the repo, for the base pickers. */ +export const BASE_TOKEN_PATH: Record = { + brand: 'color.brand.base', + primary: 'color.primary.base', +}; + +// The 100–900 scale: mix the base with white (tints) or black (shades). 500 is +// the base itself. Amounts are a 1:1 copy of the repo's global/color.json. +const SCALE: Array<[level: string, mix: ColorModify | null]> = [ + ['100', { type: 'mix', otherColor: 'white', amount: 0.94 }], + ['200', { type: 'mix', otherColor: 'white', amount: 0.75 }], + ['300', { type: 'mix', otherColor: 'white', amount: 0.50 }], + ['400', { type: 'mix', otherColor: 'white', amount: 0.25 }], + ['500', null], + ['600', { type: 'mix', otherColor: 'black', amount: 0.10 }], + ['700', { type: 'mix', otherColor: 'black', amount: 0.20 }], + ['800', { type: 'mix', otherColor: 'black', amount: 0.25 }], + ['900', { type: 'mix', otherColor: 'black', amount: 0.30 }], +]; + +// Which scale level each theme-level alias points at (from themes/light/alias). +const THEME_LEVELS: Record = { + bg: '100', border: '200', focus: '500', hover: '700', active: '900', +}; + +const ref = (path: string): ColorToken => ({ $type: 'color', $value: `{${path}}` }); +const yiq = (path: string): ColorToken => ({ $type: 'color', $value: `{${path}}`, modify: [{ type: 'color-yiq' }] }); +const lit = (value: string): ColorToken => ({ $type: 'color', $value: value }); + +/** Builds the `color.btn.*` tokens for one solid variant + its `outline-` form. */ +function buildButtonTokens(v: Variant) { + const o = `outline-${v}`; + return { + bg: { + [v]: ref(`color.${v}.base`), + [o]: lit('transparent'), + }, + text: { + [v]: yiq(`color.btn.bg.${v}`), + [o]: ref(`color.${v}.base`), + }, + border: { + [v]: ref(`color.btn.bg.${v}`), + [o]: ref(`color.${v}.base`), + }, + hover: { + bg: { + [v]: ref(`color.theme.hover.${v}`), + [o]: ref(`color.${v}.100`), + }, + text: { + [v]: yiq(`color.btn.hover.bg.${v}`), + [o]: ref(`color.theme.hover.${v}`), + }, + border: { + [v]: ref(`color.theme.hover.${v}`), + [o]: ref(`color.${v}.900`), + }, + }, + active: { + bg: { + [v]: ref(`color.theme.active.${v}`), + [o]: ref(`color.theme.bg.${v}`), + }, + text: { + [v]: yiq(`color.btn.active.bg.${v}`), + [o]: yiq(`color.btn.active.bg.${o}`), + }, + border: { + [v]: ref(`color.theme.active.${v}`), + [o]: ref(`color.theme.active.${v}`), + }, + }, + focus: { + bg: { + [v]: ref(`color.btn.bg.${v}`), + [o]: lit('inherit'), + }, + text: { + [v]: ref(`color.btn.text.${v}`), + [o]: ref(`color.btn.text.${o}`), + }, + border: { + [v]: ref(`color.btn.border.${v}`), + [o]: ref(`color.btn.border.${o}`), + }, + outline: { + [v]: ref(`color.theme.focus.${v}`), + [o]: ref(`color.theme.focus.${v}`), + }, + }, + disabled: { + bg: { + [v]: ref(`color.btn.bg.${v}`), + [o]: lit('inherit'), + }, + text: { + [v]: ref(`color.btn.text.${v}`), + [o]: ref(`color.theme.hover.${v}`), + }, + border: { + [v]: ref(`color.btn.border.${v}`), + [o]: ref(`color.theme.hover.${v}`), + }, + }, + }; +} + +/** Deep-merges plain objects (used to fold each variant's btn tokens together). */ +function deepMerge>(target: T, source: T): T { + for (const key of Object.keys(source)) { + const s = source[key]; + const t = (target as Record)[key]; + if (s && typeof s === 'object' && !Array.isArray(s) && !('$value' in (s as object)) + && t && typeof t === 'object') { + deepMerge(t as Record, s as Record); + } else { + (target as Record)[key] = s; + } + } + return target; +} + +/** + * Builds the full DTCG token object. `baseOverrides` replaces a variant's base + * colour (the `$brand` / `$primary` pickers); `tokenOverrides` pins any derived + * token to a literal value (editing a node lower in the tree), dropping its + * modifiers/references so downstream tokens recompute from the pinned value. + */ +export function buildTokenTree( + baseOverrides: Partial> = {}, + tokenOverrides: Record = {}, +): Record { + const color: Record = { + $type: 'color', + white: lit('#FFFFFF'), + black: lit('#000000'), + gray: { 100: lit('#EBEBEB') }, + }; + + const theme: Record> = {}; + for (const [level, scaleLevel] of Object.entries(THEME_LEVELS)) { + theme[level] = {}; + for (const v of VARIANTS) theme[level][v] = ref(`color.${v}.${scaleLevel}`); + } + + let btn: Record = {}; + for (const v of VARIANTS) { + // Scale + base for this variant. + const scale: Record = { + base: lit(baseOverrides[v] ?? BASE_COLORS[v]), + }; + for (const [level, mix] of SCALE) { + scale[level] = mix + ? { $type: 'color', $value: `{color.${v}.base}`, modify: [mix] } + : ref(`color.${v}.base`); + } + color[v] = scale; + btn = deepMerge(btn, buildButtonTokens(v) as Record); + } + + color.theme = theme; + color.btn = btn; + + const tree = { color, ...buildGlobalTokens() }; + + // Apply literal pins for individually-edited derived tokens. + for (const [path, value] of Object.entries(tokenOverrides)) { + setTokenValue(tree, path, value); + } + return tree; +} + +/** + * Non-colour "global" tokens that theme every variant: font family, button font + * size, border radius and button padding. These mirror the derivation chains in + * `tokens/src/core/**` — most notably border radius fanning out, through a real + * `calc()`, into the focus-ring radius (`size.btn.focus.border-radius.base`). + */ +export const SYSTEM_FONT_STACK = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif'; +export const SERIF_FONT_STACK = 'Georgia, Cambria, "Times New Roman", Times, serif'; +export const MONOSPACE_FONT_STACK = 'SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace'; + +const dim = (value: string): TokenLeaf => ({ $type: 'dimension', $value: value }); +const font = (value: string): TokenLeaf => ({ $type: 'fontFamily', $value: value }); + +function buildGlobalTokens(): Record { + return { + typography: { + // -> --pgn-typography-font-family-base (the body font the preview inherits) + font: { family: { base: font(SYSTEM_FONT_STACK) } }, + btn: { + font: { + // Faithful to the repo: the button font-family is `inherit`, so it + // follows whatever `typography.font.family.base` resolves to. + // -> --pgn-typography-btn-font-family + family: font('inherit'), + // -> --pgn-typography-btn-font-size-{base,sm,lg} + size: { + base: dim('1.125rem'), + sm: dim('.875rem'), + lg: dim('1.325rem'), + }, + }, + }, + }, + size: { + border: { radius: { base: dim('.375rem') } }, + btn: { + border: { radius: { base: dim('{size.border.radius.base}') } }, + focus: { + width: dim('2px'), + // border-gap = focus.width + focus.gap, and focus.gap === focus.width. + 'border-radius': { base: dim('calc({size.btn.border.radius.base} + {spacing.btn.focus.border-gap})') }, + }, + }, + }, + spacing: { + btn: { + padding: { + x: { base: dim('1rem') }, + y: { base: dim('.5625rem') }, + }, + focus: { + 'border-gap': dim('calc({size.btn.focus.width} + {size.btn.focus.width})'), + }, + }, + }, + }; +} + +/** A description of a top-level non-colour control and its related tokens. */ +export interface GlobalControl { + path: string; + label: string; + kind: 'font' | 'dimension'; + presets?: Array<{ label: string; value: string }>; + /** + * Tokens that reference this source (directly or through a `calc()`), so they + * recompute when it changes. + */ + derived?: string[]; + /** + * Tokens in the same family shown for context but that are not derived from + * this control. Their exact relationship is spelled out by `relatedNote`. + */ + related?: string[]; + /** Heading for the related group (defaults to "Related — set independently"). */ + relatedLabel?: string; + /** One-line explanation of how the related tokens relate to this control. */ + relatedNote?: string; +} + +export const GLOBAL_CONTROLS: GlobalControl[] = [ + { + path: 'typography.font.family.base', + label: 'Font family', + kind: 'font', + presets: [ + { label: 'System', value: SYSTEM_FONT_STACK }, + { label: 'Serif', value: SERIF_FONT_STACK }, + { label: 'Monospace', value: MONOSPACE_FONT_STACK }, + ], + // The button font-family token is the CSS keyword `inherit` — its own value + // never changes, yet buttons still follow the base via the CSS cascade. + related: ['typography.btn.font.family'], + relatedLabel: 'Consumed by buttons', + relatedNote: 'The button font-family is the CSS keyword “inherit”, not a reference to the base above — so buttons follow the base font through the CSS cascade, and its token value stays “inherit”.', + }, + { + path: 'typography.btn.font.size.base', + label: 'Button font size', + kind: 'dimension', + // sm / lg are independent literals in the token source, not multiples of base. + related: ['typography.btn.font.size.sm', 'typography.btn.font.size.lg'], + relatedLabel: 'Related — set independently', + relatedNote: 'Separate literals in the token source, so they don’t change when the value above does.', + }, + { + path: 'size.border.radius.base', + label: 'Border radius', + kind: 'dimension', + derived: ['size.btn.border.radius.base', 'size.btn.focus.border-radius.base'], + }, + { + path: 'spacing.btn.padding.x.base', + label: 'Button padding (horizontal)', + kind: 'dimension', + }, + { + path: 'spacing.btn.padding.y.base', + label: 'Button padding (vertical)', + kind: 'dimension', + }, +]; + +/** Pins the token at a dotted path to a literal value, clearing its modifiers. */ +function setTokenValue(tree: Record, path: string, value: string) { + const parts = path.split('.'); + let node: Record = tree; + for (const part of parts.slice(0, -1)) { + node = node[part] as Record; + if (!node) return; + } + const key = parts[parts.length - 1]; + const leaf = node[key] as TokenLeaf | undefined; + if (leaf && '$value' in leaf) { + // Preserve the token's $type (colour vs dimension vs fontFamily) so the + // pinned value keeps compiling correctly; just drop any modifiers. + node[key] = { $type: leaf.$type, $value: value }; + } +} From 033759402d680a472055cad1d98194f3b261c3e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 21:56:55 +0000 Subject: [PATCH 22/24] feat: prototype MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-implement the Tabs module on the prototype's modern stack (React Aria + CSS Modules over --pgn-* tokens), matching the Button / Collapsible / Stack conventions. - Behavior from React Aria's useTabList/useTab/useTabPanel + react-stately's useTabListState, replacing react-bootstrap Tabs and the MutationObserver aria-selected shim. Correct tablist/tab/tabpanel roles, arrow-key navigation, and aria-selected/aria-controls linkage. - Declarative public API preserved: variant, defaultActiveKey/activeKey, onSelect, per-tab disabled and notification. - Styling ported from src/Nav + src/Tabs SCSS into the global public class layer (src/styles/tabs.css): nav / nav-tabs / nav-pills / nav-inverse-* / nav-button-group over the same tokens, keyboard-only focus via [data-focus-visible]. - Storybook docs (Tabs.mdx + Tabs.stories.tsx) mirror src/Tabs/README.md with a controls-driven Playground and typechecked, editable live examples. - Vitest coverage for selection, keyboard nav, disabled, controlled usage, notifications and per-tab classes. Deliberate gaps documented in the docs: only the active panel is mounted, and the responsive "More…" overflow dropdown is deferred with the Dropdown migration (moreTabText retained but inert). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JXRJCx3Ny3iX9B2kvy4fYd --- prototype/src/Tabs/Tab.tsx | 38 ++++ prototype/src/Tabs/Tabs.mdx | 122 +++++++++++ prototype/src/Tabs/Tabs.stories.tsx | 225 ++++++++++++++++++++ prototype/src/Tabs/Tabs.test.tsx | 113 +++++++++++ prototype/src/Tabs/Tabs.tsx | 201 ++++++++++++++++++ prototype/src/Tabs/index.ts | 4 + prototype/src/docs/scope.tsx | 3 + prototype/src/index.ts | 4 + prototype/src/styles/index.css | 1 + prototype/src/styles/tabs.css | 305 ++++++++++++++++++++++++++++ prototype/src/styles/utilities.css | 13 ++ 11 files changed, 1029 insertions(+) create mode 100644 prototype/src/Tabs/Tab.tsx create mode 100644 prototype/src/Tabs/Tabs.mdx create mode 100644 prototype/src/Tabs/Tabs.stories.tsx create mode 100644 prototype/src/Tabs/Tabs.test.tsx create mode 100644 prototype/src/Tabs/Tabs.tsx create mode 100644 prototype/src/Tabs/index.ts create mode 100644 prototype/src/styles/tabs.css diff --git a/prototype/src/Tabs/Tab.tsx b/prototype/src/Tabs/Tab.tsx new file mode 100644 index 00000000000..ceb4074f725 --- /dev/null +++ b/prototype/src/Tabs/Tab.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import type { Key } from 'react-stately'; + +export interface TabProps { + /** + * A unique identifier for the `Tab`, distinguishing it from its siblings. + * Selecting a tab reports this value through `Tabs`' `activeKey` / `onSelect`. + */ + eventKey: Key; + /** Specifies the `Tab`'s navigation title. */ + title: React.ReactNode; + /** Specifies the panel content shown when this `Tab` is selected. */ + children?: React.ReactNode; + /** + * Specifies notification bubble content. It appears on the top-right of the + * `Tab`'s title. + */ + notification?: React.ReactNode; + /** Specifies whether the `Tab` is disabled. */ + disabled?: boolean; + /** Specifies an additional class name to append to the `Tab`'s nav link. */ + tabClassName?: string; +} + +/** + * Declarative configuration for a single tab. `Tab` is never rendered to the DOM + * itself — its parent `Tabs` reads these props to build the React Aria / + * react-stately tab collection (the tab button from `title`/`notification`, the + * panel from `children`). This mirrors `@openedx/paragon`'s `Tab`: the public + * props (`eventKey`, `title`, `notification`, `disabled`) are unchanged. + */ +function Tab(_props: TabProps): React.ReactElement | null { + // Rendering is handled by `Tabs`; a stray `` outside `` renders + // nothing rather than leaking an unstyled element into the page. + return null; +} + +export default Tab; diff --git a/prototype/src/Tabs/Tabs.mdx b/prototype/src/Tabs/Tabs.mdx new file mode 100644 index 00000000000..0ba2e88fed6 --- /dev/null +++ b/prototype/src/Tabs/Tabs.mdx @@ -0,0 +1,122 @@ +import { Meta, Title, Subtitle, Story, Controls } from '@storybook/addon-docs/blocks'; + +import * as TabsStories from './Tabs.stories'; +import { LiveExample } from '../docs/LiveExample'; + +export const examples = TabsStories.examples; + + + +Tabs + +Organize content into sections and switch between them — React Aria tab behavior, styled entirely from Paragon design tokens. + +`Tabs` is the prototype re-implementation of Paragon's `Tabs` on the modern stack +proposed in [ADR 0022](https://github.com/openedx/paragon). The declarative +`` API — including `variant`, +`defaultActiveKey` / `activeKey`, `onSelect`, and per-tab `disabled` / +`notification` — is unchanged from `@openedx/paragon`, but the internals no longer +depend on Bootstrap or `react-bootstrap`: + +- **Behavior** comes from [React Aria](https://react-spectrum.adobe.com/react-aria/)'s + `useTabList` / `useTab` / `useTabPanel` plus react-stately's `useTabListState`: + correct `role="tablist"` / `tab` / `tabpanel` semantics, arrow-key navigation, + and `aria-selected` / `aria-controls` linkage — no more manual `MutationObserver` + syncing of `aria-selected`. +- **Styling** is the global, public `nav` / `nav-tabs` / `nav-pills` / + `nav-inverse-*` / `nav-button-group` class layer (`src/styles/tabs.css`) over + the same `--pgn-*` design tokens the library ships today, so a component and a + raw `