diff --git a/.gitignore b/.gitignore index 92d25bcd..9118f242 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,8 @@ packages/website/docs/generated-docs/* packages/website/.docusaurus packages/website/.cache-loader packages/website/static/css/* + +# Playwright +packages/ui/playwright-report +packages/ui/test-results +.playwright-mcp diff --git a/.prettierignore b/.prettierignore index 804b3020..899c6a1b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,8 @@ # Add files here to ignore them from prettier formatting /dist /coverage +.claude/worktrees +.claude/launch.json packages/tokens/tokens.json packages/tokens/tokens.json diff --git a/README.md b/README.md index cc2294d5..78ab713a 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,18 @@ Developed and maintained by **[Synergy Codes](https://www.synergycodes.com/)**. ### πŸ“¦ Installation -Use one of the commands below to add **Overflow UI** to your project: +Overflow UI relies on `@base-ui/react` (headless component primitives) and `@phosphor-icons/react` (icon set) as peer dependencies. Install all three at once: ```bash -npm install @synergycodes/overflow-ui +npm install @synergycodes/overflow-ui @base-ui/react @phosphor-icons/react ``` ```bash -pnpm add @synergycodes/overflow-ui +pnpm add @synergycodes/overflow-ui @base-ui/react @phosphor-icons/react ``` ```bash -yarn add @synergycodes/overflow-ui +yarn add @synergycodes/overflow-ui @base-ui/react @phosphor-icons/react ``` ### 🎨 Import styles @@ -44,14 +44,18 @@ To make the styles use proper variables, include `data-theme` (`light` or `dark` ### πŸŽ›οΈ Use components +The library has a per-component subpath export, so you can import only what you use and let your bundler tree-shake the rest: + ```tsx -import { Input } from '@synergycodes/overflow-ui'; +import { Input } from '@synergycodes/overflow-ui/input'; // … ; ``` +The barrel import (`from '@synergycodes/overflow-ui'`) is also supported and tree-shakes equally well in modern bundlers. + ### Customization Each Overflow UI component uses CSS variables that are derived from primitive values. diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md new file mode 100644 index 00000000..2725bac3 --- /dev/null +++ b/packages/ui/CHANGELOG.md @@ -0,0 +1,61 @@ +# Changelog + +All notable changes to `@synergycodes/overflow-ui` are documented in this file. + +## 1.0.0-beta.28 + +This beta rebuilds the library on [Base UI](https://base-ui.com/) and removes +the previous MUI / Mantine / Emotion / Floating UI stack. It contains breaking +changes relative to `1.0.0-beta.27`; read the sections below before upgrading. + +### Breaking changes + +#### Dependencies + +- Removed `@mui/material`, `@mui/base`, `@mantine/core`, `@mantine/dates`, + `@emotion/*`, and `@floating-ui/react`. +- `@base-ui/react` (`^1.4.0`) is now a **peer dependency** - consumers must + install it alongside this package. +- `DatePicker` is rebuilt on `react-day-picker` + `date-fns`; `TextArea` on + `react-textarea-autosize`. `date-fns`, `react-day-picker`, and `clsx` are + bundled into the package output. + +#### Packaging + +- The build moved from a single bundle to a **multi-entry build** with a + per-component subpath export: `@synergycodes/overflow-ui/` (e.g. + `@synergycodes/overflow-ui/date-picker`). +- Importing from the package root still injects all required styles. Importing + per-component entries injects only that component's CSS, so add + `@synergycodes/overflow-ui/styles.css` once for the global layer order, + reset, and typography. +- The old `overflow-ui.js` bundle filename is retained as a compatibility shim + that re-exports the new `index.js` entry, so hard-coded paths keep resolving. + +#### Component API + +- **DatePicker**: the prop surface no longer forwards the full Mantine prop set. + It now accepts a curated list: `value`, `defaultValue`, `type`, + `valueFormat`, `placeholder`, `error`, `size`, `disabled`, `minDate`, + `maxDate`, `onChange`, `id`, `className`, `aria-label`, `aria-labelledby`. + - `valueFormat` uses `date-fns` tokens (e.g. `dd/MM/yyyy`). The legacy + `DD/MM/YYYY` (dayjs) default is accepted and converted for compatibility. + - In `range` mode, `onChange` fires `null` while a range is mid-selection and + emits the completed `[from, to]` tuple once both ends are picked. +- **Menu**: `onOpenChange` signature is now `(open: boolean, event?: Event)`. + The MUI `slotProps` / passthrough surface is no longer available. +- **Select**: `onChange` signature is `(event, value)` - the event is the first + argument, the selected value the second. +- **Switch**: `onChange` is `(checked: boolean, event: Event)`. The second + argument is the native DOM event (previously typed as a React + `ChangeEvent`, which never matched the value passed at + runtime). +- Transitions moved to the popup element, where Base UI sets + `data-starting-style` / `data-ending-style`. + +### Added + +- `@synergycodes/overflow-ui/styles.css` - standalone global stylesheet (layer + order, reset, typography) for per-component / subpath consumers. +- `Input` gains a typed `error` prop wired to the error state. +- `Snackbar` exposes proper `role="status"` / `aria-live` semantics. diff --git a/packages/ui/README.md b/packages/ui/README.md index b2836b36..0000d595 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -24,7 +24,9 @@ yarn add @synergycodes/overflow-ui ### 🎨 Import styles -Add to your style sheet or component: +Import components from the package root (the recommended default). Their styles, +including the global layer order, reset, and typography, are injected +automatically, so you only need to add the design tokens: ```css @import '@synergycodes/overflow-ui/tokens.css'; @@ -34,12 +36,18 @@ Add to your style sheet or component: import '@synergycodes/overflow-ui/tokens.css'; ``` +> **Subpath imports.** You can also import a single component directly, e.g. +> `import { DatePicker } from '@synergycodes/overflow-ui/date-picker'`. That +> injects only the component's own CSS, so import the global stylesheet once and +> **before any component**, so the cascade layers are ordered correctly: +> `import '@synergycodes/overflow-ui/styles.css'`. + ### πŸŽ›οΈ Apply the Theme To make the styles use proper variables, include data-theme (light or dark) attribute in : ```html - + ``` ### 🧱 Use components @@ -88,7 +96,6 @@ or a derived value used by the selected component: Overflow UI uses [CSS layers](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer) to separate its styles from yours. By default, CSS styles outside of any layer take precedence over what Overflow UI defines, so your styles will always win the specificity war. You can customize Overflow UI components with simple `input {}`. - ```css @layer ui.component { .separator { @@ -98,6 +105,7 @@ Overflow UI uses [CSS layers](https://developer.mozilla.org/en-US/docs/Web/CSS/@ ``` Default Overflow UI order: + ```css @layer ui.base, ui.component; ``` @@ -129,20 +137,11 @@ Edit `ui/preview-page/preview-page.tsx` to display desired components. ### πŸ“£ Important Note on Underlying Technology -> **Overflow UI is built on top of [MUI Base](https://v6.mui.com/base-ui/getting-started/), a headless component library that focuses on accessibility and logic, while leaving the styling up to us.** -> -> Thanks to MUI Base, Overflow UI provides components that are **accessible by default** and **fully customizable** through our design tokens. -> -> We are aware that **MUI Base has been deprecated**, and the MUI team recommends migrating to [Base UI](https://base-ui.com). -> However, after careful evaluation, we've chosen to **stay with MUI Base** for now because: -> -> * βœ… **Base UI is not yet mature enough** for our needs. -> * βœ… We want to ensure a stable, well-tested experience for Overflow UI users. +> **Overflow UI is built on top of [Base UI](https://base-ui.com), a headless component library that focuses on accessibility and logic, while leaving the styling up to us.** > -> This is a **conscious and informed decision**. -> We will continue to monitor Base UI's progress and will consider migrating when we feel it's the right time, ensuring a smooth and thoughtful transition for Overflow UI users. +> Thanks to Base UI, Overflow UI provides components that are **accessible by default** and **fully customizable** through our design tokens. > -> If you have any questions or concerns, feel free to reach out β€” we’re happy to share our reasoning and plans in more detail! +> Earlier `1.0.0` betas were built on the now-deprecated [MUI Base](https://v6.mui.com/base-ui/getting-started/). From `1.0.0-beta.28` the library is built on Base UI; see [CHANGELOG.md](./CHANGELOG.md) for the full list of changes. ## Showcase diff --git a/packages/ui/combine-css-bundle.mts b/packages/ui/combine-css-bundle.mts new file mode 100644 index 00000000..e4af5fdb --- /dev/null +++ b/packages/ui/combine-css-bundle.mts @@ -0,0 +1,67 @@ +import fs from 'node:fs'; +import { resolve } from 'node:path'; +import type { Plugin } from 'vite'; + +/** + * Post-build CSS steps for the multi-entry library bundle. See css-layers.md. + * + * - emits `index.css` (all component styles, prefixed with the @layer order) + * and `styles.css` (the global layer order, reset and typography); + * - emits an `overflow-ui.js` shim re-exporting the renamed `index.js` entry. + * + * Per-component stylesheets do not carry the @layer declaration; consumers + * establish the order by importing `styles.css` first (or the barrel). + */ +export function combineCssBundle(rootDir: string): Plugin { + const distDir = resolve(rootDir, 'dist'); + const stylesDir = resolve(rootDir, 'src/styles'); + + return { + name: 'overflow-ui:combine-css-bundle', + apply: 'build', + closeBundle() { + writeCombinedStylesheet(distDir, stylesDir); + writeGlobalStylesheet(distDir, stylesDir); + writeLegacyEntryShim(distDir); + }, + }; +} + +function readLayerOrder(stylesDir: string): string { + return fs.readFileSync(resolve(stylesDir, 'layers.css'), 'utf-8').trim(); +} + +function writeCombinedStylesheet(distDir: string, stylesDir: string) { + const assetsDir = resolve(distDir, 'assets'); + if (!fs.existsSync(assetsDir)) return; + + const styles = fs + .readdirSync(assetsDir) + .filter((file) => file.endsWith('.css')) + .sort() + .map((file) => fs.readFileSync(resolve(assetsDir, file), 'utf-8')) + .join('\n'); + + // index.css is consumed standalone, so it declares the @layer order itself. + const combined = `${readLayerOrder(stylesDir)}\n${styles}`; + fs.writeFileSync(resolve(distDir, 'index.css'), combined); +} + +function writeGlobalStylesheet(distDir: string, stylesDir: string) { + const globals = ['layers.css', 'globals.css', 'typography.css'] + .map((file) => fs.readFileSync(resolve(stylesDir, file), 'utf-8')) + .join('\n'); + + fs.writeFileSync(resolve(distDir, 'styles.css'), globals); +} + +function writeLegacyEntryShim(distDir: string) { + fs.writeFileSync( + resolve(distDir, 'overflow-ui.js'), + `export * from './index.js';\n`, + ); + fs.writeFileSync( + resolve(distDir, 'overflow-ui.d.ts'), + `export * from './index';\n`, + ); +} diff --git a/packages/ui/css-layers.md b/packages/ui/css-layers.md new file mode 100644 index 00000000..2272beb1 --- /dev/null +++ b/packages/ui/css-layers.md @@ -0,0 +1,32 @@ +# CSS layers + +Overflow UI emits all of its styles into two ordered cascade layers: + +```css +@layer ui.base, ui.component; +``` + +`ui.base` holds resets and primitives; `ui.component` holds component styles. +Declaring the order once, before any rule from either layer, guarantees that +`ui.component` always wins over `ui.base`, and that unlayered consumer styles +win over both. + +## Establishing the order + +The order is fixed by the **first** `@layer` declaration the browser sees, so +the declaration must load before any component rule. The declaration lives in +`styles.css` (and in the package barrel, which imports it first), so consumers +establish it by either: + +- importing from the package root - the barrel imports the declaration first; or +- importing `@synergycodes/overflow-ui/styles.css` **before** any component when + using per-component subpath imports. + +Per-component stylesheets deliberately do **not** repeat the declaration. If one +loads before `styles.css`, the first use of a layer fixes the order: a +`ui.component` rule seen before any `ui.base` rule locks it as +`[ui.component, ui.base]`, inverting the cascade. Importing `styles.css` first +avoids this. + +The combined `index.css` (consumed standalone, e.g. by Workflow Builder) carries +the declaration at its top for the same reason. diff --git a/packages/ui/package.json b/packages/ui/package.json index 187d817c..7df4ba21 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,7 +1,7 @@ { "name": "@synergycodes/overflow-ui", "type": "module", - "version": "1.0.0-beta.27", + "version": "1.0.0-beta.28", "description": "A React library for creating node-based UIs and diagram-driven applications. Perfect for React Flow users, providing ready-to-use node templates and components that work seamlessly with React Flow's ecosystem.", "keywords": [ "react", @@ -32,35 +32,38 @@ "dev": "vite build --watch", "check:built-css": "tsx ./scripts/check-built-css.ts", "preview": "vite --config preview-page/vite.preview.config.ts", + "preview:build": "vite build --config preview-page/vite.preview.config.ts", "lint": "eslint", "lint:fix": "eslint --fix", - "typecheck": "tsc --noEmit --pretty" + "typecheck": "tsc --noEmit --pretty", + "build:stats": "BUNDLE_STATS=1 vite build" }, - "module": "./dist/overflow-ui.js", + "module": "./dist/index.js", "files": [ "dist" ], - "main": "./dist/overflow-ui.js", + "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/overflow-ui.js" + "import": "./dist/index.js" }, + "./*": { + "types": "./dist/components/*/index.d.ts", + "import": "./dist/*.js" + }, + "./styles.css": "./dist/styles.css", "./tokens.css": "./dist/tokens.css" }, "dependencies": { - "@emotion/styled": "^11.14.0", - "@floating-ui/react": "^0.26.28", - "@mantine/core": "^7.17.2", - "@mantine/dates": "^7.17.2", - "@mui/base": "5.0.0-beta.62", - "@mui/material": "^6.4.7", - "@phosphor-icons/react": "^2.1.7", - "clsx": "^2.0.0" + "@phosphor-icons/react": "^2.0.0", + "clsx": "^2.0.0", + "date-fns": "^4.1.0", + "react-day-picker": "^9.14.0", + "react-textarea-autosize": "^8.5.6" }, "devDependencies": { - "@emotion/react": "^11.14.0", "@synergycodes/overflow-ui-tokens": "workspace:*", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", @@ -70,6 +73,7 @@ "postcss": "^8.5.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "rollup-plugin-visualizer": "^7.0.1", "typescript": "^5.6.3", "vite": "^6.2.2", "vite-plugin-dts": "^4.5.3", @@ -77,6 +81,7 @@ "vite-plugin-static-copy": "^2.3.1" }, "peerDependencies": { + "@base-ui/react": "^1.4.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, diff --git a/packages/ui/preview-page/main.tsx b/packages/ui/preview-page/main.tsx index c15af694..979bf41f 100644 --- a/packages/ui/preview-page/main.tsx +++ b/packages/ui/preview-page/main.tsx @@ -1,20 +1,5 @@ import { createRoot } from 'react-dom/client'; import { PreviewPage } from './preview-page'; -import styles from './preview-page.module.css'; const root = createRoot(document.querySelector('#root') as HTMLElement); - -root.render( - - - , -); - -function PreviewWrapper({ children }: { children: React.ReactNode }) { - return ( -
-
Components Testing
-
{children}
-
- ); -} +root.render(); diff --git a/packages/ui/preview-page/preview-page.module.css b/packages/ui/preview-page/preview-page.module.css index 24d629e4..bfe9b9f1 100644 --- a/packages/ui/preview-page/preview-page.module.css +++ b/packages/ui/preview-page/preview-page.module.css @@ -1,50 +1,68 @@ @import '../dist/tokens.css'; @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); -.preview-container { +html, +body, +:global(#root) { width: 100%; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; + margin: 0; + padding: 0; } -.preview-header { - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); - padding: 2rem; - composes: ax-public-h1 from global; +body { + background: #ffffff; } -.preview-content { +* { + font-family: 'Poppins', sans-serif; +} + +.preview-container { + width: 80rem; + margin: 0 auto; + padding: 1.5rem; display: flex; - align-items: center; - justify-content: center; + flex-direction: column; + gap: 2rem; + background: #ffffff; +} +.section { + border: 1px solid #e5e5e5; + border-radius: 0.5rem; + padding: 1.25rem 1.5rem; + display: flex; + flex-direction: column; gap: 1rem; + background: #ffffff; } -html, -body, -:global(#root) { - width: 100%; - height: 100%; - margin: 0; - padding: 0; +.section-header { + font-size: 0.875rem; + font-weight: 600; + color: #111827; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid #f3f4f6; + padding-bottom: 0.5rem; } -:global(#root) { +.row { display: flex; - flex-direction: column; - align-items: center; - justify-content: center; + flex-wrap: wrap; + gap: 0.75rem; + align-items: flex-start; +} - gap: 1rem; +.col { + display: flex; + flex-direction: column; + gap: 0.75rem; + align-items: flex-start; } -* { - font-family: 'Poppins', sans-serif; +.label { + font-size: 0.75rem; + color: #6b7280; + font-weight: 500; } diff --git a/packages/ui/preview-page/preview-page.tsx b/packages/ui/preview-page/preview-page.tsx index 3379602f..82e472d0 100644 --- a/packages/ui/preview-page/preview-page.tsx +++ b/packages/ui/preview-page/preview-page.tsx @@ -1,9 +1,438 @@ -import { Button } from '../src/index'; +import { + Accordion, + Avatar, + Button, + Checkbox, + DatePicker, + Input, + NavButton, + Radio, + SegmentPicker, + Select, + Separator, + Snackbar, + Status, + Switch, + TextArea, +} from '../src/index'; +import { Check, Plus, User, X } from '@phosphor-icons/react'; +import styles from './preview-page.module.css'; export function PreviewPage() { return ( - <> - - +
+ + + + + + + + + + + + + + + +
+ ); +} + +function Section({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+
{title}
+ {children} +
+ ); +} + +function ButtonSection() { + return ( +
+
+ Label buttons (medium) +
+ + + + + + + +
+ Label buttons (small) +
+ + + +
+ Disabled / loading +
+ + +
+ Icon buttons +
+ + + +
+ Icon-label buttons +
+ + + +
+
+
+ ); +} + +function NavButtonSection() { + return ( +
+
+ + + + + + + + + + + + +
+
+ ); +} + +function SwitchSection() { + return ( +
+
+
+ + + + +
+
+ + + + +
+
+
+ ); +} + +function InputSection() { + return ( +
+
+
+ + + + +
+
+ + +
+
+ } + placeholder="Start adornment" + /> + } + defaultValue="End adornment" + /> + } + endAdornment={} + defaultValue="Both" + /> +
+
+
+ ); +} + +function TextAreaSection() { + return ( +
+
+
+
+