banhatten-ui 0.1.2 → 0.2.0

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.
@@ -0,0 +1,106 @@
1
+ # Banhatten Design System — Library Customization Rule
2
+
3
+ Use this rule to customize any UI library to match the Banhatten Design System.
4
+ Copy this file into your project's `.cursor/rules/` directory.
5
+
6
+ ---
7
+
8
+ ## Source of Truth
9
+
10
+ After running `npm install banhatten-ui`, two resources are available:
11
+
12
+ 1. **Design Tokens** — `node_modules/banhatten-ui/dist/tokens/tokens.json`
13
+ 2. **Component Specs** — `node_modules/banhatten-ui/dist/specs/<component>.json`
14
+
15
+ Always read these files before making styling decisions. They are the single source of truth.
16
+
17
+ ---
18
+
19
+ ## Token Resolution
20
+
21
+ `tokens.json` has four top-level sections:
22
+
23
+ | Section | Purpose | Example |
24
+ |-------------|---------------------------------------------|----------------------------------------|
25
+ | `brand` | Raw color palette (hex values) | `"primary-600": "#2563eb"` |
26
+ | `alias` | Semantic tokens referencing `brand` | `"bg-brand": "{brand.primary-600}"` |
27
+ | `shadow` | Box-shadow CSS strings | `"md": "0px 1.75px 4px -1px ..."` |
28
+ | `radius` | Border-radius pixel values | `"sm": "8px"` |
29
+ | `spacing` | Spacing values (rem or px) | `"lg": "1rem"` |
30
+
31
+ **Resolution chain:** Component specs reference `{alias.*}`, `{shadow.*}`, `{radius.*}`, and `{spacing.*}`. Alias tokens reference `{brand.*}` which holds the final hex value.
32
+
33
+ Example:
34
+ ```
35
+ spec: "{alias.component-button-brand-bg}" → tokens alias: "{brand.primary-600}" → tokens brand: "#2563eb"
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Component Spec Schema
41
+
42
+ Each `<component>.json` in `specs/` follows this structure:
43
+
44
+ - **`base`** — Default CSS properties applied to every instance.
45
+ - **`variants`** — Named style presets (e.g. `primary`, `secondary`, `danger`). Each variant specifies `bg`, `text`, `border`, `hover`, `active` etc. using token references.
46
+ - **`sizes`** — Named size presets (e.g. `xs`, `md`, `lg`). Each size specifies `height`, `paddingX`, `paddingY`, `fontSize`, `borderRadius`, `gap` using token references or raw CSS values.
47
+ - **`states`** — `disabled`, `focus`, `error`, and other state styles.
48
+ - **`compoundVariants`** — Conditional overrides when multiple props match simultaneously.
49
+ - **`props`** — Component API: prop name, type, allowed values, and defaults.
50
+ - **`slots`** — Named sub-elements within the component (e.g. `leftIcon`, `rightIcon`).
51
+ - **`subComponents`** — Definitions for child components (e.g. `MenuItem` inside `Menu`).
52
+ - **`defaults`** — Default variant and size.
53
+ - **`accessibility`** — Required ARIA attributes and notes.
54
+
55
+ ---
56
+
57
+ ## Customization Process
58
+
59
+ ### Step 1: Global Theme
60
+
61
+ Read `tokens.json` and configure your library's global theme:
62
+
63
+ 1. **Colors** — Map every `alias.*` token to your library's color system. Resolve each alias through `brand` to get the final hex value. Create semantic color keys that match the alias names.
64
+ 2. **Shadows** — Map `shadow.*` values directly as CSS box-shadow strings.
65
+ 3. **Border Radius** — Map `radius.*` values to your library's radius/shape tokens.
66
+ 4. **Spacing** — Map `spacing.*` values to your library's spacing scale.
67
+
68
+ ### Step 2: Component Overrides
69
+
70
+ For each component your project uses:
71
+
72
+ 1. Read the matching spec from `node_modules/banhatten-ui/dist/specs/<component>.json`.
73
+ 2. Map **variants** to the library's variant/color system. Match backgrounds, text colors, borders, and hover/active/focus states exactly.
74
+ 3. Map **sizes** to the library's size system. Match height, padding, font size, border radius, and gap exactly.
75
+ 4. Map **states** (disabled opacity, focus rings) to the library's state styling.
76
+ 5. Map **compound variants** as conditional overrides.
77
+ 6. Ensure the component exposes the same **props** as the spec (or provide wrappers that do).
78
+
79
+ ### Step 3: Missing Components
80
+
81
+ For components in the specs that do not exist in your library:
82
+
83
+ 1. Create a custom component using your library's primitives and styling utilities.
84
+ 2. Follow the spec exactly for all variants, sizes, states, and props.
85
+ 3. Resolve all token references to final CSS values through `tokens.json`.
86
+ 4. Implement the accessibility requirements from the spec's `accessibility` section.
87
+
88
+ ---
89
+
90
+ ## Available Component Specs
91
+
92
+ accordion, alert, avatar, avatar-group, avatar-profile, badge, breadcrumb,
93
+ button, button-group, checkbox, checkbox-card, close-button, divider,
94
+ dropdown, dropdown-input, featured-icon, icon, input, menu, progress-bar,
95
+ radio, radio-card, sidebar, slider, tag, textarea, toggle, tooltip
96
+
97
+ ---
98
+
99
+ ## Rules
100
+
101
+ - Never hardcode hex colors — always resolve through `tokens.json` so themes stay updatable.
102
+ - Preserve the variant/size naming from specs (e.g. `primary`, `md`) in your theme configuration.
103
+ - When a spec uses `{alias.*}`, resolve it to the final value through `brand` only at the point of CSS output — keep semantic names in theme config.
104
+ - Match pixel-perfect dimensions: if the spec says `height: "40px"` and `paddingX: "{spacing.lg}"` (1rem), use those exact values.
105
+ - Focus rings must use `{alias.border-brand}` (resolved: `#2563eb`) with a 2px offset unless the spec says otherwise.
106
+ - Icon sizing follows the spec's `slots.*.iconSizeByButtonSize` mappings when present.
@@ -0,0 +1,115 @@
1
+ {
2
+ "name": "Accordion",
3
+ "description": "Expandable/collapsible content sections. Supports single (one open) or multiple (many open) modes. Chevron icon rotates on expand/collapse.",
4
+
5
+ "base": {
6
+ "display": "flex",
7
+ "flexDirection": "column"
8
+ },
9
+
10
+ "variants": {
11
+ "type": {
12
+ "single": { "description": "Only one item open at a time. Value is string." },
13
+ "multiple": { "description": "Many items can be open. Value is string[]." }
14
+ }
15
+ },
16
+
17
+ "props": {
18
+ "type": { "type": "enum", "values": ["single", "multiple"], "default": "single" },
19
+ "value": { "type": "string | string[]", "description": "Controlled open value(s). Single uses string; multiple uses string[]." },
20
+ "defaultValue": { "type": "string | string[]", "description": "Default open value(s) when uncontrolled." },
21
+ "onValueChange": { "type": "function", "description": "(value: string | string[]) => void. Called when open state changes." }
22
+ },
23
+
24
+ "subComponents": {
25
+ "AccordionItem": {
26
+ "description": "Wrapper for a single accordion section. Provides value, disabled, and showDivider context.",
27
+ "element": "div",
28
+ "base": {
29
+ "display": "flex",
30
+ "flexDirection": "column"
31
+ },
32
+ "props": {
33
+ "value": { "type": "string", "description": "Unique value for this item (used for open state and accessibility)." },
34
+ "disabled": { "type": "boolean", "default": false, "description": "When true, trigger and content use disabled styling and are not expandable." },
35
+ "showDivider": { "type": "boolean", "default": false, "description": "When true, a Divider is shown between trigger and content when expanded." }
36
+ }
37
+ },
38
+
39
+ "AccordionTrigger": {
40
+ "description": "Button that toggles expand/collapse. Optional iconLeft. Chevron rotates: expand_more (collapsed) → expand_less (expanded).",
41
+ "element": "button",
42
+ "base": {
43
+ "display": "flex",
44
+ "width": "100%",
45
+ "alignItems": "center",
46
+ "gap": "{spacing.md}",
47
+ "paddingY": "{spacing.md}",
48
+ "paddingX": "{spacing.md}",
49
+ "textAlign": "left",
50
+ "color": "{alias.text-primary}",
51
+ "transition": "background-color",
52
+ "focusRing": { "width": "2px", "color": "{alias.border-brand}", "offset": "2px" }
53
+ },
54
+ "states": {
55
+ "default": {
56
+ "cursor": "pointer",
57
+ "hover": { "bg": "{alias.bg-tertiary}" },
58
+ "expandIconColor": "{alias.icon-primary}",
59
+ "iconLeftColor": "{alias.icon-secondary}"
60
+ },
61
+ "disabled": {
62
+ "cursor": "default",
63
+ "color": "{alias.text-inactive}",
64
+ "pointerEvents": "none",
65
+ "expandIconColor": "{alias.icon-inactive}",
66
+ "iconLeftColor": "{alias.icon-inactive}"
67
+ }
68
+ },
69
+ "props": {
70
+ "iconLeft": { "type": "ReactNode", "description": "Optional icon or element shown to the left of the heading (e.g. info icon)." },
71
+ "disabled": { "type": "boolean", "description": "Overrides item-level disabled. When undefined, inherits from AccordionItem." }
72
+ },
73
+ "slots": {
74
+ "iconLeft": {
75
+ "position": "start",
76
+ "className": "accordion-icon-left",
77
+ "shrink": 0
78
+ },
79
+ "expandIcon": {
80
+ "element": "Icon",
81
+ "name": "expand_more | expand_less",
82
+ "size": "sm",
83
+ "className": "accordion-expand-icon",
84
+ "position": "end",
85
+ "shrink": 0,
86
+ "rotation": { "collapsed": "0deg", "expanded": "180deg" }
87
+ }
88
+ },
89
+ "accessibility": {
90
+ "ariaExpanded": "boolean",
91
+ "ariaControls": "contentId",
92
+ "ariaDisabled": "boolean when disabled"
93
+ }
94
+ },
95
+
96
+ "AccordionContent": {
97
+ "description": "Collapsible content panel. Renders only when item is expanded. When AccordionItem.showDivider is true, Divider renders above content.",
98
+ "element": "div",
99
+ "base": {
100
+ "paddingY": "{spacing.md}",
101
+ "paddingX": "{spacing.md}",
102
+ "color": "{alias.text-secondary}",
103
+ "fontSize": "14px"
104
+ },
105
+ "accessibility": {
106
+ "role": "region",
107
+ "ariaLabelledby": "triggerId"
108
+ }
109
+ }
110
+ },
111
+
112
+ "accessibility": {
113
+ "notes": "AccordionItem provides triggerId and contentId for aria-controls/aria-labelledby. AccordionTrigger uses aria-expanded and aria-controls."
114
+ }
115
+ }
@@ -0,0 +1,178 @@
1
+ {
2
+ "name": "Alert",
3
+ "description": "Contextual feedback banner with status icon, title, optional description, action buttons, and close control. Supports 5 semantic types across 4 emphasis levels.",
4
+
5
+ "base": {
6
+ "display": "flex",
7
+ "gap": "{spacing.md}",
8
+ "borderRadius": "{radius.sm}",
9
+ "paddingX": "{spacing.md}",
10
+ "paddingY": "{spacing.md}",
11
+ "transition": "background-color, border-color, color",
12
+ "fontSize": "14px",
13
+ "lineHeight": "20px"
14
+ },
15
+
16
+ "variants": {
17
+ "low": {
18
+ "bg": "{alias.bg-primary}",
19
+ "border": { "width": "1px", "color": "{alias.border-strong}" },
20
+ "text": "{alias.text-primary}"
21
+ },
22
+ "medium": {},
23
+ "moderate": {},
24
+ "high": {}
25
+ },
26
+
27
+ "sizes": {},
28
+
29
+ "states": {
30
+ "expanded": {
31
+ "alignItems": "flex-start",
32
+ "title": { "fontWeight": 500 },
33
+ "description": { "marginTop": "{spacing.xs}" },
34
+ "actions": { "marginTop": "{spacing.sm}", "gap": "{spacing.md}" }
35
+ },
36
+ "collapsed": {
37
+ "alignItems": "center",
38
+ "title": { "fontWeight": 400 }
39
+ }
40
+ },
41
+
42
+ "compoundVariants": [
43
+ {
44
+ "when": { "emphasis": "medium", "type": "info" },
45
+ "apply": { "bg": "{alias.bg-info-tertiary}", "border": { "width": "1px", "color": "{alias.border-info-subtle}" }, "text": "{alias.text-info}" }
46
+ },
47
+ {
48
+ "when": { "emphasis": "medium", "type": "success" },
49
+ "apply": { "bg": "{alias.bg-success-tertiary}", "border": { "width": "1px", "color": "{alias.border-success-secondary}" }, "text": "{alias.text-success}" }
50
+ },
51
+ {
52
+ "when": { "emphasis": "medium", "type": "warning" },
53
+ "apply": { "bg": "{alias.bg-warning-tertiary}", "border": { "width": "1px", "color": "{alias.border-warning-secondary}" }, "text": "{alias.text-warning-strong}" }
54
+ },
55
+ {
56
+ "when": { "emphasis": "medium", "type": "danger" },
57
+ "apply": { "bg": "{alias.bg-danger-tertiary}", "border": { "width": "1px", "color": "{alias.border-danger-secondary}" }, "text": "{alias.text-danger}" }
58
+ },
59
+ {
60
+ "when": { "emphasis": "medium", "type": "neutral" },
61
+ "apply": { "bg": "{alias.bg-secondary}", "border": { "width": "1px", "color": "{alias.border-strong}" }, "text": "{alias.text-primary}" }
62
+ },
63
+ {
64
+ "when": { "emphasis": "moderate", "type": "info" },
65
+ "apply": { "bg": "{alias.bg-info-secondary}", "border": { "width": "1px", "color": "{alias.border-info-subtle}" }, "text": "{alias.text-info}" }
66
+ },
67
+ {
68
+ "when": { "emphasis": "moderate", "type": "success" },
69
+ "apply": { "bg": "{alias.bg-success-secondary}", "border": { "width": "1px", "color": "{alias.border-success-secondary}" }, "text": "{alias.text-success}" }
70
+ },
71
+ {
72
+ "when": { "emphasis": "moderate", "type": "warning" },
73
+ "apply": { "bg": "{alias.bg-warning-secondary}", "border": { "width": "1px", "color": "{alias.border-warning-secondary}" }, "text": "{alias.text-warning-strong}" }
74
+ },
75
+ {
76
+ "when": { "emphasis": "moderate", "type": "danger" },
77
+ "apply": { "bg": "{alias.bg-danger-secondary}", "border": { "width": "1px", "color": "{alias.border-danger-secondary}" }, "text": "{alias.text-danger}" }
78
+ },
79
+ {
80
+ "when": { "emphasis": "moderate", "type": "neutral" },
81
+ "apply": { "bg": "{alias.bg-tertiary}", "border": { "width": "1px", "color": "{alias.border-strong}" }, "text": "{alias.text-primary}" }
82
+ },
83
+ {
84
+ "when": { "emphasis": "high", "type": "info" },
85
+ "apply": { "bg": "{alias.bg-info}", "text": "{alias.text-on-color}" }
86
+ },
87
+ {
88
+ "when": { "emphasis": "high", "type": "success" },
89
+ "apply": { "bg": "{alias.bg-success}", "text": "{alias.text-on-color}" }
90
+ },
91
+ {
92
+ "when": { "emphasis": "high", "type": "warning" },
93
+ "apply": { "bg": "{alias.bg-warning}", "text": "{alias.text-on-color}" }
94
+ },
95
+ {
96
+ "when": { "emphasis": "high", "type": "danger" },
97
+ "apply": { "bg": "{alias.bg-danger}", "text": "{alias.text-on-color}" }
98
+ },
99
+ {
100
+ "when": { "emphasis": "high", "type": "neutral" },
101
+ "apply": { "bg": "{alias.bg-quarterary}", "text": "{alias.text-primary}" }
102
+ },
103
+ {
104
+ "when": { "emphasis": "low" },
105
+ "apply": { "iconSize": "sm" }
106
+ },
107
+ {
108
+ "when": { "emphasis": ["medium", "moderate", "high"] },
109
+ "apply": { "iconSize": "md" }
110
+ }
111
+ ],
112
+
113
+ "props": {
114
+ "title": { "type": "string", "description": "Main title text displayed in the alert." },
115
+ "description": { "type": "string", "description": "Optional subtitle/description text (shown when expand=true)." },
116
+ "actions": { "type": "AlertAction[]", "description": "Action buttons rendered as link-style buttons (shown when expand=true)." },
117
+ "onClose": { "type": "function", "description": "Callback fired when the close button is clicked." },
118
+ "expand": { "type": "boolean", "default": false, "description": "Expand to show description and action buttons." },
119
+ "type": { "type": "enum", "values": ["info", "success", "warning", "danger", "neutral"], "default": "info" },
120
+ "emphasis": { "type": "enum", "values": ["low", "medium", "moderate", "high"], "default": "medium" },
121
+ "iconVariant": { "type": "enum", "values": ["outlined", "rounded", "sharp"], "default": "outlined" },
122
+ "iconFilled": { "type": "boolean", "default": false }
123
+ },
124
+
125
+ "slots": {
126
+ "icon": {
127
+ "position": "start",
128
+ "element": "Icon",
129
+ "iconByType": {
130
+ "info": "info",
131
+ "success": "check_circle",
132
+ "warning": "warning",
133
+ "danger": "error",
134
+ "neutral": "notifications"
135
+ },
136
+ "iconColorByType": {
137
+ "info": "{alias.text-info}",
138
+ "success": "{alias.text-success}",
139
+ "warning": "{alias.text-warning-strong}",
140
+ "danger": "{alias.text-danger}",
141
+ "neutral": "{alias.icon-primary}"
142
+ },
143
+ "highEmphasisIconColor": "{alias.text-on-color}",
144
+ "highNeutralIconColor": "{alias.icon-primary}"
145
+ },
146
+ "title": {
147
+ "fontSize": "14px",
148
+ "lineHeight": "20px"
149
+ },
150
+ "description": {
151
+ "fontSize": "14px",
152
+ "lineHeight": "20px",
153
+ "marginTop": "{spacing.xs}"
154
+ },
155
+ "actions": {
156
+ "display": "flex",
157
+ "alignItems": "center",
158
+ "gap": "{spacing.md}",
159
+ "marginTop": "{spacing.sm}",
160
+ "element": "Button",
161
+ "buttonVariant": "link-brand",
162
+ "buttonSize": "xs"
163
+ },
164
+ "closeButton": {
165
+ "position": "end",
166
+ "element": "CloseButton",
167
+ "variant": "ghost",
168
+ "size": "sm"
169
+ }
170
+ },
171
+
172
+ "defaults": { "type": "info", "emphasis": "medium", "expand": false },
173
+
174
+ "accessibility": {
175
+ "role": "alert | status",
176
+ "notes": "role='alert' and aria-live='assertive' for danger/warning types; role='status' and aria-live='polite' for info/success/neutral."
177
+ }
178
+ }
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "AvatarGroup",
3
+ "description": "Displays a row of overlapping avatars with optional overflow badge and add-member button.",
4
+
5
+ "base": {
6
+ "display": "inline-flex",
7
+ "alignItems": "center"
8
+ },
9
+
10
+ "sizes": {
11
+ "24": {
12
+ "avatarSize": "24px",
13
+ "avatarProfileSize": "xs",
14
+ "badgeSize": "24px",
15
+ "badgeFontSize": "12px",
16
+ "iconSize": "xs"
17
+ },
18
+ "32": {
19
+ "avatarSize": "32px",
20
+ "avatarProfileSize": "md",
21
+ "badgeSize": "32px",
22
+ "badgeFontSize": "14px",
23
+ "iconSize": "sm"
24
+ },
25
+ "40": {
26
+ "avatarSize": "40px",
27
+ "avatarProfileSize": "xl",
28
+ "badgeSize": "40px",
29
+ "badgeFontSize": "16px",
30
+ "iconSize": "md"
31
+ }
32
+ },
33
+
34
+ "overlap": {
35
+ "marginLeft": "-{spacing.sm}",
36
+ "note": "Every avatar after the first is offset by -ml-sm. Z-index stacking controlled by lastOnTop prop."
37
+ },
38
+
39
+ "zIndex": {
40
+ "lastOnTop": "First avatar gets highest z-index; decreasing toward last.",
41
+ "firstOnTop": "First avatar gets lowest z-index; increasing toward last."
42
+ },
43
+
44
+ "slots": {
45
+ "overflowBadge": {
46
+ "description": "+N indicator shown when avatars exceed maxVisible.",
47
+ "display": "inline-flex",
48
+ "alignItems": "center",
49
+ "justifyContent": "center",
50
+ "borderRadius": "{radius.full}",
51
+ "border": { "width": "2px", "color": "{alias.border-subtract}" },
52
+ "bg": "{alias.bg-tertiary}",
53
+ "text": "{alias.text-secondary}",
54
+ "fontWeight": 500,
55
+ "zIndex": 0
56
+ },
57
+ "addButton": {
58
+ "description": "Dashed circular button to add a new member.",
59
+ "display": "inline-flex",
60
+ "alignItems": "center",
61
+ "justifyContent": "center",
62
+ "borderRadius": "{radius.full}",
63
+ "border": { "width": "2px", "style": "dashed", "color": "{alias.border-strong}" },
64
+ "bg": "transparent",
65
+ "iconColor": "{alias.icon-tertiary}",
66
+ "hover": { "bg": "{alias.bg-tertiary}", "borderColor": "{alias.border-strong}" },
67
+ "focus": { "ring": "2px", "ringColor": "{alias.border-brand}", "ringOffset": "2px" }
68
+ }
69
+ },
70
+
71
+ "composition": {
72
+ "avatar": "AvatarProfile (shape: circle)"
73
+ },
74
+
75
+ "props": {
76
+ "size": { "type": "enum", "values": ["24", "32", "40"], "default": "32" },
77
+ "lastOnTop": { "type": "boolean", "default": true, "description": "When true, leftmost avatar is drawn on top." },
78
+ "moreAvatars": { "type": "boolean", "default": false, "description": "Show +N overflow badge after visible avatars." },
79
+ "addMore": { "type": "boolean", "default": false, "description": "Show an Add More button at the end." },
80
+ "avatars": { "type": "AvatarGroupItem[]", "description": "List of avatar items (initials, aria-label)." },
81
+ "maxVisible": { "type": "number", "default": 4, "description": "Max visible avatars before +N overflow." },
82
+ "onAddMore": { "type": "function", "description": "Called when Add More button is clicked." }
83
+ },
84
+
85
+ "defaults": { "size": "32", "lastOnTop": true, "maxVisible": 4 },
86
+
87
+ "accessibility": {
88
+ "role": "group",
89
+ "notes": "aria-label set to '{count} member(s)'. Add button has aria-label 'Add member'. Overflow badge is aria-hidden."
90
+ }
91
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "AvatarProfile",
3
+ "description": "Wraps Avatar with a border ring and drop shadow. Supports circle and rounded shapes with optional badge slots at top-right and bottom-right.",
4
+
5
+ "base": {
6
+ "display": "inline-flex",
7
+ "position": "relative",
8
+ "flexShrink": 0,
9
+ "border": { "width": "2px", "color": "{alias.border-subtract}" },
10
+ "shadow": "{shadow.sm}"
11
+ },
12
+
13
+ "variants": {
14
+ "circle": { "borderRadius": "{radius.full}" },
15
+ "rounded": { "borderRadius": "{radius.md}" }
16
+ },
17
+
18
+ "composition": {
19
+ "avatar": "Avatar (shape and size passed through)"
20
+ },
21
+
22
+ "props": {
23
+ "shape": { "type": "enum", "values": ["circle", "rounded"], "default": "circle" },
24
+ "size": { "type": "enum", "values": ["xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl"], "default": "md", "description": "Passed through to the inner Avatar." },
25
+ "initials": { "type": "string", "description": "Initials for the inner Avatar." },
26
+ "topRight": { "type": "ReactNode", "description": "Badge content at top-right (e.g. edit icon). Icon is forced to filled." },
27
+ "bottomRight": { "type": "ReactNode", "description": "Badge content at bottom-right (e.g. status icon). Icon is forced to filled." }
28
+ },
29
+
30
+ "slots": {
31
+ "badgeSlot": {
32
+ "description": "Shared styling for topRight and bottomRight badge positions.",
33
+ "position": "absolute",
34
+ "display": "flex",
35
+ "alignItems": "center",
36
+ "justifyContent": "center",
37
+ "borderRadius": "{radius.full}",
38
+ "overflow": "hidden",
39
+ "zIndex": 10,
40
+ "size": "{spacing.xl}",
41
+ "minSize": "{spacing.xl}",
42
+ "border": { "width": "2px", "color": "{alias.border-subtract}" },
43
+ "bg": "{alias.bg-brand}",
44
+ "text": "{alias.text-on-color}"
45
+ },
46
+ "topRight": {
47
+ "top": "0",
48
+ "right": "0",
49
+ "translateX": "50%",
50
+ "translateY": "-50%"
51
+ },
52
+ "bottomRight": {
53
+ "bottom": "0",
54
+ "right": "0",
55
+ "translateX": "50%",
56
+ "translateY": "50%"
57
+ }
58
+ },
59
+
60
+ "defaults": { "shape": "circle", "size": "md" },
61
+
62
+ "accessibility": {
63
+ "notes": "Badge slots are aria-hidden. Icons inside badges are forced to filled variant via cloneElement."
64
+ }
65
+ }
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "Avatar",
3
+ "description": "User representation as initials, fallback icon, or image. Optionally paired with a username and supporting text.",
4
+
5
+ "base": {
6
+ "display": "inline-flex",
7
+ "alignItems": "center",
8
+ "justifyContent": "center",
9
+ "fontWeight": 500,
10
+ "flexShrink": 0,
11
+ "bg": "{alias.bg-quarterary}",
12
+ "text": "{alias.text-secondary}",
13
+ "userSelect": "none",
14
+ "overflow": "hidden"
15
+ },
16
+
17
+ "variants": {
18
+ "circle": { "borderRadius": "{radius.full}" },
19
+ "rounded": { "borderRadius": "{radius.md}" }
20
+ },
21
+
22
+ "sizes": {
23
+ "xs": { "height": "24px", "width": "24px", "fontSize": "12px" },
24
+ "sm": { "height": "28px", "width": "28px", "fontSize": "12px" },
25
+ "md": { "height": "32px", "width": "32px", "fontSize": "14px" },
26
+ "lg": { "height": "36px", "width": "36px", "fontSize": "14px" },
27
+ "xl": { "height": "40px", "width": "40px", "fontSize": "16px" },
28
+ "2xl": { "height": "48px", "width": "48px", "fontSize": "18px" },
29
+ "3xl": { "height": "56px", "width": "56px", "fontSize": "20px" },
30
+ "4xl": { "height": "64px", "width": "64px", "fontSize": "20px" }
31
+ },
32
+
33
+ "props": {
34
+ "shape": { "type": "enum", "values": ["circle", "rounded"], "default": "circle" },
35
+ "size": { "type": "enum", "values": ["xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl"], "default": "md" },
36
+ "initials": { "type": "string", "description": "Initials to show (e.g. 'AG'). Max 2 characters. Renders initials instead of the fallback icon." },
37
+ "username": { "type": "string", "description": "Username shown beside the avatar. Renders as primary text." },
38
+ "supportingText": { "type": "string", "description": "Supporting line below username (e.g. 'View profile'). Renders as secondary text." },
39
+ "aria-label": { "type": "string", "description": "Accessible name for the avatar. Required when the avatar conveys meaning." }
40
+ },
41
+
42
+ "slots": {
43
+ "initials": {
44
+ "flexShrink": 0,
45
+ "lineHeight": "1"
46
+ },
47
+ "fallbackIcon": {
48
+ "element": "Icon",
49
+ "name": "person",
50
+ "color": "{alias.icon-secondary}",
51
+ "iconSizeByAvatarSize": {
52
+ "xs": "xs",
53
+ "sm": "xs",
54
+ "md": "sm",
55
+ "lg": "sm",
56
+ "xl": "md",
57
+ "2xl": "md",
58
+ "3xl": "lg",
59
+ "4xl": "lg"
60
+ }
61
+ },
62
+ "username": {
63
+ "fontSize": "14px",
64
+ "fontWeight": 500,
65
+ "color": "{alias.text-primary}",
66
+ "overflow": "truncate"
67
+ },
68
+ "supportingText": {
69
+ "fontSize": "12px",
70
+ "color": "{alias.text-secondary}",
71
+ "overflow": "truncate"
72
+ },
73
+ "textContainer": {
74
+ "display": "flex",
75
+ "flexDirection": "column",
76
+ "justifyContent": "center",
77
+ "gap": "{spacing.md}"
78
+ }
79
+ },
80
+
81
+ "defaults": { "shape": "circle", "size": "md" },
82
+
83
+ "accessibility": {
84
+ "role": "img",
85
+ "notes": "When aria-label is provided, the avatar circle renders with role='img' and the label. When omitted, the avatar is treated as decorative. Initials are uppercased and capped at 2 characters."
86
+ }
87
+ }