# Time Chip 00–23시 또는 00–55분을 칩 그리드로 펼쳐 여러 개를 고르는 선택 컨트롤입니다. 드래그로 구간을 한 번에 고릅니다. > 원본: `03_ASSET_GUIDE/catalog/time-chip.js` · CSS: `03_ASSET_GUIDE/styles/elements/time-chip.css` > kind `component` · category `selection` · status `stable` · specSource `measured` > 함께 쓰는 CSS: `chip.css`(칸 하나) · `button.css`(일괄 동작) --- ## 1. 구성 **Toolbar** 가 위, **Grid** 가 아래입니다. 그리드 한 칸은 **Item** 래퍼와 그 안의 **Chip** 두 겹으로 되어 있습니다. ``` ┌──────────────────────────────────────────────────────────┐ │ 업무 시간 9 / 24 [전체 선택] [전체 해제] │ ← ① Toolbar │ ② ③ ④ │ ├──────────────────────────────────────────────────────────┤ │ (00)(01)(02)(03)(04)(05)(06)(07)(08)(09)(10)(11) │ ← ⑤ Grid │ (12)(13)(14)(15)(16)(17)(18)(19)(20)(21)(22)(23) │ │ ⑥⑦ │ └──────────────────────────────────────────────────────────┘ ``` | # | 요소 | 필수 | 클래스 | 설명 | |---|---|---|---|---| | — | Container | 필수 | `atom-time-chip` | 세로 flex. 드래그 중 `[data-dragging]`. | | ① | Toolbar | 선택 | `atom-time-chip__toolbar` | 이름과 개수, 그리고 일괄 동작을 담습니다. 없어도 됩니다. | | ② | Title | 선택 | `atom-time-chip__title` | 무엇을 고르는 그리드인지 말합니다. 선택 사항입니다. | | ③ | Count | 선택 | `atom-time-chip__count` | `9 / 24`. 선택한 개수입니다. | | ④ | Actions | 선택 | `atom-time-chip__actions` | 전체 선택 · 전체 해제. [Button](./button.md) 의 `xsmall` 입니다. | | ⑤ | Grid | 필수 | `atom-time-chip__grid` | 값들이 열에 맞춰 놓이는 판입니다. | | ⑥ | Item | 필수 | `atom-time-chip__item` | 칸 하나를 감싸는 래퍼입니다. **범위 드래그 상태를 가집니다.** | | ⑦ | Chip | 필수 | `atom-chip atom-chip--type_checkbox` | [Chip](./chip.md) 의 `type_checkbox` 를 그대로 씁니다. | > [!NOTE] > **칩을 다시 정의하지 않습니다** > 알약 모양·선택 채움·비활성 처리는 [Chip](./chip.md) 이 소유하므로 여기서 다시 > 정의하지 않습니다. 이 엘리먼트가 더하는 것은 **열에 맞춰 늘어놓기 · 일괄 선택 · 범위 > 드래그** 세 가지입니다. > > 범위 상태는 칩이 아니라 래퍼에 둡니다. 칩에 붙이면 이 엘리먼트에서만 쓰는 속성이 공용 > 엘리먼트에 추가되고, 칩의 배경은 선택 채움이 쓰고 있어 예고 링을 그릴 자리도 없습니다. --- ## 2. 종류 / 크기 ### Unit 고르는 단위입니다. 값의 집합이 여기서 정해집니다. | unit | 값 | 칸 수 | | --- | --- | --- | | `hour` | `00`–`23` | 24 — 12열 2줄 | | `minute` | `00`–`55`, `step` 간격 | step 5 기준 12 — 12열 1줄 | **둘을 한 그리드에 섞지 않습니다.** 시와 분을 함께 받아야 하면 그리드를 둘 두고 각각 이름을 답니다. 한 판에 섞으면 `09` 가 9시인지 9분인지 알 수 없습니다. ### Step `minute` 에서만 씁니다. 시는 언제나 1시간 간격입니다. | step | 칸 수 | 쓰는 곳 | | --- | --- | --- | | `5` | 12 | 대부분의 시간대 선택 (기본) | | `10` | 6 | 느슨한 구간 | | `15` | 4 | 15분 단위 예약 슬롯 | | `30` | 2 | 30분 단위 예약 슬롯 | 칸이 줄면 **열 수도 함께 줄입니다.** 12열에 2칸만 놓으면 칩 두 개가 판 전체로 늘어나 버튼처럼 보입니다. [Grid 열](#grid-열) 을 보세요. ### Grid 열 `columns` 가 열 수를 정합니다. 기본은 **12** 입니다. 시(24)와 분(12) 모두 12로 나누어떨어지므로 같은 화면에 두 그리드를 놓아도 열이 맞습니다. CSS 로는 `--atom-time-chip-cols` 커스텀 프로퍼티입니다. ```html
``` > [!NOTE] > **`--atom-time-chip-cols` 는 토큰이 아닙니다** > `tokens.css` 에 없습니다. 이 엘리먼트 안에서만 쓰이는 로컬 커스텀 프로퍼티이고, > [Tree](./tree.md) 의 `--atom-tree-indent` · [Grid](./grid.md) 의 > `--atom-grid-tree-indent` 와 같은 성격입니다. `var(…, 12)` 로 폴백과 함께 읽습니다. **열 수를 고정합니다.** 자유롭게 줄바꿈되게 두면 컨테이너 폭에 따라 `14` 가 매번 다른 열에 놓여 훑어보기 어렵습니다. ### 범위 드래그 한 칩을 누른 채 끌면 지나간 구간 전체가 바뀝니다. `07` 에서 눌러 `12` 까지 끌면 `07`–`12` 여섯 칸이 함께 선택됩니다. | 단계 | 표시 | 어디에 | | --- | --- | --- | | 누른 순간 | 링 두 겹 (바깥 링에 간격) | `__item[data-anchor]` | | 끄는 중 | 링 + 옅은 채움 | `__item[data-range]` | | 손을 뗀 뒤 | 선택 채움 | 칩의 `__input:checked` | **예고는 링, 선택은 채움으로 표시합니다.** 둘 다 채움으로 그리면 확정과 예고를 색 차이로만 구분해야 합니다. 링은 선택된 칩 위에도 같은 모양으로 얹히므로 두 상태가 섞이지 않습니다. 앵커 표시는 드래그가 끝날 때까지 남깁니다. 고정된 끝이 보이므로 반대 방향으로 끌어 구간을 줄일 수 있습니다. #### 끌면 켜지나 꺼지나 **시작한 칩의 상태가 반대로 뒤집히고, 구간 전체가 그 상태를 따릅니다.** - 꺼진 칩에서 시작 → 구간 전체가 켜집니다 - 켜진 칩에서 시작 → 구간 전체가 꺼집니다 한 방향으로만 동작하면 잘못 고른 구간을 지우려고 칩을 하나씩 눌러야 합니다. ### Size | size | 칩 높이 | 칸 간격 | 쓰는 곳 | | --- | --- | --- | --- | | `medium` | 33px | `--atom-x2` (8px) | 시간대 선택이 화면의 주된 작업일 때 (기본) | | `small` | 28px | 4px | 폼 안의 한 필드로 들어갈 때 | 높이는 [Chip](./chip.md) 이 소유합니다. `--size_small` 은 **간격만** 좁히고, 칩에는 `atom-chip--size_small` 을 함께 답니다. ### 클래스 계약 `styles/elements/time-chip.css` 헤더와 원본 `registry.json` `css.blocks` 기준입니다. 클래스가 없는 값(—)은 기본 스타일 그대로입니다. | Prop | 값 | 클래스 | |---|---|---| | `size` | `small` · `medium` | `--size_small` · — (칩에는 `atom-chip--size_small` · —) | | `columns` | `number` | 클래스 없음 — `__grid` 의 `style="--atom-time-chip-cols: N"` | 상태 선택자: `[data-dragging]`(Container) · `__item[data-range]` · `__item[data-anchor]` · 칩의 `__input:checked` · `[data-selected]` · `[aria-disabled="true"]` --- ## 3. 상태 칩의 상태를 그대로 씁니다. 이 엘리먼트가 더하는 상태는 범위 드래그 둘뿐입니다. | 상태 | 표시 | 소유 | | --- | --- | --- | | 기본 | 약한 배경 | [Chip](./chip.md) | | 선택 | 솔리드 채움 | [Chip](./chip.md) — `__input:checked` | | 비활성 | 투명도 · 포인터 차단 | [Chip](./chip.md) — `aria-disabled="true"` | | 범위 예고 | 링 + 옅은 채움 | 이 엘리먼트 — `__item[data-range]` | | 범위 앵커 | 링 2겹 | 이 엘리먼트 — `__item[data-anchor]` | **비활성 칩은 범위 드래그가 지나가도 바뀌지 않습니다.** 체크 글리프(`__check`)는 넣지 않습니다. 두 자리 숫자가 24칸 늘어선 판에서 글리프가 칸 폭의 절반을 차지하고, 선택은 채움만으로 구분됩니다. 드래그 중에는 컨테이너에 `data-dragging` 을 달아 글자 선택을 막습니다. 이 속성이 없으면 구간을 끌 때 숫자에 텍스트 선택 색이 칠해집니다. --- ## 4. Props | Prop | Type | Default | 필수 | 설명 | |---|---|---|---|---| | `unit` | `"hour" \| "minute"` | `"hour"` | | 고르는 단위입니다. 값의 집합이 여기서 정해집니다. | | `step` | `5 \| 10 \| 15 \| 30` | `5` | | 분 간격입니다. `unit: "minute"` 에서만 씁니다. | | `value` | `number[]` | `[]` | | 선택된 값입니다. 시는 0–23, 분은 0–55. | | `columns` | `number` | `12` | | 그리드 열 수입니다. | | `disabledValues` | `number[]` | `[]` | | 고를 수 없는 값입니다. 범위 드래그가 지나가도 바뀌지 않습니다. | | `rangeSelect` | `boolean` | `true` | | 드래그로 구간을 고릅니다. | | `showBulkActions` | `boolean` | `true` | | 전체 선택 · 전체 해제 버튼을 둡니다. | | `title` | `string` | | | Toolbar 의 이름입니다. 없으면 Toolbar 는 개수와 버튼만 담습니다. | | `showCount` | `boolean` | `true` | | 이름 뒤에 `9 / 24` 를 붙입니다. | | `size` | `"small" \| "medium"` | `"medium"` | | 칩 높이와 칸 간격입니다. | | `label` | `string` | | ● | 그리드의 `aria-label` 입니다. | | `onChange` | `(value: number[]) => void` | | | 선택이 바뀔 때 호출됩니다. 드래그는 **손을 뗀 시점에 한 번** 부릅니다. | 구현에서는 `value` 를 주면 controlled, 생략하면 `defaultValue`(기본 `[]`)에서 시작하는 uncontrolled 입니다. `onChange` 로 넘기는 배열은 오름차순입니다. `title` · `showCount` · `showBulkActions` 가 모두 없으면 Toolbar 를 렌더하지 않습니다. ### Parts (compound) | Part | 요소 | 설명 | |---|---|---| | `TimeChip` | `
` | 위 Props 를 받습니다. Toolbar(Title · Count · Actions) · Grid · Item · Chip 과 `aria-live` 알림 영역(`atom-sr-only`)을 모두 렌더합니다. 파트는 없습니다. | --- ## 5. Component tokens | 대상 | 속성 | Token | Value | | --- | --- | --- | --- | | Container | gap | `{space.x3}` | 12px (small 은 `{space.x2}`) | | Toolbar | border-bottom | `{color.border.subtle}` | 1px | | Toolbar | padding-bottom | `{space.x2}` | 8px | | Title | font | — | 13px (small 은 12px) · `{color.fg.neutral}` | | Count | color | `{color.fg.neutral-muted}` | `tabular-nums` | | Actions | gap | `{space.x2}` | 8px | | Grid | gap | `{space.x2}` | 8px (small 은 4px) | | Grid | 열 수 | — | `--atom-time-chip-cols` · 기본 12 (토큰 아님) | | Item | radius | `{radius.full}` | 칩과 같은 모양으로 링이 앉게 | | Item · range | ring | `{color.fg.brand}` | 2px | | Item · range | 채움 | `{color.bg.side-active}` | 미선택 칸에만 · 브랜드 9% 틴트 | | Item · anchor | outline | `{color.fg.brand}` | 2px · offset 2px (링 위에 한 겹 더) | | Chip | 전부 | — | [Chip](./chip.md) 이 소유합니다 | ```json { "time-chip": { "gap": { "$type": "dimension", "default": { "$value": "{space.x3}" }, "small": { "$value": "{space.x2}" } }, "toolbar": { "border": { "$type": "color", "$value": "{color.border.subtle}" }, "padding-bottom": { "$type": "dimension", "$value": "{space.x2}" } }, "title": { "$type": "color", "$value": "{color.fg.neutral}" }, "count": { "$type": "color", "$value": "{color.fg.neutral-muted}" }, "actions-gap": { "$type": "dimension", "$value": "{space.x2}" }, "grid-gap": { "$type": "dimension", "default": { "$value": "{space.x2}" }, "small": { "$value": "4px" } }, "item-radius": { "$type": "dimension", "$value": "{radius.full}" }, "range": { "ring": { "$type": "color", "$value": "{color.fg.brand}" }, "fill": { "$type": "color", "$value": "{color.bg.side-active}" } }, "anchor": { "$type": "color", "$value": "{color.fg.brand}" } } } ``` --- ## 6. 접근성 - **역할** — 그리드는 `role="group"` 이고 `aria-label` 로 무엇을 고르는지 말합니다. 칩은 실제 `` 이므로 선택 상태가 그대로 전달됩니다. - **이름** — 칩의 보이는 글자는 `09` 뿐입니다. 스크린리더에는 `aria-label` 로 `"09시"` · `"30분"` 처럼 단위를 붙여 읽힙니다. 숫자만으로는 시인지 분인지 알 수 없습니다. - **키보드** — Tab 으로 칩 사이를 이동하고 Space 로 켜고 끕니다. **범위 선택에는 `Shift` + 클릭 또는 `Shift` + 화살표를 함께 제공해야 합니다.** 드래그는 포인터 전용 제스처라, 드래그만 있으면 키보드 사용자는 12칸을 하나씩 눌러야 합니다. - **일괄 동작 알림** — 전체 선택·전체 해제와 범위 확정은 화면이 크게 바뀌는데 초점은 그대로입니다. `aria-live="polite"` 로 `"12개 선택됨"` 을 알립니다. - **대비** — 선택 칩은 [Chip](./chip.md) 의 솔리드 채움이라 그쪽 기준을 따릅니다. 범위 예고 링은 `--atom-fg-brand` 이고, 색만으로 전달하지 않도록 **링(형태)** 과 **채움(명도)** 을 함께 씁니다. 채움은 `--atom-bg-side-active` 입니다. `--atom-bg-informative-weak` 는 칩의 기본 배경과 6/255 밖에 차이가 나지 않아 실측에서 제외했습니다. 구현의 키보드 대안: | 키 / 제스처 | 동작 | | --- | --- | | `Tab` | 칩 사이를 이동합니다(비활성 칩은 건너뜀) | | `Space` | 지금 칩을 켜고 끕니다 | | `Shift` + `←` `→` | 이웃 칩으로 초점을 옮기며 지금 칩의 상태(켜짐/꺼짐)를 이어 칠합니다 | | `Shift` + `↑` `↓` | 한 줄(`columns` 칸) 위·아래로 옮기며 사이 구간 전체를 같은 상태로 칠합니다 | | `Shift` + 클릭 | 직전에 바꾼 칩부터 누른 칩까지를 직전 상태로 칠합니다 | --- ## 7. 사용 가이드 ### 전체 선택과 전체 해제는 할 일이 있을 때만 켭니다 0개 선택된 상태에서 "전체 해제" 는 눌러도 아무 동작이 없습니다. 동작이 없는 버튼은 비활성으로 둡니다. - ✅ **Do** 0개면 전체 해제를, 전부면 전체 선택을 비활성으로 둡니다. - ✅ **Do** Count 를 함께 둡니다. 24칸에서 선택 개수를 세기는 어렵습니다. - ❌ **Don't** 전체 해제를 되돌릴 수 없게 만들지 않습니다. 잘못 누르면 선택이 모두 사라집니다. ### 열에 맞춰 놓습니다 같은 화면에 시 그리드와 분 그리드를 함께 놓을 때 둘의 열이 어긋나면 두 그리드가 서로 관련 없어 보입니다. 그래서 기본값이 12열입니다. - ✅ **Do** 열 수를 정해 두고 값이 늘 같은 자리에 오게 합니다. - ❌ **Don't** 컨테이너 폭에 따라 흐르게 두지 않습니다. `14` 가 매번 다른 열에 갑니다. ### 드래그 중에는 아직 확정이 아닙니다 드래그 중에 선택 채움을 칠하면 손을 떼기 전에 선택이 확정된 것처럼 보입니다. 방향을 되돌리면 칠했던 칸이 지워지는 것처럼 보여 조작 결과를 파악하기 어렵습니다. - ✅ **Do** 예고는 링으로, 확정은 채움으로 구분합니다. - ❌ **Don't** 드래그 중에 선택 채움을 미리 칠하지 않습니다. ### 고를 값의 범위가 넓으면 쓰지 않습니다 Time Chip 은 **값이 24개 이하로 정해져 있고 여러 개를 고르는** 화면에 씁니다. - 임의의 시각 하나 → [Time Input](./time-input.md) 을 씁니다. 4번의 키 입력으로 끝납니다. - 시작과 끝 두 값 → Time Input 두 개를 나란히 둡니다. 두 값만 고를 때는 24칸 그리드가 맞지 않습니다. - 분 단위까지 임의로 → 칸이 60개가 되므로 입력 컴포넌트를 씁니다. ### 관련 컴포넌트 - [Chip](./chip.md) — 칸 하나입니다. 알약 모양·선택 채움·비활성은 Chip 이 정의합니다. - [Time Input](./time-input.md) — 임의의 시각 **하나**를 넣을 때. 이쪽은 여러 개를 고를 때입니다. - [Button](./button.md) — Toolbar 의 일괄 동작에 `xsmall` 로 씁니다. - [Checkbox](./checkbox.md) — 값이 적고 이름이 긴 항목이면 칩 그리드보다 낫습니다. --- ## 8. 예시 ```tsx const [hours, setHours] = useState([8, 9, 10]); ``` ```html
업무 시간9 / 24
11
```