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
10 changes: 10 additions & 0 deletions .changeset/press-feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'8341-ui': patch
---

タッチでも押したことが伝わるよう、押している間の見た目(`active:`)を付けました。

- Link・Card(`href`)・Accordion・Checkbox / Radio / Switch(ラベルの文字を押しても出る)・入力欄・NotificationBanner の閉じるボタンに、マウスの hover と同じ見た目を `active:` でも付けた
- Button の hover と押した見た目を `enabled:` から `not-disabled:` に替えた。`buttonVariants` を `<a>` に当てたとき、hover も押した見た目も出なかった
- テーマで端末のタップの塗り(`-webkit-tap-highlight-color`)を消した
- iOS Safari で `:active` を効かせるには、アプリで `document` に `touchstart` の受け手を置く(README)
2 changes: 1 addition & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ DTCG では `{"colorSpace": "oklch", "components": [L, C, H], "hex": "#..."}`
- import パスは shadcn 方式: `@/components/ui/{Name}/index`(ディレクトリ名は PascalCase)、`@/lib/{name}`
- 独自のスタイルは `tv()` を `@/lib/tv` から import して書く。`tailwind-variants` の `tv` を直接使わない(独自のテーマ名を登録した `tv` でないと `text-fg text-body-md` の文字色が消える)
- **フォーカスは `focus-ring` クラス**。見た目が 44px 未満の操作対象には `tap-target`
- コンポーネント自体を作る・直すときの規約(data 属性、`enabled:hover:`、`forced-colors:`、`'use client'` など)は [docs/rulebook/04-components.md](./docs/rulebook/04-components.md)
- コンポーネント自体を作る・直すときの規約(data 属性、`not-disabled:hover:`、`forced-colors:`、`'use client'` など)は [docs/rulebook/04-components.md](./docs/rulebook/04-components.md)

### Button

Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ npx shadcn@latest add @8341/8341-base @8341/button @8341/form-field @8341/text-f
あとは使うだけです。テーマは `<html class="dark">` か `<html data-theme="dark">` でダークに切り替わり、どちらも無ければ OS の設定に従います。
テーマ(`styles/8341-ui.css`)はトークンを同梱しています。トークンが更新されたら `npx shadcn@latest add @8341/theme --overwrite` で取り込み直します。

部品は押している間の見た目(`:active`)でタッチでも押したことを伝えます。iOS Safari は `touchstart` の受け手が無いと `:active` を当てないので、アプリの全画面に共通する枠(レイアウトなど)で一度だけ受け手を置きます。インラインの `ontouchstart` 属性は CSP で止まることがあるので、スクリプトで付けます。

```tsx
useEffect(() => {
const enableActive = () => undefined;
document.addEventListener('touchstart', enableActive, { passive: true });
return () => document.removeEventListener('touchstart', enableActive);
}, []);
```

```tsx
import { Button } from '@/components/ui/Button/index';
import { FormField } from '@/components/ui/FormField/index';
Expand Down
1 change: 1 addition & 0 deletions docs/improvement/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

| 日付 | 種別 | 内容 | 再発防止 |
| ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| 2026-11-07 | 部品 | タッチで押したことが伝わるよう、hover だけだった部品(Link・Card・Accordion・Checkbox / Radio / Switch(ラベルを押しても出る)・入力欄・NotificationBanner の閉じる)に、hover と同じ見た目の `active:` を足した。ボタンの見た目(`buttonVariants`)をリンクに当てると `enabled:` が `<a>` に当たらず hover も押した見た目も出なかったので、Button を `not-disabled:` に替えた。端末のタップの塗りは消した。bouzu(#84・#89)での知見 | クラス監査に `enabled-variant`(enabled: を禁止)と `hover-without-active`(hover: だけで active: が無い部品)を追加。Button・Link のテスト。iOS Safari の touchstart の受け手を README に記載 |
| 2026-11-02 | 保守 | Node.js を 26.13.0(LTS Lithium)に更新。`devEngines` を `>=26.13.0`、`@types/node` を 26 系(`^26.6.6`)に合わせた。他の依存の版は変えていない | ADR-0020。CI は `.tool-versions` の版を使う |
| 2026-10-23 | 保守 | 全体のリファクタリング。古い参照(存在しない make ターゲット・パス・README の節名)を直し、トークンを変えた後の手順を `make generate` に統一。Makefile と重複していた pnpm スクリプト、design-review に混ざっていたコードレビュー・Next.js の項目、使われていない関数と export を削除。DESIGN.md・ルールブック 04・06・CONTRIBUTING・factory.md の重複を正本へのリンクにし、トークンの CSS 化の規則を `packages/tokens/scripts/lib/css.ts` に集めて build.ts とスライドで共有 | 規則を 1 か所にしたので、tokens.css とスライドの値の書き方がずれない。重複していた手順は正本(Makefile・COMPONENT_STRUCTURE.md・06 章・ds-factory)だけに置いた |
| 2026-10-23 | 機能 | 8341-ui を使うプロダクトに、DESIGN.md と design-principles を配るレジストリ item `@8341/design-md` を追加。DESIGN.md はコードから読み取れない考え方(プロダクトの前提・画面領域ごとの方向性・大事にすること・外した必須)と原則の必須の要約だけにし、トークンや部品の一覧は載せない | ADR-0019。原則の要約は `scripts/sync-product-design.ts` が docs/design-principles から生成し、`make check` で鮮度を検査。配布する原則のファイル一覧も registry.json の --check で追従 |
Expand Down
2 changes: 1 addition & 1 deletion docs/rulebook/02-accessibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
| A11 | 動的に出す通知だけ `role="status"` / `role="alert"` を付ける | 4.1.3 | NotificationBanner の `live` |
| A12 | `prefers-reduced-motion` で動きを止める | 2.3.3 | theme.css、`motion-reduce:` |
| A13 | 強制カラーモード(Windows ハイコントラスト)で境界とフォーカスが消えない | 1.4.11 | 各コンポーネントの `forced-colors:`、e2e の forced-colors プロジェクト |
| A14 | hover で文字の太さ・大きさを変えない。無効状態に hover を効かせない(`enabled:hover:`) | — | レビュー(Tailwind v4 の `hover:` は自動で `@media (hover: hover)` に入る) |
| A14 | hover で文字の太さ・大きさを変えない。無効状態に hover を効かせない(`not-disabled:hover:`)。hover の見た目は `active:` にも付け、タッチでも押したことを伝える | — | クラス監査の `enabled-variant`・`hover-without-active`(Tailwind v4 の `hover:` は自動で `@media (hover: hover)` に入る) |
| A15 | ストーリーはすべて axe の WCAG 2.2 AA ルールで違反ゼロ | — | Storybook addon-a11y(`test: 'error'`)+ addon-vitest |
| A16 | 幅 320px で横スクロールが出ない。文字サイズ 200% で内容が切れない | 1.4.10, 1.4.4 | Playwright の e2e(手動・都度実行) |

Expand Down
4 changes: 3 additions & 1 deletion docs/rulebook/04-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
- 状態は Base UI の data 属性で装飾する(`data-[checked]:`、`data-[invalid]:`、`data-[disabled]:`、`data-[highlighted]:`、`data-[panel-open]:`、`data-[starting-style]:`)
- **フォーカスは `focus-ring`**(`@/lib/focus` の `focusRing`)。Base UI は `data-focus-visible` を付けないため `:focus-visible` で描く
- 見た目が 44px 未満の操作対象には `tap-target`
- 無効状態に hover を効かせないよう `enabled:hover:` を使う(Tailwind v4 の `hover:` は自動で `@media (hover: hover)` に入る)
- 無効状態に hover と押した見た目を効かせないよう `not-disabled:hover:`・`not-disabled:active:` を使う。`enabled:` は `<a>` に当たらず、ボタンの見た目(`buttonVariants`)をリンクに当てたときに何も出なくなる(クラス監査の `enabled-variant`)
- `hover:` を付けたら、同じ見た目かそれより濃い見た目を `active:` にも付ける。Tailwind v4 の `hover:` は自動で `@media (hover: hover)` に入り、タッチでは出ないので、押したことは `active:` だけが伝える(クラス監査の `hover-without-active`)
- ラベルの文字を押して操作する部品(Checkbox・Radio・Switch)は、ラベルに `group/*` を付け、`group-active/*:` で四角・丸・トラックにも押した見た目を出す
- 強制カラーモードは `forced-colors:` で補う
- 論理プロパティのクラスを使う(`ps-*`、`ms-*`、`border-s-*`)
- クラス名を動的に組み立てない(`` `text-${size}` `` は Tailwind が検出できない)。variants に列挙する
Expand Down
15 changes: 15 additions & 0 deletions packages/ui/scripts/audit-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* bare-read-only … read-only: は button や無効の input にも当たる。[&:read-only:not(:disabled,button)]: を使う
* theme-var-ref … 任意値・手書き CSS からの Tailwind テーマ変数(--color-* / --spacing-* / --text-* など)の参照。
* @theme inline は使われた変数しか出力しないため未定義になりうる。常に存在する --8341-* を参照する
* enabled-variant … enabled: は <a> に当たらない。ボタンの見た目をリンクに当てると hover も押した見た目も出ない。not-disabled: を使う
* hover-without-active … hover: を書いた部品に active: が無い。タッチでは hover が出ないので、押した見た目は active: で出す
* warning
* numeric-spacing … p-4 / gap-6 など数値の余白。semantic な余白(p-inset-md、gap-stack-md)を検討する
*/
Expand Down Expand Up @@ -57,6 +59,17 @@ export function auditClasses(): Finding[] {
message: "フックを使っているのに 'use client' がありません",
});
}
const classText = text.replace(/\/\/.*$/gm, '');
if (/(?<![\w-])hover:/.test(classText) && !/active[:/]/.test(classText)) {
findings.push({
file: rel,
line: 1,
rule: 'hover-without-active',
severity: 'error',
message:
'hover: があるのに active: がありません。タッチでは hover が出ないので、押した見た目を active: で付けてください',
});
}
lines.forEach((raw, i) => {
const add = (rule: string, severity: Finding['severity'], message: string) =>
findings.push({ file: rel, line: i + 1, rule, severity, message });
Expand All @@ -77,6 +90,8 @@ export function auditClasses(): Finding[] {
'error',
'read-only: は button や無効の input にも当たります。[&:read-only:not(:disabled,button)]: を使ってください',
);
if (/(?<![\w-])enabled:/.test(line))
add('enabled-variant', 'error', 'enabled: は <a> に当たりません。not-disabled: を使ってください');
const palette = line.match(PALETTE);
if (palette)
add('tailwind-palette', 'error', `Tailwind 既定のパレットは使えません: ${palette[0]}(semantic の色を使う)`);
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/ui/Accordion/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const accordionVariants = tv({
item: 'border-b border-border',
header: 'm-0 text-body-md font-bold',
trigger: [
'group/trigger flex min-h-target-min w-full cursor-pointer items-center rounded-xs border-0 bg-transparent px-inset-sm py-inset-md text-start font-bold text-fg hover:bg-muted',
'group/trigger flex min-h-target-min w-full cursor-pointer items-center rounded-xs border-0 bg-transparent px-inset-sm py-inset-md text-start font-bold text-fg hover:bg-muted active:bg-muted',
focusRing,
],
icon: 'ms-gap-md shrink-0 text-icon-key transition-transform group-data-[panel-open]/trigger:rotate-180',
Expand Down
13 changes: 12 additions & 1 deletion packages/ui/src/components/ui/Button/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vite-plus/test';
import { Button } from './index';
import { Button, buttonVariants } from './index';

describe('Button', () => {
it('ボタンの見た目をリンクに当てても、押した見た目が出る(:enabled は a に当たらない)', () => {
render(
<a href="/apply" className={buttonVariants({ variant: 'primary' }).root()}>
申請を始める
</a>,
);
const link = screen.getByRole('link', { name: '申請を始める' });
expect(link).toHaveClass('not-disabled:active:bg-key-active');
expect(link.className).not.toMatch(/(?<![\w-])enabled:/);
});

it('既定で type="button"(フォーム内で意図せず送信しない)', () => {
render(<Button>保存する</Button>);
expect(screen.getByRole('button')).toHaveAttribute('type', 'button');
Expand Down
8 changes: 4 additions & 4 deletions packages/ui/src/components/ui/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@ export const buttonVariants = tv({
*/
variant: {
primary: {
root: 'border-key bg-key text-fg-on-key enabled:hover:border-key-hover enabled:hover:bg-key-hover enabled:active:border-key-active enabled:active:bg-key-active',
root: 'border-key bg-key text-fg-on-key not-disabled:hover:border-key-hover not-disabled:hover:bg-key-hover not-disabled:active:border-key-active not-disabled:active:bg-key-active',
},
secondary: {
root: 'border-border-key bg-surface text-fg-key enabled:hover:bg-key-subtle enabled:active:bg-key-subtle-hover',
root: 'border-border-key bg-surface text-fg-key not-disabled:hover:bg-key-subtle not-disabled:active:bg-key-subtle-hover',
},
tertiary: {
root: 'border-transparent bg-transparent text-fg-key underline underline-offset-[0.2em] enabled:hover:bg-key-subtle enabled:active:bg-key-subtle-hover disabled:border-transparent disabled:bg-transparent',
root: 'border-transparent bg-transparent text-fg-key underline underline-offset-[0.2em] not-disabled:hover:bg-key-subtle not-disabled:active:bg-key-subtle-hover disabled:border-transparent disabled:bg-transparent',
},
danger: {
root: 'border-danger bg-danger text-fg-on-key enabled:hover:border-danger-hover enabled:hover:bg-danger-hover enabled:active:border-danger-active enabled:active:bg-danger-active',
root: 'border-danger bg-danger text-fg-on-key not-disabled:hover:border-danger-hover not-disabled:hover:bg-danger-hover not-disabled:active:border-danger-active not-disabled:active:bg-danger-active',
},
},
/** sm は見た目 36px だが、タップ領域は 44px を確保する */
Expand Down
12 changes: 9 additions & 3 deletions packages/ui/src/components/ui/Card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,24 @@ export const cardVariants = tv({
body: 'flex flex-1 flex-col gap-stack-sm p-inset-xl',
eyebrow: 'm-0 text-body-sm text-fg-subtle',
title: 'm-0 text-heading-xs',
link: 'text-fg-link underline decoration-1 underline-offset-[0.2em] outline-none after:absolute after:inset-0 after:content-[""] group-hover/card:decoration-(length:--8341-border-width-underline-hover)', // allow-outline-none: フォーカスはカード全体に focus-ring-within で描く
link: 'text-fg-link underline decoration-1 underline-offset-[0.2em] outline-none after:absolute after:inset-0 after:content-[""] group-hover/card:decoration-(length:--8341-border-width-underline-hover) group-active/card:decoration-(length:--8341-border-width-underline-hover)', // allow-outline-none: フォーカスはカード全体に focus-ring-within で描く
content: '[&>p]:m-0',
footer: 'flex items-center justify-between gap-gap-sm px-inset-xl pb-inset-xl text-fg-subtle',
arrow: 'ms-auto text-icon-key transition-transform group-hover/card:translate-x-gap-xs',
arrow:
'ms-auto text-icon-key transition-transform group-hover/card:translate-x-gap-xs group-active/card:translate-x-gap-xs',
},
variants: {
variant: {
outlined: { root: 'border-border bg-surface' },
filled: { root: 'border-transparent bg-subtle' },
},
interactive: {
true: { root: [focusRingWithin, 'hover:border-border-key hover:shadow-1'] },
true: {
root: [
focusRingWithin,
'hover:border-border-key hover:shadow-1 active:border-border-key active:bg-subtle active:shadow-1',
],
},
},
},
defaultVariants: { variant: 'outlined' },
Expand Down
4 changes: 3 additions & 1 deletion packages/ui/src/components/ui/Checkbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import { type VariantProps, tv } from '@/lib/tv';
export const choiceVariants = tv({
slots: {
label: [
'flex w-fit cursor-pointer items-start gap-gap-sm text-body-md text-fg',
'group/choice flex w-fit cursor-pointer items-start gap-gap-sm text-body-md text-fg',
// 1 行の高さ(16px × 1.7)に上下の余白を足して 44px のタップ領域にする
'min-h-target-min py-[calc((var(--8341-size-target-min)-var(--8341-text-body-md-font-size)*var(--8341-text-body-md-line-height))/2)]',
'has-[[data-disabled]]:cursor-not-allowed has-[[data-disabled]]:text-fg-disabled',
],
control: [
'group/control mt-[calc((var(--8341-text-body-md-font-size)*var(--8341-text-body-md-line-height)-var(--choice-box))/2)] inline-grid size-(--choice-box) shrink-0 place-items-center',
'border-2 border-border-input bg-surface hover:border-border-strong hover:bg-muted',
// ラベルの文字を押しても、四角や丸に押した見た目を出す
'group-active/choice:not-data-[disabled]:border-border-strong group-active/choice:not-data-[disabled]:bg-muted',
'data-[invalid]:border-border-danger',
'data-[disabled]:border-border-disabled data-[disabled]:bg-disabled',
focusRing,
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/ui/FormField/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const controlVariants = tv({
'block w-full rounded-md border border-border-input bg-surface px-inset-md py-inset-sm',
// 16px 未満だと iOS Safari がフォーカス時に拡大するため、文字サイズは固定
'text-body-md leading-(--8341-text-dense-md-line-height) text-fg placeholder:text-fg-subtle',
'transition-colors hover:border-border-strong focus-visible:border-border-strong',
'transition-colors hover:border-border-strong active:border-border-strong focus-visible:border-border-strong',
focusRing,
// エラーは色に加えて線を太くし、形でも示す
'data-[invalid]:border-border-danger data-[invalid]:shadow-[inset_0_0_0_1px_var(--8341-color-border-danger)] aria-invalid:border-border-danger aria-invalid:shadow-[inset_0_0_0_1px_var(--8341-color-border-danger)]',
Expand Down
Loading
Loading