mirror of
https://github.com/penpot/penpot.git
synced 2026-04-27 12:18:32 +00:00
✨ 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)
This commit is contained in:
parent
10cfd99525
commit
42ea5def4f
@ -1,4 +0,0 @@
|
||||
{
|
||||
"presets": [],
|
||||
"plugins": []
|
||||
}
|
||||
266
frontend/packages/ui/AGENTS.md
Normal file
266
frontend/packages/ui/AGENTS.md
Normal file
@ -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 (
|
||||
<div className={clsx(styles.root, className)} {...rest}>
|
||||
<span>{label}</span>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<typeof MyComponent>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
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(<MyComponent label="test" />);
|
||||
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}]` | `<Text as="span" typography="headline-small" />` |
|
||||
| `{:keys [class title children] :rest props}` | `{ className, title, children, ...rest }` |
|
||||
| `[:> "div" props ...]` | `<div className={...} {...rest}>...</div>` |
|
||||
|
||||
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';
|
||||
```
|
||||
100
frontend/packages/ui/eslint.config.mjs
Normal file
100
frontend/packages/ui/eslint.config.mjs
Normal file
@ -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",
|
||||
},
|
||||
}),
|
||||
];
|
||||
@ -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",
|
||||
|
||||
@ -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";
|
||||
|
||||
15
frontend/packages/ui/src/lib/_ds/_borders.scss
Normal file
15
frontend/packages/ui/src/lib/_ds/_borders.scss
Normal file
@ -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);
|
||||
39
frontend/packages/ui/src/lib/_ds/_sizes.scss
Normal file
39
frontend/packages/ui/src/lib/_ds/_sizes.scss
Normal file
@ -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);
|
||||
13
frontend/packages/ui/src/lib/_ds/_utils.scss
Normal file
13
frontend/packages/ui/src/lib/_ds/_utils.scss
Normal file
@ -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;
|
||||
}
|
||||
139
frontend/packages/ui/src/lib/_ds/typography.scss
Normal file
139
frontend/packages/ui/src/lib/_ds/typography.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
44
frontend/packages/ui/src/lib/buttons/Button.module.scss
Normal file
44
frontend/packages/ui/src/lib/buttons/Button.module.scss
Normal file
@ -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;
|
||||
}
|
||||
80
frontend/packages/ui/src/lib/buttons/Button.spec.tsx
Normal file
80
frontend/packages/ui/src/lib/buttons/Button.spec.tsx
Normal file
@ -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(<Button>Click me</Button>);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders as a <button> by default", () => {
|
||||
render(<Button>Click me</Button>);
|
||||
expect(screen.getByRole("button", { name: "Click me" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders as an <a> when `to` is provided", () => {
|
||||
render(<Button to="https://example.com">Go</Button>);
|
||||
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(<Button>Primary</Button>);
|
||||
const btn = container.querySelector("button");
|
||||
expect(btn?.className).toContain("button-primary");
|
||||
});
|
||||
|
||||
it("applies secondary variant class", () => {
|
||||
const { container } = render(<Button variant="secondary">Sec</Button>);
|
||||
expect(container.querySelector("button")?.className).toContain(
|
||||
"button-secondary",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies ghost variant class", () => {
|
||||
const { container } = render(<Button variant="ghost">Ghost</Button>);
|
||||
expect(container.querySelector("button")?.className).toContain(
|
||||
"button-ghost",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies destructive variant class", () => {
|
||||
const { container } = render(<Button variant="destructive">Danger</Button>);
|
||||
expect(container.querySelector("button")?.className).toContain(
|
||||
"button-destructive",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders an Icon when `icon` is provided", () => {
|
||||
const { container } = render(<Button icon="pin">With icon</Button>);
|
||||
expect(container.querySelector("svg")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("does not render an Icon when `icon` is not provided", () => {
|
||||
const { container } = render(<Button>No icon</Button>);
|
||||
expect(container.querySelector("svg")).toBeNull();
|
||||
});
|
||||
|
||||
it("merges custom className", () => {
|
||||
const { container } = render(<Button className="custom">Click me</Button>);
|
||||
expect(container.querySelector("button")?.className).toContain("custom");
|
||||
});
|
||||
|
||||
it("calls onRef with the DOM node", () => {
|
||||
const onRef = vi.fn();
|
||||
render(<Button onRef={onRef}>Ref</Button>);
|
||||
expect(onRef).toHaveBeenCalledWith(expect.any(HTMLButtonElement));
|
||||
});
|
||||
|
||||
it("forwards arbitrary props to the root element", () => {
|
||||
render(<Button data-testid="my-btn">Click</Button>);
|
||||
expect(screen.getByTestId("my-btn")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
57
frontend/packages/ui/src/lib/buttons/Button.stories.tsx
Normal file
57
frontend/packages/ui/src/lib/buttons/Button.stories.tsx
Normal file
@ -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<typeof Button>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
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" },
|
||||
};
|
||||
118
frontend/packages/ui/src/lib/buttons/Button.tsx
Normal file
118
frontend/packages/ui/src/lib/buttons/Button.tsx
Normal file
@ -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 <a>;
|
||||
// otherwise as <button>.
|
||||
type ButtonAsButton = ButtonBaseProps &
|
||||
Omit<ComponentPropsWithRef<"button">, keyof ButtonBaseProps> & {
|
||||
/** When set, renders as an anchor element pointing to this URL. */
|
||||
to?: undefined;
|
||||
};
|
||||
|
||||
type ButtonAsAnchor = ButtonBaseProps &
|
||||
Omit<ComponentPropsWithRef<"a">, keyof ButtonBaseProps> & {
|
||||
/** When set, renders as an anchor element pointing to this URL. */
|
||||
to: string;
|
||||
};
|
||||
|
||||
export type ButtonProps = ButtonAsButton | ButtonAsAnchor;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getComputedClass(
|
||||
variant: ButtonVariant,
|
||||
isLink: boolean,
|
||||
className?: string,
|
||||
) {
|
||||
return clsx(
|
||||
styles.button,
|
||||
{
|
||||
[styles["button-link"]]: isLink,
|
||||
[styles["button-primary"]]: variant === "primary",
|
||||
[styles["button-secondary"]]: variant === "secondary",
|
||||
[styles["button-ghost"]]: variant === "ghost",
|
||||
[styles["button-destructive"]]: variant === "destructive",
|
||||
},
|
||||
className,
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonContent({
|
||||
icon,
|
||||
children,
|
||||
}: {
|
||||
icon?: IconId;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{icon && <Icon iconId={icon} size="m" />}
|
||||
<span className={styles["label-wrapper"]}>{children}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ButtonInner(props: ButtonProps) {
|
||||
const { variant = "primary", icon, onRef, className, children } = props;
|
||||
|
||||
if (props.to !== undefined) {
|
||||
// Anchor branch
|
||||
const { to, variant: _v, icon: _i, onRef: _r, ...rest } = props;
|
||||
return (
|
||||
<a
|
||||
href={to}
|
||||
className={getComputedClass(variant, true, className)}
|
||||
ref={(node) => onRef?.(node)}
|
||||
{...rest}
|
||||
>
|
||||
<ButtonContent icon={icon}>{children}</ButtonContent>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// Button branch
|
||||
const { variant: _v, icon: _i, onRef: _r, to: _t, ...rest } = props;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={getComputedClass(variant, false, className)}
|
||||
ref={(node) => onRef?.(node)}
|
||||
{...rest}
|
||||
>
|
||||
<ButtonContent icon={icon}>{children}</ButtonContent>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export const Button = memo(ButtonInner);
|
||||
127
frontend/packages/ui/src/lib/buttons/_buttons.scss
Normal file
127
frontend/packages/ui/src/lib/buttons/_buttons.scss
Normal file
@ -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%);
|
||||
}
|
||||
}
|
||||
@ -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(<Example />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
@ -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<typeof Example>;
|
||||
|
||||
@ -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() {
|
||||
<button onClick={() => setCount(0)}>Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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(<Icon iconId="pin" />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders an svg element", () => {
|
||||
const { container } = render(<Icon iconId="pin" />);
|
||||
expect(container.querySelector("svg")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("references the correct icon sprite href", () => {
|
||||
const { container } = render(<Icon iconId="add" />);
|
||||
const use = container.querySelector("use");
|
||||
expect(use?.getAttribute("href")).toBe("#icon-add");
|
||||
});
|
||||
|
||||
it("defaults to medium size (16px viewport)", () => {
|
||||
const { container } = render(<Icon iconId="pin" />);
|
||||
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(<Icon iconId="pin" size="l" />);
|
||||
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(<Icon iconId="pin" size="s" />);
|
||||
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(
|
||||
<Icon iconId="pin" className="custom-class" />,
|
||||
);
|
||||
expect(container.querySelector("svg")?.getAttribute("class")).toContain(
|
||||
"custom-class",
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards arbitrary svg props", () => {
|
||||
const { container } = render(
|
||||
<Icon iconId="pin" data-testid="my-icon" aria-label="pin icon" />,
|
||||
);
|
||||
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);
|
||||
});
|
||||
});
|
||||
@ -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<typeof Icon>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Small: Story = {
|
||||
args: { size: "s" },
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: { size: "l" },
|
||||
};
|
||||
359
frontend/packages/ui/src/lib/foundations/assets/Icon.tsx
Normal file
359
frontend/packages/ui/src/lib/foundations/assets/Icon.tsx
Normal file
@ -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 <use> 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 (
|
||||
<svg
|
||||
className={clsx(styles.icon, className)}
|
||||
width={viewportSize}
|
||||
height={viewportSize}
|
||||
{...rest}
|
||||
>
|
||||
<use
|
||||
href={`#icon-${iconId}`}
|
||||
width={sizePx}
|
||||
height={sizePx}
|
||||
x={offset}
|
||||
y={offset}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export const Icon = memo(IconInner);
|
||||
@ -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(
|
||||
<Heading typography="display">Hello</Heading>,
|
||||
);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should render as h1 by default", () => {
|
||||
const { container } = render(<Heading typography="display">Hello</Heading>);
|
||||
const element = container.querySelector("h1");
|
||||
expect(element).toBeTruthy();
|
||||
expect(element?.textContent).toBe("Hello");
|
||||
});
|
||||
|
||||
it("should render the correct heading level", () => {
|
||||
const { container } = render(
|
||||
<Heading level={3} typography="title-medium">
|
||||
Level 3
|
||||
</Heading>,
|
||||
);
|
||||
const element = container.querySelector("h3");
|
||||
expect(element).toBeTruthy();
|
||||
expect(element?.textContent).toBe("Level 3");
|
||||
});
|
||||
|
||||
it("should apply typography class", () => {
|
||||
const { container } = render(
|
||||
<Heading typography="title-large">Styled</Heading>,
|
||||
);
|
||||
const element = container.querySelector("h1");
|
||||
expect(element?.className).toContain("title-large-typography");
|
||||
});
|
||||
|
||||
it("should merge custom className with typography class", () => {
|
||||
const { container } = render(
|
||||
<Heading typography="display" className="custom-class">
|
||||
Merged
|
||||
</Heading>,
|
||||
);
|
||||
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(
|
||||
<Heading typography="body-small" data-testid="my-heading" id="heading-1">
|
||||
Attrs
|
||||
</Heading>,
|
||||
);
|
||||
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(
|
||||
<Heading level={level} typography="body-medium">
|
||||
H{level}
|
||||
</Heading>,
|
||||
);
|
||||
const element = container.querySelector(`h${level}`);
|
||||
expect(element).toBeTruthy();
|
||||
expect(element?.textContent).toBe(`H${level}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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 }) => <Heading {...args}>{children}</Heading>,
|
||||
} satisfies Meta<typeof Heading>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
@ -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 (
|
||||
<Tag className={resolvedClass} {...rest}>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
export const Heading = memo(HeadingInner);
|
||||
@ -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(<Text typography="body-medium">Hello</Text>);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should render with default 'p' tag", () => {
|
||||
const { container } = render(<Text typography="body-medium">Hello</Text>);
|
||||
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(
|
||||
<Text as="span" typography="display">
|
||||
Custom
|
||||
</Text>,
|
||||
);
|
||||
const element = container.querySelector("span");
|
||||
expect(element).toBeTruthy();
|
||||
expect(element?.textContent).toBe("Custom");
|
||||
});
|
||||
|
||||
it("should apply typography class", () => {
|
||||
const { container } = render(<Text typography="body-medium">Styled</Text>);
|
||||
const element = container.querySelector("p");
|
||||
expect(element?.className).toContain("body-medium-typography");
|
||||
});
|
||||
|
||||
it("should merge custom className with typography class", () => {
|
||||
const { container } = render(
|
||||
<Text typography="display" className="custom-class">
|
||||
Merged
|
||||
</Text>,
|
||||
);
|
||||
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(
|
||||
<Text typography="body-small" data-testid="my-text" id="text-1">
|
||||
Attrs
|
||||
</Text>,
|
||||
);
|
||||
const element = container.querySelector("p");
|
||||
expect(element?.getAttribute("data-testid")).toBe("my-text");
|
||||
expect(element?.getAttribute("id")).toBe("text-1");
|
||||
});
|
||||
});
|
||||
@ -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 }) => <Text {...args}>{children}</Text>,
|
||||
} satisfies Meta<typeof Text>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
37
frontend/packages/ui/src/lib/foundations/typography/Text.tsx
Normal file
37
frontend/packages/ui/src/lib/foundations/typography/Text.tsx
Normal file
@ -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<T extends ElementType = "p"> = {
|
||||
/** The HTML element or component to render. Defaults to `"p"`. */
|
||||
as?: T;
|
||||
/** The typography style to apply. */
|
||||
typography: TypographyId;
|
||||
};
|
||||
|
||||
export type TextProps<T extends ElementType = "p"> = TextOwnProps<T> &
|
||||
Omit<ComponentPropsWithRef<T>, keyof TextOwnProps<T>>;
|
||||
|
||||
function TextInner<T extends ElementType = "p">(props: TextProps<T>) {
|
||||
const { as, typography, className, children, ...rest } = props;
|
||||
const Tag = as ?? "p";
|
||||
const resolvedClass = clsx(typographyClassMap[typography], className);
|
||||
|
||||
return (
|
||||
<Tag className={resolvedClass} {...rest}>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
export const Text = memo(TextInner) as typeof TextInner;
|
||||
@ -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");
|
||||
}
|
||||
@ -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<string> = new Set(typographyIds);
|
||||
@ -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<Record<string, string>> = {
|
||||
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"],
|
||||
};
|
||||
25
frontend/packages/ui/src/lib/product/Cta.module.scss
Normal file
25
frontend/packages/ui/src/lib/product/Cta.module.scss
Normal file
@ -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%;
|
||||
}
|
||||
55
frontend/packages/ui/src/lib/product/Cta.spec.tsx
Normal file
55
frontend/packages/ui/src/lib/product/Cta.spec.tsx
Normal file
@ -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(<Cta title="Hello" />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should render the title text", () => {
|
||||
const { getByText } = render(<Cta title="Upgrade your plan" />);
|
||||
expect(getByText("Upgrade your plan")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should render children in the message area", () => {
|
||||
const { getByText } = render(
|
||||
<Cta title="Notice">
|
||||
<span>Read more details here</span>
|
||||
</Cta>,
|
||||
);
|
||||
expect(getByText("Read more details here")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should render with data-testid attribute", () => {
|
||||
const { container } = render(<Cta title="Test" />);
|
||||
const element = container.querySelector("[data-testid='cta']");
|
||||
expect(element).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should merge custom className", () => {
|
||||
const { container } = render(
|
||||
<Cta title="Styled" className="custom-class" />,
|
||||
);
|
||||
const element = container.querySelector("[data-testid='cta']");
|
||||
expect(element?.className).toContain("custom-class");
|
||||
});
|
||||
|
||||
it("should pass through additional HTML attributes", () => {
|
||||
const { container } = render(
|
||||
<Cta title="Attrs" id="my-cta" aria-label="call to action" />,
|
||||
);
|
||||
const element = container.querySelector("[data-testid='cta']");
|
||||
expect(element?.getAttribute("id")).toBe("my-cta");
|
||||
expect(element?.getAttribute("aria-label")).toBe("call to action");
|
||||
});
|
||||
});
|
||||
46
frontend/packages/ui/src/lib/product/Cta.stories.tsx
Normal file
46
frontend/packages/ui/src/lib/product/Cta.stories.tsx
Normal file
@ -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 }) => (
|
||||
<Cta {...args}>
|
||||
{children ?? (
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.75rem",
|
||||
color: "var(--color-foreground-secondary)",
|
||||
}}
|
||||
>
|
||||
If you'd like to increase this limit, write to us at{" "}
|
||||
<a style={{ color: "var(--color-accent-primary)" }} href="#">
|
||||
support@penpot.app
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</Cta>
|
||||
),
|
||||
} satisfies Meta<typeof Cta>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
37
frontend/packages/ui/src/lib/product/Cta.tsx
Normal file
37
frontend/packages/ui/src/lib/product/Cta.tsx
Normal file
@ -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 (
|
||||
<div className={clsx(styles.cta, className)} data-testid="cta" {...rest}>
|
||||
<div className={styles["cta-title"]}>
|
||||
<Text
|
||||
as="span"
|
||||
typography="headline-small"
|
||||
className={styles["placeholder-title"]}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles["cta-message"]}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Cta = memo(CtaInner);
|
||||
37
frontend/packages/ui/stylelint.config.mjs
Normal file
37
frontend/packages/ui/stylelint.config.mjs
Normal file
@ -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]+)*$",
|
||||
},
|
||||
};
|
||||
@ -14,8 +14,7 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"jsx": "react-jsx",
|
||||
"types": ["vite/client", "vitest"],
|
||||
"baseUrl": "."
|
||||
"types": ["vite/client", "vitest"]
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": "",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/// <reference types='vitest' />
|
||||
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'),
|
||||
|
||||
237
frontend/pnpm-lock.yaml
generated
237
frontend/pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user