@mindlogic-ai/logician-ui 5.0.0-alpha.1 → 5.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/components/Badge/Badge.d.ts.map +1 -1
  2. package/dist/components/Badge/Badge.js +5 -3
  3. package/dist/components/Badge/Badge.js.map +1 -1
  4. package/dist/components/Badge/Badge.mjs +5 -3
  5. package/dist/components/Badge/Badge.mjs.map +1 -1
  6. package/dist/components/Badge/Badge.types.d.ts +7 -5
  7. package/dist/components/Badge/Badge.types.d.ts.map +1 -1
  8. package/dist/components/LineGraph/seriesColors.d.ts +2 -0
  9. package/dist/components/LineGraph/seriesColors.d.ts.map +1 -1
  10. package/dist/components/LineGraph/seriesColors.js.map +1 -1
  11. package/dist/components/LineGraph/seriesColors.mjs.map +1 -1
  12. package/dist/components/Select/Select.d.ts +5 -1
  13. package/dist/components/Select/Select.d.ts.map +1 -1
  14. package/dist/components/Select/Select.js +2 -2
  15. package/dist/components/Select/Select.js.map +1 -1
  16. package/dist/components/Select/Select.mjs +3 -3
  17. package/dist/components/Select/Select.mjs.map +1 -1
  18. package/dist/components/Select/Select.styles.d.ts +38 -0
  19. package/dist/components/Select/Select.styles.d.ts.map +1 -1
  20. package/dist/components/Select/Select.styles.js +35 -0
  21. package/dist/components/Select/Select.styles.js.map +1 -1
  22. package/dist/components/Select/Select.styles.mjs +35 -2
  23. package/dist/components/Select/Select.styles.mjs.map +1 -1
  24. package/dist/components/Select/Select.types.d.ts +10 -0
  25. package/dist/components/Select/Select.types.d.ts.map +1 -1
  26. package/dist/components/Select/SelectField.d.ts +1 -1
  27. package/dist/components/Select/SelectField.d.ts.map +1 -1
  28. package/dist/components/Select/SelectField.js +3 -2
  29. package/dist/components/Select/SelectField.js.map +1 -1
  30. package/dist/components/Select/SelectField.mjs +3 -2
  31. package/dist/components/Select/SelectField.mjs.map +1 -1
  32. package/dist/components/Select/index.d.ts +1 -0
  33. package/dist/components/Select/index.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/components/Badge/Badge.stories.tsx +193 -3
  36. package/src/components/Badge/Badge.tsx +5 -3
  37. package/src/components/Badge/Badge.types.ts +7 -5
  38. package/src/components/Badge/Badge.usage.mdx +265 -0
  39. package/src/components/LineGraph/seriesColors.ts +2 -0
  40. package/src/components/Select/Comparison.stories.tsx +19 -0
  41. package/src/components/Select/Select.stories.tsx +55 -0
  42. package/src/components/Select/Select.styles.ts +36 -1
  43. package/src/components/Select/Select.tsx +13 -3
  44. package/src/components/Select/Select.types.ts +11 -0
  45. package/src/components/Select/SelectField.tsx +7 -3
  46. package/src/components/Select/index.ts +1 -0
  47. package/src/foundations/BrandColour.mdx +301 -0
  48. package/src/foundations/BrandColour.stories.tsx +334 -0
  49. package/src/guidelines/ComponentPageTemplate.mdx +84 -0
  50. package/src/guidelines/FeedbackActions.mdx +162 -23
  51. package/src/guidelines/_components/Lang.tsx +175 -0
  52. package/src/guidelines/_components/TokenTable.tsx +136 -0
  53. package/src/theme/SemanticTokens.mdx +26 -0
  54. package/src/theme/claude.md +3 -0
@@ -1,7 +1,14 @@
1
- import { Flex } from '@chakra-ui/react';
1
+ import type { ReactNode } from 'react';
2
+ import { Flex, Stack } from '@chakra-ui/react';
2
3
  import { Meta, StoryObj } from '@storybook/react';
3
4
 
4
- import { Badge, badgeVariants } from '.';
5
+ import { T } from '../../guidelines/_components/Lang';
6
+ import { TokenTable } from '../../guidelines/_components/TokenTable';
7
+ import { Card } from '../Card';
8
+ import { ClockIcon, MdError } from '../Icon';
9
+ import { Tag } from '../Tag';
10
+ import { Subtext, Text } from '../Typography';
11
+ import { Badge, badgeVariants, badgeVariantStyles } from '.';
5
12
 
6
13
  const meta = {
7
14
  title: 'Components/Badge',
@@ -14,7 +21,7 @@ const meta = {
14
21
  control: 'select',
15
22
  options: badgeVariants,
16
23
  description:
17
- 'Colour role. `neutral` (grey wash) is the default; the rest report a status.',
24
+ '`neutral` (default): an attribute with no state. `success` / `warning` / `danger` / `info`: a well-defined meaning every user shares; `info` = a non-error system fact (scope, permission, read-only, New). Labels that only classify → `Tag` `category`.',
18
25
  },
19
26
  size: {
20
27
  control: 'select',
@@ -80,3 +87,186 @@ export const Sizes: Story = {
80
87
  </Flex>
81
88
  ),
82
89
  };
90
+
91
+ // ───────────────────────── Usage-page specimens ─────────────────────────
92
+ // Pulled into `Badge.usage.mdx` with <Canvas of={…} />. Hidden from the
93
+ // sidebar (`!dev`) so the component's story list stays short; being stories,
94
+ // they type-check with the rest of the code. `tags` must be written out on
95
+ // each story: the indexer reads CSF statically and does not follow a spread.
96
+
97
+ /** A settings card like FactChat's group credit limit. */
98
+ const LimitCard = ({
99
+ title,
100
+ value,
101
+ badge,
102
+ bg = 'bg.surface',
103
+ }: {
104
+ title: string;
105
+ value: string;
106
+ badge?: ReactNode;
107
+ bg?: string;
108
+ }) => (
109
+ <Card bg={bg} p={4} minW="180px">
110
+ <Flex gap={2} align="center" mb={1}>
111
+ <Subtext>{title}</Subtext>
112
+ {badge}
113
+ </Flex>
114
+ <Text fontWeight="bold" fontSize="lg">
115
+ {value}
116
+ </Text>
117
+ </Card>
118
+ );
119
+
120
+ const CREDIT_CARDS = [
121
+ { title: '남은 크레딧', value: '0' },
122
+ { title: '이번 달 사용', value: '8,489' },
123
+ { title: '개인 한도', value: '2,000' },
124
+ ];
125
+
126
+ export const DoScopeLabel: Story = {
127
+ tags: ['!dev'],
128
+ parameters: { layout: 'padded' },
129
+ name: 'Usage / Do: info for a scope',
130
+ render: () => (
131
+ <LimitCard
132
+ title="개인 크레딧 한도"
133
+ value="2,000"
134
+ badge={<Badge variant="info">이 그룹만</Badge>}
135
+ />
136
+ ),
137
+ };
138
+
139
+ export const DontStatusForTeams: Story = {
140
+ tags: ['!dev'],
141
+ parameters: { layout: 'padded' },
142
+ name: "Usage / Don't: status colours to tell teams apart",
143
+ render: () => (
144
+ <Flex gap={2}>
145
+ <Badge variant="success">마케팅</Badge>
146
+ <Badge variant="danger">영업</Badge>
147
+ <Badge variant="warning">개발</Badge>
148
+ </Flex>
149
+ ),
150
+ };
151
+
152
+ export const DoCategoryTags: Story = {
153
+ tags: ['!dev'],
154
+ parameters: { layout: 'padded' },
155
+ name: 'Usage / Do: Tag category for groups',
156
+ render: () => (
157
+ <Flex gap={2}>
158
+ <Tag category="blue">마케팅</Tag>
159
+ <Tag category="purple">영업</Tag>
160
+ <Tag category="brown">개발</Tag>
161
+ </Flex>
162
+ ),
163
+ };
164
+
165
+ export const DontNeutralOnLayer: Story = {
166
+ tags: ['!dev'],
167
+ parameters: { layout: 'padded' },
168
+ name: "Usage / Don't: neutral on layer.1",
169
+ render: () => (
170
+ <LimitCard
171
+ bg="layer.1"
172
+ title="개인 크레딧 한도"
173
+ value="2,000"
174
+ badge={<Badge variant="neutral">이 그룹만</Badge>}
175
+ />
176
+ ),
177
+ };
178
+
179
+ export const DontRepeatOnEveryCard: Story = {
180
+ tags: ['!dev'],
181
+ parameters: { layout: 'padded' },
182
+ name: "Usage / Don't: the same badge on every card",
183
+ render: () => (
184
+ <Flex gap={3} wrap="wrap">
185
+ {CREDIT_CARDS.map((card) => (
186
+ <LimitCard
187
+ key={card.title}
188
+ {...card}
189
+ badge={<Badge variant="info">이 그룹만</Badge>}
190
+ />
191
+ ))}
192
+ </Flex>
193
+ ),
194
+ };
195
+
196
+ export const DoOnceOnHeading: Story = {
197
+ tags: ['!dev'],
198
+ parameters: { layout: 'padded' },
199
+ name: 'Usage / Do: once, on the section heading',
200
+ render: () => (
201
+ <Stack gap={3}>
202
+ <Flex gap={2} align="center">
203
+ <Text as="h3" fontWeight="bold">
204
+ 크레딧
205
+ </Text>
206
+ <Badge variant="info">이 그룹만</Badge>
207
+ </Flex>
208
+ <Flex gap={3} wrap="wrap">
209
+ {CREDIT_CARDS.map((card) => (
210
+ <LimitCard key={card.title} {...card} />
211
+ ))}
212
+ </Flex>
213
+ </Stack>
214
+ ),
215
+ };
216
+
217
+ export const DoTextCarriesMeaning: Story = {
218
+ tags: ['!dev'],
219
+ parameters: { layout: 'padded' },
220
+ name: 'Usage / Do: the words carry the meaning',
221
+ render: () => (
222
+ <Flex gap={2}>
223
+ <Badge variant="warning" gap={1}>
224
+ <ClockIcon boxSize="12px" aria-hidden />
225
+ 대기 중
226
+ </Badge>
227
+ <Badge variant="danger" gap={1}>
228
+ <MdError boxSize="12px" aria-hidden />
229
+ 결제 실패
230
+ </Badge>
231
+ </Flex>
232
+ ),
233
+ };
234
+
235
+ export const DontColourOnly: Story = {
236
+ tags: ['!dev'],
237
+ parameters: { layout: 'padded' },
238
+ name: "Usage / Don't: colour with no words",
239
+ render: () => (
240
+ <Flex gap={3} align="center">
241
+ <Text>결제 1</Text>
242
+ <Badge variant="success" aria-hidden>
243
+ {'\u00A0'}
244
+ </Badge>
245
+ <Text>결제 2</Text>
246
+ <Badge variant="danger" aria-hidden>
247
+ {'\u00A0'}
248
+ </Badge>
249
+ </Flex>
250
+ ),
251
+ };
252
+
253
+ /** Tokens each variant reads, resolved from `Badge.styles.ts` and the theme. */
254
+ export const UsageTokens: Story = {
255
+ tags: ['!dev'],
256
+ parameters: { layout: 'padded' },
257
+ name: 'Usage / Tokens',
258
+ render: () => (
259
+ <TokenTable
260
+ rows={badgeVariants.flatMap((variant) => [
261
+ {
262
+ token: badgeVariantStyles[variant].bgColor,
263
+ use: <T en={`${variant} · background`} ko={`${variant} · 배경`} />,
264
+ },
265
+ {
266
+ token: badgeVariantStyles[variant].color,
267
+ use: <T en={`${variant} · text`} ko={`${variant} · 글자`} />,
268
+ },
269
+ ])}
270
+ />
271
+ ),
272
+ };
@@ -8,9 +8,11 @@ import { BadgeOwnProps } from './Badge.types';
8
8
  /**
9
9
  * Badge — a small, non-interactive label.
10
10
  *
11
- * `neutral` (default) is the grey-wash everyday badge; `success` / `warning` /
12
- * `danger` / `info` report a status. For an unread/notification number use
13
- * `CountBadge`; for a removable or selectable label use `Tag`.
11
+ * `neutral` (default) is the everyday badge; `success` / `warning` /
12
+ * `danger` / `info` carry a well-defined meaning (a status, or for `info` a
13
+ * system fact such as scope or permission). For an unread/notification number
14
+ * use `CountBadge`; for a removable, selectable or category-only label use
15
+ * `Tag`.
14
16
  *
15
17
  * @example
16
18
  * ```tsx
@@ -7,11 +7,13 @@ import type { PolymorphicProps } from '../../types/polymorphic';
7
7
  /**
8
8
  * Badge variants (5.0).
9
9
  *
10
- * - `neutral` (default): neutral `bg.muted` with `secondary.fgStrong`
11
- * text — the everyday label ("Pro", "Beta", "Draft").
12
- * - `success` / `warning` / `danger` / `info`: the status family's soft
13
- * `{status}.bg` surface with `{status}.fg` text. Use these ONLY when the
14
- * badge reports a status — never as decoration.
10
+ * - `neutral` (default): `bg.muted` + `secondary.fgStrong` — an attribute
11
+ * with no state ("Pro", "Beta", "Draft").
12
+ * - `success` / `warning` / `danger` / `info`: `{status}.bg` + `{status}.fg`,
13
+ * for a meaning every user shares and that is well defined. `info` is a
14
+ * non-error system fact: scope ("이 그룹만"), permission, read-only, "New".
15
+ * A label that only classifies or groups is a `Tag` with `category`, never
16
+ * a status colour. The same label gets the same variant on every screen.
15
17
  *
16
18
  * **Breaking in 5.0:** `primary` and `secondary` were removed. At runtime they
17
19
  * still render (as `neutral`) and warn once in development.
@@ -0,0 +1,265 @@
1
+ import { ArgTypes, Canvas, Meta } from '@storybook/blocks';
2
+
3
+ import { En, Ko, LangToggle, Verdict } from '../../guidelines/_components/Lang';
4
+ import * as BadgeStories from './Badge.stories';
5
+
6
+ <Meta of={BadgeStories} name="Usage" />
7
+
8
+ <LangToggle />
9
+
10
+ <En>
11
+
12
+ # Badge
13
+
14
+ A short, non-interactive label that states an attribute of the thing next to
15
+ it: a status, a plan, or a scope.
16
+
17
+ ## When to use
18
+
19
+ - To state a fixed attribute next to a title or value: "Pro", "Beta".
20
+ - To report a status the user should read: 완료, 대기 중, 실패.
21
+ - To give a well-defined system fact such as scope, permission, read-only or
22
+ "New": 이 그룹만.
23
+
24
+ ## When not to use
25
+
26
+ - The label only classifies or groups and has no fixed meaning → `Tag` with
27
+ `category`.
28
+ - The user can click, select or remove it → `Tag` (`selected`, close button).
29
+ - An unread count → `CountBadge`. An explanation → `Tooltip` or `InfoSprinkle`.
30
+ - A sentence-length status → `Banner` or `Toast`.
31
+
32
+ ## Variants and what they mean
33
+
34
+ </En>
35
+
36
+ <Ko>
37
+
38
+ # Badge
39
+
40
+ 옆에 있는 대상의 속성(상태, 요금제, 적용 범위)을 짧게 알려 주는 라벨입니다. 누르거나
41
+ 선택할 수 없습니다.
42
+
43
+ ## 언제 쓰나요
44
+
45
+ - 제목이나 값 옆에 고정된 속성을 표시할 때: "Pro", "Beta".
46
+ - 사용자가 읽어야 할 상태를 알릴 때: 완료, 대기 중, 실패.
47
+ - 적용 범위, 권한, 읽기 전용, "New"처럼 뜻이 명확한 시스템 사실을 알릴 때: 이 그룹만.
48
+
49
+ ## 언제 쓰지 않나요
50
+
51
+ - 뜻은 없고 분류나 묶음만 나타낸다면 `Tag`의 `category`를 씁니다.
52
+ - 사용자가 누르거나 고르거나 지울 수 있다면 `Tag`(`selected`, 닫기 버튼)를 씁니다.
53
+ - 읽지 않은 개수는 `CountBadge`, 설명은 `Tooltip`이나 `InfoSprinkle`을 씁니다.
54
+ - 한 문장 길이의 상태 안내는 `Banner`나 `Toast`를 씁니다.
55
+
56
+ ## 변형과 뜻
57
+
58
+ </Ko>
59
+
60
+ <ArgTypes of={BadgeStories} include={['variant']} />
61
+
62
+ <En>
63
+
64
+ | Variant | Means | Use for |
65
+ | --- | --- | --- |
66
+ | `neutral` (default) | An attribute with no state | Plan, version, "Draft" |
67
+ | `info` | A well-defined system fact; nothing is wrong | Scope (이 그룹만), "New", read-only, permission |
68
+ | `success` | Finished or healthy | 완료, 연결됨 |
69
+ | `warning` | Needs attention, still works | 대기 중, 만료 임박 |
70
+ | `danger` | Failed or blocked | 실패, 차단됨 |
71
+
72
+ ### Choosing a colour
73
+
74
+ 1. **Does the colour carry a meaning every user shares** (a status, a scope, a
75
+ risk)? → Badge with a status variant. The same label always gets the same
76
+ variant, on every screen.
77
+ 2. **Does it only tell items apart?** → `Tag` with `category` (twelve hues, no
78
+ meaning). Never borrow a status colour for this.
79
+ 3. **Neither?** → `neutral`.
80
+
81
+ ## Do / Don't
82
+
83
+ </En>
84
+
85
+ <Ko>
86
+
87
+ | 변형 | 뜻 | 예 |
88
+ | --- | --- | --- |
89
+ | `neutral` (기본) | 상태가 없는 속성 | 요금제, 버전, "Draft" |
90
+ | `info` | 뜻이 명확한 시스템 사실이며, 문제가 있다는 뜻은 아님 | 적용 범위(이 그룹만), "New", 읽기 전용, 권한 |
91
+ | `success` | 끝났거나 정상 | 완료, 연결됨 |
92
+ | `warning` | 확인이 필요하지만 동작은 함 | 대기 중, 만료 임박 |
93
+ | `danger` | 실패했거나 막힘 | 실패, 차단됨 |
94
+
95
+ ### 색 고르기
96
+
97
+ 1. **모든 사용자가 같은 뜻으로 읽는 색인가요?**(상태, 적용 범위, 위험) 그렇다면 상태
98
+ 변형의 Badge를 씁니다. 같은 라벨은 어느 화면에서나 같은 변형을 씁니다.
99
+ 2. **항목을 나누기만 하나요?** 그렇다면 `Tag`의 `category`(뜻이 없는 열두 가지 색)를
100
+ 씁니다. 이때 상태색을 빌려 쓰지 않습니다.
101
+ 3. **둘 다 아니라면** `neutral`을 씁니다.
102
+
103
+ ## 이렇게 하세요 / 이렇게 하지 마세요
104
+
105
+ </Ko>
106
+
107
+ <Canvas of={BadgeStories.DoScopeLabel} />
108
+ <Verdict
109
+ kind="do"
110
+ en="Use info for a scope label. Scope is a fixed system meaning, and the colour lifts it off the card."
111
+ ko="적용 범위 라벨에는 info를 씁니다. 적용 범위는 뜻이 정해진 시스템 사실이고, 색이 있어 카드 위에서 잘 보입니다."
112
+ />
113
+
114
+ <Canvas of={BadgeStories.DontStatusForTeams} />
115
+ <Verdict
116
+ kind="dont"
117
+ en="Borrow status colours to tell teams apart. Readers will take 영업 to mean an error."
118
+ ko="팀을 나누려고 상태색을 빌려 쓰지 마세요. 읽는 사람은 '영업'을 오류로 받아들입니다."
119
+ />
120
+
121
+ <Canvas of={BadgeStories.DoCategoryTags} />
122
+ <Verdict
123
+ kind="do"
124
+ en="Use Tag category hues for labels that only classify."
125
+ ko="분류만 하는 라벨에는 Tag의 category 색을 씁니다."
126
+ />
127
+
128
+ <Canvas of={BadgeStories.DontNeutralOnLayer} />
129
+ <Verdict
130
+ kind="dont"
131
+ en="Put a neutral badge on a layer.1 surface. In light mode layer.1 and bg.muted are the same value, so the badge disappears. On layered grounds use a status variant, or no badge."
132
+ ko="layer.1 면 위에 neutral 배지를 두지 마세요. 라이트 모드에서 layer.1과 bg.muted는 같은 값이라 배지가 보이지 않습니다. 겹친 면 위에서는 상태 변형을 쓰거나 배지를 빼세요."
133
+ />
134
+
135
+ <Canvas of={BadgeStories.DontRepeatOnEveryCard} />
136
+ <Verdict
137
+ kind="dont"
138
+ en="Repeat the same badge on every card in a row. If all three say the same thing, the badge stops telling the reader anything."
139
+ ko="한 줄의 카드마다 같은 배지를 반복하지 마세요. 세 개가 모두 같은 말을 하면 배지는 아무것도 알려 주지 못합니다."
140
+ />
141
+
142
+ <Canvas of={BadgeStories.DoOnceOnHeading} />
143
+ <Verdict
144
+ kind="do"
145
+ en="Say it once, on the section heading, when it applies to the whole section."
146
+ ko="섹션 전체에 해당한다면 섹션 제목 옆에 한 번만 붙입니다."
147
+ />
148
+
149
+ <Canvas of={BadgeStories.DoTextCarriesMeaning} />
150
+ <Verdict
151
+ kind="do"
152
+ en="Let the words carry the meaning. Colour and icon back it up but never replace it (WCAG 1.4.1)."
153
+ ko="뜻은 문구가 전합니다. 색과 아이콘은 문구를 거들 뿐, 대신하지 않습니다(WCAG 1.4.1)."
154
+ />
155
+
156
+ <Canvas of={BadgeStories.DontColourOnly} />
157
+ <Verdict
158
+ kind="dont"
159
+ en="Use a coloured badge with no words. Someone who cannot tell green from red, or who uses a screen reader, gets nothing."
160
+ ko="문구 없이 색만 있는 배지를 쓰지 마세요. 초록과 빨강을 구분하기 어려운 사람이나 화면 낭독기 사용자에게는 아무 정보도 전달되지 않습니다."
161
+ />
162
+
163
+ <En>
164
+
165
+ ## Content
166
+
167
+ - One to three words. No full stop.
168
+ - Sentence case in English ("This group only", not "This Group Only"). No
169
+ 「」, quotation marks or brackets around Korean labels.
170
+ - Describe a state, not an action: 하위 그룹 포함, not "Include subgroups",
171
+ which reads as an option that is still off.
172
+ - Use the same words for the same state everywhere. If one screen says 대기 중,
173
+ another should not say 보류.
174
+
175
+ ## Accessibility
176
+
177
+ What Badge does for you:
178
+
179
+ - It renders as plain text (`<span>`), so the label is read as part of the
180
+ content around it. It is not focusable and has no role, which is right for a
181
+ label that does nothing.
182
+
183
+ What you still have to do:
184
+
185
+ - **Words, not colour alone** (WCAG 1.4.1). Every badge has a visible text
186
+ label; an icon inside it is decorative and gets `aria-hidden`.
187
+ - **Mind the accessible name it joins.** A badge inside a button or link
188
+ becomes part of that control's name (WCAG 4.1.2). Keep the visible label and
189
+ the spoken name the same (WCAG 2.5.3), and put the badge outside when it
190
+ would make the name long or misleading, for example a whole-row link.
191
+ - Place it next to a heading, not inside the `<h2>`, so heading navigation
192
+ reads the heading alone.
193
+ - For a clickable badge (a citation link, `as="a"`), the badge becomes the
194
+ control: give it a name that makes sense on its own.
195
+
196
+ ## Tokens
197
+
198
+ The table is generated from `Badge.styles.ts` and the theme.
199
+
200
+ </En>
201
+
202
+ <Ko>
203
+
204
+ ## 문구
205
+
206
+ - 한 단어에서 세 단어까지 씁니다. 마침표를 찍지 않습니다.
207
+ - 영어는 문장형 대소문자를 씁니다("This group only", "This Group Only"가 아님).
208
+ 한국어 라벨에는 「」, 따옴표, 괄호를 두르지 않습니다.
209
+ - 동작이 아니라 상태를 적습니다. "하위 그룹 포함"이라고 쓰고 "하위 그룹 포함하기"처럼
210
+ 아직 꺼져 있는 선택지로 읽히는 문구는 피합니다.
211
+ - 같은 상태에는 어디서나 같은 말을 씁니다. 한 화면에서 "대기 중"이라고 썼다면 다른
212
+ 화면에서 "보류"라고 쓰지 않습니다.
213
+
214
+ ## 접근성
215
+
216
+ Badge가 알아서 하는 일
217
+
218
+ - 일반 텍스트(`<span>`)로 그려지므로 주변 내용과 함께 읽힙니다. 아무 동작이 없는
219
+ 라벨이라 초점을 받지 않고 역할(role)도 없습니다.
220
+
221
+ 직접 챙겨야 하는 일
222
+
223
+ - **색만으로 알리지 않습니다**(WCAG 1.4.1). 모든 배지에는 눈에 보이는 문구가 있어야
224
+ 하며, 안에 넣은 아이콘은 장식이므로 `aria-hidden`을 붙입니다.
225
+ - **배지가 들어가는 접근 가능한 이름을 확인합니다.** 버튼이나 링크 안의 배지는 그
226
+ 컨트롤 이름의 일부가 됩니다(WCAG 4.1.2). 눈에 보이는 문구와 읽히는 이름이 같아야
227
+ 하고(WCAG 2.5.3), 행 전체를 감싼 링크처럼 이름이 길어지거나 뜻이 흐려지면 배지를
228
+ 컨트롤 밖에 둡니다.
229
+ - 제목 옆에 두고 `<h2>` 안에는 넣지 않습니다. 그래야 제목으로 이동할 때 제목만
230
+ 읽힙니다.
231
+ - 누를 수 있는 배지(인용 링크, `as="a"`)는 그 자체가 컨트롤이 됩니다. 배지만 읽어도
232
+ 뜻이 통하는 이름을 붙입니다.
233
+
234
+ ## 토큰
235
+
236
+ 아래 표는 `Badge.styles.ts`와 테마에서 자동으로 만듭니다.
237
+
238
+ </Ko>
239
+
240
+ <Canvas of={BadgeStories.UsageTokens} sourceState="none" />
241
+
242
+ <En>
243
+
244
+ ## Related
245
+
246
+ - **Tag**: classify (`category`), select (`selected`) or dismiss.
247
+ - **CountBadge**: numbers, such as unread counts.
248
+ - **Banner** / **Toast**: a sentence-length status.
249
+ - **InfoSprinkle** / **Tooltip**: an explanation.
250
+ - **Foundations/Brand & colour usage**: why status colours are never
251
+ decoration.
252
+
253
+ </En>
254
+
255
+ <Ko>
256
+
257
+ ## 관련 컴포넌트
258
+
259
+ - **Tag**: 분류(`category`), 선택(`selected`), 삭제.
260
+ - **CountBadge**: 읽지 않은 개수 같은 숫자.
261
+ - **Banner** / **Toast**: 한 문장 길이의 상태 안내.
262
+ - **InfoSprinkle** / **Tooltip**: 설명.
263
+ - **Foundations/Brand & colour usage**: 상태색을 장식으로 쓰지 않는 이유.
264
+
265
+ </Ko>
@@ -8,6 +8,8 @@ import { SYSTEM_HUES } from '@/theme/palette.generated';
8
8
  * that carry no meaning in the system. Red, yellow and green are the danger,
9
9
  * warning and success colours, and blue is info and links, so none of them
10
10
  * may colour a series by default — a red "UV" line reads as an error.
11
+ * (Blue is reserved for data as a whole: a single-measure chart is a blue
12
+ * monochrome ramp — SemanticTokens.mdx, "Blue means data and links".)
11
13
  *
12
14
  * Every stroke clears 3:1 against the page in both modes. Cyan, mint and
13
15
  * orange are too light at their base value in light mode (≈1.9:1), so they
@@ -54,6 +54,25 @@ export const SideBySide = () => (
54
54
  <SelectField options={categoryOptions} placeholder="Select" invalid />
55
55
  <Textarea placeholder="Textarea" invalid />
56
56
  </Row>
57
+ <Row label='Select variant="ghost" — default / invalid / disabled'>
58
+ <SelectField
59
+ variant="ghost"
60
+ options={categoryOptions}
61
+ placeholder="Select"
62
+ />
63
+ <SelectField
64
+ variant="ghost"
65
+ options={categoryOptions}
66
+ placeholder="Select"
67
+ invalid
68
+ />
69
+ <SelectField
70
+ variant="ghost"
71
+ options={categoryOptions}
72
+ placeholder="Select"
73
+ disabled
74
+ />
75
+ </Row>
57
76
  <Row label="Disabled">
58
77
  <Input placeholder="Input" disabled />
59
78
  <SelectField options={categoryOptions} placeholder="Select" disabled />
@@ -2,10 +2,12 @@ import { useState } from 'react';
2
2
  import {
3
3
  Box,
4
4
  createListCollection,
5
+ Flex,
5
6
  HStack,
6
7
  Portal,
7
8
  Span,
8
9
  Stack,
10
+ Text,
9
11
  } from '@chakra-ui/react';
10
12
  import { Meta } from '@storybook/react';
11
13
 
@@ -78,6 +80,59 @@ export const Disabled = () => (
78
80
  </Box>
79
81
  );
80
82
 
83
+ /**
84
+ * `variant="ghost"` — no resting border, sized to its content. For dense
85
+ * settings rows where a column of bordered triggers outweighs the card.
86
+ * Hover shows the control border.
87
+ */
88
+ export const Ghost = () => {
89
+ const rows = [
90
+ ['크레딧 한도 변경', '관리자가 내 크레딧 한도를 바꾸면 알려드립니다.'],
91
+ ['크레딧 잔량 부족', '내 크레딧이 얼마 남지 않으면 알려드립니다.'],
92
+ ['결제 실패', '결제에 실패하거나 자동으로 환불되면 알려드립니다.'],
93
+ ];
94
+ const channelOptions = [
95
+ { label: '앱 알림', value: 'app' },
96
+ { label: '앱 알림 + 이메일', value: 'app_email' },
97
+ { label: '끄기', value: 'off' },
98
+ ];
99
+ return (
100
+ <Box
101
+ maxW="640px"
102
+ borderWidth="1px"
103
+ borderColor="border.subtle"
104
+ borderRadius="12px"
105
+ >
106
+ {rows.map(([title, description], index) => (
107
+ <Flex
108
+ key={title}
109
+ align="center"
110
+ justify="space-between"
111
+ gap={4}
112
+ px={4}
113
+ py={3}
114
+ borderTopWidth={index === 0 ? 0 : '1px'}
115
+ borderColor="border.subtle"
116
+ >
117
+ <Box>
118
+ <Text fontWeight="semibold">{title}</Text>
119
+ <Text color="fg.muted" fontSize="sm">
120
+ {description}
121
+ </Text>
122
+ </Box>
123
+ <SelectField
124
+ variant="ghost"
125
+ size="sm"
126
+ ariaLabel={title}
127
+ options={channelOptions}
128
+ defaultValue="app"
129
+ />
130
+ </Flex>
131
+ ))}
132
+ </Box>
133
+ );
134
+ };
135
+
81
136
  export const ManyOptions = () => {
82
137
  const manyOptions = Array.from({ length: 100 }, (_, i) => ({
83
138
  label: `Option ${i + 1}`,
@@ -8,7 +8,7 @@
8
8
  * update these values to match.
9
9
  */
10
10
 
11
- import { disabledControl } from '@/utils/disabledControl';
11
+ import { disabledControl, disabledText } from '@/utils/disabledControl';
12
12
 
13
13
  const focusVars = {
14
14
  '--focus-color': 'var(--chakra-colors-primary-main)',
@@ -47,6 +47,31 @@ export const triggerStyles = {
47
47
  },
48
48
  };
49
49
 
50
+ /**
51
+ * `variant="ghost"` delta on top of `triggerStyles`, for dense rows (settings
52
+ * lists) where a column of bordered triggers outweighs the card around them.
53
+ * No border at rest; hover brings back `border.control`, focus/open keep the
54
+ * normal ring, and an invalid field keeps its red border at rest so an error
55
+ * is never hidden. The chevron identifies the control (`fg.muted`, well over
56
+ * the 3:1 non-text floor), which is what lets the resting border go.
57
+ */
58
+ export const ghostTriggerStyles = {
59
+ bg: 'transparent',
60
+ borderColor: 'transparent',
61
+ // Content-sized: Chakra caps the value at `maxW: 80%` of the trigger and
62
+ // overlays the chevron, which is circular once the trigger sizes to the
63
+ // value — it truncates to "앱 …". Lift the cap and reserve the chevron's
64
+ // room (indicator group = icon + padding on both sides) instead.
65
+ pe: 'calc(var(--select-trigger-padding-x) * 2 + 1rem)',
66
+ '& [data-part=value-text]': { maxW: 'none', whiteSpace: 'nowrap' },
67
+ _hover: { borderColor: 'border.control' },
68
+ _disabled: {
69
+ ...disabledText,
70
+ cursor: 'not-allowed',
71
+ fontWeight: 'semibold',
72
+ },
73
+ };
74
+
50
75
  /** Combobox text input — the typeable control. */
51
76
  export const inputStyles = {
52
77
  ...fieldBase,
@@ -62,6 +87,16 @@ export const contentStyles = {
62
87
  padding: '4px',
63
88
  };
64
89
 
90
+ /**
91
+ * Dropdown panel for a ghost trigger — at least as wide as the trigger, but
92
+ * grown to the longest option, since a content-sized trigger is often
93
+ * narrower than the options it opens.
94
+ */
95
+ export const ghostContentStyles = {
96
+ width: 'max-content',
97
+ minW: 'var(--reference-width)',
98
+ };
99
+
65
100
  /** Individual option for both Select and Combobox. */
66
101
  export const itemStyles = {
67
102
  borderRadius: '4px',