Initial commit

This commit is contained in:
2026-03-10 21:30:52 -03:00
commit 72a4f0be26
145 changed files with 14881 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
import type { StorybookConfig } from '@storybook/nextjs-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(ts|tsx)'],
addons: ['@storybook/addon-docs'],
framework: {
name: '@storybook/nextjs-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
};
export default config;

View File

@@ -0,0 +1,18 @@
import type { Preview } from '@storybook/react';
import '../src/tokens/tokens.css';
const preview: Preview = {
parameters: {
layout: 'centered',
backgrounds: {
default: 'page',
values: [
{ name: 'page', value: '#f8f9fa' },
{ name: 'surface', value: '#ffffff' },
{ name: 'dark', value: '#212529' },
],
},
},
};
export default preview;

View File

@@ -0,0 +1,3 @@
import { react } from '@dwellops/config/eslint';
export default react;

47
packages/ui/package.json Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "@dwellops/ui",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./tokens": "./src/tokens/tokens.css",
"./Button": {
"types": "./src/Button/Button.tsx",
"default": "./src/Button/Button.tsx"
},
"./Card": {
"types": "./src/Card/Card.tsx",
"default": "./src/Card/Card.tsx"
}
},
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint src",
"storybook": "storybook dev -p 6006",
"storybook:build": "storybook build"
},
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/node": "catalog:",
"storybook": "catalog:",
"@storybook/nextjs-vite": "catalog:",
"@storybook/addon-docs": "catalog:",
"@storybook/react": "catalog:",
"eslint": "catalog:",
"typescript-eslint": "catalog:",
"eslint-plugin-react": "catalog:",
"eslint-plugin-react-hooks": "catalog:",
"eslint-plugin-jsx-a11y": "catalog:",
"@dwellops/config": "workspace:*"
}
}

View File

@@ -0,0 +1,150 @@
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
border: var(--border-width-default) solid transparent;
border-radius: var(--border-radius-base);
font-family: var(--font-family-base);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-tight);
cursor: pointer;
text-decoration: none;
transition:
background-color var(--transition-fast),
border-color var(--transition-fast),
color var(--transition-fast),
box-shadow var(--transition-fast);
white-space: nowrap;
user-select: none;
&:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}
&:disabled,
&[aria-disabled='true'] {
cursor: not-allowed;
opacity: 0.5;
}
}
/* ── Variants ────────────────────────────────────────────────── */
.primary {
background-color: var(--color-interactive-default);
border-color: var(--color-interactive-default);
color: var(--color-text-inverse);
&:hover:not(:disabled):not([aria-disabled='true']) {
background-color: var(--color-interactive-hover);
border-color: var(--color-interactive-hover);
}
&:active:not(:disabled):not([aria-disabled='true']) {
background-color: var(--color-interactive-active);
border-color: var(--color-interactive-active);
}
}
.secondary {
background-color: var(--color-bg-surface);
border-color: var(--color-border-default);
color: var(--color-text-primary);
&:hover:not(:disabled):not([aria-disabled='true']) {
background-color: var(--color-bg-subtle);
}
}
.ghost {
background-color: transparent;
border-color: transparent;
color: var(--color-interactive-default);
&:hover:not(:disabled):not([aria-disabled='true']) {
background-color: var(--color-brand-50);
}
}
.danger {
background-color: var(--color-error-500);
border-color: var(--color-error-500);
color: var(--color-text-inverse);
&:hover:not(:disabled):not([aria-disabled='true']) {
background-color: var(--color-error-700);
border-color: var(--color-error-700);
}
}
/* ── Sizes ────────────────────────────────────────────────────── */
.sm {
font-size: var(--font-size-sm);
padding: var(--space-1) var(--space-3);
min-height: 2rem;
}
.md {
font-size: var(--font-size-base);
padding: var(--space-2) var(--space-4);
min-height: 2.5rem;
}
.lg {
font-size: var(--font-size-lg);
padding: var(--space-3) var(--space-6);
min-height: 3rem;
}
/* ── Modifiers ────────────────────────────────────────────────── */
.fullWidth {
width: 100%;
}
.loading {
position: relative;
& .label {
opacity: 0;
}
}
/* ── Sub-elements ─────────────────────────────────────────────── */
.icon {
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
.label {
display: inline-flex;
align-items: center;
}
.spinner {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.spinnerDot {
width: 1em;
height: 1em;
border: 2px solid currentcolor;
border-top-color: transparent;
border-radius: var(--border-radius-full);
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,73 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Primitives/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'ghost', 'danger'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
loading: { control: 'boolean' },
disabled: { control: 'boolean' },
fullWidth: { control: 'boolean' },
},
parameters: {
docs: {
description: {
component: `
Core interactive button primitive. Follows WCAG 2.2 AA requirements:
visible focus ring, aria-disabled, aria-busy states.
`,
},
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: { variant: 'primary', children: 'Save changes' },
};
export const Secondary: Story = {
args: { variant: 'secondary', children: 'Cancel' },
};
export const Ghost: Story = {
args: { variant: 'ghost', children: 'Learn more' },
};
export const Danger: Story = {
args: { variant: 'danger', children: 'Delete record' },
};
export const Small: Story = {
args: { size: 'sm', children: 'Small button' },
};
export const Large: Story = {
args: { size: 'lg', children: 'Large button' },
};
export const Loading: Story = {
args: { loading: true, children: 'Saving…' },
};
export const Disabled: Story = {
args: { disabled: true, children: 'Not available' },
};
export const FullWidth: Story = {
args: { fullWidth: true, children: 'Full-width action' },
parameters: {
layout: 'padded',
},
};

View File

@@ -0,0 +1,87 @@
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import styles from './Button.module.css';
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
export type ButtonSize = 'sm' | 'md' | 'lg';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Visual variant of the button. */
variant?: ButtonVariant;
/** Size of the button. */
size?: ButtonSize;
/** Renders a full-width block button. */
fullWidth?: boolean;
/** Shows a loading spinner and disables interaction. */
loading?: boolean;
/** Icon placed before the label. */
leadingIcon?: ReactNode;
/** Icon placed after the label. */
trailingIcon?: ReactNode;
children: ReactNode;
}
/**
* Core interactive button primitive.
*
* All interactive affordances follow WCAG 2.2 AA:
* - Minimum 44 × 44 px touch target (sm/md/lg).
* - Visible focus ring via `--focus-ring` token.
* - `aria-disabled` and `aria-busy` set appropriately.
*
* @example
* ```tsx
* <Button variant="primary" onClick={handleSave}>Save changes</Button>
* ```
*/
export function Button({
variant = 'primary',
size = 'md',
fullWidth = false,
loading = false,
leadingIcon,
trailingIcon,
disabled,
children,
className,
...rest
}: ButtonProps) {
const isDisabled = disabled || loading;
return (
<button
{...rest}
disabled={isDisabled}
aria-disabled={isDisabled}
aria-busy={loading}
className={[
styles.button,
styles[variant],
styles[size],
fullWidth ? styles.fullWidth : '',
loading ? styles.loading : '',
className ?? '',
]
.filter(Boolean)
.join(' ')}
>
{leadingIcon && (
<span className={styles.icon} aria-hidden="true">
{leadingIcon}
</span>
)}
<span className={styles.label}>{children}</span>
{trailingIcon && (
<span className={styles.icon} aria-hidden="true">
{trailingIcon}
</span>
)}
{loading && (
<span className={styles.spinner} role="status" aria-label="Loading">
<span className={styles.spinnerDot} />
</span>
)}
</button>
);
}
export default Button;

View File

@@ -0,0 +1,44 @@
.card {
background-color: var(--color-bg-surface);
border: var(--border-width-default) solid var(--color-border-default);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-sm);
overflow: hidden;
}
.header {
padding: var(--space-4) var(--space-6);
border-block-end: var(--border-width-default) solid var(--color-border-subtle);
}
.title {
margin: 0;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
line-height: var(--line-height-tight);
}
.description {
margin: var(--space-1) 0 0;
font-size: var(--font-size-sm);
color: var(--color-text-muted);
line-height: var(--line-height-normal);
}
.body {
padding: var(--space-6);
}
.noPadding {
padding: 0;
}
.footer {
padding: var(--space-4) var(--space-6);
border-block-start: var(--border-width-default) solid var(--color-border-subtle);
background-color: var(--color-bg-subtle);
display: flex;
align-items: center;
gap: var(--space-3);
}

View File

@@ -0,0 +1,65 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from '../Button/Button';
import { Card } from './Card';
const meta: Meta<typeof Card> = {
title: 'Primitives/Card',
component: Card,
tags: ['autodocs'],
parameters: {
layout: 'padded',
docs: {
description: {
component: 'Surface container for grouped content.',
},
},
},
};
export default meta;
type Story = StoryObj<typeof Card>;
export const Default: Story = {
args: {
title: 'Unit 101',
description: 'Sunrise Ridge HOA',
children: 'Resident information and details would appear here.',
},
};
export const WithFooter: Story = {
args: {
title: 'Pending dues',
description: 'Q1 2026 assessment',
children: 'Outstanding balance: $350.00',
footer: (
<>
<Button variant="primary" size="sm">
Pay now
</Button>
<Button variant="ghost" size="sm">
View statement
</Button>
</>
),
},
};
export const NoPadding: Story = {
args: {
title: 'Document list',
noPadding: true,
children: (
<ul style={{ margin: 0, padding: '1rem 1.5rem', listStyle: 'none' }}>
<li>HOA Bylaws 2025.pdf</li>
<li>Meeting Minutes Q4 2025.pdf</li>
</ul>
),
},
};
export const ContentOnly: Story = {
args: {
children: 'A simple card with no title or footer.',
},
};

View File

@@ -0,0 +1,60 @@
import type { HTMLAttributes, ReactNode } from 'react';
import styles from './Card.module.css';
export interface CardProps extends HTMLAttributes<HTMLElement> {
/** Card heading text. */
title?: string;
/** Optional description or metadata shown below the title. */
description?: string;
/** Actions rendered in the card footer (e.g. buttons). */
footer?: ReactNode;
/** Removes the default padding from the content area. */
noPadding?: boolean;
children?: ReactNode;
}
/**
* Surface container for grouped content.
*
* Uses a `<article>` element by default for semantic grouping.
* Override with `as` if a different element is needed.
*
* @example
* ```tsx
* <Card title="Unit 101" description="Sunrise Ridge HOA">
* <p>Resident details here.</p>
* </Card>
* ```
*/
export function Card({
title,
description,
footer,
noPadding = false,
children,
className,
...rest
}: CardProps) {
return (
<article {...rest} className={[styles.card, className ?? ''].filter(Boolean).join(' ')}>
{(title || description) && (
<header className={styles.header}>
{title && <h3 className={styles.title}>{title}</h3>}
{description && <p className={styles.description}>{description}</p>}
</header>
)}
<div
className={[styles.body, noPadding ? styles.noPadding : '']
.filter(Boolean)
.join(' ')}
>
{children}
</div>
{footer && <footer className={styles.footer}>{footer}</footer>}
</article>
);
}
export default Card;

5
packages/ui/src/css.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/** TypeScript declarations for CSS Module files. */
declare module '*.module.css' {
const styles: Record<string, string>;
export default styles;
}

5
packages/ui/src/index.ts Normal file
View File

@@ -0,0 +1,5 @@
export { Button } from './Button/Button';
export type { ButtonProps, ButtonVariant, ButtonSize } from './Button/Button';
export { Card } from './Card/Card';
export type { CardProps } from './Card/Card';

View File

@@ -0,0 +1,140 @@
/**
* Design token definitions for dwellops-platform.
* All values are expressed as CSS custom properties.
* Import this file once at the app root, then reference tokens throughout.
*/
:root {
/* ── Color: Primitives ───────────────────────────────────────── */
--color-neutral-0: #ffffff;
--color-neutral-50: #f8f9fa;
--color-neutral-100: #f1f3f5;
--color-neutral-200: #e9ecef;
--color-neutral-300: #dee2e6;
--color-neutral-400: #ced4da;
--color-neutral-500: #adb5bd;
--color-neutral-600: #6c757d;
--color-neutral-700: #495057;
--color-neutral-800: #343a40;
--color-neutral-900: #212529;
--color-neutral-1000: #000000;
--color-brand-50: #e8f4fd;
--color-brand-100: #bee3f8;
--color-brand-200: #90cdf4;
--color-brand-300: #63b3ed;
--color-brand-400: #4299e1;
--color-brand-500: #3182ce;
--color-brand-600: #2b6cb0;
--color-brand-700: #2c5282;
--color-brand-800: #2a4365;
--color-brand-900: #1a365d;
--color-success-50: #f0fdf4;
--color-success-500: #22c55e;
--color-success-700: #15803d;
--color-warning-50: #fffbeb;
--color-warning-500: #f59e0b;
--color-warning-700: #b45309;
--color-error-50: #fef2f2;
--color-error-500: #ef4444;
--color-error-700: #b91c1c;
/* ── Color: Semantic ─────────────────────────────────────────── */
--color-bg-page: var(--color-neutral-50);
--color-bg-surface: var(--color-neutral-0);
--color-bg-subtle: var(--color-neutral-100);
--color-bg-muted: var(--color-neutral-200);
--color-text-primary: var(--color-neutral-900);
--color-text-secondary: var(--color-neutral-700);
--color-text-muted: var(--color-neutral-600);
--color-text-inverse: var(--color-neutral-0);
--color-border-default: var(--color-neutral-300);
--color-border-subtle: var(--color-neutral-200);
--color-border-strong: var(--color-neutral-500);
--color-interactive-default: var(--color-brand-600);
--color-interactive-hover: var(--color-brand-700);
--color-interactive-active: var(--color-brand-800);
--color-interactive-focus: var(--color-brand-500);
--color-interactive-disabled: var(--color-neutral-400);
/* ── Spacing ─────────────────────────────────────────────────── */
--space-0: 0;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
--space-16: 4rem;
--space-20: 5rem;
--space-24: 6rem;
/* ── Typography ──────────────────────────────────────────────── */
--font-family-base:
system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-family-mono: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-size-2xl: 1.5rem;
--font-size-3xl: 1.875rem;
--font-size-4xl: 2.25rem;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--line-height-tight: 1.25;
--line-height-snug: 1.375;
--line-height-normal: 1.5;
--line-height-relaxed: 1.625;
/* ── Border ──────────────────────────────────────────────────── */
--border-width-default: 1px;
--border-width-medium: 2px;
--border-radius-sm: 0.25rem;
--border-radius-base: 0.375rem;
--border-radius-md: 0.5rem;
--border-radius-lg: 0.75rem;
--border-radius-xl: 1rem;
--border-radius-full: 9999px;
/* ── Shadow ──────────────────────────────────────────────────── */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-base: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
/* ── Transition ──────────────────────────────────────────────── */
--transition-fast: 100ms ease-in-out;
--transition-base: 200ms ease-in-out;
--transition-slow: 300ms ease-in-out;
/* ── Focus ring ──────────────────────────────────────────────── */
--focus-ring: 0 0 0 3px var(--color-brand-300);
/* ── Z-index ─────────────────────────────────────────────────── */
--z-below: -1;
--z-base: 0;
--z-raised: 10;
--z-dropdown: 100;
--z-sticky: 200;
--z-overlay: 300;
--z-modal: 400;
--z-toast: 500;
--z-tooltip: 600;
}

View File

@@ -0,0 +1,9 @@
{
"extends": "@dwellops/config/tsconfig/react-library.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"noEmit": true
},
"include": ["src"]
}

File diff suppressed because one or more lines are too long