Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,34 @@ export { fooVariants };
- Animations use `cf-animate-*` utility classes or `var(--cf-duration-*)` / `var(--cf-ease-*)` tokens
- Light/dark mode via `[data-theme="light"]` / `[data-theme="dark"]` selectors in `styles/index.css`

## Mobile-state stories — `MobileBaseline` contract

Every **interactive primitive** (Button, Input, NavDropdown, DataTable, Sheet,
Tabs, Switch, Select, anything users tap/type/drag) **MUST** export a story
named `MobileBaseline` that renders the component at the canonical mobile
viewport (375 px, iPhone SE). This makes the mobile-first contract visible
at story-review time, not after a real device exposes the regression.

```ts
import { VIEWPORT_MOBILE } from '../_shared/viewports';

export const MobileBaseline: Story = {
parameters: { viewport: VIEWPORT_MOBILE },
// …render the component — verify ≥ 44×44 px tap targets + ≥ 16 px input
// font-size + clamp() widths.
};
```

Shared viewport constants live in `src/stories/_shared/viewports.ts`
(`VIEWPORT_MOBILE`, `VIEWPORT_MOBILE_LANDSCAPE`, `VIEWPORT_TABLET`,
`VIEWPORT_DESKTOP`). Never redeclare these constants per-story — import them.

**Tier-1 pilot scope (this cycle).** `Button`, `Input`, `NavDropdown`. Backfill
of remaining 91 stories is deferred to Tier 2, one task per category.

**Source.** docs-ai `.cofoundy/specs/architecture-v1.md` §8 + the canonical PRD
at `docs.cofoundy.dev/team/docs-ai/mobile-first-contract-v1` (§7).

## AI Assistant Rules

1. **ALWAYS add Storybook stories** for new components
Expand Down
15 changes: 15 additions & 0 deletions src/stories/_shared/viewports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Mobile-first contract — canonical viewport constants for Storybook stories.
//
// Source: docs-ai .cofoundy/specs/architecture-v1.md §8 +
// vault/team/docs-ai/mobile-first-contract-v1.mdx §7.
//
// Every interactive primitive MUST export a `MobileBaseline` story using
// VIEWPORT_MOBILE. See packages/ui/CLAUDE.md → "Mobile-state stories".
//
// The string values match `@storybook/addon-viewport` defaults so they work
// out-of-the-box with `addon-essentials` (which bundles the viewport addon).

export const VIEWPORT_MOBILE = { defaultViewport: 'mobile1' } // 375 px (iPhone SE)
export const VIEWPORT_MOBILE_LANDSCAPE = { defaultViewport: 'mobile2' } // 414 px (iPhone 13 Pro Max)
export const VIEWPORT_TABLET = { defaultViewport: 'tablet' } // 768 px (iPad)
export const VIEWPORT_DESKTOP = { defaultViewport: 'responsive' } // full width
13 changes: 8 additions & 5 deletions src/stories/chat-widget/FloatingWidget.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import { FloatingWindow } from '../../components/chat-widget/FloatingWindow'
import { ChatWidgetFloating } from '../../components/chat-widget/ChatWidgetFloating'

// ============================================================================
// Viewport configurations for responsive testing
// Viewport configurations — sourced from the canonical _shared module so the
// mobile-first contract has a single SSOT. See packages/ui/CLAUDE.md.
// ============================================================================

const VIEWPORT_MOBILE = { defaultViewport: 'mobile1' } // 375px (iPhone SE)
const VIEWPORT_MOBILE_LANDSCAPE = { defaultViewport: 'mobile2' } // 414px landscape
const VIEWPORT_TABLET = { defaultViewport: 'tablet' } // 768px (iPad)
const VIEWPORT_DESKTOP = { defaultViewport: 'responsive' } // Full width
import {
VIEWPORT_MOBILE,
VIEWPORT_MOBILE_LANDSCAPE,
VIEWPORT_TABLET,
VIEWPORT_DESKTOP,
} from '../_shared/viewports'

// ============================================================================
// FloatingLauncher Stories
Expand Down
21 changes: 21 additions & 0 deletions src/stories/navigation/NavDropdown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
NavDropdown,
type NavDropdownItem,
} from "../../components/navigation/NavDropdown";
import { VIEWPORT_MOBILE } from "../_shared/viewports";

const meta: Meta<typeof NavDropdown> = {
title: "Navigation/NavDropdown",
Expand Down Expand Up @@ -274,6 +275,26 @@ export const InNavbar: Story = {
},
};

// Mobile-first contract baseline — 375 px (iPhone SE).
// Verifies the 44 px tap-target claim from the component header against an
// actual mobile viewport. Required per packages/ui/CLAUDE.md.
export const MobileBaseline: Story = {
args: {
label: "Servicios",
variant: "dark",
items: serviceItems,
},
parameters: {
viewport: VIEWPORT_MOBILE,
docs: {
description: {
story:
"Mobile baseline (375 px, iPhone SE). Trigger and menu items must keep ≥ 44×44 px touch targets — auditable visually at this viewport.",
},
},
},
};

// Keyboard navigation demo
export const KeyboardNavigation: Story = {
args: {
Expand Down
21 changes: 21 additions & 0 deletions src/stories/ui/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from '../../components/ui/button';
import { Send, Plus, Check, X } from 'lucide-react';
import { VIEWPORT_MOBILE } from '../_shared/viewports';

const meta: Meta<typeof Button> = {
title: 'UI/Button',
Expand Down Expand Up @@ -109,6 +110,26 @@ export const IconOnly: Story = {
},
};

/**
* Mobile-first contract baseline (375 px, iPhone SE). Verifies 44×44 px tap
* targets at `size='lg'` and that the default size remains usable on mobile.
* Required per packages/ui/CLAUDE.md → "Mobile-state stories".
*/
export const MobileBaseline: Story = {
parameters: { viewport: VIEWPORT_MOBILE },
render: () => (
<div className="flex flex-col gap-3 p-4">
<Button size="lg">Primary CTA (lg ≥ 44 px)</Button>
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button size="icon" aria-label="Add">
<Plus className="w-4 h-4" />
</Button>
</div>
),
};

export const AllVariants: Story = {
render: () => (
<div className="flex flex-col gap-4">
Expand Down
18 changes: 18 additions & 0 deletions src/stories/ui/Input.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Input } from '../../components/ui/input';
import { VIEWPORT_MOBILE } from '../_shared/viewports';

const meta: Meta<typeof Input> = {
title: 'UI/Input',
Expand Down Expand Up @@ -69,6 +70,23 @@ export const WithLabel: Story = {
),
};

/**
* Mobile-first contract baseline (375 px). Verifies the input renders with
* `font-size ≥ 16px` (iOS auto-zooms below 16 px on focus) and tap target
* height respects the 44 px floor. Required per packages/ui/CLAUDE.md.
*/
export const MobileBaseline: Story = {
parameters: { viewport: VIEWPORT_MOBILE },
render: () => (
<div className="flex flex-col gap-3 p-4 w-full">
<Input placeholder="Tap me — should not zoom on iOS" />
<Input type="email" placeholder="email@example.com" />
<Input type="tel" placeholder="+51 9XX XXX XXX" />
<Input disabled placeholder="Disabled" />
</div>
),
};

export const AllTypes: Story = {
render: () => (
<div className="flex flex-col gap-4 w-[300px]">
Expand Down
Loading