diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 27de3b9..72c9172 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,7 +4,7 @@ name: Publish on: push: branches: - - feature/semver + - main jobs: test: diff --git a/CHANGELOG.md b/CHANGELOG.md index e56894c..b76dfea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## 0.7.2 (2025-07-03) + +This was a version bump only, there were no code changes. + +## 0.7.1 (2025-07-03) + +This was a version bump only, there were no code changes. + +## 0.7.0 (2025-07-03) + +### 🚀 Features + +- add pill, label, and text-input components ([6a25a69](https://github.com/craftzing/design-system/commit/6a25a69)) + +### ❤️ Thank You + +- Bavo Vanderghote + ## 0.6.0 (2025-04-30) ### 🚀 Features diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8ee4d63 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,422 @@ +# CLAUDE.md - Craftzing Design System + +## Project Overview + +This is a **web components-based design system** built with **Lit** and managed as an **NX monorepo**. The system provides reusable UI components using modern web standards and is designed for enterprise-level applications. + +**Key Technologies:** + +- **Lit** - For building web components +- **NX** - Monorepo management and build orchestration +- **TypeScript** - Primary programming language +- **Storybook** - Component documentation and testing +- **Style Dictionary** - Design tokens transformation +- **Tokens Studio** - Design token management +- **Chromatic** - Visual testing and review + +## Project Structure + +``` +design-system/ +├── packages/ # Published packages +│ ├── core/ # Core utilities and base classes +│ ├── design-system/ # Main component library +│ └── tokens/ # Design tokens package +├── libs/ # Internal libraries +│ ├── storybook/ # Storybook configuration +│ └── tokens/ # Token transformation utilities +├── dist/ # Build output +├── tmp/ # Temporary build files +└── node_modules/ # Dependencies +``` + +## Architecture + +### Monorepo Structure + +- **Type**: NX-powered monorepo with workspaces +- **Package Manager**: npm with workspaces +- **Build System**: SWC for fast TypeScript compilation +- **Bundler**: Rollup with TypeScript plugin +- **Testing**: Vitest for unit tests +- **Linting**: ESLint with NX plugin for module boundaries + +### Component Architecture + +- **Base Class**: `CraftzingElement` extends `LitElement` +- **Component Definition**: Auto-registration with dependency injection +- **Styling**: CSS-in-JS with Lit's `css` template literal +- **Props**: TypeScript interfaces with Lit decorators +- **Exports**: ESM with proper TypeScript declarations + +## Our Stack for Component Development + +When using the Figma MCP server or generating components from designs, use the following technology stack: + +### Primary Technologies + +- **Framework**: Lit (Web Components) +- **Language**: TypeScript +- **Styling**: CSS-in-JS with Lit's `css` template literal +- **Design Tokens**: CSS custom properties from `/packages/tokens/dist/css/tokens.css` + +### Component Implementation Stack + +- **Base Class**: Always extend `CraftzingElement` (imported from `craftzing-design-system-test-core`) +- **Property Decorators**: Use Lit's `@property()` decorator for reactive properties +- **Templates**: Use Lit's `html` template literal for rendering +- **Event Handling**: Use Lit's event binding syntax (`@event="${handler}"`) +- **Conditional Rendering**: Use Lit directives (`ifDefined`, `classMap`, etc.) + +### Styling Guidelines + +- **CSS Variables**: Always use design tokens from tokens.css (e.g., `var(--primary-500)`, `var(--space-m)`) +- **CSS Nesting**: Use native CSS nesting for component styles +- **BEM-like Classes**: Use component-specific class naming (e.g., `.button`, `.button--primary`) +- **Responsive Design**: Use token-based breakpoints and spacing +- **Accessibility**: Include proper ARIA attributes and focus states + +### Utility Libraries + +- **Class Management**: `classnames` for conditional CSS classes +- **Spread Attributes**: `@open-wc/lit-helpers` for spreading undeclared attributes +- **Conditional Values**: `lit/directives/if-defined` for optional attributes + +### Code Generation Context + +When generating code from Figma designs: + +- Use semantic HTML elements where appropriate +- Implement proper TypeScript interfaces for component props +- Include comprehensive accessibility attributes +- Use design tokens for all visual properties (colors, spacing, typography) +- Follow the existing component patterns and file structure +- Build components highly composable +- Always use slots when the slot component is used in Figma +- Include proper exports and component registration + +## Key Files and Entry Points + +### Main Entry Points + +- `/packages/design-system/src/index.ts` - Main component exports +- `/packages/core/src/index.ts` - Core utilities and base classes +- `/packages/tokens/dist/css/tokens.css` - Design tokens as CSS variables + +### Configuration Files + +- `/nx.json` - NX workspace configuration +- `/package.json` - Root package with workspace scripts +- `/tsconfig.base.json` - Base TypeScript configuration +- `/eslint.config.mjs` - ESLint configuration +- `/vitest.workspace.ts` - Vitest workspace configuration + +### Storybook Configuration + +- `/libs/storybook/.storybook/main.ts` - Storybook configuration +- `/libs/storybook/.storybook/preview.ts` - Global Storybook settings +- Stories Location: `packages/**/src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)` + +## Scripts and Commands + +### Development + +```bash +npm run storybook # Start Storybook development server +npm run storybook:build # Build Storybook for production +``` + +### Design Tokens + +```bash +npm run tokens:build # Build tokens from Tokens Studio to CSS +npm run tokens:test # Run token tests +npm run tokens:test:update # Update token snapshots +``` + +### Build and Release + +```bash +npm run clean # Clean dist directories +npm run version # Version packages using semantic versioning +npm run chromatic # Run visual regression testing +``` + +### NX Commands + +```bash +nx run : # Run specific project targets +nx build # Build specific project +nx test # Test specific project +nx lint # Lint specific project +``` + +## Component Development Patterns + +### Component Structure + +Each component follows this structure: + +``` +component-name/ +├── component-name.component.ts # Main component implementation +├── component-name.types.ts # TypeScript interfaces +├── component-name.css # External styles (optional) +├── component-name.stories.ts # Storybook stories +├── component-name.test.ts # Unit tests +├── component-name.ts # Re-exports and registration +└── index.ts # Public API exports +``` + +### Base Component Class + +All components extend `CraftzingElement` which provides: + +- Automatic component registration +- Dependency injection system +- Helper for undeclared attributes +- Custom element definition utilities + +### Example Component Implementation + +```typescript +import { CraftzingElement } from 'craftzing-design-system-test-core'; + +class MyComponent extends CraftzingElement { + @property({ reflect: true }) + variant: 'primary' | 'secondary' = 'primary'; + + static styles = css` + /* styles */ + `; + + render() { + return html`
Content
`; + } +} +``` + +## Design Tokens Workflow + +### Token Management + +1. **Source**: Tokens Studio (Figma plugin) syncs to `/packages/tokens/tokens-studio/tokens.json` +2. **Transform**: Style Dictionary processes tokens to CSS variables +3. **Output**: Generated CSS files in `/packages/tokens/dist/css/` +4. **Usage**: Import tokens as CSS variables in components + +### Token Categories + +- **Colors**: Brand colors, semantic colors, neutral palette +- **Typography**: Font weights, sizes, line heights +- **Spacing**: Consistent spacing scale +- **Sizes**: Component dimensions and breakpoints + +### Token Usage in Components + +```typescript +static styles = css` + .button { + background-color: var(--primary-500); + color: var(--typography-white); + padding: var(--space-xs) var(--space-m); + font-size: var(--size-m); + } +`; +``` + +## Testing Strategy + +### Unit Testing + +- **Framework**: Vitest with jsdom +- **Location**: `*.test.ts` files alongside components +- **Coverage**: @vitest/coverage-v8 for coverage reports +- **Helpers**: @open-wc/testing for web component testing + +### Visual Testing + +- **Chromatic**: Automated visual regression testing +- **Storybook**: Component isolation and documentation +- **Stories**: Required for all components + +### Token Testing + +- **Snapshot Testing**: Ensures token output consistency +- **Location**: `/packages/tokens/tests/` +- **Update**: `npm run tokens:test:update` + +## Development Workflow + +### Adding New Components + +1. Create component directory in `/packages/design-system/src/lib/` +2. Implement component class extending `CraftzingElement` +3. Add TypeScript interfaces +4. Create Storybook stories +5. Write unit tests +6. Export from main index.ts +7. Update documentation + +### Storybook Story Guidelines + +When creating stories for a component built with Lit, **always define the component at the top of the file** using the component's static `define()` method: + +```typescript +import { CZButton } from './button.component.js'; + +CZButton.define('cz-button'); // Define component at the top + +export default { + title: 'Components/Button', + component: 'cz-button', + // ... rest of config +}; +``` + +### Working with Tokens + +1. Update tokens in Tokens Studio (Figma) +2. Sync changes to tokens.json +3. Run `npm run tokens:build` +4. Test token changes with `npm run tokens:test` +5. Update snapshots if needed + +### Release Process + +1. Development happens on feature branches +2. Use semantic versioning with `npm run version` +3. Visual review through Chromatic +4. Automated releases for packages/\* only + +## Build and Deployment + +### Build Targets + +- **Components**: Compiled to ES modules with TypeScript declarations +- **Tokens**: CSS variables and utility files +- **Storybook**: Static site for component documentation + +### Output Structure + +``` +dist/ +├── packages/ +│ ├── design-system/ # Component library build +│ ├── core/ # Core utilities build +│ └── tokens/ # Token files (CSS, JS, etc.) +└── storybook/ # Storybook static files +``` + +## Key Dependencies + +### Core Dependencies + +- `lit` - Web components framework +- `@open-wc/lit-helpers` - Lit utilities +- `classnames` - CSS class management +- `style-dictionary` - Token transformation + +### Development Dependencies + +- `nx` - Monorepo management +- `@nx/js`, `@nx/storybook` - NX plugins +- `@swc/core` - Fast TypeScript compilation +- `vitest` - Testing framework +- `chromatic` - Visual testing + +## TypeScript Configuration + +### Key Settings + +- **Target**: ES2015 for broad compatibility +- **Module**: ESNext with bundler resolution +- **Decorators**: Enabled for Lit decorators +- **Plugins**: ts-lit-plugin for Lit templates +- **Paths**: Workspace-relative imports + +### Module Boundaries + +ESLint enforces module boundaries: + +- Packages can depend on other packages +- Components should use proper imports +- No circular dependencies + +## Environment Setup + +### Requirements + +- Node.js (version specified in package.json) +- npm (with workspaces support) +- Modern browser for Storybook + +### Getting Started + +1. `git clone ` +2. `npm install` +3. `npm run storybook` - Start development +4. `npm run tokens:build` - Build tokens + +## Best Practices + +### Component Development + +- Always extend `CraftzingElement` +- Use TypeScript interfaces for props +- Include Storybook stories for all components +- Write unit tests for component logic +- Use design tokens for styling +- Follow semantic versioning + +### Code Quality + +- ESLint for code consistency +- Prettier for formatting +- TypeScript strict mode +- Comprehensive test coverage +- Visual regression testing + +### Performance + +- Tree-shakable exports +- Lazy loading for large components +- Efficient re-rendering with Lit +- Optimized build outputs + +## Troubleshooting + +### Common Issues + +1. **Component not registering**: Check `define()` call in stories +2. **Token not found**: Ensure tokens are built and imported +3. **Build errors**: Check TypeScript configuration and imports +4. **Storybook not loading**: Verify story files match pattern + +### Debug Commands + +```bash +nx run design-system:build --verbose # Verbose build output +nx run tokens:test --watch # Watch token tests +nx run storybook:storybook --verbose # Debug Storybook +``` + +## Documentation + +### Existing Documentation + +- `/README.md` - Project overview and getting started +- `/packages/*/README.md` - Package-specific documentation +- Storybook - Interactive component documentation +- `/CHANGELOG.md` - Release notes + +### Additional Resources + +- [Lit Documentation](https://lit.dev/) +- [NX Documentation](https://nx.dev/) +- [Style Dictionary](https://styledictionary.com/) +- [Tokens Studio](https://tokens.studio/) + +--- + +_This documentation is designed to help Claude instances quickly understand and work with the Craftzing Design System codebase. For questions or updates, refer to the project maintainers._ diff --git a/README.md b/README.md index 7ce58a3..f1dd2f7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +⚠️ This repo is a work in progress and should not be used as a template for new design systems (yet). + # DesignSystem This repository contains an example setup for a web components-based design system. diff --git a/package-lock.json b/package-lock.json index 6fcf37a..35ed48b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21627,24 +21627,24 @@ }, "packages/core": { "name": "craftzing-design-system-test-core", - "version": "0.6.0", + "version": "0.7.2", "dependencies": { "lit": "^3.2.1" } }, "packages/design-system": { "name": "craftzing-design-system-test", - "version": "0.6.0", + "version": "0.7.2", "dependencies": { "@open-wc/lit-helpers": "^0.7.0", "classnames": "^2.5.1", - "craftzing-design-system-test-core": "0.6.0", + "craftzing-design-system-test-core": "0.7.2", "lit": "^3.2.1" } }, "packages/tokens": { "name": "craftzing-design-system-test-tokens", - "version": "0.6.0", + "version": "0.7.2", "dependencies": { "style-dictionary": "^4.3.3", "vitest": "^1.3.1" diff --git a/package.json b/package.json index d62b433..bdbf9cc 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "clean": "rm -rf dist", "storybook": "nx storybook storybook", "storybook:build": "nx build-storybook storybook", - "chromatic": "chromatic --project-token $(grep CHROMATIC_PROJECT_TOKEN .env | cut -d '=' -f2) -o=dist/storybook --only-changed", + "chromatic": "chromatic --project-token $(grep CHROMATIC_PROJECT_TOKEN .env | cut -d '=' -f2) -o=dist/storybook --only-changed --build-script-name=storybook:build", "tokens:build": "nx run tokens:build", "tokens:test": "nx run tokens:test", "tokens:test:update": "nx run tokens:test -u", diff --git a/packages/core/package.json b/packages/core/package.json index 74d7026..311deec 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "craftzing-design-system-test-core", - "version": "0.6.0", + "version": "0.7.2", "type": "module", "exports": { ".": { diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 17de481..abb2b0d 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -1,6 +1,6 @@ { "name": "craftzing-design-system-test", - "version": "0.6.0", + "version": "0.7.2", "type": "module", "main": "./index.js", "module": "./index.js", @@ -33,6 +33,6 @@ "lit": "^3.2.1", "@open-wc/lit-helpers": "^0.7.0", "classnames": "^2.5.1", - "craftzing-design-system-test-core": "0.6.0" + "craftzing-design-system-test-core": "0.7.2" } } diff --git a/packages/design-system/project.json b/packages/design-system/project.json index 84ab158..c4f10ac 100644 --- a/packages/design-system/project.json +++ b/packages/design-system/project.json @@ -14,7 +14,10 @@ "assets": ["packages/design-system/*.md"], "generateExportsField": true, "additionalEntryPoints": [ - "packages/design-system/src/lib/button/button.component.ts" + "packages/design-system/src/lib/button/button.ts", + "packages/design-system/src/lib/label/label.ts", + "packages/design-system/src/lib/pill/pill.ts", + "packages/design-system/src/lib/text-input/text-input.ts" ], "swcrc": "packages/design-system/.swcrc" } diff --git a/packages/design-system/src/index.ts b/packages/design-system/src/index.ts index ec14e41..67dc5a9 100644 --- a/packages/design-system/src/index.ts +++ b/packages/design-system/src/index.ts @@ -1 +1,6 @@ export * from './lib/button/index.ts'; +export * from './lib/pill/index.ts'; +export * from './lib/label/index.ts'; +export * from './lib/text-input/index.ts'; +export * from './lib/card/index.ts'; +export * from './lib/heading/index.ts'; diff --git a/packages/design-system/src/lib/button/button.component.ts b/packages/design-system/src/lib/button/button.component.ts index dffc5e0..da442fb 100644 --- a/packages/design-system/src/lib/button/button.component.ts +++ b/packages/design-system/src/lib/button/button.component.ts @@ -23,14 +23,14 @@ class CZButton extends CraftzingElement implements CZButtonProps { align-items: center; justify-content: center; - line-height: 1.2; - font-weight: 700; + line-height: var(--line-height-140); + font-weight: var(--bold); cursor: pointer; text-decoration: none; transition: all 0.2s ease-in-out 0s; - border: 0.125rem solid transparent; + border: var(--space-xxxs) solid transparent; &:active { transform: scale(0.95); diff --git a/packages/design-system/src/lib/button/index.ts b/packages/design-system/src/lib/button/index.ts index 98de4cf..9e6c3df 100644 --- a/packages/design-system/src/lib/button/index.ts +++ b/packages/design-system/src/lib/button/index.ts @@ -1,2 +1,2 @@ -export * from './button.ts'; +export * from './button.component.ts'; export type { CZButtonProps } from './button.types.ts'; diff --git a/packages/design-system/src/lib/card/card.component.ts b/packages/design-system/src/lib/card/card.component.ts new file mode 100644 index 0000000..8085ee2 --- /dev/null +++ b/packages/design-system/src/lib/card/card.component.ts @@ -0,0 +1,93 @@ +import { css, CSSResultGroup } from 'lit'; +import { html, literal } from 'lit/static-html.js'; +import { spread } from '@open-wc/lit-helpers'; +import classNames from 'classnames'; +import { CraftzingElement } from 'craftzing-design-system-test-core'; + +class CZCard extends CraftzingElement { + static styles: CSSResultGroup = css` + .card { + box-sizing: border-box; + display: flex; + flex-direction: column; + background-color: var(--white); + border: 1px solid var(--primary-500); + border-radius: var(--space-xxxs); + overflow: hidden; + } + + .card-header { + box-sizing: border-box; + display: flex; + flex-direction: column; + padding: var(--space-m) var(--space-m) var(--space-s) var(--space-m); + border-bottom: 1px solid var(--primary-500); + } + + .card-body { + box-sizing: border-box; + display: flex; + flex-direction: column; + padding: var(--space-s) var(--space-m); + flex: 1; + } + + .card-actions { + box-sizing: border-box; + display: flex; + flex-direction: row; + gap: var(--space-m); + align-items: center; + justify-content: flex-start; + padding: var(--space-s) var(--space-m) var(--space-m) var(--space-m); + border-top: 1px solid var(--primary-500); + } + `; + + private _hasHeaderContent = false; + private _hasActionsContent = false; + + private _handleHeaderSlotChange = (e: Event) => { + const slot = e.target as HTMLSlotElement; + this._hasHeaderContent = slot.assignedElements().length > 0; + this.requestUpdate(); + }; + + private _handleActionsSlotChange = (e: Event) => { + const slot = e.target as HTMLSlotElement; + this._hasActionsContent = slot.assignedElements().length > 0; + this.requestUpdate(); + }; + + render() { + const tag = literal`div`; + + return html`<${tag} + ${spread(this.undeclaredAttributes)} + class="${classNames(['card'])}" + part="base" + > + ${this._hasHeaderContent ? html` +
+ +
+ ` : html` + + `} + +
+ +
+ + ${this._hasActionsContent ? html` +
+ +
+ ` : html` + + `} + `; + } +} + +export { CZCard }; \ No newline at end of file diff --git a/packages/design-system/src/lib/card/card.stories.ts b/packages/design-system/src/lib/card/card.stories.ts new file mode 100644 index 0000000..2ac3c12 --- /dev/null +++ b/packages/design-system/src/lib/card/card.stories.ts @@ -0,0 +1,44 @@ +import { html } from 'lit'; +import './card.js'; +import '../button/button.js'; +import '../heading/heading.js'; + +export default { + title: 'Components/Card', + component: 'cz-card', + tags: ['autodocs'], +}; + +export const Default = () => html` + + Medium heading +

This is the card body content. It can contain any HTML elements.

+
+ Button label + Button label +
+
+`; + +export const NoHeader = () => html` + +

This card has no header, just body content and actions.

+
+ Button label + Button label +
+
+`; + +export const NoActions = () => html` + + Medium heading +

This card has no actions section, just header and body content.

+
+`; + +export const BodyOnly = () => html` + +

This card contains only body content with no header or actions.

+
+`; \ No newline at end of file diff --git a/packages/design-system/src/lib/card/card.ts b/packages/design-system/src/lib/card/card.ts new file mode 100644 index 0000000..7a97b57 --- /dev/null +++ b/packages/design-system/src/lib/card/card.ts @@ -0,0 +1,11 @@ +import { CZCard } from './card.component.ts'; + +export { CZCard }; + +CZCard.define('cz-card'); + +declare global { + interface HTMLElementTagNameMap { + 'cz-card': CZCard; + } +} diff --git a/packages/design-system/src/lib/card/index.ts b/packages/design-system/src/lib/card/index.ts new file mode 100644 index 0000000..d89699b --- /dev/null +++ b/packages/design-system/src/lib/card/index.ts @@ -0,0 +1 @@ +export * from './card.component.ts'; diff --git a/packages/design-system/src/lib/heading/heading.component.ts b/packages/design-system/src/lib/heading/heading.component.ts new file mode 100644 index 0000000..1a4b362 --- /dev/null +++ b/packages/design-system/src/lib/heading/heading.component.ts @@ -0,0 +1,62 @@ +import { css, CSSResultGroup } from 'lit'; +import { html, unsafeStatic } from 'lit/static-html.js'; +import { property } from 'lit/decorators.js'; +import { spread } from '@open-wc/lit-helpers'; +import classNames from 'classnames'; +import { CraftzingElement } from 'craftzing-design-system-test-core'; +import { HeadingSize, HeadingLevel, HeadingProps } from './heading.types.js'; + +class CZHeading extends CraftzingElement implements HeadingProps { + static styles: CSSResultGroup = css` + :host { + display: block; + box-sizing: border-box; + padding: 0.125rem 0; + } + + .heading { + font-family: 'Inter', sans-serif; + font-weight: 700; + font-style: normal; + line-height: 1.4; + color: var(--neutral-700, #4b4855); + margin: 0; + white-space: nowrap; + } + + .heading--large { + font-size: 2rem; + } + + .heading--medium { + font-size: 1.75rem; + } + + .heading--small { + font-size: 1.5rem; + } + `; + + @property({ reflect: true }) + size: HeadingSize = 'large'; + + @property({ attribute: 'heading-tag', reflect: true }) + headingTag: HeadingLevel = 'h1'; + + render() { + const tag = unsafeStatic(this.headingTag); + + return html`<${tag} + ${spread(this.undeclaredAttributes)} + class="${classNames([ + 'heading', + `heading--${this.size}`, + ])}" + part="base" + > + + `; + } +} + +export { CZHeading }; \ No newline at end of file diff --git a/packages/design-system/src/lib/heading/heading.stories.ts b/packages/design-system/src/lib/heading/heading.stories.ts new file mode 100644 index 0000000..6c6531d --- /dev/null +++ b/packages/design-system/src/lib/heading/heading.stories.ts @@ -0,0 +1,70 @@ +import { CZHeading } from './heading.component.js'; + +CZHeading.define('cz-heading'); + +export default { + title: 'Components/Heading', + component: 'cz-heading', + argTypes: { + size: { + control: { type: 'select' }, + options: ['large', 'medium', 'small'], + }, + headingTag: { + control: { type: 'select' }, + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + }, + }, + parameters: { + docs: { + description: { + component: 'A heading component with adjustable visual size and semantic HTML tag level.', + }, + }, + }, +}; + +export const Default = { + args: { + size: 'large', + headingTag: 'h1', + }, + render: (args) => ` + + Large heading + + `, +}; + +export const Sizes = { + render: () => ` +
+ Large heading + Medium heading + Small heading +
+ `, +}; + +export const SemanticLevels = { + render: () => ` +
+ H1 with large size + H2 with medium size + H3 with small size + H4 with large size + H5 with medium size + H6 with small size +
+ `, +}; + +export const IndependentSizeAndTag = { + render: () => ` +
+ H1 with small visual size + H6 with large visual size + H2 with medium visual size +
+ `, +}; \ No newline at end of file diff --git a/packages/design-system/src/lib/heading/heading.ts b/packages/design-system/src/lib/heading/heading.ts new file mode 100644 index 0000000..4c5d117 --- /dev/null +++ b/packages/design-system/src/lib/heading/heading.ts @@ -0,0 +1,11 @@ +import { CZHeading } from './heading.component.ts'; + +export { CZHeading }; + +CZHeading.define('cz-heading'); + +declare global { + interface HTMLElementTagNameMap { + 'cz-heading': CZHeading; + } +} diff --git a/packages/design-system/src/lib/heading/heading.types.ts b/packages/design-system/src/lib/heading/heading.types.ts new file mode 100644 index 0000000..32c1826 --- /dev/null +++ b/packages/design-system/src/lib/heading/heading.types.ts @@ -0,0 +1,15 @@ +export type HeadingSize = 'large' | 'medium' | 'small'; + +export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; + +export interface HeadingProps { + /** + * Visual size of the heading + */ + size?: HeadingSize; + + /** + * Semantic HTML tag level + */ + headingTag?: HeadingLevel; +} \ No newline at end of file diff --git a/packages/design-system/src/lib/heading/index.ts b/packages/design-system/src/lib/heading/index.ts new file mode 100644 index 0000000..1530078 --- /dev/null +++ b/packages/design-system/src/lib/heading/index.ts @@ -0,0 +1,2 @@ +export * from './heading.ts'; +export type { HeadingProps } from './heading.types.ts'; diff --git a/packages/design-system/src/lib/label/index.ts b/packages/design-system/src/lib/label/index.ts new file mode 100644 index 0000000..10b3d00 --- /dev/null +++ b/packages/design-system/src/lib/label/index.ts @@ -0,0 +1,2 @@ +export * from './label.component.ts'; +export type { CZLabelProps } from './label.types.ts'; diff --git a/packages/design-system/src/lib/label/label.component.ts b/packages/design-system/src/lib/label/label.component.ts new file mode 100644 index 0000000..fe8bc54 --- /dev/null +++ b/packages/design-system/src/lib/label/label.component.ts @@ -0,0 +1,57 @@ +import { css, CSSResultGroup } from 'lit'; +import { html, literal } from 'lit/static-html.js'; +import { property } from 'lit/decorators.js'; +import { spread } from '@open-wc/lit-helpers'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { CZLabelProps } from './label.types.ts'; + +import { CraftzingElement } from 'craftzing-design-system-test-core'; + +class CZLabel extends CraftzingElement implements CZLabelProps { + static styles: CSSResultGroup = css` + .label { + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: flex-start; + + padding: var(--space-xxxs) 0; + + font-family: sans-serif; + font-weight: var(--semibold); + font-size: var(--size-m); + line-height: var(--line-height-140); + text-align: left; + white-space: nowrap; + + color: var(--neutral-700); + + &:where([disabled], [aria-disabled='true']) { + opacity: 0.5; + cursor: default; + } + } + `; + + @property({ type: String, reflect: true }) + for: string = undefined; + + @property({ type: Boolean, reflect: true }) + disabled = false; + + render() { + const tag = literal`label`; + + return html`<${tag} + ${spread(this.undeclaredAttributes)} + class="label" + for="${ifDefined(this.for)}" + ?disabled="${this.disabled}" + part="base" + > + + `; + } +} + +export { CZLabel }; \ No newline at end of file diff --git a/packages/design-system/src/lib/label/label.stories.ts b/packages/design-system/src/lib/label/label.stories.ts new file mode 100644 index 0000000..25df705 --- /dev/null +++ b/packages/design-system/src/lib/label/label.stories.ts @@ -0,0 +1,48 @@ +import { html } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; + +import { CZLabel } from './label.component.js'; +import { CZLabelProps } from './label.types.js'; + +CZLabel.define('cz-label'); + +export default { + title: "Components/Label", + component: "cz-label", + tags: ["autodocs"], + render: ({ for: htmlFor, disabled }: CZLabelProps) => html` + Label + `, + argTypes: { + for: { + control: { type: 'text' }, + }, + disabled: { + control: { type: 'boolean' }, + }, + }, +}; + +export const Default = { + args: { + for: undefined, + disabled: false, + }, +}; + +export const WithFor = { + args: { + for: 'input-id', + disabled: false, + }, +}; + +export const Disabled = { + args: { + for: undefined, + disabled: true, + }, +}; \ No newline at end of file diff --git a/packages/design-system/src/lib/label/label.test.ts b/packages/design-system/src/lib/label/label.test.ts new file mode 100644 index 0000000..0a385ca --- /dev/null +++ b/packages/design-system/src/lib/label/label.test.ts @@ -0,0 +1,36 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { CZLabel } from './label.ts'; + +describe('CZLabel', () => { + it('should render with default properties', async () => { + const el = await fixture(html`Label text`); + + expect(el.for).to.equal(undefined); + expect(el.disabled).to.equal(false); + }); + + it('should render with for attribute', async () => { + const el = await fixture(html`Label text`); + + expect(el.for).to.equal('input-id'); + }); + + it('should render as disabled', async () => { + const el = await fixture(html`Label text`); + + expect(el.disabled).to.equal(true); + }); + + it('should render slot content', async () => { + const el = await fixture(html`Test Content`); + + expect(el.textContent?.trim()).to.equal('Test Content'); + }); + + it('should render as label element', async () => { + const el = await fixture(html`Label text`); + + const labelElement = el.shadowRoot?.querySelector('label'); + expect(labelElement).to.exist; + }); +}); \ No newline at end of file diff --git a/packages/design-system/src/lib/label/label.ts b/packages/design-system/src/lib/label/label.ts new file mode 100644 index 0000000..9efd0e5 --- /dev/null +++ b/packages/design-system/src/lib/label/label.ts @@ -0,0 +1,11 @@ +import { CZLabel } from './label.component.ts'; + +export { CZLabel }; + +CZLabel.define('cz-label'); + +declare global { + interface HTMLElementTagNameMap { + 'cz-label': CZLabel; + } +} diff --git a/packages/design-system/src/lib/label/label.types.ts b/packages/design-system/src/lib/label/label.types.ts new file mode 100644 index 0000000..3f578dc --- /dev/null +++ b/packages/design-system/src/lib/label/label.types.ts @@ -0,0 +1,4 @@ +export type CZLabelProps = { + for?: string; + disabled?: boolean; +} \ No newline at end of file diff --git a/packages/design-system/src/lib/pill/index.ts b/packages/design-system/src/lib/pill/index.ts new file mode 100644 index 0000000..8101481 --- /dev/null +++ b/packages/design-system/src/lib/pill/index.ts @@ -0,0 +1,2 @@ +export * from './pill.component.ts'; +export type { CZPillProps, CZPillVariant, CZPillSize } from './pill.types.ts'; diff --git a/packages/design-system/src/lib/pill/pill.component.ts b/packages/design-system/src/lib/pill/pill.component.ts new file mode 100644 index 0000000..1c5bab5 --- /dev/null +++ b/packages/design-system/src/lib/pill/pill.component.ts @@ -0,0 +1,101 @@ +import { css, CSSResultGroup } from 'lit'; +import { html, literal } from 'lit/static-html.js'; +import { property } from 'lit/decorators.js'; +import { spread } from '@open-wc/lit-helpers'; +import classNames from 'classnames'; +import { + CZPillProps, + CZPillVariant, + CZPillSize, +} from './pill.types.ts'; + +import { CraftzingElement } from 'craftzing-design-system-test-core'; + +class CZPill extends CraftzingElement implements CZPillProps { + static styles: CSSResultGroup = css` + .pill { + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + + border-radius: var(--space-s); + padding: var(--space-xxxs) var(--space-xxs); + + font-family: sans-serif; + font-weight: var(--semibold); + font-size: var(--size-m); + line-height: var(--line-height-140); + text-align: center; + white-space: nowrap; + + transition: all 0.2s ease-in-out 0s; + + &:where([disabled], [aria-disabled='true']) { + cursor: default; + pointer-events: none; + opacity: 0.5; + } + } + + .pill--variant-default { + background-color: var(--primary-600); + color: var(--typography-white); + + &:where([disabled], [aria-disabled='true']) { + background-color: var(--neutral-200); + color: var(--typography-white); + } + } + + .pill--variant-error { + background-color: var(--error-600); + color: var(--typography-white); + + &:where([disabled], [aria-disabled='true']) { + background-color: var(--neutral-200); + color: var(--typography-white); + } + } + + .pill--size-large { + padding: var(--space-xxxs) var(--space-xxs); + font-size: var(--size-m); + } + + .pill--size-medium { + padding: var(--space-xxxs) var(--space-xxs); + font-size: var(--size-s); + } + `; + + @property({ reflect: true }) + variant: CZPillVariant = 'default'; + + @property({ type: String, reflect: true }) + size: CZPillSize = 'large'; + + @property({ type: Boolean, reflect: true }) + disabled = false; + + render() { + const tag = literal`div`; + + return html`<${tag} + ${spread(this.undeclaredAttributes)} + class="${classNames([ + 'pill', + `pill--variant-${this.variant}`, + `pill--size-${this.size}`, + ])}" + ?disabled="${this.disabled}" + part="base" + > + + + + `; + } +} + +export { CZPill }; \ No newline at end of file diff --git a/packages/design-system/src/lib/pill/pill.stories.ts b/packages/design-system/src/lib/pill/pill.stories.ts new file mode 100644 index 0000000..b2d4c1c --- /dev/null +++ b/packages/design-system/src/lib/pill/pill.stories.ts @@ -0,0 +1,88 @@ +import { html } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { Meta, StoryObj } from '@storybook/web-components'; +import { CZPill } from './pill.component.ts'; +import { CZPillProps } from './pill.types.ts'; + +CZPill.define('cz-pill'); + +const meta: Meta = { + title: 'Components/Pill', + component: 'cz-pill', + render: ({ variant, size, disabled }: CZPillProps) => html` + Pill content + `, + argTypes: { + variant: { + control: 'select', + options: ['default', 'error'], + }, + size: { + control: 'select', + options: ['large', 'medium'], + }, + disabled: { + control: 'boolean', + }, + }, + args: { + variant: 'default', + size: 'large', + disabled: false, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + variant: 'default', + size: 'large', + disabled: false, + }, +}; + +export const Error: Story = { + args: { + variant: 'error', + size: 'large', + disabled: false, + }, +}; + +export const Medium: Story = { + args: { + variant: 'default', + size: 'medium', + disabled: false, + }, +}; + +export const ErrorMedium: Story = { + args: { + variant: 'error', + size: 'medium', + disabled: false, + }, +}; + +export const Disabled: Story = { + args: { + variant: 'default', + size: 'large', + disabled: true, + }, +}; + +export const DisabledMedium: Story = { + args: { + variant: 'default', + size: 'medium', + disabled: true, + }, +}; \ No newline at end of file diff --git a/packages/design-system/src/lib/pill/pill.test.ts b/packages/design-system/src/lib/pill/pill.test.ts new file mode 100644 index 0000000..4c3ad4a --- /dev/null +++ b/packages/design-system/src/lib/pill/pill.test.ts @@ -0,0 +1,36 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { CZPill } from './pill.ts'; + +describe('CZPill', () => { + it('should render with default properties', async () => { + const el = await fixture(html`Pill content`); + + expect(el.variant).to.equal('default'); + expect(el.size).to.equal('large'); + expect(el.disabled).to.equal(false); + }); + + it('should render with custom variant', async () => { + const el = await fixture(html`Pill content`); + + expect(el.variant).to.equal('default'); + }); + + it('should render with custom size', async () => { + const el = await fixture(html`Pill content`); + + expect(el.size).to.equal('large'); + }); + + it('should render as disabled', async () => { + const el = await fixture(html`Pill content`); + + expect(el.disabled).to.equal(true); + }); + + it('should render slot content', async () => { + const el = await fixture(html`Test Content`); + + expect(el.textContent?.trim()).to.equal('Test Content'); + }); +}); \ No newline at end of file diff --git a/packages/design-system/src/lib/pill/pill.ts b/packages/design-system/src/lib/pill/pill.ts new file mode 100644 index 0000000..c05f30c --- /dev/null +++ b/packages/design-system/src/lib/pill/pill.ts @@ -0,0 +1,11 @@ +import { CZPill } from './pill.component.ts'; + +export { CZPill }; + +CZPill.define('cz-pill'); + +declare global { + interface HTMLElementTagNameMap { + 'cz-pill': CZPill; + } +} diff --git a/packages/design-system/src/lib/pill/pill.types.ts b/packages/design-system/src/lib/pill/pill.types.ts new file mode 100644 index 0000000..d4e73f3 --- /dev/null +++ b/packages/design-system/src/lib/pill/pill.types.ts @@ -0,0 +1,7 @@ +export type CZPillVariant = 'default' | 'error'; +export type CZPillSize = 'large' | 'medium'; +export type CZPillProps = { + variant?: CZPillVariant; + size?: CZPillSize; + disabled?: boolean; +} \ No newline at end of file diff --git a/packages/design-system/src/lib/text-input/index.ts b/packages/design-system/src/lib/text-input/index.ts new file mode 100644 index 0000000..db649a4 --- /dev/null +++ b/packages/design-system/src/lib/text-input/index.ts @@ -0,0 +1,6 @@ +export * from './text-input.component.ts'; +export type { + CZTextInputProps, + CZTextInputState, + CZTextInputType, +} from './text-input.types.ts'; diff --git a/packages/design-system/src/lib/text-input/text-input.component.ts b/packages/design-system/src/lib/text-input/text-input.component.ts new file mode 100644 index 0000000..d317ba2 --- /dev/null +++ b/packages/design-system/src/lib/text-input/text-input.component.ts @@ -0,0 +1,226 @@ +import { css, CSSResultGroup } from 'lit'; +import { html } from 'lit/static-html.js'; +import { property, state } from 'lit/decorators.js'; +import { spread } from '@open-wc/lit-helpers'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import classNames from 'classnames'; +import { + CZTextInputProps, + CZTextInputState, + CZTextInputType, +} from './text-input.types.ts'; + +import { CraftzingElement } from 'craftzing-design-system-test-core'; + +class CZTextInput extends CraftzingElement implements CZTextInputProps { + static styles: CSSResultGroup = css` + .text-input { + position: relative; + display: inline-block; + width: 100%; + border-radius: var(--space-xxxs); + } + + .text-input__field { + box-sizing: border-box; + width: 100%; + padding: var(--space-xxxs) var(--space-xxs); + + font-family: sans-serif; + font-weight: var(--regular); + font-size: var(--size-m); + line-height: var(--line-height-140); + + color: var(--typography-black); + background-color: transparent; + border: 1px solid var(--neutral-600); + border-radius: var(--space-xxxs); + + outline: none; + transition: all 0.2s ease-in-out; + + &::placeholder { + color: var(--neutral-500); + } + + &:hover { + border-color: var(--primary-600); + } + + &:focus { + border-color: var(--primary-600); + outline: none; + } + + &:focus-visible { + border-color: var(--primary-600); + outline: 3px solid var(--neutral-600); + outline-offset: 1px; + } + + &:where([disabled]) { + cursor: not-allowed; + opacity: 0.5; + background-color: var(--neutral-100); + } + + &:where([readonly]) { + cursor: default; + background-color: transparent; + border: none; + border-left: 1px solid var(--primary-600); + border-radius: 0; + + &:focus, + &:focus-visible { + border: none; + border-left: 1px solid var(--primary-600); + outline: none; + } + } + } + + .text-input--state-error .text-input__field { + border-color: var(--error-600); + background-color: var(--error-50); + + &:hover, + &:focus { + border-color: var(--error-600); + box-shadow: 0 0 0 1px var(--error-600); + } + } + `; + + @property({ type: String, reflect: true }) + type: CZTextInputType = 'text'; + + @property({ type: String, reflect: true }) + state: CZTextInputState = 'default'; + + @property({ type: String }) + value = ''; + + @property({ type: String }) + placeholder = ''; + + @property({ type: Boolean, reflect: true }) + disabled = false; + + @property({ type: Boolean, reflect: true }) + readonly = false; + + @property({ type: Boolean, reflect: true }) + required = false; + + @property({ type: String, reflect: true }) + id: string = undefined; + + @property({ type: String }) + name: string = undefined; + + @property({ type: String }) + autocomplete: string = undefined; + + @property({ type: Number }) + maxlength: number = undefined; + + @property({ type: Number }) + minlength: number = undefined; + + @property({ type: String }) + pattern: string = undefined; + + @property({ type: String, attribute: 'aria-describedby' }) + ariaDescribedBy: string = undefined; + + @property({ type: Boolean, attribute: 'aria-invalid' }) + ariaInvalidProp = false; + + @property({ type: Boolean, attribute: 'aria-required' }) + ariaRequiredProp = false; + + @state() + private _hasFocus = false; + + private _handleInput(event: Event) { + const target = event.target as HTMLInputElement; + this.value = target.value; + + // Dispatch custom input event + this.dispatchEvent(new CustomEvent('input', { + detail: { value: this.value }, + bubbles: true, + composed: true + })); + } + + private _handleChange(event: Event) { + const target = event.target as HTMLInputElement; + this.value = target.value; + + // Dispatch custom change event + this.dispatchEvent(new CustomEvent('change', { + detail: { value: this.value }, + bubbles: true, + composed: true + })); + } + + private _handleFocus() { + this._hasFocus = true; + this.dispatchEvent(new CustomEvent('focus', { + bubbles: true, + composed: true + })); + } + + private _handleBlur() { + this._hasFocus = false; + this.dispatchEvent(new CustomEvent('blur', { + bubbles: true, + composed: true + })); + } + + render() { + const computedAriaInvalid = this.ariaInvalidProp || this.state === 'error'; + const computedAriaRequired = this.ariaRequiredProp || this.required; + + return html` +
+ +
+ `; + } +} + +export { CZTextInput }; \ No newline at end of file diff --git a/packages/design-system/src/lib/text-input/text-input.stories.ts b/packages/design-system/src/lib/text-input/text-input.stories.ts new file mode 100644 index 0000000..d0bf54e --- /dev/null +++ b/packages/design-system/src/lib/text-input/text-input.stories.ts @@ -0,0 +1,165 @@ +import { html } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; + +import { CZTextInput } from './text-input.component.js'; +import { CZTextInputProps } from './text-input.types.js'; +import { CZLabel } from '../label/label.component.js'; + +CZTextInput.define('cz-text-input'); +CZLabel.define('cz-label'); + +export default { + title: "Components/TextInput", + component: "cz-text-input", + tags: ["autodocs"], + render: ({ type, state, value, placeholder, disabled, readonly, required, id, name }: CZTextInputProps) => html``, + argTypes: { + type: { + control: { type: 'select' }, + options: ['text', 'email', 'password', 'tel', 'url', 'search'], + }, + state: { + control: { type: 'select' }, + options: ['default', 'error'], + }, + value: { + control: { type: 'text' }, + }, + placeholder: { + control: { type: 'text' }, + }, + disabled: { + control: { type: 'boolean' }, + }, + readonly: { + control: { type: 'boolean' }, + }, + required: { + control: { type: 'boolean' }, + }, + id: { + control: { type: 'text' }, + }, + name: { + control: { type: 'text' }, + }, + }, +}; + +export const Default = { + args: { + type: 'text', + state: 'default', + placeholder: 'Enter text...', + disabled: false, + readonly: false, + required: false, + }, +}; + +export const WithValue = { + args: { + type: 'text', + state: 'default', + value: 'Text', + disabled: false, + readonly: false, + required: false, + }, +}; + +export const Error = { + args: { + type: 'text', + state: 'error', + placeholder: 'Placeholder', + disabled: false, + readonly: false, + required: false, + }, +}; + +export const Disabled = { + args: { + type: 'text', + state: 'default', + placeholder: 'Disabled input', + disabled: true, + readonly: false, + required: false, + }, +}; + +export const Readonly = { + args: { + type: 'text', + state: 'default', + value: 'Readonly value', + readonly: true, + disabled: false, + required: false, + }, +}; + +export const Email = { + args: { + type: 'email', + state: 'default', + placeholder: 'Enter email...', + disabled: false, + readonly: false, + required: false, + }, +}; + +export const Password = { + args: { + type: 'password', + state: 'default', + placeholder: 'Enter password...', + disabled: false, + readonly: false, + required: false, + }, +}; + +export const WithLabel = { + render: () => html` +
+ Email Address + +
+ `, +}; + +export const WithLabelAndError = { + render: () => html` +
+ Email Address + +
+ `, +}; + diff --git a/packages/design-system/src/lib/text-input/text-input.test.ts b/packages/design-system/src/lib/text-input/text-input.test.ts new file mode 100644 index 0000000..4e9ab06 --- /dev/null +++ b/packages/design-system/src/lib/text-input/text-input.test.ts @@ -0,0 +1,143 @@ +import { expect, fixture, html, aTimeout } from '@open-wc/testing'; +import { CZTextInput } from './text-input.ts'; + +describe('CZTextInput', () => { + it('should render with default properties', async () => { + const el = await fixture(html``); + + expect(el.type).to.equal('text'); + expect(el.state).to.equal('default'); + expect(el.value).to.equal(''); + expect(el.disabled).to.equal(false); + expect(el.readonly).to.equal(false); + expect(el.required).to.equal(false); + }); + + it('should render with custom properties', async () => { + const el = await fixture(html` + + `); + + expect(el.type).to.equal('email'); + expect(el.state).to.equal('error'); + expect(el.value).to.equal('test@example.com'); + expect(el.placeholder).to.equal('Enter email'); + expect(el.disabled).to.equal(true); + expect(el.required).to.equal(true); + expect(el.id).to.equal('test-input'); + expect(el.name).to.equal('email'); + }); + + it('should update value on input', async () => { + const el = await fixture(html``); + + const input = el.shadowRoot?.querySelector('input') as HTMLInputElement; + expect(input).to.exist; + + input.value = 'new value'; + input.dispatchEvent(new Event('input')); + + await aTimeout(0); + expect(el.value).to.equal('new value'); + }); + + it('should dispatch custom events', async () => { + const el = await fixture(html``); + + let inputEventFired = false; + let changeEventFired = false; + let focusEventFired = false; + let blurEventFired = false; + + el.addEventListener('input', () => { inputEventFired = true; }); + el.addEventListener('change', () => { changeEventFired = true; }); + el.addEventListener('focus', () => { focusEventFired = true; }); + el.addEventListener('blur', () => { blurEventFired = true; }); + + const input = el.shadowRoot?.querySelector('input') as HTMLInputElement; + + input.value = 'test'; + input.dispatchEvent(new Event('input')); + input.dispatchEvent(new Event('change')); + input.dispatchEvent(new Event('focus')); + input.dispatchEvent(new Event('blur')); + + await aTimeout(0); + + expect(inputEventFired).to.be.true; + expect(changeEventFired).to.be.true; + expect(focusEventFired).to.be.true; + expect(blurEventFired).to.be.true; + }); + + it('should set aria attributes correctly', async () => { + const el = await fixture(html` + + `); + + const input = el.shadowRoot?.querySelector('input') as HTMLInputElement; + + expect(input.getAttribute('aria-invalid')).to.equal('true'); + expect(input.getAttribute('aria-required')).to.equal('true'); + expect(input.getAttribute('aria-describedby')).to.equal('error-msg'); + }); + + it('should work with label for accessibility', async () => { + const container = await fixture(html` +
+ + +
+ `); + + const label = container.querySelector('label') as HTMLLabelElement; + const input = container.querySelector('cz-text-input') as CZTextInput; + const inputField = input.shadowRoot?.querySelector('input') as HTMLInputElement; + + expect(label.getAttribute('for')).to.equal('test-input'); + expect(inputField.getAttribute('id')).to.equal('test-input'); + + // Test that clicking label focuses input + let focusEventFired = false; + input.addEventListener('focus', () => { focusEventFired = true; }); + + label.click(); + await aTimeout(0); + + expect(focusEventFired).to.be.true; + }); + + it('should handle different input types', async () => { + const types = ['text', 'email', 'password', 'tel', 'url', 'search']; + + for (const type of types) { + const el = await fixture(html``); + const input = el.shadowRoot?.querySelector('input') as HTMLInputElement; + + expect(input.type).to.equal(type); + } + }); + + it('should apply error state correctly', async () => { + const el = await fixture(html``); + + const container = el.shadowRoot?.querySelector('.text-input') as HTMLElement; + const input = el.shadowRoot?.querySelector('input') as HTMLInputElement; + + expect(container.classList.contains('text-input--state-error')).to.be.true; + expect(input.getAttribute('aria-invalid')).to.equal('true'); + }); +}); \ No newline at end of file diff --git a/packages/design-system/src/lib/text-input/text-input.ts b/packages/design-system/src/lib/text-input/text-input.ts new file mode 100644 index 0000000..b63cf45 --- /dev/null +++ b/packages/design-system/src/lib/text-input/text-input.ts @@ -0,0 +1,11 @@ +import { CZTextInput } from './text-input.component.ts'; + +export { CZTextInput }; + +CZTextInput.define('cz-text-input'); + +declare global { + interface HTMLElementTagNameMap { + 'cz-text-input': CZTextInput; + } +} diff --git a/packages/design-system/src/lib/text-input/text-input.types.ts b/packages/design-system/src/lib/text-input/text-input.types.ts new file mode 100644 index 0000000..d4a4f21 --- /dev/null +++ b/packages/design-system/src/lib/text-input/text-input.types.ts @@ -0,0 +1,21 @@ +export type CZTextInputState = 'default' | 'error'; +export type CZTextInputType = 'text' | 'email' | 'password' | 'tel' | 'url' | 'search'; + +export type CZTextInputProps = { + type?: CZTextInputType; + state?: CZTextInputState; + value?: string; + placeholder?: string; + disabled?: boolean; + readonly?: boolean; + required?: boolean; + id?: string; + name?: string; + autocomplete?: string; + maxlength?: number; + minlength?: number; + pattern?: string; + 'aria-describedby'?: string; + 'aria-invalid'?: boolean; + 'aria-required'?: boolean; +} \ No newline at end of file diff --git a/packages/tokens/package.json b/packages/tokens/package.json index e52b38e..e3af88f 100644 --- a/packages/tokens/package.json +++ b/packages/tokens/package.json @@ -1,6 +1,6 @@ { "name": "craftzing-design-system-test-tokens", - "version": "0.6.0", + "version": "0.7.2", "private": true, "type": "module", "main": "./dist/index.js",