diff --git a/apps/docs/PRODUCT.md b/apps/docs/PRODUCT.md new file mode 100644 index 00000000..a24eab19 --- /dev/null +++ b/apps/docs/PRODUCT.md @@ -0,0 +1,53 @@ +# Product + + + +## Platform + +web + +## Users + +React developers evaluating or using Luke UI to build accessible interfaces. + +## Product Purpose + +Luke UI is a React design system built with React Aria Components. Its documentation helps +developers understand the system, evaluate its behaviour, and use its components and themes through +accurate, runnable examples. + +## Positioning + +Luke UI combines accessible React Aria behavior with statically generated styling and a themable +component system. Themes may intentionally have substantially different visual identities. + +## Operating Context + +Developers browse the documentation, inspect component examples and source code, and use the +playground to try examples before adopting them in an application. + +## Capabilities and Constraints + +- Luke UI is pre-1.0. Its public API is intentionally unstable while the design system is being + shaped. +- Documentation examples must remain accurate and runnable. +- Components must remain accessible and usable across themes, even when their visual treatments + differ substantially. + +## Brand Commitments + +The documentation and components do not have a fixed visual identity yet. Their visual design may +change, and themes can look substantially different from one another. + +## Evidence on Hand + +The repository contains the public component package, two bundled themes, hosted documentation, +runnable examples, a playground, Storybook stories, and automated unit, browser, and visual checks. +Do not claim customer adoption without evidence. + +## Product Principles + +- Demonstrate behaviour with accurate, runnable examples. +- Treat accessibility and interaction quality as product behaviour. +- Let themes express genuinely different visual identities. +- Change immature pre-1.0 contracts when a better design requires it. diff --git a/apps/docs/src/components/example-block.browser.test.tsx b/apps/docs/src/components/example-block.browser.test.tsx new file mode 100644 index 00000000..0a7af271 --- /dev/null +++ b/apps/docs/src/components/example-block.browser.test.tsx @@ -0,0 +1,48 @@ +import '../styles/app.css'; +import '@luke-ui/react/themes/tactile.css'; +import { IconSpritesheetProvider } from '@luke-ui/react/icon'; +import spriteSheetHref from '@luke-ui/react/spritesheet.svg?url&no-inline'; +import { tactileThemeClassName } from '@luke-ui/react/themes'; +import { act } from 'react'; +import type { Root } from 'react-dom/client'; +import { createRoot } from 'react-dom/client'; +import { afterEach, expect, test } from 'vite-plus/test'; +import { page } from 'vite-plus/test/context'; +import { ExampleLoadingState } from './example-block'; + +let container: HTMLElement | undefined; +let root: Root | undefined; +const exampleTitle = 'Combobox Field — Basic'; +const loadingLabel = `Loading ${exampleTitle} example`; + +afterEach(() => { + if (root) act(() => root?.unmount()); + container?.remove(); + container = undefined; + root = undefined; +}); + +test('shows a named loading state in a frame that reserves the preview space', () => { + renderExample(exampleTitle); + + const loadingState = page.getByRole('region', { name: loadingLabel }); + expect(page.getByText(exampleTitle, { exact: true })).toBeVisible(); + expect(page.getByRole('status', { name: loadingLabel })).toBeVisible(); + expect(loadingState.element().getBoundingClientRect().height).toBeGreaterThanOrEqual(96); + + const playgroundPlaceholder = page.getByText('Open in playground', { exact: true }).element(); + expect(playgroundPlaceholder.closest('[inert]')?.inert).toBe(true); +}); + +function renderExample(title: string) { + container = document.body.appendChild(document.createElement('div')); + container.className = `luke-ui-theme ${tactileThemeClassName}`; + root = createRoot(container); + act(() => { + root?.render( + + + , + ); + }); +} diff --git a/apps/docs/src/components/example-block.tsx b/apps/docs/src/components/example-block.tsx index aac688a7..1cb799a6 100644 --- a/apps/docs/src/components/example-block.tsx +++ b/apps/docs/src/components/example-block.tsx @@ -1,10 +1,12 @@ import { Box } from '@luke-ui/react/box'; import { Button } from '@luke-ui/react/button'; import { Icon } from '@luke-ui/react/icon'; +import { LoadingSkeleton } from '@luke-ui/react/loading-skeleton'; +import { LoadingSpinner } from '@luke-ui/react/loading-spinner'; import { button } from '@luke-ui/react/recipes'; import { createLink } from '@tanstack/react-router'; import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock'; -import type { ComponentType, JSX } from 'react'; +import type { ComponentType, JSX, ReactNode } from 'react'; import { Suspense, use, useId, useState } from 'react'; import { Link as RacLink } from 'react-aria-components/Link'; import { encodeCodeHash } from '../lib/playground-hash'; @@ -32,9 +34,7 @@ type ExampleBlockProps = { export function ExampleBlock(props: ExampleBlockProps): JSX.Element { return ( - Loading example…} - > + }> ); @@ -64,9 +64,8 @@ function ExampleContent({ mode, src, title }: ExampleBlockProps): JSX.Element { const [PreviewComponent, source] = result.data; return ( - - - {title} + - + } + title={title} + > @@ -101,6 +102,68 @@ function ExampleContent({ mode, src, title }: ExampleBlockProps): JSX.Element { /> ) : null} + + ); +} + +export function ExampleLoadingState({ mode, title }: Pick) { + const loadingLabel = `Loading ${title} example`; + const isFullBleed = mode === 'full-bleed'; + + return ( + } ariaLabel={loadingLabel} title={title}> + + + + + + + ); +} + +function ExampleLoadingActions() { + return ( + + + + + Open in playground + + + + + + + ); +} + +type ExampleFrameProps = { + actions?: ReactNode; + ariaLabel?: string; + children: ReactNode; + title: string; +}; + +function ExampleFrame({ actions, ariaLabel, children, title }: ExampleFrameProps) { + return ( + + + {title} + {actions} + + {children} ); }