diff --git a/.gitignore b/.gitignore index 01fd8c2..a49d2c2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ /.github/* !/.github/workflows/ /CLAUDE.md +/.playwright-mcp/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 84ebe0d..aa13d95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/react/package.json b/react/package.json index e42bdb8..bdd3ce8 100644 --- a/react/package.json +++ b/react/package.json @@ -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", @@ -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" }, diff --git a/react/src/IpponField.tsx b/react/src/IpponField.tsx new file mode 100644 index 0000000..c318e12 --- /dev/null +++ b/react/src/IpponField.tsx @@ -0,0 +1,12 @@ +import { clsx } from 'clsx'; +import type { DataSelectableWithChildren } from './DataSelectable.ts'; + +type IpponFieldProps = DataSelectableWithChildren<{ + className?: string; +}>; + +export const IpponField = (props: IpponFieldProps) => ( +
+ {props.children} +
+); diff --git a/react/src/IpponHelperText.tsx b/react/src/IpponHelperText.tsx new file mode 100644 index 0000000..f01b716 --- /dev/null +++ b/react/src/IpponHelperText.tsx @@ -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) => ( + + {props.children} + +); diff --git a/react/src/IpponInputText.tsx b/react/src/IpponInputText.tsx new file mode 100644 index 0000000..de14f31 --- /dev/null +++ b/react/src/IpponInputText.tsx @@ -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) => ( + +); diff --git a/react/src/IpponLabel.tsx b/react/src/IpponLabel.tsx new file mode 100644 index 0000000..4055d94 --- /dev/null +++ b/react/src/IpponLabel.tsx @@ -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) => ( + +); diff --git a/react/src/index.ts b/react/src/index.ts index 87e0c69..34c06f8 100644 --- a/react/src/index.ts +++ b/react/src/index.ts @@ -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'; diff --git a/react/stories/IpponField.stories.tsx b/react/stories/IpponField.stories.tsx new file mode 100644 index 0000000..2082b0d --- /dev/null +++ b/react/stories/IpponField.stories.tsx @@ -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) => ( + + Label + + + {args.helper} + + + ), +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +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 }, +}; diff --git a/react/stories/IpponHelperText.stories.tsx b/react/stories/IpponHelperText.stories.tsx new file mode 100644 index 0000000..0a34ac4 --- /dev/null +++ b/react/stories/IpponHelperText.stories.tsx @@ -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; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Error: Story = { + args: { variant: 'error', children: 'Error message' }, +}; + +export const Success: Story = { + args: { variant: 'success', children: 'Success message' }, +}; diff --git a/react/stories/IpponInputText.stories.tsx b/react/stories/IpponInputText.stories.tsx new file mode 100644 index 0000000..6da2fa7 --- /dev/null +++ b/react/stories/IpponInputText.stories.tsx @@ -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; + +export default meta; + +type Story = StoryObj; + +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 }, +}; diff --git a/react/stories/IpponLabel.stories.tsx b/react/stories/IpponLabel.stories.tsx new file mode 100644 index 0000000..b2b5e44 --- /dev/null +++ b/react/stories/IpponLabel.stories.tsx @@ -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; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/react/test/IpponField.spec.tsx b/react/test/IpponField.spec.tsx new file mode 100644 index 0000000..b45b898 --- /dev/null +++ b/react/test/IpponField.spec.tsx @@ -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( + + Label + + Helper text + , + ); + + expect(screen.getByTestId('ippon-field')).toHaveClass('ippon-field'); + expect(screen.getByLabelText('Label')).toHaveAccessibleDescription('Helper text'); + }); + + it('should merge additional className', () => { + render(); + + expect(screen.getByTestId('ippon-field')).toHaveClass('ippon-field', '-custom'); + }); +}); diff --git a/react/test/IpponHelperText.spec.tsx b/react/test/IpponHelperText.spec.tsx new file mode 100644 index 0000000..4e7888b --- /dev/null +++ b/react/test/IpponHelperText.spec.tsx @@ -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( + + Helper text + , + ); + + 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(); + + expect(getIpponHelperText()).toHaveClass('ippon-helper-text', `-${variant}`); + }); + + it('should merge additional className', () => { + render(); + + expect(getIpponHelperText()).toHaveClass('ippon-helper-text', '-custom'); + }); +}); diff --git a/react/test/IpponInputText.spec.tsx b/react/test/IpponInputText.spec.tsx new file mode 100644 index 0000000..8da1721 --- /dev/null +++ b/react/test/IpponInputText.spec.tsx @@ -0,0 +1,77 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { render, screen, configure, cleanup } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; +import { IpponInputText } from '../src'; + +configure({ + testIdAttribute: 'data-selector', +}); + +const getIpponInputText = () => screen.getByTestId('ippon-input-text'); + +describe('IpponInputText', () => { + afterEach(cleanup); + + it('should be like pattern library', () => { + render(); + + const input = getIpponInputText(); + + expect(input).toHaveClass('ippon-input-text'); + expect(input.tagName).toBe('INPUT'); + expect(input).toHaveAttribute('type', 'text'); + }); + + it.each(['error', 'success'] as const)('should have variant %s', (variant) => { + render(); + + expect(getIpponInputText()).toHaveClass('ippon-input-text', `-${variant}`); + }); + + it('should forward native input props', () => { + render( + , + ); + + const input = getIpponInputText(); + + expect(input).toHaveAttribute('placeholder', 'Placeholder'); + expect(input).toBeDisabled(); + expect(input).toHaveAttribute('aria-describedby', 'helper'); + }); + + it('should be invalid with variant error', () => { + render(); + + expect(getIpponInputText()).toHaveAttribute('aria-invalid', 'true'); + }); + + it('should not be invalid without variant error', () => { + render(); + + expect(getIpponInputText()).not.toHaveAttribute('aria-invalid'); + }); + + it('should keep the given aria-invalid', () => { + render(); + + expect(getIpponInputText()).toHaveAttribute('aria-invalid', 'false'); + }); + + it('should keep the given type', () => { + render(); + + expect(getIpponInputText()).toHaveAttribute('type', 'search'); + }); + + it('should merge additional className', () => { + render(); + + expect(getIpponInputText()).toHaveClass('ippon-input-text', '-custom'); + }); +}); diff --git a/react/test/IpponLabel.spec.tsx b/react/test/IpponLabel.spec.tsx new file mode 100644 index 0000000..7a223a2 --- /dev/null +++ b/react/test/IpponLabel.spec.tsx @@ -0,0 +1,35 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { render, screen, configure, cleanup } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; +import { IpponLabel } from '../src'; + +configure({ + testIdAttribute: 'data-selector', +}); + +const getIpponLabel = () => screen.getByTestId('ippon-label'); + +describe('IpponLabel', () => { + afterEach(cleanup); + + it('should be like pattern library', () => { + render( + + Label + , + ); + + const label = getIpponLabel(); + + expect(label).toHaveClass('ippon-label'); + expect(label.tagName).toBe('LABEL'); + expect(label).toHaveAttribute('for', 'input'); + expect(label).toHaveTextContent('Label'); + }); + + it('should merge additional className', () => { + render(); + + expect(getIpponLabel()).toHaveClass('ippon-label', '-custom'); + }); +}); diff --git a/styles/package.json b/styles/package.json index 2f06787..50d466b 100644 --- a/styles/package.json +++ b/styles/package.json @@ -1,6 +1,6 @@ { "name": "@ippon-ui/styles", - "version": "0.0.8", + "version": "0.0.9", "description": "Ippon UI Pattern Library", "repository": { "type": "git", diff --git a/styles/src/atom/_atom.scss b/styles/src/atom/_atom.scss index 7315a0f..e086851 100644 --- a/styles/src/atom/_atom.scss +++ b/styles/src/atom/_atom.scss @@ -1,6 +1,9 @@ @use 'badge/badge'; @use 'button/button'; +@use 'helper-text/helper-text'; @use 'icon/icon'; +@use 'input-text/input-text'; +@use 'label/label'; @use 'meter/meter'; @use 'progress/progress'; @use 'separator/separator'; diff --git a/styles/src/atom/atom.pug b/styles/src/atom/atom.pug index ec36831..4b172c4 100644 --- a/styles/src/atom/atom.pug +++ b/styles/src/atom/atom.pug @@ -16,10 +16,16 @@ block content include:componentDoc(height=620) badge/badge.md .tikui-vertical-spacing--line include:componentDoc(height=740) button/button.md + .tikui-vertical-spacing--line + include:componentDoc(height=40) helper-text/helper-text.md .tikui-vertical-spacing--line include:componentDoc(height=50) icon/icon.md + .tikui-vertical-spacing--line + include:componentDoc(height=450) input-text/input-text.md .tikui-vertical-spacing--line include:componentDoc(height=60) ion/ion.md + .tikui-vertical-spacing--line + include:componentDoc(height=40) label/label.md .tikui-vertical-spacing--line include:componentDoc(height=150) meter/meter.md .tikui-vertical-spacing--line diff --git a/styles/src/atom/helper-text/_helper-text.scss b/styles/src/atom/helper-text/_helper-text.scss new file mode 100644 index 0000000..0d9398e --- /dev/null +++ b/styles/src/atom/helper-text/_helper-text.scss @@ -0,0 +1,15 @@ +@use '../../quark/typography'; + +.ippon-helper-text { + @include typography.body-small; + + color: var(--ippon-color-neutral-text-icon-tertiary); + + &.-error { + color: var(--ippon-color-error-text-icon-on-secondary); + } + + &.-success { + color: var(--ippon-color-success-text-icon-on-secondary); + } +} diff --git a/styles/src/atom/helper-text/helper-text.code.pug b/styles/src/atom/helper-text/helper-text.code.pug new file mode 100644 index 0000000..65018e6 --- /dev/null +++ b/styles/src/atom/helper-text/helper-text.code.pug @@ -0,0 +1,5 @@ +include helper-text.mixin.pug + ++ippon-helper-text Helper text ++ippon-helper-text({ variant: 'error' }) Error message ++ippon-helper-text({ variant: 'success' }) Success message diff --git a/styles/src/atom/helper-text/helper-text.md b/styles/src/atom/helper-text/helper-text.md new file mode 100644 index 0000000..916a9c5 --- /dev/null +++ b/styles/src/atom/helper-text/helper-text.md @@ -0,0 +1,13 @@ +## Helper text + +A contextual help or feedback message for a form control. + +**Alternatives:** + +- Default +- Error `-error` +- Success `-success` + +**Accessibility:** + +- Link it to its control through `aria-describedby` so assistive technologies announce it as the control description diff --git a/styles/src/atom/helper-text/helper-text.mixin.pug b/styles/src/atom/helper-text/helper-text.mixin.pug new file mode 100644 index 0000000..1098ce7 --- /dev/null +++ b/styles/src/atom/helper-text/helper-text.mixin.pug @@ -0,0 +1,5 @@ +mixin ippon-helper-text(options) + - const { id, variant } = options || {}; + - const variantClass = variant ? `-${variant}` : null; + span.ippon-helper-text(class=variantClass, id=id) + block diff --git a/styles/src/atom/helper-text/helper-text.render.pug b/styles/src/atom/helper-text/helper-text.render.pug new file mode 100644 index 0000000..1b6202e --- /dev/null +++ b/styles/src/atom/helper-text/helper-text.render.pug @@ -0,0 +1,5 @@ +extends /layout + +block body + div(style='display: flex; gap: 1rem') + include helper-text.code.pug diff --git a/styles/src/atom/input-text/_input-text.scss b/styles/src/atom/input-text/_input-text.scss new file mode 100644 index 0000000..8ee303f --- /dev/null +++ b/styles/src/atom/input-text/_input-text.scss @@ -0,0 +1,79 @@ +@use '../../quark/typography'; + +.ippon-input-text { + box-sizing: border-box; + border: 1px solid var(--ippon-color-neutral-border); + border-radius: var(--ippon-radius-m); + background-color: var(--ippon-color-neutral-surface-primary); + padding: var(--ippon-size-12) var(--ippon-size-16); + width: 100%; + height: var(--ippon-size-48); + color: var(--ippon-color-neutral-text-icon-primary); + + @include typography.body-medium; + + &::placeholder { + color: var(--ippon-color-neutral-text-icon-tertiary); + } + + &:enabled:hover { + background-color: var(--ippon-color-neutral-surface-primary-hover); + } + + &:enabled:focus { + outline: 2px solid var(--ippon-color-brand-border); + outline-offset: 2px; + border-color: var(--ippon-color-brand-border); + + &::placeholder { + color: transparent; + } + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } + + &.-error { + border-color: var(--ippon-color-error-border); + color: var(--ippon-color-error-text-icon-on-secondary); + + &::placeholder { + color: var(--ippon-color-error-text-icon-on-secondary); + } + + &:enabled:placeholder-shown:not(:focus) { + background-color: var(--ippon-color-error-surface-secondary); + + &:hover { + background-color: var(--ippon-color-error-surface-secondary-hover); + } + } + + &:enabled:focus { + border-color: var(--ippon-color-error-border); + } + } + + &.-success { + border-color: var(--ippon-color-success-border); + color: var(--ippon-color-success-text-icon-on-secondary); + + &::placeholder { + color: var(--ippon-color-success-text-icon-on-secondary); + } + + &:enabled:placeholder-shown:not(:focus) { + background-color: var(--ippon-color-success-surface-secondary); + + &:hover { + background-color: var(--ippon-color-success-surface-secondary-hover); + } + } + + &:enabled:focus { + border-color: var(--ippon-color-success-border); + } + } +} diff --git a/styles/src/atom/input-text/input-text.code.pug b/styles/src/atom/input-text/input-text.code.pug new file mode 100644 index 0000000..140b01f --- /dev/null +++ b/styles/src/atom/input-text/input-text.code.pug @@ -0,0 +1,9 @@ +include input-text.mixin.pug + ++ippon-input-text({ placeholder: 'Placeholder' }) ++ippon-input-text({ value: 'Value' }) ++ippon-input-text({ variant: 'error', placeholder: 'Placeholder' }) ++ippon-input-text({ variant: 'error', placeholder: 'Placeholder', value: 'Value' }) ++ippon-input-text({ variant: 'success', placeholder: 'Placeholder' }) ++ippon-input-text({ variant: 'success', placeholder: 'Placeholder', value: 'Value' }) ++ippon-input-text({ placeholder: 'Placeholder', disabled: true }) diff --git a/styles/src/atom/input-text/input-text.md b/styles/src/atom/input-text/input-text.md new file mode 100644 index 0000000..718e6fb --- /dev/null +++ b/styles/src/atom/input-text/input-text.md @@ -0,0 +1,23 @@ +## Input text + +A native text field carrying the visual container: border, radius and background. + +**Alternatives:** + +- Default +- Error `-error` (adds `aria-invalid="true"`, error border; tinted background while the placeholder shows) +- Success `-success` (success border; same background mechanics as error) + +**States:** + +- Hover (highlighted background) +- Focus (brand outline, hidden placeholder; an error or success field keeps its status border) +- Placeholder shown (an empty, unfocused error or success field gets the tinted background; focused, filled or placeholder-less it keeps the resting one) +- Disabled `disabled` (dimmed field and `not-allowed` cursor) + +**Accessibility:** + +- Name the field with a _Label_ atom linked through `for`/`id` +- Describe it with a _Helper text_ atom linked through `aria-describedby` + +> The placeholder is a hint, not a label: it disappears on input. diff --git a/styles/src/atom/input-text/input-text.mixin.pug b/styles/src/atom/input-text/input-text.mixin.pug new file mode 100644 index 0000000..1909f8d --- /dev/null +++ b/styles/src/atom/input-text/input-text.mixin.pug @@ -0,0 +1,14 @@ +mixin ippon-input-text(options) + - const { id, type, placeholder, value, variant, disabled, describedBy } = options || {}; + - const variantClass = variant ? `-${variant}` : null; + - const invalid = variant === 'error' ? 'true' : null; + input.ippon-input-text( + class=variantClass, + id=id, + type=type || 'text', + placeholder=placeholder, + value=value, + disabled=disabled, + aria-describedby=describedBy, + aria-invalid=invalid + ) diff --git a/styles/src/atom/input-text/input-text.render.pug b/styles/src/atom/input-text/input-text.render.pug new file mode 100644 index 0000000..c894c55 --- /dev/null +++ b/styles/src/atom/input-text/input-text.render.pug @@ -0,0 +1,5 @@ +extends /layout + +block body + div(style='display: flex; flex-direction: column; gap: 1rem') + include input-text.code.pug diff --git a/styles/src/atom/label/_label.scss b/styles/src/atom/label/_label.scss new file mode 100644 index 0000000..19f13f4 --- /dev/null +++ b/styles/src/atom/label/_label.scss @@ -0,0 +1,7 @@ +@use '../../quark/typography'; + +.ippon-label { + @include typography.body-medium; + + color: var(--ippon-color-neutral-text-icon-tertiary); +} diff --git a/styles/src/atom/label/label.code.pug b/styles/src/atom/label/label.code.pug new file mode 100644 index 0000000..58ac1b8 --- /dev/null +++ b/styles/src/atom/label/label.code.pug @@ -0,0 +1,3 @@ +include label.mixin.pug + ++ippon-label({ for: 'example-input' }) Label diff --git a/styles/src/atom/label/label.md b/styles/src/atom/label/label.md new file mode 100644 index 0000000..60bc930 --- /dev/null +++ b/styles/src/atom/label/label.md @@ -0,0 +1,7 @@ +## Label + +A label naming a form control. + +**Accessibility:** + +- Link it to its control through `for`/`id`: clicking the label focuses the control and assistive technologies announce it as the control name diff --git a/styles/src/atom/label/label.mixin.pug b/styles/src/atom/label/label.mixin.pug new file mode 100644 index 0000000..65991af --- /dev/null +++ b/styles/src/atom/label/label.mixin.pug @@ -0,0 +1,4 @@ +mixin ippon-label(options) + - const { for: forId } = options || {}; + label.ippon-label(for=forId) + block diff --git a/styles/src/atom/label/label.render.pug b/styles/src/atom/label/label.render.pug new file mode 100644 index 0000000..3a57904 --- /dev/null +++ b/styles/src/atom/label/label.render.pug @@ -0,0 +1,4 @@ +extends /layout + +block body + include label.code.pug diff --git a/styles/src/molecule/_molecule.scss b/styles/src/molecule/_molecule.scss index 72c3aa3..a71a866 100644 --- a/styles/src/molecule/_molecule.scss +++ b/styles/src/molecule/_molecule.scss @@ -1,3 +1,4 @@ +@use 'field/field'; @use 'import-file/import-file'; @use 'table/table'; @use 'tabs/tabs'; diff --git a/styles/src/molecule/field/_field.scss b/styles/src/molecule/field/_field.scss new file mode 100644 index 0000000..78acc2f --- /dev/null +++ b/styles/src/molecule/field/_field.scss @@ -0,0 +1,5 @@ +.ippon-field { + display: flex; + flex-direction: column; + gap: var(--ippon-size-4); +} diff --git a/styles/src/molecule/field/field.code.pug b/styles/src/molecule/field/field.code.pug new file mode 100644 index 0000000..742be24 --- /dev/null +++ b/styles/src/molecule/field/field.code.pug @@ -0,0 +1,18 @@ +include /atom/helper-text/helper-text.mixin.pug +include /atom/input-text/input-text.mixin.pug +include /atom/label/label.mixin.pug + +include field.mixin.pug + +mixin ippon-field-example(options) + - const { id, helper, variant, ...inputOptions } = options || {}; + - const helperId = `${id}-helper`; + +ippon-field + +ippon-label({ for: id }) Label + +ippon-input-text({ id, variant, describedBy: helperId, ...inputOptions }) + +ippon-helper-text({ id: helperId, variant })= helper + ++ippon-field-example({ id: 'default-input', placeholder: 'Placeholder', helper: 'Helper text' }) ++ippon-field-example({ id: 'error-input', placeholder: 'Placeholder', value: 'Value', helper: 'Error message', variant: 'error' }) ++ippon-field-example({ id: 'success-input', placeholder: 'Placeholder', value: 'Value', helper: 'Helper text', variant: 'success' }) ++ippon-field-example({ id: 'disabled-input', placeholder: 'Placeholder', helper: 'Helper text', disabled: true }) diff --git a/styles/src/molecule/field/field.md b/styles/src/molecule/field/field.md new file mode 100644 index 0000000..2380069 --- /dev/null +++ b/styles/src/molecule/field/field.md @@ -0,0 +1,12 @@ +## Field + +A vertical composition of a _Label_ atom, any form control (_Input text_ today, textarea or select tomorrow) and a _Helper text_ atom. + +**Wiring (done by the caller):** + +- Link the label to the control through `for`/`id` +- Link the helper text to the control through `aria-describedby` +- Pass the same variant (`error` or `success`) to the control and the helper text +- Give a variant control a placeholder: the tinted background only shows through `:placeholder-shown` + +> The field itself carries no state: the control and the helper text do. diff --git a/styles/src/molecule/field/field.mixin.pug b/styles/src/molecule/field/field.mixin.pug new file mode 100644 index 0000000..69de4f4 --- /dev/null +++ b/styles/src/molecule/field/field.mixin.pug @@ -0,0 +1,3 @@ +mixin ippon-field + .ippon-field + block diff --git a/styles/src/molecule/field/field.render.pug b/styles/src/molecule/field/field.render.pug new file mode 100644 index 0000000..f16fe7e --- /dev/null +++ b/styles/src/molecule/field/field.render.pug @@ -0,0 +1,5 @@ +extends /layout + +block body + div(style='display: flex; flex-direction: column; gap: 1.5rem') + include field.code.pug diff --git a/styles/src/molecule/molecule.pug b/styles/src/molecule/molecule.pug index 337215e..2c6a365 100644 --- a/styles/src/molecule/molecule.pug +++ b/styles/src/molecule/molecule.pug @@ -12,6 +12,8 @@ block content h1#molecules.tikui-title-main Molecules .tikui-vertical-spacing--line include /documentation/atomic-design/quote/molecule + .tikui-vertical-spacing--line + include:componentDoc(height=480) field/field.md .tikui-vertical-spacing--line include:componentDoc(height=370) import-file/import-file.md .tikui-vertical-spacing--line