From 42ea5def4f0a33a87e6a71d3991ba1b04ee7ef41 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 7 Apr 2026 20:36:30 +0000 Subject: [PATCH] :sparkles: Bootstrap @penpot/ui TypeScript component library with DS migrations - Configure @penpot/ui package with full tooling: ESLint 9, Stylelint, Prettier, Vitest, Storybook (react-vite), vite-plugin-dts, React Compiler - Add AGENTS.md with architecture overview, conventions, and migration table - Fix Storybook react-docgen TypeScript parse error by removing empty .babelrc that disabled Babel's built-in typescript plugin - Fix vite.config.mts for @vitejs/plugin-react v6 (reactCompilerPreset()) - Fix tsconfig.storybook.json (remove emitDecoratorMetadata without decorator) - Add shared SCSS foundations: _borders, _sizes, _utils, typography - Migrate Text, Heading (foundations/typography) with stories and tests - Migrate Icon (foundations/assets) with full iconIds catalogue and stories - Migrate Cta (product) with stories and tests - Migrate Button (buttons) with polymorphic anchor/button rendering, 4 variants, optional icon, and onRef callback; add _buttons.scss partial - All checks passing: lint, check-fmt, typecheck, test (41 tests) --- frontend/packages/ui/.babelrc | 4 - frontend/packages/ui/AGENTS.md | 266 +++++++++++++ frontend/packages/ui/eslint.config.mjs | 100 +++++ frontend/packages/ui/package.json | 30 +- frontend/packages/ui/src/index.ts | 18 +- .../packages/ui/src/lib/_ds/_borders.scss | 15 + frontend/packages/ui/src/lib/_ds/_sizes.scss | 39 ++ frontend/packages/ui/src/lib/_ds/_utils.scss | 13 + .../packages/ui/src/lib/_ds/typography.scss | 139 +++++++ .../ui/src/lib/buttons/Button.module.scss | 44 +++ .../ui/src/lib/buttons/Button.spec.tsx | 80 ++++ .../ui/src/lib/buttons/Button.stories.tsx | 57 +++ .../packages/ui/src/lib/buttons/Button.tsx | 118 ++++++ .../packages/ui/src/lib/buttons/_buttons.scss | 127 +++++++ .../ui/src/lib/example/Example.spec.tsx | 8 +- ...Example.stories.ts => Example.stories.tsx} | 6 +- .../packages/ui/src/lib/example/Example.tsx | 5 +- .../lib/foundations/assets/Icon.module.scss | 10 + .../src/lib/foundations/assets/Icon.spec.tsx | 70 ++++ .../lib/foundations/assets/Icon.stories.tsx | 40 ++ .../ui/src/lib/foundations/assets/Icon.tsx | 359 ++++++++++++++++++ .../foundations/typography/Heading.spec.tsx | 82 ++++ .../typography/Heading.stories.tsx | 84 ++++ .../lib/foundations/typography/Heading.tsx | 40 ++ .../lib/foundations/typography/Text.spec.tsx | 64 ++++ .../foundations/typography/Text.stories.tsx | 126 ++++++ .../src/lib/foundations/typography/Text.tsx | 37 ++ .../typography/typography.module.scss | 51 +++ .../lib/foundations/typography/typography.ts | 27 ++ .../typography/typographyClassMap.ts | 24 ++ .../ui/src/lib/product/Cta.module.scss | 25 ++ .../packages/ui/src/lib/product/Cta.spec.tsx | 55 +++ .../ui/src/lib/product/Cta.stories.tsx | 46 +++ frontend/packages/ui/src/lib/product/Cta.tsx | 37 ++ frontend/packages/ui/stylelint.config.mjs | 37 ++ frontend/packages/ui/tsconfig.json | 3 +- frontend/packages/ui/tsconfig.storybook.json | 1 - frontend/packages/ui/vite.config.mts | 9 +- frontend/pnpm-lock.yaml | 237 +++++++++++- 39 files changed, 2503 insertions(+), 30 deletions(-) delete mode 100644 frontend/packages/ui/.babelrc create mode 100644 frontend/packages/ui/AGENTS.md create mode 100644 frontend/packages/ui/eslint.config.mjs create mode 100644 frontend/packages/ui/src/lib/_ds/_borders.scss create mode 100644 frontend/packages/ui/src/lib/_ds/_sizes.scss create mode 100644 frontend/packages/ui/src/lib/_ds/_utils.scss create mode 100644 frontend/packages/ui/src/lib/_ds/typography.scss create mode 100644 frontend/packages/ui/src/lib/buttons/Button.module.scss create mode 100644 frontend/packages/ui/src/lib/buttons/Button.spec.tsx create mode 100644 frontend/packages/ui/src/lib/buttons/Button.stories.tsx create mode 100644 frontend/packages/ui/src/lib/buttons/Button.tsx create mode 100644 frontend/packages/ui/src/lib/buttons/_buttons.scss rename frontend/packages/ui/src/lib/example/{Example.stories.ts => Example.stories.tsx} (57%) create mode 100644 frontend/packages/ui/src/lib/foundations/assets/Icon.module.scss create mode 100644 frontend/packages/ui/src/lib/foundations/assets/Icon.spec.tsx create mode 100644 frontend/packages/ui/src/lib/foundations/assets/Icon.stories.tsx create mode 100644 frontend/packages/ui/src/lib/foundations/assets/Icon.tsx create mode 100644 frontend/packages/ui/src/lib/foundations/typography/Heading.spec.tsx create mode 100644 frontend/packages/ui/src/lib/foundations/typography/Heading.stories.tsx create mode 100644 frontend/packages/ui/src/lib/foundations/typography/Heading.tsx create mode 100644 frontend/packages/ui/src/lib/foundations/typography/Text.spec.tsx create mode 100644 frontend/packages/ui/src/lib/foundations/typography/Text.stories.tsx create mode 100644 frontend/packages/ui/src/lib/foundations/typography/Text.tsx create mode 100644 frontend/packages/ui/src/lib/foundations/typography/typography.module.scss create mode 100644 frontend/packages/ui/src/lib/foundations/typography/typography.ts create mode 100644 frontend/packages/ui/src/lib/foundations/typography/typographyClassMap.ts create mode 100644 frontend/packages/ui/src/lib/product/Cta.module.scss create mode 100644 frontend/packages/ui/src/lib/product/Cta.spec.tsx create mode 100644 frontend/packages/ui/src/lib/product/Cta.stories.tsx create mode 100644 frontend/packages/ui/src/lib/product/Cta.tsx create mode 100644 frontend/packages/ui/stylelint.config.mjs diff --git a/frontend/packages/ui/.babelrc b/frontend/packages/ui/.babelrc deleted file mode 100644 index ab1c311509..0000000000 --- a/frontend/packages/ui/.babelrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "presets": [], - "plugins": [] -} diff --git a/frontend/packages/ui/AGENTS.md b/frontend/packages/ui/AGENTS.md new file mode 100644 index 0000000000..b019c47ec4 --- /dev/null +++ b/frontend/packages/ui/AGENTS.md @@ -0,0 +1,266 @@ +# @penpot/ui – Agent Instructions + +TypeScript + React component library that forms the Penpot design system (DS). +Components are built in TypeScript/TSX, styled with CSS Modules (SCSS), tested +with Vitest + Testing Library, and documented with Storybook. + +This package lives under `frontend/packages/ui/` and is published as the +`@penpot/ui` internal package consumed by the main `frontend/` ClojureScript +application. + +## Architecture + +``` +frontend/packages/ui/ +├── src/ +│ ├── index.ts # Barrel – all public exports +│ └── lib/ +│ ├── _ds/ # Shared SCSS foundations (mixins, tokens) +│ │ ├── _borders.scss # Border-radius and border-width tokens +│ │ ├── _sizes.scss # Size tokens ($sz-*) +│ │ ├── _utils.scss # px2rem() helper +│ │ └── typography.scss # use-typography() mixin + font styles +│ ├── buttons/ # Button components +│ │ ├── _buttons.scss # Shared button placeholder/variant styles +│ │ ├── Button.tsx +│ │ ├── Button.module.scss +│ │ ├── Button.stories.tsx +│ │ └── Button.spec.tsx +│ ├── example/ # Example component (reference) +│ ├── foundations/ +│ │ ├── assets/ # Icon component +│ │ │ ├── Icon.tsx +│ │ │ ├── Icon.module.scss +│ │ │ ├── Icon.stories.tsx +│ │ │ └── Icon.spec.tsx +│ │ └── typography/ # Text, Heading components + shared utilities +│ └── product/ # Product-level components (e.g. Cta) +├── eslint.config.mjs # ESLint 9 flat config (TypeScript + React) +├── stylelint.config.mjs # Stylelint config (mirrors frontend/) +├── vite.config.mts # Vite lib build + Vitest config +├── tsconfig.json +├── tsconfig.lib.json +├── tsconfig.spec.json +└── tsconfig.storybook.json +``` + +Components are organised to mirror the CLJS source tree +`frontend/src/app/main/ui/ds/`: + +| CLJS path | TS path | +|-----------|---------| +| `ds/foundations/typography/text.cljs` | `src/lib/foundations/typography/Text.tsx` | +| `ds/foundations/typography/heading.cljs` | `src/lib/foundations/typography/Heading.tsx` | +| `ds/foundations/assets/icon.cljs` | `src/lib/foundations/assets/Icon.tsx` | +| `ds/product/cta.cljs` | `src/lib/product/Cta.tsx` | +| `ds/buttons/button.cljs` | `src/lib/buttons/Button.tsx` | + +### Known Tooling Notes + +- **No `.babelrc` in this package.** The `react-docgen` plugin used by + Storybook calls `@babel/core`'s `loadPartialConfig`. If a `.babelrc` is + present with empty `presets: []` it disables the default `typescript` Babel + plugin, causing `import type` to fail in story files. Keep the `.babelrc` + deleted. +- **`@vitejs/plugin-react` v6** removed the `babel` option. Use + `reactCompilerPreset()` from the same package instead of passing + `babel: { plugins: ['babel-plugin-react-compiler'] }`. + +Every migrated component must have: +- `ComponentName.tsx` – the React component +- `ComponentName.module.scss` – CSS Module styles +- `ComponentName.stories.tsx` – Storybook stories +- `ComponentName.spec.tsx` – Vitest unit tests + +## Development Commands + +All commands must be run from `frontend/packages/ui/`. + +```bash +# Build the library (outputs to dist/) +pnpm run build + +# Watch mode (rebuilds on file changes) +pnpm run watch + +# Type-check all tsconfig projects +pnpm run typecheck + +# Run unit tests (Vitest, single pass) +pnpm run test + +# Run tests in watch mode +pnpm run test:watch + +# Launch Storybook dev server +pnpm run storybook + +# Build Storybook static site +pnpm run build:storybook + +# Lint TypeScript/TSX (ESLint) +pnpm run lint:ts + +# Lint SCSS (Stylelint) +pnpm run lint:scss + +# Lint all (TS/TSX + SCSS) +pnpm run lint + +# Auto-fix formatting – TS/TSX only +pnpm run fmt:ts + +# Auto-fix formatting – SCSS only +pnpm run fmt:scss + +# Auto-fix formatting – all (TS/TSX + SCSS) +pnpm run fmt + +# Check formatting – TS/TSX only +pnpm run check-fmt:ts + +# Check formatting – SCSS only +pnpm run check-fmt:scss + +# Check formatting – all (TS/TSX + SCSS) +pnpm run check-fmt +``` + +Always run the following checks after making changes and before committing: + +```bash +pnpm run lint +pnpm run check-fmt +pnpm run typecheck +pnpm run test +``` + +## Component Conventions + +### Naming + +- PascalCase filenames: `MyComponent.tsx`, `MyComponent.module.scss` +- Named exports only — no default exports for components +- Export from `src/index.ts` (both the component and its props type) + +### Component Structure + +```tsx +import { type ComponentPropsWithRef, memo } from "react"; +import clsx from "clsx"; +import styles from "./MyComponent.module.scss"; + +export interface MyComponentProps extends ComponentPropsWithRef<"div"> { + /** Required prop description */ + label: string; +} + +function MyComponentInner({ label, className, children, ...rest }: MyComponentProps) { + return ( +
+ {label} + {children} +
+ ); +} + +export const MyComponent = memo(MyComponentInner); +``` + +Key rules: +- Wrap every component with `React.memo` (mirrors CLJS `mf/memo`) +- Use CSS Modules for all styles (`styles.className`, never inline styles) +- Use `clsx` to merge class names (mirrors CLJS `stl/css-case`) +- Spread `...rest` onto the root element to pass through HTML attributes +- Use `className` (not `class`) and merge it with the component's own class + +### Props + +- Accept an optional `className` prop and merge it with `clsx` +- For polymorphic components (variable tag), use an `as` prop typed as + `ElementType` and default to a sensible HTML element +- Validate variants/options with TypeScript union types, not runtime checks + +### Styling + +SCSS files live next to the component file and follow these rules: + +- Import shared SCSS foundations with `@use`: + ```scss + @use "../../_ds/typography.scss" as t; + @use "../../_ds/_utils.scss" as *; + ``` +- Use `px2rem()` for all hard-coded pixel values +- Use CSS custom properties for design tokens (`var(--color-*)`, + `var(--sp-*)`) +- Use `@include t.use-typography("headline-small")` for typography +- Use logical properties: `margin-inline-start`, `padding-block-end`, etc. + (not `margin-left`, `padding-bottom`) +- Flat selectors — avoid deep nesting + +### Storybook Stories + +```tsx +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { MyComponent } from "./MyComponent"; + +const meta = { + title: "Category/MyComponent", // mirrors CLJS story category + component: MyComponent, + args: { label: "Default label" }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; +``` + +### Unit Tests + +```tsx +import { render } from "@testing-library/react"; +import { MyComponent } from "./MyComponent"; + +describe("MyComponent", () => { + it("should render successfully", () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); +}); +``` + +- Use `@testing-library/react` — test rendered output, not implementation +- No snapshot tests +- Cover: renders correctly, prop variations, className merging, HTML + attribute pass-through +- **SVG `className` is an `SVGAnimatedString` in JSDOM** — use + `svg.getAttribute("class")` instead of `svg.className` in tests +- **`toHaveAttribute` is not available** (no `@testing-library/jest-dom` setup) — + use `element.getAttribute("attr")` directly + +## Migration from CLJS DS + +When migrating a component from `frontend/src/app/main/ui/ds/`: + +| CLJS pattern | TypeScript equivalent | +|--------------|-----------------------| +| `mf/defc cta* {::mf/wrap [mf/memo]}` | `export const Cta = memo(CtaInner)` | +| `(stl/css :root)` | `styles.root` (CSS Module) | +| `(stl/css-case :a cond :b true)` | `clsx({ [styles.a]: cond, [styles.b]: true })` | +| `(d/append-class cls (stl/css :root))` | `clsx(styles.root, className)` | +| `[:> text* {:as "span" :typography t/headline-small}]` | `` | +| `{:keys [class title children] :rest props}` | `{ className, title, children, ...rest }` | +| `[:> "div" props ...]` | `
...
` | + +CLJS schema validation (`:map [:title :string]`) is replaced by TypeScript +`interface` / prop types — no runtime validation needed. + +## Exports + +Every public symbol must be re-exported from `src/index.ts`: + +```ts +export { MyComponent } from './lib/category/MyComponent'; +export type { MyComponentProps } from './lib/category/MyComponent'; +``` diff --git a/frontend/packages/ui/eslint.config.mjs b/frontend/packages/ui/eslint.config.mjs new file mode 100644 index 0000000000..b3143bd596 --- /dev/null +++ b/frontend/packages/ui/eslint.config.mjs @@ -0,0 +1,100 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import tseslint from "typescript-eslint"; +import pluginReact from "eslint-plugin-react"; +import pluginReactHooks from "eslint-plugin-react-hooks"; +import pluginJsxA11y from "eslint-plugin-jsx-a11y"; +import pluginImport from "eslint-plugin-import"; +import globals from "globals"; + +/** @type {import("eslint").Linter.Config[]} */ +export default [ + { + ignores: ["dist/**", "node_modules/**", ".storybook/**"], + }, + + // TypeScript + TSX source files + ...tseslint.config({ + files: ["src/**/*.{ts,tsx}"], + extends: [ + tseslint.configs.recommended, + pluginReact.configs.flat.recommended, + ], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { + ...globals.browser, + ...globals.es2022, + }, + parserOptions: { + ecmaFeatures: { jsx: true }, + }, + }, + plugins: { + "react-hooks": pluginReactHooks, + "jsx-a11y": pluginJsxA11y, + import: pluginImport, + }, + settings: { + react: { version: "detect" }, + }, + rules: { + // React + "react/react-in-jsx-scope": "off", // Not needed with React 17+ JSX transform + "react/prop-types": "off", // TypeScript handles prop validation + "react/display-name": "off", // memo-wrapped inner functions don't need display names + + // React Hooks + ...pluginReactHooks.configs["recommended-latest"].rules, + + // Imports + "import/no-duplicates": "error", + "import/no-unresolved": "off", // TypeScript handles resolution + "import/first": "error", + + // TypeScript — relax rules that conflict with common patterns + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + + // General + "no-console": ["warn", { allow: ["warn", "error"] }], + "prefer-const": "error", + "no-var": "error", + }, + }), + + // Vitest spec files — add test globals + ...tseslint.config({ + files: ["src/**/*.spec.{ts,tsx}"], + languageOptions: { + globals: { + ...globals.browser, + ...globals.es2022, + describe: "readonly", + it: "readonly", + expect: "readonly", + beforeEach: "readonly", + afterEach: "readonly", + beforeAll: "readonly", + afterAll: "readonly", + vi: "readonly", + }, + }, + }), + + // Storybook story files — relax some rules + ...tseslint.config({ + files: ["src/**/*.stories.{ts,tsx}"], + rules: { + "import/first": "off", + "@typescript-eslint/no-unused-vars": "off", + }, + }), +]; diff --git a/frontend/packages/ui/package.json b/frontend/packages/ui/package.json index 54d980acf2..b571a95de5 100644 --- a/frontend/packages/ui/package.json +++ b/frontend/packages/ui/package.json @@ -10,8 +10,22 @@ "./style.css": "./dist/style.css" }, "scripts": { + "build": "vite build", "watch": "vite build --watch", - "build": "vite build" + "typecheck": "tsc --build", + "test": "vitest run", + "test:watch": "vitest", + "storybook": "storybook dev -p 6006", + "build:storybook": "storybook build", + "lint:ts": "eslint src", + "lint:scss": "stylelint 'src/**/*.scss'", + "lint": "pnpm run lint:ts && pnpm run lint:scss", + "fmt:ts": "prettier -w 'src/**/*.{ts,tsx}'", + "fmt:scss": "prettier -w 'src/**/*.scss'", + "fmt": "pnpm run fmt:ts && pnpm run fmt:scss", + "check-fmt:ts": "prettier -c 'src/**/*.{ts,tsx}'", + "check-fmt:scss": "prettier -c 'src/**/*.scss'", + "check-fmt": "pnpm run check-fmt:ts && pnpm run check-fmt:scss" }, "devDependencies": { "@babel/core": "^7.14.5", @@ -24,13 +38,25 @@ "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^6.0.1", "babel-plugin-react-compiler": "^1.0.0", + "clsx": "^2.1.1", + "eslint": "^9.39.2", "eslint-plugin-import": "2.32.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "7.0.1", + "postcss-scss": "^4.0.9", + "prettier": "3.8.1", "react-compiler-runtime": "^1.0.0", + "sass": "^1.98.0", "storybook": "10.3.5", - "vite-plugin-dts": "^4.5.4" + "stylelint": "^17.4.0", + "stylelint-config-standard-scss": "^17.0.0", + "stylelint-scss": "^7.0.0", + "stylelint-use-logical-spec": "^5.0.1", + "typescript": "^6.0.2", + "typescript-eslint": "^8.58.0", + "vite-plugin-dts": "^4.5.4", + "vitest": "^4.1.3" }, "peerDependencies": { "react": ">=19.2", diff --git a/frontend/packages/ui/src/index.ts b/frontend/packages/ui/src/index.ts index 19ed9b5a98..065942d8dd 100644 --- a/frontend/packages/ui/src/index.ts +++ b/frontend/packages/ui/src/index.ts @@ -1 +1,17 @@ -export * from './lib/example/Example'; +export * from "./lib/example/Example"; +export { Text } from "./lib/foundations/typography/Text"; +export type { TextProps } from "./lib/foundations/typography/Text"; +export { Heading } from "./lib/foundations/typography/Heading"; +export type { HeadingProps } from "./lib/foundations/typography/Heading"; +export { + Typography, + typographyIds, + typographySet, +} from "./lib/foundations/typography/typography"; +export type { TypographyId } from "./lib/foundations/typography/typography"; +export { Cta } from "./lib/product/Cta"; +export type { CtaProps } from "./lib/product/Cta"; +export { Icon, iconIds } from "./lib/foundations/assets/Icon"; +export type { IconId, IconProps } from "./lib/foundations/assets/Icon"; +export { Button } from "./lib/buttons/Button"; +export type { ButtonProps, ButtonVariant } from "./lib/buttons/Button"; diff --git a/frontend/packages/ui/src/lib/_ds/_borders.scss b/frontend/packages/ui/src/lib/_ds/_borders.scss new file mode 100644 index 0000000000..52ec40d174 --- /dev/null +++ b/frontend/packages/ui/src/lib/_ds/_borders.scss @@ -0,0 +1,15 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "utils" as *; + +$br-4: px2rem(4); +$br-6: px2rem(6); +$br-8: px2rem(8); +$br-12: px2rem(12); +$br-circle: 50%; +$b-1: px2rem(1); +$b-2: px2rem(2); diff --git a/frontend/packages/ui/src/lib/_ds/_sizes.scss b/frontend/packages/ui/src/lib/_ds/_sizes.scss new file mode 100644 index 0000000000..265675a804 --- /dev/null +++ b/frontend/packages/ui/src/lib/_ds/_sizes.scss @@ -0,0 +1,39 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "utils" as *; + +$sz-1: px2rem(1); +$sz-6: px2rem(6); +$sz-12: px2rem(12); +$sz-14: px2rem(14); +$sz-16: px2rem(16); +$sz-24: px2rem(24); +$sz-28: px2rem(28); +$sz-32: px2rem(32); +$sz-36: px2rem(36); +$sz-40: px2rem(40); +$sz-48: px2rem(48); +$sz-64: px2rem(64); +$sz-88: px2rem(88); +$sz-96: px2rem(96); +$sz-120: px2rem(120); +$sz-154: px2rem(154); +$sz-160: px2rem(160); +$sz-192: px2rem(192); +$sz-200: px2rem(200); +$sz-224: px2rem(224); +$sz-252: px2rem(252); +$sz-284: px2rem(284); +$sz-352: px2rem(352); +$sz-364: px2rem(364); +$sz-384: px2rem(384); +$sz-400: px2rem(400); +$sz-430: px2rem(430); +$sz-480: px2rem(480); +$sz-500: px2rem(500); +$sz-512: px2rem(512); +$sz-520: px2rem(520); diff --git a/frontend/packages/ui/src/lib/_ds/_utils.scss b/frontend/packages/ui/src/lib/_ds/_utils.scss new file mode 100644 index 0000000000..a455cb0318 --- /dev/null +++ b/frontend/packages/ui/src/lib/_ds/_utils.scss @@ -0,0 +1,13 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "sass:math"; + +@function px2rem($value) { + $rem-value: math.div($value, 16) * 1rem; + + @return $rem-value; +} diff --git a/frontend/packages/ui/src/lib/_ds/typography.scss b/frontend/packages/ui/src/lib/_ds/typography.scss new file mode 100644 index 0000000000..7068854f57 --- /dev/null +++ b/frontend/packages/ui/src/lib/_ds/typography.scss @@ -0,0 +1,139 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "utils" as *; + +$_font-weight-regular: 400; +$_font-weight-medium: 500; +$_font-lineheight-dense: 1.2; +$_font-lineheight-compact: 1.3; +$_font-lineheight-normal: 1.4; +$_fs-12: px2rem(12); +$_fs-14: px2rem(14); +$_fs-16: px2rem(16); +$_fs-18: px2rem(18); +$_fs-20: px2rem(20); +$_fs-24: px2rem(24); +$_fs-36: px2rem(36); + +@mixin _font-style-display { + font-family: worksans, vazirmatn, sans-serif; + font-optical-sizing: auto; + font-weight: $_font-weight-regular; + line-height: $_font-lineheight-normal; + font-size: $_fs-36; +} + +@mixin _font-style-title-large { + font-family: worksans, vazirmatn, sans-serif; + font-optical-sizing: auto; + font-weight: $_font-weight-regular; + line-height: $_font-lineheight-normal; + font-size: $_fs-24; +} + +@mixin _font-style-title-medium { + font-family: worksans, vazirmatn, sans-serif; + font-optical-sizing: auto; + font-weight: $_font-weight-regular; + line-height: $_font-lineheight-normal; + font-size: $_fs-20; +} + +@mixin _font-style-title-small { + font-family: worksans, vazirmatn, sans-serif; + font-optical-sizing: auto; + font-weight: $_font-weight-regular; + line-height: $_font-lineheight-dense; + font-size: $_fs-14; +} + +@mixin _font-style-headline-large { + font-family: worksans, vazirmatn, sans-serif; + font-optical-sizing: auto; + font-weight: $_font-weight-regular; + line-height: $_font-lineheight-normal; + font-size: $_fs-18; + text-transform: uppercase; +} + +@mixin _font-style-headline-medium { + font-family: worksans, vazirmatn, sans-serif; + font-optical-sizing: auto; + font-weight: $_font-weight-regular; + line-height: $_font-lineheight-normal; + font-size: $_fs-16; + text-transform: uppercase; +} + +@mixin _font-style-headline-small { + font-family: worksans, vazirmatn, sans-serif; + font-optical-sizing: auto; + font-weight: $_font-weight-medium; + line-height: $_font-lineheight-dense; + font-size: $_fs-12; + text-transform: uppercase; +} + +@mixin _font-style-body-large { + font-family: worksans, vazirmatn, sans-serif; + font-optical-sizing: auto; + font-weight: $_font-weight-regular; + line-height: $_font-lineheight-normal; + font-size: $_fs-16; +} + +@mixin _font-style-body-medium { + font-family: worksans, vazirmatn, sans-serif; + font-optical-sizing: auto; + font-weight: $_font-weight-regular; + line-height: $_font-lineheight-normal; + font-size: $_fs-14; +} + +@mixin _font-style-body-small { + font-family: worksans, vazirmatn, sans-serif; + font-optical-sizing: auto; + font-weight: $_font-weight-regular; + line-height: $_font-lineheight-compact; + font-size: $_fs-12; +} + +@mixin _font-style-code-font { + font-family: "Roboto Mono", monospace; + font-optical-sizing: auto; + font-weight: $_font-weight-regular; + line-height: $_font-lineheight-dense; + font-size: $_fs-12; +} + +@mixin use-typography($typography-name) { + @if $typography-name == "display" { + @include _font-style-display; + } @else if $typography-name == "title-large" { + @include _font-style-title-large; + } @else if $typography-name == "title-medium" { + @include _font-style-title-medium; + } @else if $typography-name == "title-small" { + @include _font-style-title-small; + } @else if $typography-name == "headline-large" { + @include _font-style-headline-large; + } @else if $typography-name == "headline-medium" { + @include _font-style-headline-medium; + } @else if $typography-name == "headline-small" { + @include _font-style-headline-small; + } @else if $typography-name == "body-large" { + @include _font-style-body-large; + } @else if $typography-name == "body-medium" { + @include _font-style-body-medium; + } @else if $typography-name == "body-small" { + @include _font-style-body-small; + } @else if $typography-name == "code-font" { + @include _font-style-code-font; + } @else { + @error "Unknown typography: " + $typography-name; + } +} diff --git a/frontend/packages/ui/src/lib/buttons/Button.module.scss b/frontend/packages/ui/src/lib/buttons/Button.module.scss new file mode 100644 index 0000000000..6dbcb80aea --- /dev/null +++ b/frontend/packages/ui/src/lib/buttons/Button.module.scss @@ -0,0 +1,44 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "../_ds/typography.scss" as t; +@use "buttons" as *; + +.button { + @extend %base-button; + @include t.use-typography("headline-small"); + + padding: 0 var(--sp-m); + display: inline-flex; + align-items: center; + column-gap: var(--sp-xs); +} + +.button-primary { + @extend %base-button-primary; +} + +.button-secondary { + @extend %base-button-secondary; +} + +.button-ghost { + @extend %base-button-ghost; +} + +.button-destructive { + @extend %base-button-destructive; +} + +.button-link { + &:hover { + text-decoration: none; + } +} + +.label-wrapper { + display: contents; +} diff --git a/frontend/packages/ui/src/lib/buttons/Button.spec.tsx b/frontend/packages/ui/src/lib/buttons/Button.spec.tsx new file mode 100644 index 0000000000..31437d290b --- /dev/null +++ b/frontend/packages/ui/src/lib/buttons/Button.spec.tsx @@ -0,0 +1,80 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import { render, screen } from "@testing-library/react"; +import { Button } from "./Button"; + +describe("Button", () => { + it("should render successfully", () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); + + it("renders as a ); + expect(screen.getByRole("button", { name: "Click me" })).toBeTruthy(); + }); + + it("renders as an when `to` is provided", () => { + render(); + const link = screen.getByRole("link", { name: "Go" }); + expect(link.tagName).toBe("A"); + expect(link.getAttribute("href")).toBe("https://example.com"); + }); + + it("applies primary variant class by default", () => { + const { container } = render(); + const btn = container.querySelector("button"); + expect(btn?.className).toContain("button-primary"); + }); + + it("applies secondary variant class", () => { + const { container } = render(); + expect(container.querySelector("button")?.className).toContain( + "button-secondary", + ); + }); + + it("applies ghost variant class", () => { + const { container } = render(); + expect(container.querySelector("button")?.className).toContain( + "button-ghost", + ); + }); + + it("applies destructive variant class", () => { + const { container } = render(); + expect(container.querySelector("button")?.className).toContain( + "button-destructive", + ); + }); + + it("renders an Icon when `icon` is provided", () => { + const { container } = render(); + expect(container.querySelector("svg")).not.toBeNull(); + }); + + it("does not render an Icon when `icon` is not provided", () => { + const { container } = render(); + expect(container.querySelector("svg")).toBeNull(); + }); + + it("merges custom className", () => { + const { container } = render(); + expect(container.querySelector("button")?.className).toContain("custom"); + }); + + it("calls onRef with the DOM node", () => { + const onRef = vi.fn(); + render(); + expect(onRef).toHaveBeenCalledWith(expect.any(HTMLButtonElement)); + }); + + it("forwards arbitrary props to the root element", () => { + render(); + expect(screen.getByTestId("my-btn")).toBeTruthy(); + }); +}); diff --git a/frontend/packages/ui/src/lib/buttons/Button.stories.tsx b/frontend/packages/ui/src/lib/buttons/Button.stories.tsx new file mode 100644 index 0000000000..07df9d2f2b --- /dev/null +++ b/frontend/packages/ui/src/lib/buttons/Button.stories.tsx @@ -0,0 +1,57 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { iconIds } from "../foundations/assets/Icon"; +import { Button } from "./Button"; + +const meta = { + title: "Buttons/Button", + component: Button, + args: { + children: "Lorem ipsum", + variant: "primary", + }, + argTypes: { + icon: { + options: [undefined, ...iconIds], + control: { type: "select" }, + }, + variant: { + options: ["primary", "secondary", "ghost", "destructive"], + control: { type: "select" }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Primary: Story = { + args: { variant: "primary" }, +}; + +export const Secondary: Story = { + args: { variant: "secondary" }, +}; + +export const Ghost: Story = { + args: { variant: "ghost" }, +}; + +export const Destructive: Story = { + args: { variant: "destructive" }, +}; + +export const WithIcon: Story = { + args: { icon: "effects" }, +}; + +export const AsLink: Story = { + args: { to: "https://penpot.app" }, +}; diff --git a/frontend/packages/ui/src/lib/buttons/Button.tsx b/frontend/packages/ui/src/lib/buttons/Button.tsx new file mode 100644 index 0000000000..53bae987e3 --- /dev/null +++ b/frontend/packages/ui/src/lib/buttons/Button.tsx @@ -0,0 +1,118 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import { type ComponentPropsWithRef, memo } from "react"; +import clsx from "clsx"; +import { Icon, type IconId } from "../foundations/assets/Icon"; +import styles from "./Button.module.scss"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type ButtonVariant = "primary" | "secondary" | "ghost" | "destructive"; + +interface ButtonBaseProps { + /** Visual variant. Defaults to "primary". */ + variant?: ButtonVariant; + /** Optional icon ID rendered before the label. */ + icon?: IconId; + /** Callback that receives the underlying DOM element ref. */ + onRef?: (node: HTMLElement | null) => void; + className?: string; + children?: React.ReactNode; +} + +// Conditional props: when `to` is provided the component renders as ; +// otherwise as + ); +} + +export const Button = memo(ButtonInner); diff --git a/frontend/packages/ui/src/lib/buttons/_buttons.scss b/frontend/packages/ui/src/lib/buttons/_buttons.scss new file mode 100644 index 0000000000..9b22029248 --- /dev/null +++ b/frontend/packages/ui/src/lib/buttons/_buttons.scss @@ -0,0 +1,127 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "../_ds/_borders.scss" as *; +@use "../_ds/_sizes.scss" as *; +@use "../_ds/_utils.scss" as *; + +%base-button { + --button-bg-color: initial; + --button-fg-color: initial; + --button-hover-bg-color: initial; + --button-hover-fg-color: initial; + --button-active-bg-color: initial; + --button-active-fg-color: initial; + --button-disabled-bg-color: initial; + --button-disabled-fg-color: initial; + --button-border-color: var(--button-bg-color); + --button-focus-inner-ring-color: initial; + --button-focus-outer-ring-color: initial; + --button-width: initial; + --button-height: #{$sz-32}; + + appearance: none; + width: var(--button-width); + height: var(--button-height); + background: var(--button-bg-color); + color: var(--button-fg-color); + border: $b-1 solid var(--button-border-color); + border-radius: $br-8; + + &:hover { + --button-bg-color: var(--button-hover-bg-color); + --button-fg-color: var(--button-hover-fg-color); + } + + &:active, + &[aria-pressed="true"] { + --button-bg-color: var(--button-active-bg-color); + --button-fg-color: var(--button-active-fg-color); + } + + &:focus-visible { + outline: var(--button-focus-inner-ring-color) solid #{px2rem(2)}; + outline-offset: -#{px2rem(3)}; + + --button-border-color: var(--button-focus-outer-ring-color); + --button-fg-color: var(--button-focus-fg-color); + } + + &:disabled { + --button-bg-color: var(--button-disabled-bg-color); + --button-fg-color: var(--button-disabled-fg-color); + } +} + +%base-button-primary { + --button-bg-color: var(--color-accent-primary); + --button-fg-color: var(--color-background-secondary); + --button-hover-bg-color: var(--color-accent-tertiary); + --button-hover-fg-color: var(--color-background-secondary); + --button-active-bg-color: var(--color-accent-tertiary); + --button-active-fg-color: var(--color-background-secondary); + --button-disabled-bg-color: var(--color-accent-primary-muted); + --button-disabled-fg-color: var(--color-background-secondary); + --button-focus-bg-color: var(--color-accent-primary); + --button-focus-fg-color: var(--color-background-secondary); + --button-focus-inner-ring-color: var(--color-background-secondary); + --button-focus-outer-ring-color: var(--color-accent-primary); + + &:active, + &[aria-pressed="true"] { + box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgb(0 0 0 / 20%); + } +} + +%base-button-secondary { + --button-bg-color: var(--color-background-tertiary); + --button-fg-color: var(--color-foreground-secondary); + --button-hover-bg-color: var(--color-background-tertiary); + --button-hover-fg-color: var(--color-accent-primary); + --button-active-bg-color: var(--color-background-quaternary); + --button-active-fg-color: var(--color-accent-primary); + --button-disabled-bg-color: transparent; + --button-disabled-fg-color: var(--color-foreground-secondary); + --button-focus-bg-color: var(--color-background-tertiary); + --button-focus-fg-color: var(--color-foreground-primary); + --button-focus-inner-ring-color: var(--color-background-secondary); + --button-focus-outer-ring-color: var(--color-accent-primary); +} + +%base-button-ghost { + --button-bg-color: transparent; + --button-fg-color: var(--color-foreground-secondary); + --button-hover-bg-color: var(--color-background-tertiary); + --button-hover-fg-color: var(--color-accent-primary); + --button-active-bg-color: var(--color-background-quaternary); + --button-active-fg-color: var(--color-accent-primary); + --button-disabled-bg-color: transparent; + --button-disabled-fg-color: var(--color-accent-primary-muted); + --button-focus-bg-color: transparent; + --button-focus-fg-color: var(--color-foreground-secondary); + --button-focus-inner-ring-color: transparent; + --button-focus-outer-ring-color: var(--color-accent-primary); +} + +%base-button-destructive { + --button-bg-color: var(--color-accent-error); + --button-fg-color: var(--color-foreground-primary); + --button-hover-bg-color: var(--color-background-error); + --button-hover-fg-color: var(--color-foreground-primary); + --button-active-bg-color: var(--color-accent-error); + --button-active-fg-color: var(--color-foreground-primary); + --button-disabled-bg-color: var(--color-background-error); + --button-disabled-fg-color: var(--color-accent-error); + --button-focus-bg-color: var(--color-accent-error); + --button-focus-fg-color: var(--color-foreground-primary); + --button-focus-inner-ring-color: var(--color-background-primary); + --button-focus-outer-ring-color: var(--color-accent-primary); + + &:active, + &[aria-pressed="true"] { + box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgb(0 0 0 / 20%); + } +} diff --git a/frontend/packages/ui/src/lib/example/Example.spec.tsx b/frontend/packages/ui/src/lib/example/Example.spec.tsx index e3022639d9..9b676d6b49 100644 --- a/frontend/packages/ui/src/lib/example/Example.spec.tsx +++ b/frontend/packages/ui/src/lib/example/Example.spec.tsx @@ -1,9 +1,9 @@ -import { render } from '@testing-library/react'; +import { render } from "@testing-library/react"; -import Example from './Example'; +import Example from "./Example"; -describe('Example', () => { - it('should render successfully', () => { +describe("Example", () => { + it("should render successfully", () => { const { baseElement } = render(); expect(baseElement).toBeTruthy(); }); diff --git a/frontend/packages/ui/src/lib/example/Example.stories.ts b/frontend/packages/ui/src/lib/example/Example.stories.tsx similarity index 57% rename from frontend/packages/ui/src/lib/example/Example.stories.ts rename to frontend/packages/ui/src/lib/example/Example.stories.tsx index 4dfb60c9e7..eb144c8278 100644 --- a/frontend/packages/ui/src/lib/example/Example.stories.ts +++ b/frontend/packages/ui/src/lib/example/Example.stories.tsx @@ -1,8 +1,8 @@ -import { Example } from './Example'; -import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Example } from "./Example"; +import type { Meta, StoryObj } from "@storybook/react-vite"; const meta = { - title: 'UI/Example', + title: "UI/Example", component: Example, } satisfies Meta; diff --git a/frontend/packages/ui/src/lib/example/Example.tsx b/frontend/packages/ui/src/lib/example/Example.tsx index 908a8e3fd9..94522bad9c 100644 --- a/frontend/packages/ui/src/lib/example/Example.tsx +++ b/frontend/packages/ui/src/lib/example/Example.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; -import styles from './Example.module.css'; +import { useState } from "react"; +import styles from "./Example.module.css"; export function Example() { const [count, setCount] = useState(0); @@ -14,7 +14,6 @@ export function Example() { - ); } diff --git a/frontend/packages/ui/src/lib/foundations/assets/Icon.module.scss b/frontend/packages/ui/src/lib/foundations/assets/Icon.module.scss new file mode 100644 index 0000000000..67add3cdd9 --- /dev/null +++ b/frontend/packages/ui/src/lib/foundations/assets/Icon.module.scss @@ -0,0 +1,10 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +.icon { + fill: var(--icon-fill-color, none); + stroke: var(--icon-stroke-color, currentcolor); +} diff --git a/frontend/packages/ui/src/lib/foundations/assets/Icon.spec.tsx b/frontend/packages/ui/src/lib/foundations/assets/Icon.spec.tsx new file mode 100644 index 0000000000..df065b9205 --- /dev/null +++ b/frontend/packages/ui/src/lib/foundations/assets/Icon.spec.tsx @@ -0,0 +1,70 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import { render } from "@testing-library/react"; +import { Icon, iconIds } from "./Icon"; + +describe("Icon", () => { + it("should render successfully", () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); + + it("renders an svg element", () => { + const { container } = render(); + expect(container.querySelector("svg")).not.toBeNull(); + }); + + it("references the correct icon sprite href", () => { + const { container } = render(); + const use = container.querySelector("use"); + expect(use?.getAttribute("href")).toBe("#icon-add"); + }); + + it("defaults to medium size (16px viewport)", () => { + const { container } = render(); + const svg = container.querySelector("svg"); + expect(svg?.getAttribute("width")).toBe("16"); + expect(svg?.getAttribute("height")).toBe("16"); + }); + + it("renders large size with 32px viewport", () => { + const { container } = render(); + const svg = container.querySelector("svg"); + expect(svg?.getAttribute("width")).toBe("32"); + expect(svg?.getAttribute("height")).toBe("32"); + }); + + it("renders small size with 16px viewport and offset use", () => { + const { container } = render(); + const use = container.querySelector("use"); + expect(use?.getAttribute("width")).toBe("12"); + // offset = (16 - 12) / 2 = 2 + expect(use?.getAttribute("x")).toBe("2"); + }); + + it("forwards className to the svg element", () => { + const { container } = render( + , + ); + expect(container.querySelector("svg")?.getAttribute("class")).toContain( + "custom-class", + ); + }); + + it("forwards arbitrary svg props", () => { + const { container } = render( + , + ); + const svg = container.querySelector("svg"); + expect(svg?.getAttribute("data-testid")).toBe("my-icon"); + expect(svg?.getAttribute("aria-label")).toBe("pin icon"); + }); + + it("exports a non-empty iconIds array", () => { + expect(iconIds.length).toBeGreaterThan(0); + }); +}); diff --git a/frontend/packages/ui/src/lib/foundations/assets/Icon.stories.tsx b/frontend/packages/ui/src/lib/foundations/assets/Icon.stories.tsx new file mode 100644 index 0000000000..780e41c317 --- /dev/null +++ b/frontend/packages/ui/src/lib/foundations/assets/Icon.stories.tsx @@ -0,0 +1,40 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Icon, iconIds } from "./Icon"; + +const meta = { + title: "Foundations/Assets/Icon", + component: Icon, + args: { + iconId: "pin", + size: "m", + }, + argTypes: { + iconId: { + options: iconIds, + control: { type: "select" }, + }, + size: { + options: ["s", "m", "l"], + control: { type: "radio" }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Small: Story = { + args: { size: "s" }, +}; + +export const Large: Story = { + args: { size: "l" }, +}; diff --git a/frontend/packages/ui/src/lib/foundations/assets/Icon.tsx b/frontend/packages/ui/src/lib/foundations/assets/Icon.tsx new file mode 100644 index 0000000000..e36493794d --- /dev/null +++ b/frontend/packages/ui/src/lib/foundations/assets/Icon.tsx @@ -0,0 +1,359 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import { type ComponentPropsWithRef, memo } from "react"; +import clsx from "clsx"; +import styles from "./Icon.module.scss"; + +// --------------------------------------------------------------------------- +// Icon ID catalogue +// --------------------------------------------------------------------------- + +export const iconIds = [ + "absolute", + "add", + "align-bottom", + "align-content-column-around", + "align-content-column-between", + "align-content-column-center", + "align-content-column-end", + "align-content-column-evenly", + "align-content-column-start", + "align-content-column-stretch", + "align-content-row-around", + "align-content-row-between", + "align-content-row-center", + "align-content-row-end", + "align-content-row-evenly", + "align-content-row-start", + "align-content-row-stretch", + "align-horizontal-center", + "align-items-column-center", + "align-items-column-end", + "align-items-column-start", + "align-items-row-center", + "align-items-row-end", + "align-items-row-start", + "align-left", + "align-right", + "align-self-column-bottom", + "align-self-column-center", + "align-self-column-stretch", + "align-self-column-top", + "align-self-row-center", + "align-self-row-left", + "align-self-row-right", + "align-self-row-stretch", + "align-top", + "align-vertical-center", + "arrow", + "arrow-down", + "arrow-left", + "arrow-right", + "arrow-up", + "arrow-up-right", + "asc-sort", + "at", + "board", + "boards-thumbnail", + "boolean-difference", + "boolean-exclude", + "boolean-flatten", + "boolean-intersection", + "boolean-union", + "broken-link", + "bug", + "character-a", + "character-b", + "character-c", + "character-d", + "character-e", + "character-f", + "character-g", + "character-h", + "character-i", + "character-j", + "character-k", + "character-l", + "character-m", + "character-n", + "character-ntilde", + "character-o", + "character-p", + "character-q", + "character-r", + "character-s", + "character-t", + "character-u", + "character-v", + "character-w", + "character-x", + "character-y", + "character-z", + "clip-content", + "clipboard", + "clock", + "close", + "close-small", + "code", + "column", + "column-reverse", + "comments", + "component", + "component-copy", + "constraint-horizontal", + "constraint-vertical", + "corner-bottom", + "corner-bottom-left", + "corner-bottom-right", + "corner-center", + "corner-radius", + "corner-top", + "corner-top-left", + "corner-top-right", + "crown", + "curve", + "delete", + "delete-text", + "desc-sort", + "detach", + "detached", + "distribute-horizontally", + "distribute-vertical-spacing", + "document", + "download", + "drop", + "drop-shadow", + "easing-ease", + "easing-ease-in", + "easing-ease-in-out", + "easing-ease-out", + "easing-linear", + "effects", + "elipse", + "exit", + "expand", + "external-link", + "eye-off", + "feedback", + "fill-content", + "filter", + "fixed-width", + "fit-content", + "flex", + "flex-grid", + "flex-horizontal", + "flex-vertical", + "flip-horizontal", + "flip-vertical", + "folder", + "gap-horizontal", + "gap-vertical", + "graphics", + "grid", + "grid-column", + "grid-columns", + "grid-gutter", + "grid-margin", + "grid-row", + "grid-rows", + "grid-square", + "group", + "gutter-horizontal", + "gutter-vertical", + "help", + "hide", + "history", + "hsva", + "hug-content", + "icon", + "img", + "inner-shadow", + "info", + "import-export", + "interaction", + "join-nodes", + "justify-content-column-around", + "justify-content-column-between", + "justify-content-column-center", + "justify-content-column-end", + "justify-content-column-evenly", + "justify-content-column-start", + "justify-content-row-around", + "justify-content-row-between", + "justify-content-row-center", + "justify-content-row-end", + "justify-content-row-evenly", + "justify-content-row-start", + "layers", + "library", + "locate", + "lock", + "margin", + "margin-bottom", + "margin-left", + "margin-left-right", + "margin-right", + "margin-top", + "margin-top-bottom", + "mask", + "masked", + "menu", + "merge-nodes", + "move", + "msg-error", + "msg-neutral", + "msg-success", + "msg-warning", + "number", + "open-link", + "padding-bottom", + "padding-extended", + "padding-left", + "padding-left-right", + "padding-right", + "padding-top", + "padding-top-bottom", + "path", + "pentool", + "percentage", + "picker", + "pin", + "play", + "puzzle", + "rectangle", + "reload", + "remove", + "reorder", + "rgba", + "rgba-complementary", + "rotation", + "row", + "row-reverse", + "search", + "separate-nodes", + "settings", + "shown", + "size-horizontal", + "size-vertical", + "snap-nodes", + "status-alert", + "status-tick", + "status-update", + "status-wrong", + "stroke-arrow", + "stroke-center", + "stroke-circle", + "stroke-dashed", + "stroke-diamond", + "stroke-dotted", + "stroke-inside", + "stroke-mixed", + "stroke-outside", + "stroke-rectangle", + "stroke-rounded", + "stroke-size", + "stroke-squared", + "stroke-solid", + "stroke-triangle", + "svg", + "swatches", + "switch", + "text", + "text-align-center", + "text-align-left", + "text-align-right", + "text-auto-height", + "text-auto-width", + "text-bottom", + "text-fixed", + "text-font-family", + "text-font-size", + "text-font-weight", + "text-justify", + "text-letterspacing", + "text-lineheight", + "text-lowercase", + "text-ltr", + "text-middle", + "text-mixed", + "text-palette", + "text-paragraph", + "text-rtl", + "text-stroked", + "text-top", + "text-typography", + "text-underlined", + "text-uppercase", + "thumbnail", + "tick", + "tokens", + "to-corner", + "to-curve", + "tree", + "unlock", + "user", + "variant", + "vertical-align-items-center", + "vertical-align-items-end", + "vertical-align-items-start", + "view-as-icons", + "view-as-list", + "wrap", +] as const; + +export type IconId = (typeof iconIds)[number]; + +// --------------------------------------------------------------------------- +// Icon size constants +// --------------------------------------------------------------------------- + +const ICON_SIZE_L = 32; +const ICON_SIZE_M = 16; +const ICON_SIZE_S = 12; + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +export interface IconProps extends Omit< + ComponentPropsWithRef<"svg">, + "children" +> { + /** Icon identifier — must be one of the registered icon IDs */ + iconId: IconId; + /** Visual size variant. Defaults to "m" (16 px). */ + size?: "s" | "m" | "l"; +} + +function IconInner({ iconId, size = "m", className, ...rest }: IconProps) { + const sizePx = + size === "l" ? ICON_SIZE_L : size === "s" ? ICON_SIZE_S : ICON_SIZE_M; + + // For "s" and "m" the SVG viewport is always 16 px; the is offset to + // centre the smaller glyph inside it. + const viewportSize = size === "l" ? ICON_SIZE_L : ICON_SIZE_M; + const offset = size === "s" || size === "m" ? (ICON_SIZE_M - sizePx) / 2 : 0; + + return ( + + + + ); +} + +export const Icon = memo(IconInner); diff --git a/frontend/packages/ui/src/lib/foundations/typography/Heading.spec.tsx b/frontend/packages/ui/src/lib/foundations/typography/Heading.spec.tsx new file mode 100644 index 0000000000..226f68bea8 --- /dev/null +++ b/frontend/packages/ui/src/lib/foundations/typography/Heading.spec.tsx @@ -0,0 +1,82 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) KALEIDOS INC + */ + +import { render } from "@testing-library/react"; + +import { Heading } from "./Heading"; + +describe("Heading", () => { + it("should render successfully", () => { + const { baseElement } = render( + Hello, + ); + expect(baseElement).toBeTruthy(); + }); + + it("should render as h1 by default", () => { + const { container } = render(Hello); + const element = container.querySelector("h1"); + expect(element).toBeTruthy(); + expect(element?.textContent).toBe("Hello"); + }); + + it("should render the correct heading level", () => { + const { container } = render( + + Level 3 + , + ); + const element = container.querySelector("h3"); + expect(element).toBeTruthy(); + expect(element?.textContent).toBe("Level 3"); + }); + + it("should apply typography class", () => { + const { container } = render( + Styled, + ); + const element = container.querySelector("h1"); + expect(element?.className).toContain("title-large-typography"); + }); + + it("should merge custom className with typography class", () => { + const { container } = render( + + Merged + , + ); + const element = container.querySelector("h1"); + expect(element?.className).toContain("display-typography"); + expect(element?.className).toContain("custom-class"); + }); + + it("should pass through additional HTML attributes", () => { + const { container } = render( + + Attrs + , + ); + const element = container.querySelector("h1"); + expect(element?.getAttribute("data-testid")).toBe("my-heading"); + expect(element?.getAttribute("id")).toBe("heading-1"); + }); + + it("should render all heading levels (1-6)", () => { + const levels = [1, 2, 3, 4, 5, 6] as const; + for (const level of levels) { + const { container } = render( + + H{level} + , + ); + const element = container.querySelector(`h${level}`); + expect(element).toBeTruthy(); + expect(element?.textContent).toBe(`H${level}`); + } + }); +}); diff --git a/frontend/packages/ui/src/lib/foundations/typography/Heading.stories.tsx b/frontend/packages/ui/src/lib/foundations/typography/Heading.stories.tsx new file mode 100644 index 0000000000..b1d19de9fe --- /dev/null +++ b/frontend/packages/ui/src/lib/foundations/typography/Heading.stories.tsx @@ -0,0 +1,84 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) KALEIDOS INC + */ + +import type { Meta, StoryObj } from "@storybook/react-vite"; + +import { Heading } from "./Heading"; +import { typographyIds } from "./typography"; + +const meta = { + title: "Foundations/Typography/Heading", + component: Heading, + argTypes: { + level: { + options: [1, 2, 3, 4, 5, 6], + control: { type: "select" }, + }, + typography: { + options: typographyIds, + control: { type: "select" }, + }, + }, + parameters: { + controls: { exclude: ["children"] }, + }, + args: { + children: "Lorem ipsum", + }, + render: ({ children, ...args }) => {children}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + level: 1, + typography: "display", + }, +}; + +export const Level2: Story = { + args: { + level: 2, + typography: "title-large", + children: "Title Large h2", + }, +}; + +export const Level3: Story = { + args: { + level: 3, + typography: "title-medium", + children: "Title Medium h3", + }, +}; + +export const Level4: Story = { + args: { + level: 4, + typography: "headline-large", + children: "Headline Large h4", + }, +}; + +export const Level5: Story = { + args: { + level: 5, + typography: "headline-medium", + children: "Headline Medium h5", + }, +}; + +export const Level6: Story = { + args: { + level: 6, + typography: "headline-small", + children: "Headline Small h6", + }, +}; diff --git a/frontend/packages/ui/src/lib/foundations/typography/Heading.tsx b/frontend/packages/ui/src/lib/foundations/typography/Heading.tsx new file mode 100644 index 0000000000..f5a93b1db7 --- /dev/null +++ b/frontend/packages/ui/src/lib/foundations/typography/Heading.tsx @@ -0,0 +1,40 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) KALEIDOS INC + */ + +import { type ComponentPropsWithRef, memo } from "react"; +import clsx from "clsx"; + +import type { TypographyId } from "./typography"; +import { typographyClassMap } from "./typographyClassMap"; + +type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; +type HeadingTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + +export interface HeadingProps extends Omit< + ComponentPropsWithRef<"h1">, + "level" +> { + /** The heading level (1–6). Defaults to `1`. */ + level?: HeadingLevel; + /** The typography style to apply. */ + typography: TypographyId; +} + +function HeadingInner(props: HeadingProps) { + const { level = 1, typography, className, children, ...rest } = props; + const Tag: HeadingTag = `h${level}`; + const resolvedClass = clsx(typographyClassMap[typography], className); + + return ( + + {children} + + ); +} + +export const Heading = memo(HeadingInner); diff --git a/frontend/packages/ui/src/lib/foundations/typography/Text.spec.tsx b/frontend/packages/ui/src/lib/foundations/typography/Text.spec.tsx new file mode 100644 index 0000000000..7d4ce8c660 --- /dev/null +++ b/frontend/packages/ui/src/lib/foundations/typography/Text.spec.tsx @@ -0,0 +1,64 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) KALEIDOS INC + */ + +import { render } from "@testing-library/react"; + +import { Text } from "./Text"; + +describe("Text", () => { + it("should render successfully", () => { + const { baseElement } = render(Hello); + expect(baseElement).toBeTruthy(); + }); + + it("should render with default 'p' tag", () => { + const { container } = render(Hello); + const element = container.querySelector("p"); + expect(element).toBeTruthy(); + expect(element?.textContent).toBe("Hello"); + }); + + it("should render with a custom tag via 'as' prop", () => { + const { container } = render( + + Custom + , + ); + const element = container.querySelector("span"); + expect(element).toBeTruthy(); + expect(element?.textContent).toBe("Custom"); + }); + + it("should apply typography class", () => { + const { container } = render(Styled); + const element = container.querySelector("p"); + expect(element?.className).toContain("body-medium-typography"); + }); + + it("should merge custom className with typography class", () => { + const { container } = render( + + Merged + , + ); + const element = container.querySelector("p"); + expect(element?.className).toContain("display-typography"); + expect(element?.className).toContain("custom-class"); + }); + + it("should pass through additional HTML attributes", () => { + const { container } = render( + + Attrs + , + ); + const element = container.querySelector("p"); + expect(element?.getAttribute("data-testid")).toBe("my-text"); + expect(element?.getAttribute("id")).toBe("text-1"); + }); +}); diff --git a/frontend/packages/ui/src/lib/foundations/typography/Text.stories.tsx b/frontend/packages/ui/src/lib/foundations/typography/Text.stories.tsx new file mode 100644 index 0000000000..0f66eadcbb --- /dev/null +++ b/frontend/packages/ui/src/lib/foundations/typography/Text.stories.tsx @@ -0,0 +1,126 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) KALEIDOS INC + */ + +import type { Meta, StoryObj } from "@storybook/react-vite"; + +import { Text } from "./Text"; +import { typographyIds } from "./typography"; + +const meta = { + title: "Foundations/Typography/Text", + component: Text, + argTypes: { + typography: { + options: typographyIds, + control: { type: "select" }, + }, + as: { + control: { type: "text" }, + }, + }, + parameters: { + controls: { exclude: ["children"] }, + }, + args: { + children: "Lorem ipsum", + }, + render: ({ children, ...args }) => {children}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + typography: "display", + }, +}; + +export const CustomTag: Story = { + args: { + typography: "display", + as: "li", + }, +}; + +export const Display: Story = { + args: { + typography: "display", + children: "Display 400 36px/1.4 Work Sans", + }, +}; + +export const TitleLarge: Story = { + args: { + typography: "title-large", + children: "Title Large 400 24px/1.4 Work Sans", + }, +}; + +export const TitleMedium: Story = { + args: { + typography: "title-medium", + children: "Title Medium 400 20px/1.4 Work Sans", + }, +}; + +export const TitleSmall: Story = { + args: { + typography: "title-small", + children: "Title Small 400 14px/1.2 Work Sans", + }, +}; + +export const HeadlineLarge: Story = { + args: { + typography: "headline-large", + children: "Headline Large 400 18px/1.4 Work Sans", + }, +}; + +export const HeadlineMedium: Story = { + args: { + typography: "headline-medium", + children: "Headline Medium 400 16px/1.4 Work Sans", + }, +}; + +export const HeadlineSmall: Story = { + args: { + typography: "headline-small", + children: "Headline Small 500 12px/1.2 Work Sans", + }, +}; + +export const BodyLarge: Story = { + args: { + typography: "body-large", + children: "Body Large 400 16px/1.4 Work Sans", + }, +}; + +export const BodyMedium: Story = { + args: { + typography: "body-medium", + children: "Body Medium 400 14px/1.3 Work Sans", + }, +}; + +export const BodySmall: Story = { + args: { + typography: "body-small", + children: "Body Small 400 12px/1.3 Work Sans", + }, +}; + +export const CodeFont: Story = { + args: { + typography: "code-font", + children: "Code Font 400 12px/1.2 Roboto Mono", + }, +}; diff --git a/frontend/packages/ui/src/lib/foundations/typography/Text.tsx b/frontend/packages/ui/src/lib/foundations/typography/Text.tsx new file mode 100644 index 0000000000..67db795c1d --- /dev/null +++ b/frontend/packages/ui/src/lib/foundations/typography/Text.tsx @@ -0,0 +1,37 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) KALEIDOS INC + */ + +import { type ComponentPropsWithRef, type ElementType, memo } from "react"; +import clsx from "clsx"; + +import type { TypographyId } from "./typography"; +import { typographyClassMap } from "./typographyClassMap"; + +type TextOwnProps = { + /** The HTML element or component to render. Defaults to `"p"`. */ + as?: T; + /** The typography style to apply. */ + typography: TypographyId; +}; + +export type TextProps = TextOwnProps & + Omit, keyof TextOwnProps>; + +function TextInner(props: TextProps) { + const { as, typography, className, children, ...rest } = props; + const Tag = as ?? "p"; + const resolvedClass = clsx(typographyClassMap[typography], className); + + return ( + + {children} + + ); +} + +export const Text = memo(TextInner) as typeof TextInner; diff --git a/frontend/packages/ui/src/lib/foundations/typography/typography.module.scss b/frontend/packages/ui/src/lib/foundations/typography/typography.module.scss new file mode 100644 index 0000000000..f3ebaf5789 --- /dev/null +++ b/frontend/packages/ui/src/lib/foundations/typography/typography.module.scss @@ -0,0 +1,51 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "../../_ds/typography" as t; + +.display-typography { + @include t.use-typography("display"); +} + +.title-large-typography { + @include t.use-typography("title-large"); +} + +.title-medium-typography { + @include t.use-typography("title-medium"); +} + +.title-small-typography { + @include t.use-typography("title-small"); +} + +.headline-large-typography { + @include t.use-typography("headline-large"); +} + +.headline-medium-typography { + @include t.use-typography("headline-medium"); +} + +.headline-small-typography { + @include t.use-typography("headline-small"); +} + +.body-large-typography { + @include t.use-typography("body-large"); +} + +.body-medium-typography { + @include t.use-typography("body-medium"); +} + +.body-small-typography { + @include t.use-typography("body-small"); +} + +.code-font-typography { + @include t.use-typography("code-font"); +} diff --git a/frontend/packages/ui/src/lib/foundations/typography/typography.ts b/frontend/packages/ui/src/lib/foundations/typography/typography.ts new file mode 100644 index 0000000000..7d01d6a941 --- /dev/null +++ b/frontend/packages/ui/src/lib/foundations/typography/typography.ts @@ -0,0 +1,27 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) KALEIDOS INC + */ + +export const Typography = { + display: "display", + titleLarge: "title-large", + titleMedium: "title-medium", + titleSmall: "title-small", + headlineLarge: "headline-large", + headlineMedium: "headline-medium", + headlineSmall: "headline-small", + bodyLarge: "body-large", + bodyMedium: "body-medium", + bodySmall: "body-small", + codeFont: "code-font", +} as const; + +export type TypographyId = (typeof Typography)[keyof typeof Typography]; + +export const typographyIds: TypographyId[] = Object.values(Typography); + +export const typographySet: ReadonlySet = new Set(typographyIds); diff --git a/frontend/packages/ui/src/lib/foundations/typography/typographyClassMap.ts b/frontend/packages/ui/src/lib/foundations/typography/typographyClassMap.ts new file mode 100644 index 0000000000..79c96c738e --- /dev/null +++ b/frontend/packages/ui/src/lib/foundations/typography/typographyClassMap.ts @@ -0,0 +1,24 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) KALEIDOS INC + */ + +import styles from "./typography.module.scss"; + +/** Maps a TypographyId value to its corresponding CSS-module class name. */ +export const typographyClassMap: Readonly> = { + display: styles["display-typography"], + "title-large": styles["title-large-typography"], + "title-medium": styles["title-medium-typography"], + "title-small": styles["title-small-typography"], + "headline-large": styles["headline-large-typography"], + "headline-medium": styles["headline-medium-typography"], + "headline-small": styles["headline-small-typography"], + "body-large": styles["body-large-typography"], + "body-medium": styles["body-medium-typography"], + "body-small": styles["body-small-typography"], + "code-font": styles["code-font-typography"], +}; diff --git a/frontend/packages/ui/src/lib/product/Cta.module.scss b/frontend/packages/ui/src/lib/product/Cta.module.scss new file mode 100644 index 0000000000..482de1e23d --- /dev/null +++ b/frontend/packages/ui/src/lib/product/Cta.module.scss @@ -0,0 +1,25 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "../_ds/_borders.scss" as *; +@use "../_ds/typography.scss" as t; + +.cta { + border-radius: $br-8; + border: $b-1 solid var(--color-accent-primary-muted); + background: var(--color-accent-select); + padding: var(--sp-m); +} + +.cta-title { + color: var(--color-foreground-primary); + line-height: 1.2; + margin-block-end: var(--sp-m); +} + +.cta-message { + line-height: 100%; +} diff --git a/frontend/packages/ui/src/lib/product/Cta.spec.tsx b/frontend/packages/ui/src/lib/product/Cta.spec.tsx new file mode 100644 index 0000000000..aff9ac991e --- /dev/null +++ b/frontend/packages/ui/src/lib/product/Cta.spec.tsx @@ -0,0 +1,55 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) KALEIDOS INC + */ + +import { render } from "@testing-library/react"; + +import { Cta } from "./Cta"; + +describe("Cta", () => { + it("should render successfully", () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); + + it("should render the title text", () => { + const { getByText } = render(); + expect(getByText("Upgrade your plan")).toBeTruthy(); + }); + + it("should render children in the message area", () => { + const { getByText } = render( + + Read more details here + , + ); + expect(getByText("Read more details here")).toBeTruthy(); + }); + + it("should render with data-testid attribute", () => { + const { container } = render(); + const element = container.querySelector("[data-testid='cta']"); + expect(element).toBeTruthy(); + }); + + it("should merge custom className", () => { + const { container } = render( + , + ); + const element = container.querySelector("[data-testid='cta']"); + expect(element?.className).toContain("custom-class"); + }); + + it("should pass through additional HTML attributes", () => { + const { container } = render( + , + ); + const element = container.querySelector("[data-testid='cta']"); + expect(element?.getAttribute("id")).toBe("my-cta"); + expect(element?.getAttribute("aria-label")).toBe("call to action"); + }); +}); diff --git a/frontend/packages/ui/src/lib/product/Cta.stories.tsx b/frontend/packages/ui/src/lib/product/Cta.stories.tsx new file mode 100644 index 0000000000..87abcfeb3a --- /dev/null +++ b/frontend/packages/ui/src/lib/product/Cta.stories.tsx @@ -0,0 +1,46 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) KALEIDOS INC + */ + +import type { Meta, StoryObj } from "@storybook/react-vite"; + +import { Cta } from "./Cta"; + +const meta = { + title: "Product/CTA", + component: Cta, + argTypes: { + title: { + control: { type: "text" }, + }, + }, + args: { + title: "Autosaved versions will be kept for 7 days.", + }, + render: ({ children, ...args }) => ( + + {children ?? ( + + If you'd like to increase this limit, write to us at{" "} + + support@penpot.app + + + )} + + ), +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/frontend/packages/ui/src/lib/product/Cta.tsx b/frontend/packages/ui/src/lib/product/Cta.tsx new file mode 100644 index 0000000000..eba4d0966e --- /dev/null +++ b/frontend/packages/ui/src/lib/product/Cta.tsx @@ -0,0 +1,37 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright (c) KALEIDOS INC + */ + +import { type ComponentPropsWithRef, memo } from "react"; +import clsx from "clsx"; + +import { Text } from "../foundations/typography/Text"; +import styles from "./Cta.module.scss"; + +export interface CtaProps extends ComponentPropsWithRef<"div"> { + /** The title text displayed at the top of the CTA block. */ + title: string; +} + +function CtaInner({ title, className, children, ...rest }: CtaProps) { + return ( +
+
+ + {title} + +
+
{children}
+
+ ); +} + +export const Cta = memo(CtaInner); diff --git a/frontend/packages/ui/stylelint.config.mjs b/frontend/packages/ui/stylelint.config.mjs new file mode 100644 index 0000000000..de82582d4a --- /dev/null +++ b/frontend/packages/ui/stylelint.config.mjs @@ -0,0 +1,37 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +import postcssScss from "postcss-scss"; + +/** @type {import("stylelint").Config} */ +export default { + extends: ["stylelint-config-standard-scss"], + plugins: ["stylelint-scss", "stylelint-use-logical-spec"], + overrides: [ + { + files: ["**/*.scss"], + customSyntax: postcssScss, + }, + ], + rules: { + "at-rule-no-unknown": null, + "declaration-property-value-no-unknown": null, + "selector-pseudo-class-no-unknown": [ + true, + { ignorePseudoClasses: ["global"] }, + ], + + // scss + "scss/comment-no-empty": null, + "scss/at-rule-no-unknown": true, + // TODO: this rule should be enabled to follow scss conventions + "scss/load-no-partial-leading-underscore": null, + // This allows using the characters - or _ as a prefix and is ISO compliant with the Sass specification. + "scss/dollar-variable-pattern": "^[-_]?([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + // This allows using the characters - or _ as a prefix and is ISO compliant with the Sass specification. + "scss/at-mixin-pattern": "^[-_]?([a-z][a-z0-9]*)(-[a-z0-9]+)*$", + }, +}; diff --git a/frontend/packages/ui/tsconfig.json b/frontend/packages/ui/tsconfig.json index 988cd3dd6d..24846a8be4 100644 --- a/frontend/packages/ui/tsconfig.json +++ b/frontend/packages/ui/tsconfig.json @@ -14,8 +14,7 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, "jsx": "react-jsx", - "types": ["vite/client", "vitest"], - "baseUrl": "." + "types": ["vite/client", "vitest"] }, "files": [], "include": [], diff --git a/frontend/packages/ui/tsconfig.storybook.json b/frontend/packages/ui/tsconfig.storybook.json index 4a43d7d4fa..09df52fad6 100644 --- a/frontend/packages/ui/tsconfig.storybook.json +++ b/frontend/packages/ui/tsconfig.storybook.json @@ -1,7 +1,6 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "emitDecoratorMetadata": true, "outDir": "", "module": "esnext", "moduleResolution": "bundler" diff --git a/frontend/packages/ui/vite.config.mts b/frontend/packages/ui/vite.config.mts index a1f91b23c1..2e254d824a 100644 --- a/frontend/packages/ui/vite.config.mts +++ b/frontend/packages/ui/vite.config.mts @@ -1,6 +1,6 @@ /// import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; +import react, { reactCompilerPreset } from '@vitejs/plugin-react'; import dts from 'vite-plugin-dts'; import * as path from 'path'; import { copyFileSync } from 'node:fs'; @@ -22,11 +22,8 @@ const copyCssPlugin = () => ({ export default defineConfig(() => ({ root: import.meta.dirname, plugins: [ - react({ - babel: { - plugins: ['babel-plugin-react-compiler'], - }, - }), + react(), + reactCompilerPreset(), dts({ entryRoot: 'src', tsconfigPath: path.join(import.meta.dirname, 'tsconfig.lib.json'), diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5fa2751411..28a351c311 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -317,9 +317,15 @@ importers: babel-plugin-react-compiler: specifier: ^1.0.0 version: 1.0.0 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + eslint: + specifier: ^9.39.2 + version: 9.39.2 eslint-plugin-import: specifier: 2.32.0 - version: 2.32.0(eslint@9.39.2) + version: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2)(typescript@6.0.2))(eslint@9.39.2) eslint-plugin-jsx-a11y: specifier: 6.10.2 version: 6.10.2(eslint@9.39.2) @@ -329,15 +335,45 @@ importers: eslint-plugin-react-hooks: specifier: 7.0.1 version: 7.0.1(eslint@9.39.2) + postcss-scss: + specifier: ^4.0.9 + version: 4.0.9(postcss@8.5.8) + prettier: + specifier: 3.8.1 + version: 3.8.1 react-compiler-runtime: specifier: ^1.0.0 version: 1.0.0(react@19.2.4) + sass: + specifier: ^1.98.0 + version: 1.98.0 storybook: specifier: 10.3.5 version: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + stylelint: + specifier: ^17.4.0 + version: 17.4.0(typescript@6.0.2) + stylelint-config-standard-scss: + specifier: ^17.0.0 + version: 17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@6.0.2)) + stylelint-scss: + specifier: ^7.0.0 + version: 7.0.0(stylelint@17.4.0(typescript@6.0.2)) + stylelint-use-logical-spec: + specifier: ^5.0.1 + version: 5.0.1(stylelint@17.4.0(typescript@6.0.2)) + typescript: + specifier: ^6.0.2 + version: 6.0.2 + typescript-eslint: + specifier: ^8.58.0 + version: 8.58.0(eslint@9.39.2)(typescript@6.0.2) vite-plugin-dts: specifier: ^4.5.4 version: 4.5.4(@types/node@25.5.2)(rollup@4.57.1)(typescript@6.0.2)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.28.0)(sass-embedded@1.98.0)(sass@1.98.0)(yaml@2.8.2)) + vitest: + specifier: ^4.1.3 + version: 4.1.3(@types/node@25.5.2)(@vitest/browser-playwright@4.1.3)(@vitest/coverage-v8@4.1.3)(jsdom@29.0.2(canvas@3.2.1))(vite@8.0.7(@types/node@25.5.2)(esbuild@0.28.0)(sass-embedded@1.98.0)(sass@1.98.0)(yaml@2.8.2)) text-editor: devDependencies: @@ -2264,6 +2300,65 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@typescript-eslint/eslint-plugin@8.58.0': + resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.58.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.58.0': + resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.58.0': + resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.58.0': + resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.58.0': + resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.58.0': + resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.58.0': + resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.58.0': + resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.58.0': + resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.58.0': + resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitejs/plugin-react@6.0.1': resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2827,6 +2922,10 @@ packages: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} engines: {node: '>=6'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -3402,6 +3501,10 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@9.39.2: resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5908,6 +6011,12 @@ packages: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -5953,6 +6062,13 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typescript-eslint@8.58.0: + resolution: {integrity: sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.8.2: resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} engines: {node: '>=14.17'} @@ -7921,6 +8037,97 @@ snapshots: '@types/triple-beam@1.3.5': {} + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2)(typescript@6.0.2))(eslint@9.39.2)(typescript@6.0.2)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.58.0(eslint@9.39.2)(typescript@6.0.2) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@9.39.2)(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.2)(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.0 + eslint: 9.39.2 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.58.0(eslint@9.39.2)(typescript@6.0.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + '@typescript-eslint/visitor-keys': 8.58.0 + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.39.2 + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.58.0(typescript@6.0.2)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@6.0.2) + '@typescript-eslint/types': 8.58.0 + debug: 4.4.3(supports-color@5.5.0) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.58.0': + dependencies: + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 + + '@typescript-eslint/tsconfig-utils@8.58.0(typescript@6.0.2)': + dependencies: + typescript: 6.0.2 + + '@typescript-eslint/type-utils@8.58.0(eslint@9.39.2)(typescript@6.0.2)': + dependencies: + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.2)(typescript@6.0.2) + debug: 4.4.3(supports-color@5.5.0) + eslint: 9.39.2 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.58.0': {} + + '@typescript-eslint/typescript-estree@8.58.0(typescript@6.0.2)': + dependencies: + '@typescript-eslint/project-service': 8.58.0(typescript@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@6.0.2) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 + debug: 4.4.3(supports-color@5.5.0) + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@6.0.2) + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.58.0(eslint@9.39.2)(typescript@6.0.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + eslint: 9.39.2 + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.58.0': + dependencies: + '@typescript-eslint/types': 8.58.0 + eslint-visitor-keys: 5.0.1 + '@vitejs/plugin-react@6.0.1(babel-plugin-react-compiler@1.0.0)(vite@8.0.7(@types/node@25.5.2)(esbuild@0.28.0)(sass-embedded@1.98.0)(sass@1.98.0)(yaml@2.8.2))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 @@ -8620,6 +8827,8 @@ snapshots: clsx@1.2.1: {} + clsx@2.1.1: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -9267,16 +9476,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint@9.39.2): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2)(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint@9.39.2): dependencies: debug: 3.2.7 optionalDependencies: + '@typescript-eslint/parser': 8.58.0(eslint@9.39.2)(typescript@6.0.2) eslint: 9.39.2 eslint-import-resolver-node: 0.3.10 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(eslint@9.39.2): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2)(typescript@6.0.2))(eslint@9.39.2): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -9287,7 +9497,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2 eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint@9.39.2) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.2)(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint@9.39.2) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -9298,6 +9508,8 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.58.0(eslint@9.39.2)(typescript@6.0.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -9364,6 +9576,8 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint-visitor-keys@5.0.1: {} + eslint@9.39.2: dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) @@ -12042,6 +12256,10 @@ snapshots: triple-beam@1.4.1: {} + ts-api-utils@2.5.0(typescript@6.0.2): + dependencies: + typescript: 6.0.2 + ts-dedent@2.2.0: {} tsconfig-paths@3.15.0: @@ -12108,6 +12326,17 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typescript-eslint@8.58.0(eslint@9.39.2)(typescript@6.0.2): + dependencies: + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.2)(typescript@6.0.2))(eslint@9.39.2)(typescript@6.0.2) + '@typescript-eslint/parser': 8.58.0(eslint@9.39.2)(typescript@6.0.2) + '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2) + '@typescript-eslint/utils': 8.58.0(eslint@9.39.2)(typescript@6.0.2) + eslint: 9.39.2 + typescript: 6.0.2 + transitivePeerDependencies: + - supports-color + typescript@5.8.2: {} typescript@6.0.2: {}