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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ dist/
/.github/*
!/.github/workflows/
/CLAUDE.md
/.playwright-mcp/
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to the Ippon UI packages are documented in this file, so con

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), with one entry per release listing the affected package versions.

## 2026-07-08 — @ippon-ui/styles 0.0.9 · @ippon-ui/react 0.0.8

### Added

- `input-text` atom: native 48px text field carrying the visual container (border, 4px radius, background), full-width by default, with hover, focus (brand outline, hidden placeholder) and `disabled` (dimmed) states. The `-error` (setting `aria-invalid="true"`) and `-success` alternatives carry a status border and a status-colored text and placeholder, and show a tinted status background while the placeholder is visible (empty, unfocused field).
- `label` atom: names a form control, linked to it through `for`/`id`.
- `helper-text` atom: contextual help or feedback message linked to its control through `aria-describedby`, with `-error` and `-success` alternatives.
- `field` molecule: vertical composition of a label, any form control and a helper text; the caller wires `for`/`id`, `aria-describedby` and the variant.
- `IpponInputText` React component forwarding native `input` props, deriving `aria-invalid` from the `error` variant.
- `IpponLabel`, `IpponHelperText` and `IpponField` React components.

## 2026-07-07 — @ippon-ui/styles 0.0.8 · @ippon-ui/react 0.0.7

### Added
Expand Down
4 changes: 2 additions & 2 deletions react/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ippon-ui/react",
"description": "Ippon UI React Component Library",
"version": "0.0.7",
"version": "0.0.8",
"license": "Apache-2.0",
"repository": {
"type": "git",
Expand Down Expand Up @@ -39,7 +39,7 @@
},
"peerDependencies": {
"@ippon-ui/icons": "~0.0.2",
"@ippon-ui/styles": "~0.0.8",
"@ippon-ui/styles": "~0.0.9",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
Expand Down
12 changes: 12 additions & 0 deletions react/src/IpponField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { clsx } from 'clsx';
import type { DataSelectableWithChildren } from './DataSelectable.ts';

type IpponFieldProps = DataSelectableWithChildren<{
className?: string;
}>;

export const IpponField = (props: IpponFieldProps) => (
<div className={clsx('ippon-field', props.className)} data-selector={props.dataSelector}>
{props.children}
</div>
);
23 changes: 23 additions & 0 deletions react/src/IpponHelperText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { clsx } from 'clsx';
import type { DataSelectableWithChildren } from './DataSelectable.ts';
import { optionalToAlternativeClass } from './CAP.ts';

type IpponHelperTextProps = DataSelectableWithChildren<{
id?: string;
variant?: 'error' | 'success';
className?: string;
}>;

export const IpponHelperText = (props: IpponHelperTextProps) => (
<span
className={clsx(
'ippon-helper-text',
optionalToAlternativeClass(props.variant),
props.className,
)}
id={props.id}
data-selector={props.dataSelector}
>
{props.children}
</span>
);
26 changes: 26 additions & 0 deletions react/src/IpponInputText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { clsx } from 'clsx';
import type { ComponentProps } from 'react';
import type { DataSelectable } from './DataSelectable.ts';
import { optionalToAlternativeClass } from './CAP.ts';

type IpponInputTextProps = DataSelectable<
ComponentProps<'input'> & {
variant?: 'error' | 'success';
}
>;

export const IpponInputText = ({
variant,
dataSelector,
className,
type,
...inputProps
}: IpponInputTextProps) => (
<input
aria-invalid={variant === 'error' || undefined}
{...inputProps}
type={type ?? 'text'}
className={clsx('ippon-input-text', optionalToAlternativeClass(variant), className)}
data-selector={dataSelector}
/>
);
17 changes: 17 additions & 0 deletions react/src/IpponLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { clsx } from 'clsx';
import type { DataSelectableWithChildren } from './DataSelectable.ts';

type IpponLabelProps = DataSelectableWithChildren<{
htmlFor?: string;
className?: string;
}>;

export const IpponLabel = (props: IpponLabelProps) => (
<label
className={clsx('ippon-label', props.className)}
htmlFor={props.htmlFor}
data-selector={props.dataSelector}
>
{props.children}
</label>
);
4 changes: 4 additions & 0 deletions react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ export { IpponButtonCard } from './IpponButtonCard.tsx';
export { IpponCard } from './IpponCard.tsx';
export { IpponContainer } from './IpponContainer.tsx';
export { IpponDropdown } from './IpponDropdown.tsx';
export { IpponField } from './IpponField.tsx';
export { IpponGrid, IpponGridSlot } from './IpponGrid.tsx';
export { IpponHelperText } from './IpponHelperText.tsx';
export { IpponHSpace, IpponHSpaceSlot } from './IpponHSpace.tsx';
export { IpponIcon } from './IpponIcon.tsx';
export { IpponImportFile } from './IpponImportFile.tsx';
export { IpponInputText } from './IpponInputText.tsx';
export { IpponIon } from './IpponIon.tsx';
export { IpponLabel } from './IpponLabel.tsx';
export { IpponLinkButton } from './IpponLinkButton.tsx';
export { IpponLinkButtonCard } from './IpponLinkButtonCard.tsx';
export { IpponMeter } from './IpponMeter.tsx';
Expand Down
58 changes: 58 additions & 0 deletions react/stories/IpponField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { IpponField } from '../src/IpponField.tsx';
import { IpponHelperText } from '../src/IpponHelperText.tsx';
import { IpponInputText } from '../src/IpponInputText.tsx';
import { IpponLabel } from '../src/IpponLabel.tsx';

type IpponFieldStoryArgs = {
variant?: 'error' | 'success';
disabled?: boolean;
helper?: string;
};

const meta = {
title: 'Molecule/Field',
argTypes: {
variant: {
control: 'inline-radio',
options: [undefined, 'error', 'success'],
},
disabled: { control: 'boolean' },
},
args: {
helper: 'Helper text',
},
render: (args) => (
<IpponField>
<IpponLabel htmlFor="field-input">Label</IpponLabel>
<IpponInputText
id="field-input"
variant={args.variant}
disabled={args.disabled}
placeholder="Placeholder"
aria-describedby="field-input-helper"
/>
<IpponHelperText id="field-input-helper" variant={args.variant}>
{args.helper}
</IpponHelperText>
</IpponField>
),
} satisfies Meta<IpponFieldStoryArgs>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const Error: Story = {
args: { variant: 'error', helper: 'Error message' },
};

export const Success: Story = {
args: { variant: 'success' },
};

export const Disabled: Story = {
args: { disabled: true },
};
30 changes: 30 additions & 0 deletions react/stories/IpponHelperText.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { IpponHelperText } from '../src/IpponHelperText.tsx';

const meta = {
title: 'Atom/HelperText',
component: IpponHelperText,
args: {
children: 'Helper text',
},
argTypes: {
variant: {
control: 'inline-radio',
options: [undefined, 'error', 'success'],
},
},
} satisfies Meta<typeof IpponHelperText>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const Error: Story = {
args: { variant: 'error', children: 'Error message' },
};

export const Success: Story = {
args: { variant: 'success', children: 'Success message' },
};
34 changes: 34 additions & 0 deletions react/stories/IpponInputText.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { IpponInputText } from '../src/IpponInputText.tsx';

const meta = {
title: 'Atom/InputText',
component: IpponInputText,
args: {
placeholder: 'Placeholder',
},
argTypes: {
variant: {
control: 'inline-radio',
options: [undefined, 'error', 'success'],
},
},
} satisfies Meta<typeof IpponInputText>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const Error: Story = {
args: { variant: 'error', defaultValue: 'Value' },
};

export const Success: Story = {
args: { variant: 'success', defaultValue: 'Value' },
};

export const Disabled: Story = {
args: { disabled: true },
};
16 changes: 16 additions & 0 deletions react/stories/IpponLabel.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { IpponLabel } from '../src/IpponLabel.tsx';

const meta = {
title: 'Atom/Label',
component: IpponLabel,
args: {
children: 'Label',
},
} satisfies Meta<typeof IpponLabel>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};
31 changes: 31 additions & 0 deletions react/test/IpponField.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { afterEach, describe, expect, it } from 'vitest';
import { render, screen, configure, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import { IpponField, IpponHelperText, IpponInputText, IpponLabel } from '../src';

configure({
testIdAttribute: 'data-selector',
});

describe('IpponField', () => {
afterEach(cleanup);

it('should be like pattern library', () => {
render(
<IpponField dataSelector="ippon-field">
<IpponLabel htmlFor="input">Label</IpponLabel>
<IpponInputText id="input" aria-describedby="input-helper" />
<IpponHelperText id="input-helper">Helper text</IpponHelperText>
</IpponField>,
);

expect(screen.getByTestId('ippon-field')).toHaveClass('ippon-field');
expect(screen.getByLabelText('Label')).toHaveAccessibleDescription('Helper text');
});

it('should merge additional className', () => {
render(<IpponField className="-custom" dataSelector="ippon-field" />);

expect(screen.getByTestId('ippon-field')).toHaveClass('ippon-field', '-custom');
});
});
40 changes: 40 additions & 0 deletions react/test/IpponHelperText.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { afterEach, describe, expect, it } from 'vitest';
import { render, screen, configure, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import { IpponHelperText } from '../src';

configure({
testIdAttribute: 'data-selector',
});

const getIpponHelperText = () => screen.getByTestId('ippon-helper-text');

describe('IpponHelperText', () => {
afterEach(cleanup);

it('should be like pattern library', () => {
render(
<IpponHelperText id="input-helper" dataSelector="ippon-helper-text">
Helper text
</IpponHelperText>,
);

const helper = getIpponHelperText();

expect(helper).toHaveClass('ippon-helper-text');
expect(helper).toHaveAttribute('id', 'input-helper');
expect(helper).toHaveTextContent('Helper text');
});

it.each(['error', 'success'] as const)('should have variant %s', (variant) => {
render(<IpponHelperText variant={variant} dataSelector="ippon-helper-text" />);

expect(getIpponHelperText()).toHaveClass('ippon-helper-text', `-${variant}`);
});

it('should merge additional className', () => {
render(<IpponHelperText className="-custom" dataSelector="ippon-helper-text" />);

expect(getIpponHelperText()).toHaveClass('ippon-helper-text', '-custom');
});
});
Loading
Loading