@tap-payments/os-micro-frontend-shared 0.3.253 → 0.3.255

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 (186) hide show
  1. package/build/_chunks/{chunk-J3CPRU2X.js → chunk-2FZQAWTT.js} +1 -1
  2. package/build/_chunks/{chunk-OQA73I3Z.js → chunk-2QYAHYYH.js} +1 -1
  3. package/build/_chunks/{chunk-6Q5NWGGT.js → chunk-3ARGSIW6.js} +1 -1
  4. package/build/_chunks/{chunk-RGKJLODO.js → chunk-467AH657.js} +1 -1
  5. package/build/_chunks/{chunk-F7TSJOX6.js → chunk-53FTAIGE.js} +1 -1
  6. package/build/_chunks/{chunk-24X3BEJR.js → chunk-5B3BM4HA.js} +1 -1
  7. package/build/_chunks/{chunk-4FOFVTQ2.js → chunk-5CIBM232.js} +1 -1
  8. package/build/_chunks/{chunk-IO5DNKRE.js → chunk-667MADJD.js} +1 -1
  9. package/build/_chunks/{chunk-2J4W2CAI.js → chunk-6AISJMRA.js} +1 -1
  10. package/build/_chunks/{chunk-DN3GVPG7.js → chunk-6JO37S7T.js} +1 -1
  11. package/build/_chunks/{chunk-ECXZJHTU.js → chunk-6PNJ5QPQ.js} +1 -1
  12. package/build/_chunks/{chunk-SPZGODAQ.js → chunk-6QJCOTT4.js} +1 -1
  13. package/build/_chunks/{chunk-PIO2RM2Z.js → chunk-7EBCOZWQ.js} +1 -1
  14. package/build/_chunks/{chunk-DYQCOTHJ.js → chunk-7PCMUVPG.js} +1 -1
  15. package/build/_chunks/chunk-AGUMSHB4.js +1 -0
  16. package/build/_chunks/{chunk-GPLYYFGL.js → chunk-BHQDWVM3.js} +1 -1
  17. package/build/_chunks/{chunk-5VSOWOX2.js → chunk-BLPTYUL5.js} +1 -1
  18. package/build/_chunks/{chunk-CAZWVRDN.js → chunk-BNHPA7NL.js} +1 -1
  19. package/build/_chunks/{chunk-BDL56M7A.js → chunk-BVZCPC4B.js} +1 -1
  20. package/build/_chunks/chunk-C2QLJW5U.js +1 -0
  21. package/build/_chunks/{chunk-LLOK2FPM.js → chunk-CUGWXBZN.js} +1 -1
  22. package/build/_chunks/{chunk-LBQWWXKH.js → chunk-DJB73WJX.js} +1 -1
  23. package/build/_chunks/{chunk-Y2HHEHH2.js → chunk-E7OZE7FK.js} +2 -2
  24. package/build/_chunks/{chunk-JLL4EOYJ.js → chunk-ECCMCTOM.js} +1 -1
  25. package/build/_chunks/{chunk-IUN4DEUF.js → chunk-EOEZUZCE.js} +1 -1
  26. package/build/_chunks/{chunk-4Y2COEZC.js → chunk-FEZOSRP2.js} +1 -1
  27. package/build/_chunks/{chunk-3GA46JLW.js → chunk-FXLC7QM6.js} +1 -1
  28. package/build/_chunks/{chunk-DRBZWQC7.js → chunk-G7FUKYC4.js} +1 -1
  29. package/build/_chunks/{chunk-YAO5SBRE.js → chunk-GRNUANVM.js} +1 -1
  30. package/build/_chunks/chunk-GUU2BANS.js +1 -0
  31. package/build/_chunks/{chunk-IZLK35VJ.js → chunk-IDIANGVR.js} +2 -2
  32. package/build/_chunks/chunk-IDPTEI5C.js +1 -0
  33. package/build/_chunks/chunk-IRIZPPXM.js +1 -0
  34. package/build/_chunks/{chunk-CWNDTPM5.js → chunk-J7KCH7KM.js} +1 -1
  35. package/build/_chunks/{chunk-J2QN5EF5.js → chunk-KGG6V7HL.js} +1 -1
  36. package/build/_chunks/{chunk-Q7C7A5RK.js → chunk-KP2UMMK5.js} +1 -1
  37. package/build/_chunks/{chunk-FGZVDVG2.js → chunk-LHPUWQK3.js} +1 -1
  38. package/build/_chunks/{chunk-7UHNYLD7.js → chunk-LPVUMEPP.js} +1 -1
  39. package/build/_chunks/{chunk-HKFOCHKQ.js → chunk-LSJ4DU5A.js} +1 -1
  40. package/build/_chunks/{chunk-JNIXJXK3.js → chunk-M6YVU7V3.js} +1 -1
  41. package/build/_chunks/{chunk-C4ZTNHLR.js → chunk-MZ4CDPE6.js} +1 -1
  42. package/build/_chunks/{chunk-DFH5QS2C.js → chunk-N4CCG67Z.js} +1 -1
  43. package/build/_chunks/{chunk-MVWBQNBX.js → chunk-N63SPB7J.js} +1 -1
  44. package/build/_chunks/{chunk-PGG4PIRO.js → chunk-NU6TMNMX.js} +1 -1
  45. package/build/_chunks/{chunk-ETTNSCF2.js → chunk-NUJYDXCY.js} +1 -1
  46. package/build/_chunks/{chunk-SKJZPJBN.js → chunk-OGUM23ZV.js} +1 -1
  47. package/build/_chunks/{chunk-UZIT72EG.js → chunk-OTAHIU6Z.js} +1 -1
  48. package/build/_chunks/{chunk-MAPBEJDO.js → chunk-PCY4RMGU.js} +1 -1
  49. package/build/_chunks/{chunk-JO7EW2A3.js → chunk-PGMH7MPJ.js} +1 -1
  50. package/build/_chunks/{chunk-X26MSUES.js → chunk-Q2XPHAIX.js} +1 -1
  51. package/build/_chunks/{chunk-XKMKTJWE.js → chunk-QCVRHMCL.js} +1 -1
  52. package/build/_chunks/chunk-QK77GKMF.js +2 -0
  53. package/build/_chunks/{chunk-NVNFBBM3.js → chunk-QLBJFLHC.js} +1 -1
  54. package/build/_chunks/chunk-RNBTIRD7.js +2 -0
  55. package/build/_chunks/{chunk-2QA5MXMV.js → chunk-SFQF3NEZ.js} +1 -1
  56. package/build/_chunks/{chunk-JCEHXR4N.js → chunk-SNMSU243.js} +1 -1
  57. package/build/_chunks/{chunk-OGARNN6I.js → chunk-SQ6X7XQI.js} +1 -1
  58. package/build/_chunks/{chunk-HLGWZAIE.js → chunk-T6FSTHWB.js} +1 -1
  59. package/build/_chunks/{chunk-AO5K32SU.js → chunk-TP36G4JZ.js} +1 -1
  60. package/build/_chunks/{chunk-PEV5GRRD.js → chunk-TSI47ZRD.js} +1 -1
  61. package/build/_chunks/{chunk-G5OEG7OX.js → chunk-TXVDWYET.js} +1 -1
  62. package/build/_chunks/{chunk-IXZBLV5N.js → chunk-U7BM5XPI.js} +1 -1
  63. package/build/_chunks/{chunk-6SBWV2KY.js → chunk-UDF64CIJ.js} +1 -1
  64. package/build/_chunks/{chunk-IULAG2J7.js → chunk-URDUQGSO.js} +1 -1
  65. package/build/_chunks/{chunk-CK64PBMR.js → chunk-USHKRLP3.js} +1 -1
  66. package/build/_chunks/{chunk-WUBVGS4H.js → chunk-UVXP2WAT.js} +1 -1
  67. package/build/_chunks/{chunk-YGEN2NPL.js → chunk-VKTXI56O.js} +1 -1
  68. package/build/_chunks/{chunk-ACIQTR6D.js → chunk-VTM5XAA4.js} +1 -1
  69. package/build/_chunks/{chunk-RRBFJGS6.js → chunk-WX3CZG6V.js} +1 -1
  70. package/build/_chunks/{chunk-JC7NZ5DQ.js → chunk-YQKWAH6Y.js} +1 -1
  71. package/build/_chunks/{chunk-2FRVCEJY.js → chunk-YXRP3YVA.js} +1 -1
  72. package/build/_chunks/{chunk-IQUQQVHU.js → chunk-ZDRR6O4Q.js} +1 -1
  73. package/build/_chunks/chunk-ZTZOHMJ3.js +1 -0
  74. package/build/components/ActionsDropdownMenu/index.js +1 -1
  75. package/build/components/ActiveFilterChip/index.js +1 -1
  76. package/build/components/Alert/index.js +1 -1
  77. package/build/components/AnimatedSpinnerIcon/index.js +1 -1
  78. package/build/components/AnimatedSpinnerIcon/style.d.ts +1 -1
  79. package/build/components/AppServices/index.js +1 -1
  80. package/build/components/AppServicesBar/index.js +1 -1
  81. package/build/components/AppWindowWrapper/index.js +1 -1
  82. package/build/components/AreaChart/index.js +1 -1
  83. package/build/components/Autocomplete/index.js +1 -1
  84. package/build/components/AvatarStack/index.js +1 -1
  85. package/build/components/Calender/index.js +1 -1
  86. package/build/components/Card/index.js +1 -1
  87. package/build/components/ColorPicker/index.js +1 -1
  88. package/build/components/ColumnViews/index.js +1 -1
  89. package/build/components/ConfirmDialog/index.js +1 -1
  90. package/build/components/CountryFilter/index.js +1 -1
  91. package/build/components/CountryFlag/index.js +1 -1
  92. package/build/components/CurrenciesMenu/index.js +1 -1
  93. package/build/components/CurrencyAmount/index.js +1 -1
  94. package/build/components/CurrencyAmountInput/index.js +1 -1
  95. package/build/components/CurrencySymbol/index.js +1 -1
  96. package/build/components/DateFilter/index.js +1 -1
  97. package/build/components/DeviceIcon/index.js +1 -1
  98. package/build/components/DropdownMenu/index.js +1 -1
  99. package/build/components/EmptyState/index.js +1 -1
  100. package/build/components/ErrorToast/index.js +1 -1
  101. package/build/components/FileDropbox/index.js +1 -1
  102. package/build/components/FilePreview/index.js +1 -1
  103. package/build/components/FileUploader/index.js +1 -1
  104. package/build/components/FilterDropdown/index.js +1 -1
  105. package/build/components/Filters/index.js +1 -1
  106. package/build/components/FlippingCard/index.js +1 -1
  107. package/build/components/FormSelect/index.js +1 -1
  108. package/build/components/GroupedSelect/index.js +1 -1
  109. package/build/components/HeatMap/index.js +1 -1
  110. package/build/components/InputBase/index.js +1 -1
  111. package/build/components/Inputs/index.js +1 -1
  112. package/build/components/JSONViewer/components/JSONSearchBar/styles.d.ts +2 -2
  113. package/build/components/JSONViewer/index.js +1 -1
  114. package/build/components/LeftPeekRightExpandingChip/index.js +1 -1
  115. package/build/components/ListColumnFilter/index.js +1 -1
  116. package/build/components/ListHeader/index.js +1 -1
  117. package/build/components/ListHeader/types.d.ts +2 -0
  118. package/build/components/ListLayout/index.js +1 -1
  119. package/build/components/Loader/index.js +1 -1
  120. package/build/components/Logo/index.js +1 -1
  121. package/build/components/MFWidgetLoader/index.js +1 -1
  122. package/build/components/MaskedText/index.js +1 -1
  123. package/build/components/MultiSelect/index.js +1 -1
  124. package/build/components/NestedDropdown/index.js +1 -1
  125. package/build/components/NestedMultiSelect/index.js +1 -1
  126. package/build/components/PaymentSourceFilter/index.js +1 -1
  127. package/build/components/PhoneFilter/index.js +1 -1
  128. package/build/components/PhoneInput/index.js +1 -1
  129. package/build/components/ProgressRing/index.js +1 -1
  130. package/build/components/RadioGroup/index.js +1 -1
  131. package/build/components/RangeCalender/index.js +1 -1
  132. package/build/components/RatingCount/index.js +1 -1
  133. package/build/components/ReferenceTypeFilter/index.js +1 -1
  134. package/build/components/RightLeftExpandingCenterChip/index.js +1 -1
  135. package/build/components/SearchButton/SearchButton.d.ts +2 -7
  136. package/build/components/SearchButton/constants.d.ts +37 -0
  137. package/build/components/SearchButton/icons.d.ts +3 -0
  138. package/build/components/SearchButton/index.d.ts +3 -2
  139. package/build/components/SearchButton/index.js +1 -1
  140. package/build/components/SearchButton/{styles.d.ts → style.d.ts} +173 -173
  141. package/build/components/SearchButton/type.d.ts +28 -0
  142. package/build/components/SearchInput/index.js +1 -1
  143. package/build/components/SheetViewChip/index.js +1 -1
  144. package/build/components/Sidebar/index.js +1 -1
  145. package/build/components/StatusBar/index.js +1 -1
  146. package/build/components/StatusButton/index.js +1 -1
  147. package/build/components/StatusFilter/index.js +1 -1
  148. package/build/components/StatusIcons/index.js +1 -1
  149. package/build/components/StatusLabel/index.js +1 -1
  150. package/build/components/TableCells/CustomCells/ActionCell/style.d.ts +1 -1
  151. package/build/components/TableCells/CustomCells/PayoutReportCell/style.d.ts +1 -1
  152. package/build/components/TableCells/CustomCells/StatusCell/style.d.ts +1 -1
  153. package/build/components/TableCells/index.js +1 -1
  154. package/build/components/TableReports/index.js +1 -1
  155. package/build/components/TextInput/index.js +1 -1
  156. package/build/components/Timepicker/index.js +1 -1
  157. package/build/components/ToggleView/index.js +1 -1
  158. package/build/components/Toolbar/index.js +1 -1
  159. package/build/components/ToolbarIcon/index.js +1 -1
  160. package/build/components/TreeDropdown/index.js +1 -1
  161. package/build/components/VerificationIcon/index.js +1 -1
  162. package/build/components/ViewColumnRadioFilter/index.js +1 -1
  163. package/build/components/VirtualTables/components/style.d.ts +1 -1
  164. package/build/components/VirtualTables/index.js +1 -1
  165. package/build/components/Widget/index.js +1 -1
  166. package/build/components/index.d.ts +1 -1
  167. package/build/components/index.js +1 -1
  168. package/build/components/primitives/index.js +1 -1
  169. package/build/constants/assets.d.ts +3 -0
  170. package/build/constants/index.js +1 -1
  171. package/build/datetime/components/index.js +1 -1
  172. package/build/hooks/index.js +1 -1
  173. package/build/index.js +1 -1
  174. package/build/utils/index.js +1 -1
  175. package/docs/README.md +40 -0
  176. package/package.json +2 -2
  177. package/src/components/TreeDropdown/README.md +604 -0
  178. package/src/components/primitives/README.md +507 -0
  179. package/build/_chunks/chunk-2GSUGMI7.js +0 -2
  180. package/build/_chunks/chunk-3TTOSX76.js +0 -1
  181. package/build/_chunks/chunk-BHUTYBAK.js +0 -2
  182. package/build/_chunks/chunk-CZWXKHO7.js +0 -1
  183. package/build/_chunks/chunk-DRWSBTEH.js +0 -1
  184. package/build/_chunks/chunk-M6JX6YSB.js +0 -1
  185. package/build/_chunks/chunk-UFOU3EJK.js +0 -1
  186. package/build/_chunks/chunk-UOERDOEI.js +0 -1
@@ -0,0 +1,507 @@
1
+ # `primitives` — Tier 1 Contract
2
+
3
+ The **smallest buildable UI blocks** in the library — leaf atoms that Tier 2 components and MFEs compose on top of. This file is the single source of truth for creating, modifying, naming, and styling a primitive. PRs that violate it fail `eslint-plugin-boundaries` or get caught in review.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Purpose & philosophy](#1-purpose--philosophy)
8
+ 2. [How to import a primitive](#2-how-to-import-a-primitive)
9
+ 3. [The file contract](#3-the-file-contract)
10
+ 4. [How to create a new primitive](#4-how-to-create-a-new-primitive)
11
+ 5. [How to modify an existing primitive](#5-how-to-modify-an-existing-primitive)
12
+ 6. [Naming conventions](#6-naming-conventions)
13
+ 7. [Styling conventions](#7-styling-conventions) — includes [7.3 Required theme access](#73-required-theme-access), [7.3.1 V2 palette tokens](#731-v2-palette-tokens), [7.3.2 V2 elevation tokens](#732-v2-elevation-tokens), [7.3.3 V2 spacing tokens](#733-v2-spacing-tokens), [7.3.4 V2 radius tokens](#734-v2-radius-tokens), [7.3.5 V2 blur tokens](#735-v2-blur-tokens), [7.3.6 V2 typography tokens](#736-v2-typography-tokens), [7.3.7 Frozen hex](#737-frozen-hex-no-matching-v2-token)
14
+ 8. [Structural patterns](#8-structural-patterns)
15
+ 9. [Documented restyling exceptions](#9-documented-restyling-exceptions)
16
+ 10. [Dependency isolation & enforcement](#10-dependency-isolation--enforcement)
17
+ 11. [What does not belong in a primitive](#11-what-does-not-belong-in-a-primitive)
18
+ 12. [Reference — current primitives](#12-reference--current-primitives)
19
+ 13. [Storybook](#13-storybook)
20
+ 14. [PR checklist](#14-pr-checklist)
21
+
22
+ ---
23
+
24
+ ## 1. Purpose & philosophy
25
+
26
+ A primitive is a **leaf atom**: the smallest component you can build anything on top of — one visual/interactive concept, no composition of other primitives, no domain knowledge, no shared hook modules, no data fetching.
27
+
28
+ **Litmus test:** if you remove a piece and what remains is still a complete, usable UI atom on its own, that piece either stays (it's part of the same concept) or moves to Tier 2 (it's a second concept). If Tier 2 would have to reimplement behavior because the primitive bundled too much or imported a sibling, the boundary is wrong.
29
+
30
+ Three properties every rule below protects:
31
+
32
+ | Property | Meaning |
33
+ | ---------------------- | --------------------------------------------------------------------------------------------------------- |
34
+ | **Predictable weight** | Importing a primitive never pulls in another primitive, a shared hook module, or domain logic. |
35
+ | **Always restyleable** | Consumers reshape it with `styled()` or `sx` — never fork it. See [7.1](#71-the-restyling-contract). |
36
+ | **Stable names** | Twelve MFEs share these names. One convention beats a locally clever one. See [6](#6-naming-conventions). |
37
+
38
+ This folder is **isolated** from other `src/components` composites. MFEs import it via `@components/primitives`; Tier 2 components compose these primitives later.
39
+
40
+ **Legacy is discovery only.** `src/components` shows _which_ atoms to extract, not _how_ to structure them. Match legacy _behavior_; discard legacy _structure_ (`Wrapper`, `Styled*` prefixes, `Base` suffixes, bare boolean names).
41
+
42
+ ---
43
+
44
+ ## 2. How to import a primitive
45
+
46
+ `primitives/index.ts` is the **only** public entry point for this folder. No per-primitive paths.
47
+
48
+ ```tsx
49
+ import { Button, Input, Icon, buttonClassNames, type ButtonProps, type ButtonClassNames } from '@components/primitives'
50
+ ```
51
+
52
+ | Import | Allowed |
53
+ | ----------------------------------------- | ----------------------------------------------------------------- |
54
+ | `@components/primitives` | Yes — default export, types, and `{name}ClassNames` |
55
+ | `@components` | Yes — when the name is re-exported from `src/components/index.ts` |
56
+ | `@components/primitives/Button` | No — use the barrel; enforced by `no-restricted-imports` |
57
+ | `@components/primitives/Button/style` | No — deep import |
58
+ | `@components/primitives/Button/constants` | No — deep import |
59
+ | `@components/primitives/Button/Button` | No — bypasses public API |
60
+ | `@v2/components/primitives` | No — this folder moved; use `@components/primitives` |
61
+
62
+ Barrel rules:
63
+
64
+ - Each primitive's `index.ts`: `export { default }` + explicit `export type { … }` + `export { <name>ClassNames }` (internal wiring — consumers use the barrel only).
65
+ - `primitives/index.ts`: explicit named re-exports only — **never `export *`**.
66
+
67
+ ---
68
+
69
+ ## 3. The file contract
70
+
71
+ **Five required files per primitive** (+ optional `icons.tsx`).
72
+
73
+ ```
74
+ primitives/Button/
75
+ ├── Button.tsx # implementation
76
+ ├── constants.ts # classNames, lookup maps, values imported from more than one file
77
+ ├── style.ts # styled Parts only; imports maps from ./constants
78
+ ├── type.ts # props + public types
79
+ └── index.ts # public API — export { buttonClassNames }
80
+ ```
81
+
82
+ | File | Contains | Does NOT contain |
83
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
84
+ | **`constants.ts`** | `<name>ClassNamePrefix`, `<name>ClassNames`, `<Primitive>ClassNames` type, lookup maps, values imported from more than one file | `styled()` components, React nodes |
85
+ | **`type.ts`** | Props and component-specific public types | const values, classNames |
86
+ | **`style.ts`** | `styled()` Parts/shells, `get<What>()` helpers importing from `./constants` | `as const` lookup tables |
87
+ | **`<Name>.tsx`** | Component logic, JSX, private handlers | lookup maps — those stay in `./constants` |
88
+ | **`icons.tsx`** (when needed) | Inline `<svg>` exports only | components, hooks, styled parts, lookup maps |
89
+
90
+ A named export in `constants.ts` must be **imported from more than one file**. A value imported by a single module is inlined there (with a freeze comment when it is a hex that has no v2 token — [7.3.7](#737-frozen-hex-no-matching-v2-token)). Repeated references inside that one file still count as a single import.
91
+
92
+ | Forbidden | Goes instead |
93
+ | -------------------------------------------- | ------------------------------------------------------------- |
94
+ | `hooks/` | Tier 2 — or private logic inline in `.tsx` |
95
+ | `utils.ts` | Private function in `.tsx`, or global `utils/` if shared |
96
+ | lookup maps in `style.ts` / `.tsx` | **`constants.ts`** |
97
+ | a `constants.ts` export imported by one file | inline the value in that file |
98
+ | sub-`components/` | Styled Parts in `style.ts` |
99
+ | `types.ts` | `type.ts` — singular, always |
100
+ | story/test files | `.storybook/stories/components/primitives/<Name>.stories.tsx` |
101
+ | inline `<svg>` in `.tsx` | **`icons.tsx`** on the owning primitive |
102
+ | `@constants` icon URL maps / asset refs | **`constants.ts`** |
103
+
104
+ **`icons.tsx` (SVG only):** when a primitive renders its own inline SVG markup, add `icons.tsx` (e.g. `Checkbox/icons.tsx`, `MenuItem/icons.tsx`). Export `<svg>` React nodes only — no lookup maps, no `@constants` URL maps, no styled Parts. Asset URL maps belong in **`constants.ts`**. **Internal only** — do not re-export from `primitives/index.ts`.
105
+
106
+ **Promotion to Tier 2** when a folder needs any of: a dedicated hook/util file, composition of two concepts (field + helper text, input + form wiring), or imports from another primitive. Tier 2 lives outside this folder, same five required files plus optional `hooks/` and `utils.ts`. Tier 2 imports primitives via `@components/primitives` only.
107
+
108
+ ---
109
+
110
+ ## 4. How to create a new primitive
111
+
112
+ 1. **Discover** — find legacy usages; list what varies (sizes, variants, states). Ignore legacy structure.
113
+ 2. **Confirm scope** — one concept, no sibling primitive needed, no shared hook module, no domain meaning. Otherwise build Tier 2.
114
+ 3. **Scaffold** five files under `src/components/primitives/<Name>/` (+ optional `icons.tsx`).
115
+ 4. **`type.ts`** — extend a base type that carries `className` (HTML attributes or MUI props); declare `sx` explicitly; JSDoc non-obvious props.
116
+ 5. **`constants.ts`** — `<name>ClassNamePrefix`, `<name>ClassNames` (built from prefix + slot), lookup maps, and values imported from more than one file. Inline a value that is imported by only one module.
117
+ 6. **`style.ts`** — one bare `<Name>Styled` (interactive/`ref` target), optional shell/Parts; import maps from `./constants`. Tokens come from required `theme.v2` ([7.3](#73-required-theme-access)).
118
+ 7. **`<Name>.tsx`** — `forwardRef` + `memo`, wire slot classNames via `joinClassNames` from `@utils`, JSDoc with restyling note, `data-testid="<Name>"`.
119
+ 8. **`index.ts`** — default export + types from `./type` + `export { <name>ClassNames, type <Primitive>ClassNames }` from `./constants`.
120
+ 9. **Register** in `primitives/index.ts` (append, explicit named exports).
121
+ 10. **Storybook** — see [13](#13-storybook); include `{Name}Styled` and `{Name}StyledSlots`.
122
+ 11. **Verify** — `yarn lint && yarn build:types`.
123
+
124
+ `Button/` is the reference implementation for all five files.
125
+
126
+ ---
127
+
128
+ ## 5. How to modify an existing primitive
129
+
130
+ | Change | Where |
131
+ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------- |
132
+ | New prop | `type.ts` (+ JSDoc); style-only props as `$`-prefixed transient props in `style.ts` |
133
+ | Visuals | `style.ts` only — never inline `sx` in `.tsx` as the source of truth. Required theme access: [7.3](#73-required-theme-access) |
134
+ | New variant/size | Extend union in `type.ts`; add lookup-map entry in **`constants.ts`**; consume in `style.ts` |
135
+ | New slot / Part | Add to `<name>ClassNames` in **`constants.ts`**; wire in `.tsx` via `joinClassNames` |
136
+ | Const value | **`constants.ts` only** — never add consts to `style.ts` or `.tsx` |
137
+ | Behavior | Private handler/function in `.tsx`; if it outgrows the file → Tier 2 |
138
+ | Breaking change | Update call sites, stories, [reference table](#12-reference--current-primitives), call out in PR — 12 MFEs consume this |
139
+ | Structural change | Re-verify restyling — shell changes move where `className`/`sx` land |
140
+
141
+ ---
142
+
143
+ ## 6. Naming conventions
144
+
145
+ ### Quick reference
146
+
147
+ | What | Convention | Example |
148
+ | -------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------- |
149
+ | Folder / component file | `PascalCase`, match exactly | `Button/` · `Button.tsx` |
150
+ | Props file | `type.ts` — never `types.ts` | `type.ts` |
151
+ | Constants file | `constants.ts` — all const values + slot classNames | `constants.ts` |
152
+ | Slot classNames object | `<name>ClassNames` — camelCase, `as const` | `buttonClassNames` |
153
+ | Slot className prefix | `<name>ClassNamePrefix` in **`constants.ts`** — `os-shared-<Primitive>` | `buttonClassNamePrefix` |
154
+ | Slot className string | `` `${prefix}-<slot>` `` — never repeat the prefix per slot | `` `${buttonClassNamePrefix}-startIcon` `` |
155
+ | Slot classNames type | `<Primitive>ClassNames` in **`constants.ts`** — `(typeof xClassNames)[keyof …]` | `ButtonClassNames` in `constants.ts` |
156
+ | Lookup map in constants | `camelCase` `as const` — e.g. `sizeStyles`, `variantStyles` | `sizeStyles` in `Button/constants.ts` |
157
+ | Class composition | `joinClassNames(...)` from `@utils` | `joinClassNames(buttonClassNames.root, className)` |
158
+ | Props interface | `<Name>Props` | `ButtonProps` |
159
+ | Public sub-type | `<Name><Concept>` | `ButtonVariant`, `InputSize` |
160
+ | Story file | `<Name>.stories.tsx` under `.storybook/stories/components/primitives/` | `Button.stories.tsx` |
161
+ | Component name | `PascalCase`, no `Base` suffix | `Input` not `InputBase` |
162
+ | Boolean prop | `is` / `has` / `can` / `should` — unless native HTML or established MUI prop | `isLoading`; inherited `disabled`, `fullWidth` |
163
+ | Event prop / handler | `on` / `handle` | `onChange` · `handleKeyDown` |
164
+ | Interactive styled element | `<Name>Styled` — bare, one per primitive, `ref` target | `ButtonStyled` |
165
+ | Outer shell | `<Name><Purpose>Styled` — gets `className`/`sx` when rendered | `CheckboxLabelStyled`, `InputFieldStyled` |
166
+ | Inner Part | `<Name><Role>Styled` — no consumer `className`/`sx`/`ref` | `ChipTextStyled`, `ProgressBarFillStyled` |
167
+ | Style transient prop | `$` prefix + `shouldForwardProp` | `$variant`, `$size` |
168
+ | Style lookup / helper | `get<What>()` in `style.ts` · lookup maps in **`constants.ts`** | `getFieldHeight` · `sizeStyles` |
169
+ | Value sets | string union or `as const` object — never TS `enum` | `'sm' \| 'md'` |
170
+
171
+ ### Styled-export decision rule
172
+
173
+ 1. Interactive/measurable DOM node? → bare `<Name>Styled`.
174
+ 2. Outer shell grouping the interactive node with something else? → `<Name><Purpose>Styled`.
175
+ 3. Everything else → Part: `<Name><Role>Styled`.
176
+
177
+ ### Banned
178
+
179
+ - `Styled<Name>` prefix · `<Name>Component` · `<Name>Root` · bare `Wrapper`/`Container`
180
+ - Trailing `export { X }` re-exports in `style.ts` — use `export const`
181
+ - `export *` in any `index.ts`
182
+ - Plain MUI `Box`/`span` with inline `sx` in `.tsx` need no `Styled` name (e.g. Button loading overlay)
183
+
184
+ Prefer full words (`Button`, not `Btn`). Callback props describe events (`onSave`), not instructions (`save`). Internal-only props use `@internal` JSDoc, not `_` prefixes.
185
+
186
+ ---
187
+
188
+ ## 7. Styling conventions
189
+
190
+ MUI + Emotion. Reusable styles in `style.ts` via `styled()`; one-off overrides via `sx`.
191
+
192
+ ### 7.1 The restyling contract
193
+
194
+ Every primitive must work with all three:
195
+
196
+ ```typescript
197
+ const Compact = styled(Button)({ height: 24, padding: '0 8px' })
198
+ ```
199
+
200
+ ```tsx
201
+ <Button sx={{ marginTop: 1 }} />
202
+ ```
203
+
204
+ ```tsx
205
+ import { buttonClassNames, Button } from '@components/primitives'
206
+
207
+ const ButtonStyled = styled(Button)({
208
+ [`& .${buttonClassNames.startIcon}`]: { marginRight: 4 },
209
+ })
210
+ ```
211
+
212
+ Requirements:
213
+
214
+ 1. Props type includes `className` (HTML or MUI base type).
215
+ 2. Explicit `sx?: SxProps<Theme>`, forwarded to the correct node.
216
+ 3. `forwardRef` on the interactive/measurable node — bare `<Name>Styled` — unless listed in [9](#9-documented-restyling-exceptions).
217
+ 4. Never destructure away `className` and drop it.
218
+ 5. **With a shell:** `className`/`sx` → shell (perceived box); `ref` → bare `<Name>Styled`.
219
+ 6. **Slot classNames:** every rendered DOM node gets its slot class from `<name>ClassNames` in `constants.ts`. Root/shell merges with consumer `className` via `joinClassNames(slotClass, className)`.
220
+
221
+ Verify in Storybook:
222
+
223
+ - `styled(YourPrimitive)({ border: '2px solid red' })` — border on perceived root ([13](#13-storybook)).
224
+ - `styled(YourPrimitive)({ [\`& .\${classNames.slot}\`]: { … } })` — targets inner Part.
225
+
226
+ ### 7.2 `styled()` vs `sx` vs inline `style`
227
+
228
+ | Tool | Use for |
229
+ | ------------ | --------------------------------------------------------------------------- |
230
+ | `styled()` | Reusable variant styles in `style.ts` |
231
+ | `sx` | Per-instance / consumer overrides — every primitive accepts and forwards it |
232
+ | `style={{}}` | **Never** — bypasses theme, breaks CSP nonces, wins specificity over `sx` |
233
+
234
+ ### 7.3 Required theme access
235
+
236
+ Primitives depend on the **custom app theme** created by `appTheme` in `src/theme`. The MUI module augmentation declares `v2: ThemeV2` on both `Theme` and `ThemeOptions`, and `appTheme` always mounts the complete v2 token set. Read `theme.v2` directly. Tests, Storybook, and any other primitive renderer must provide `appTheme`. If a required design token is missing, add it to the v2 theme contract before consuming it.
237
+
238
+ ### 7.3.1 V2 palette tokens
239
+
240
+ V2 colors live on `theme.v2.palette` (mounted by `appTheme`), not `theme.palette`. `theme.palette.foreground` is a type error. Non-primitive code must not import `@v2/theme` (enforced by `no-restricted-imports`).
241
+
242
+ Destructure the slot once at the top of the `styled()` callback — do not sprinkle `theme.v2.palette…` through `style.ts`.
243
+
244
+ Prefer the **original color scales** (`base`, `grey`, `brand`, `success`, `error`, `warning`) over semantic aliases that point at the same hex (`foreground`, `background`, `border`, `interactive`, `status`). Use a semantic group only when no scale key matches. `brand` lives at `palette.brand`, not under `interactive`.
245
+
246
+ ```ts
247
+ export const ButtonStyled = styled(ButtonBase)(({ theme }) => {
248
+ const { base, grey, brand } = theme.v2.palette
249
+ return {
250
+ color: grey[500],
251
+ backgroundColor: base.white,
252
+ border: `1px solid ${grey[100]}`,
253
+ // brand[500], not interactive.primary.default or foreground.brand
254
+ '&:hover': { color: brand[500] },
255
+ }
256
+ })
257
+ ```
258
+
259
+ ### 7.3.2 V2 elevation tokens
260
+
261
+ V2 shadows live on the required `theme.v2.elevation` slot, not `theme.shadows`. Keys and CSS values live in `src/theme/_v2/elevation.ts`. The v2 slot is built in `src/theme/_v2/theme.ts` and mounted by `appTheme`.
262
+
263
+ ```ts
264
+ boxShadow: theme.v2.elevation[300],
265
+ ```
266
+
267
+ ### 7.3.3 V2 spacing tokens
268
+
269
+ V2 spacing lives on the required `theme.v2.spacing` slot, not `theme.spacing`. The function is pixel-in, pixel-out (`src/theme/_v2/utils.ts`): `spacing(8)` returns `'8px'`. It is **not** MUI's 8px multiplier — `theme.v2.spacing(1)` is `1px`, not `8px`.
270
+
271
+ Call the function directly at the use site. Do not extract a `(factor) => \`${factor}px\`` stand-in.
272
+
273
+ ```ts
274
+ export const ButtonStyled = styled(ButtonBase)(({ theme }) => ({
275
+ gap: theme.v2.spacing(8),
276
+ padding: `${theme.v2.spacing(8)} ${theme.v2.spacing(18)}`,
277
+ }))
278
+ ```
279
+
280
+ ### 7.3.4 V2 radius tokens
281
+
282
+ V2 radii live on the required `theme.v2.radius` slot, not `theme.shape.borderRadius`. Same pixel-in, pixel-out helper as spacing: `radius(4)` returns `'4px'`. Call it directly at the use site.
283
+
284
+ ```ts
285
+ export const ButtonStyled = styled(ButtonBase)(({ theme }) => ({
286
+ borderRadius: theme.v2.radius(4),
287
+ }))
288
+ ```
289
+
290
+ Leave `borderRadius: '50%'` and other non-pixel radii as-is — `radius()` only produces `px` strings.
291
+
292
+ ### 7.3.5 V2 blur tokens
293
+
294
+ V2 blur lives on the required `theme.v2.blur` slot as an object (same shape as elevation), not a pixel-in function. Keys and CSS values live in `src/theme/_v2/blur.ts`. Values are CSS `filter` / `backdrop-filter` strings. The v2 slot is built in `src/theme/_v2/theme.ts` and mounted by `appTheme`. Do not invent keys outside that map.
295
+
296
+ ```ts
297
+ backdropFilter: theme.v2.blur[12],
298
+ ```
299
+
300
+ ### 7.3.6 V2 typography tokens
301
+
302
+ V2 type lives on the required `theme.v2.typography` slot, not `theme.typography`. Families, weights, and the Tap DS type scale (Heading–Micro) live in `src/theme/_v2/typography.ts`. Each role is a CSS style object (`fontFamily`, `fontSize`, `lineHeight`, `fontWeight`, `letterSpacing`). Spread a role at the use site, or pass its path to the Typography primitive as `textVariant`. The primitive resolves text variants and named `weight` values against the active `theme.v2.typography`; an omitted or invalid `textVariant` uses `body.default.regular`. MUI's `variant` defaults to `subtitle2`. Do not invent roles or weights the scale does not define.
303
+
304
+ ```ts
305
+ <Typography textVariant="body.large.semibold" />
306
+ <Typography textVariant="body.large.semibold" weight="bold" sx={{ fontSize: 18 }} />
307
+
308
+ export const LabelStyled = styled('span')(({ theme }) => ({
309
+ ...theme.v2.typography.textVariants.body.default.medium,
310
+ color: theme.v2.palette.grey[900],
311
+ }))
312
+ ```
313
+
314
+ Standalone weight: `theme.v2.typography.fontWeightBold` (600). Do not use legacy `theme.typography`.
315
+
316
+ ### 7.3.7 Frozen hex (no matching v2 token)
317
+
318
+ Same-hex v1 colours map to `theme.v2.palette` scale tokens (`base`, `grey`, `brand`, `success`, `error`, `warning`). When a spec hex has **no equal** v2 scale value, freeze the literal — do not keep the v1 theme key, and do not pick a nearby token as a silent substitute.
319
+
320
+ If the hex is **imported from more than one file**, name it in `constants.ts` and comment the source plus why it is not a v2 token. Exception by explicit decision: `FieldFeedback` feedback colours resolve from v2 scale tokens (`error[500]`, `warning[500]`, `success[700]`, `brand[500]`) instead of frozen hexes.
321
+
322
+ If the hex is imported by **one file only**, leave it at the call site with the same comment — even when that file references it more than once:
323
+
324
+ ```ts
325
+ // Off-track grey — no equal v2 scale hex (`grey[200]` is `#E4E6E9`).
326
+ backgroundColor: '#D9D9D9',
327
+ ```
328
+
329
+ If Figma already names a missing scale step the primitive needs, add that token to the v2 theme contract ([7.3](#73-required-theme-access)) instead of freezing.
330
+
331
+ ### 7.4–7.7 Other rules
332
+
333
+ - **`$` props** — style-only, filtered via `shouldForwardProp` (see `ChipStyled` in `Chip/style.ts`).
334
+ - **Theme tokens** — primitives use `theme.v2.palette` / `theme.v2.typography` / `theme.v2.spacing` / `theme.v2.radius` / `theme.v2.elevation` / `theme.v2.blur`; otherwise `theme.breakpoints` over raw `@media`. See [7.3](#73-required-theme-access).
335
+ - **`style.ts` contents** — styled Parts and `get<What>()` helpers only. Import lookup maps from `./constants`. No lookup tables, and no named constants that are imported by only one file. No MUI `name`/`slot` or slot-class maps. No business logic.
336
+ - **Variants** — lookup object in **`constants.ts`**, not `if`/`else` chains. Responsive via `theme.breakpoints.*` in `style.ts`.
337
+ - **Icons / decorative affordances** — no inline `<svg>` in `<Name>.tsx`. Put inline SVG in `icons.tsx` ([3](#3-the-file-contract)). Asset URLs and icon src maps go in **`constants.ts`**. For a standalone icon atom, use the **`Icon`** primitive (`src` + `alt`). Do not import `Icon` from another primitive — rebuild a local Part instead.
338
+
339
+ ---
340
+
341
+ ## 8. Structural patterns
342
+
343
+ Three shapes cover every primitive:
344
+
345
+ **Simple leaf** — `className`/`sx`/`ref` on one node:
346
+
347
+ `Button`, `Icon`, `Switch`, `Skeleton`, `Avatar`, `Divider`, `Collapse`, `Badge`, `Slider`, `CopyButton`, `Typography`, `RadioButton`, `Backdrop`, `ProgressBar`, `Chip`, `Accordion`, `Dialog`, `MenuItem`, `ToggleButtons`, `LazyImage`, `FieldFeedback`, `ScrollLoader`.
348
+
349
+ **Shell + interactive** — `className`/`sx` on shell, `ref` on inner node:
350
+
351
+ `Checkbox`, `CircularProgress`, `Input`, `Textarea`, `Menu` (see [9](#9-documented-restyling-exceptions) for `Input` / `Textarea` / `Menu` `ref`).
352
+
353
+ **Inner Parts** — styled pieces with no public restyling surface:
354
+
355
+ `ProgressBarFillStyled`, `ChipTextStyled`, `ChipCopyButtonStyled`, `MenuItemIndicatorSlotStyled`, `ToggleButtonsButtonStyled`, `FieldFeedbackMessageStyled`, `FieldFeedbackIconStyled`, `CircularProgressLabelTextStyled`, etc.
356
+
357
+ Add a shell only when the interactive node cannot carry the perceived bounding box (label beside checkbox, border around field chrome).
358
+
359
+ ---
360
+
361
+ ## 9. Documented restyling exceptions
362
+
363
+ Intentional deviations from [7.1](#71-the-restyling-contract). Do not "fix" without updating this table and the component JSDoc.
364
+
365
+ | Primitive | Exception | Why |
366
+ | ------------------------------ | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
367
+ | `Tooltip` | No `forwardRef`; `className`/`sx` → popper bubble | MUI clones child and owns positioning — no owned interactive node |
368
+ | `Checkbox`, `CircularProgress` | With label shell: `ref` on bare `<Name>Styled`; `className`/`sx` on shell | Shell is perceived box; inner node stays interactive |
369
+ | `Input` | `ref` → native `<input>` via `inputRef`; `className`/`sx` → `InputFieldStyled` | MUI `Input` root is a `<div>` — ref there breaks `.focus()` / form libs |
370
+ | `Textarea` | `ref` → native `<textarea>` via `inputRef`; `className`/`sx` → `TextareaFieldStyled` | Same MUI `InputBase` root issue as `Input` |
371
+ | `Menu` | `ref` / `className` → `MenuStyled` (Popper); `sx` → `MenuPanelStyled` | `className` lands on Popper with the `popper` slot; panel chrome via `sx` / `panelProps` |
372
+ | `LazyImage` | `ref` → `<img>`; `className`/`sx` → lazy-load wrapper | Library owns the wrapper node — restyle the perceived box, ref the image |
373
+
374
+ ---
375
+
376
+ ## 10. Dependency isolation & enforcement
377
+
378
+ ```mermaid
379
+ flowchart LR
380
+ tier2["Tier 2 components"] --> primitives
381
+ primitives --> foundation["shared utils, constants, theme, MUI"]
382
+ ```
383
+
384
+ | From | May import | May not import |
385
+ | ------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
386
+ | `components/primitives/*` | `@utils`, `@constants`, global `src/types`, `@v2/theme`, MUI, `./icons` | another primitive's component/`index.ts`/other files; Tier 2; shared hook module (`src/hooks/*`) |
387
+
388
+ **React hooks inside a primitive are fine** (`useState`, `useEffect`). The ban is on importing **shared hook modules** that carry app/domain state.
389
+
390
+ **No primitive imports another primitive** — not via `index.ts`, not wrapped in `styled()`. When two primitives share an affordance (e.g. `Chip` copy icon vs `CopyButton`), each owns local asset refs in **`constants.ts`** + a local styled Part, promote pure logic to `@utils`, or compose both in Tier 2.
391
+
392
+ Enforced by `eslint-plugin-boundaries` and `no-restricted-imports` in [`eslint.config.js`](../../../eslint.config.js):
393
+
394
+ - Files classified by path (`primitive` before `component` — first match wins).
395
+ - Primitives may only depend on `type` / `util` / `constant` elements (plus same-folder `./style`, `./type`).
396
+ - Tier 2 `component` elements may import primitives via their `index.ts`.
397
+ - Naming and styling rules are review-only — see [14](#14-pr-checklist).
398
+
399
+ ---
400
+
401
+ ## 11. What does not belong in a primitive
402
+
403
+ Promote to Tier 2 (outside `src/components/primitives/`) instead:
404
+
405
+ | Does not belong | Example |
406
+ | ----------------------------------- | ---------------------------------------------------------------------------------- |
407
+ | Helper / error / hint **text** | `Input` has `error` boolean (border only); message text is Tier 2 |
408
+ | Form-library wiring | `react-hook-form` `Controller` → `RHF*` Tier 2 component |
409
+ | Domain formatting / validation | Currency, IBAN, merchant lookups |
410
+ | Data fetching / shared domain hooks | API calls, global store |
411
+ | Two independent concepts composed | Field + helper + clear button |
412
+ | Import of another primitive | Rebuild locally or compose in Tier 2 ([10](#10-dependency-isolation--enforcement)) |
413
+ | Auto-generated `id` | Caller passes `id` for label/`aria-*` wiring |
414
+ | Outer layout margins | Consumers position with `sx` |
415
+
416
+ **Known boundary — `Input` floating label:** the optional `label` prop renders floating-label chrome (`FormControl` shell) inside the primitive today. The agreed direction is to shrink `Input` to the bare bordered field and move floating-label layout to Tier 2 when that component is built. Until then, document any change here and in the reference table.
417
+
418
+ ---
419
+
420
+ ## 12. Reference — current primitives
421
+
422
+ 28 primitives exported from [`index.ts`](./index.ts).
423
+
424
+ | Primitive | Interactive (`ref`) | Shell | Parts / inline | Notes |
425
+ | ------------------ | -------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
426
+ | `Button` | `ButtonStyled` | — | `content`, `startIcon`, `endIcon`, `loading`, `loadingSpinner` | `constants.ts`; export `buttonClassNames`; `$variant`, `$size`, `$fullWidth`, `$isLoading` |
427
+ | `Icon` | `IconStyled` | — | — | `constants.ts`; `root`; single `<img>` |
428
+ | `Checkbox` | `CheckboxStyled` | `CheckboxLabelStyled` | `control`, `label` | `constants.ts`; `root` (label shell), `control`, `label`; `icons.tsx` |
429
+ | `Switch` | `SwitchStyled` | — | — | `constants.ts`; `root`; MUI `Switch` |
430
+ | `Typography` | `TypographyStyled` | — | — | `constants.ts`; `root`; `$textVariant`, `$weight`; MUI `variant` defaults to `subtitle2`; MUI `noWrap` for single-line ellipsis; omitted/invalid `textVariant` → `body.default.regular` |
431
+ | `RadioButton` | `RadioButtonStyled` | — | — | `constants.ts`; `root`; `icons.tsx` |
432
+ | `CircularProgress` | `CircularProgressStyled` | `CircularProgressLabelStyled` | `progress`, `labelOverlay`, `labelText` | `constants.ts`; `root`, `progress`, `labelOverlay`, `labelText`; `hasLabel` when determinate |
433
+ | `Tooltip` | — | — | `popper`, `tooltip`, `arrow`, `content`, `title`, `supportingText`, `wrapper` | `constants.ts`; optional `supportingText`; `className` → popper ([9](#9-documented-restyling-exceptions)) |
434
+ | `Skeleton` | `SkeletonStyled` | — | — | `constants.ts`; `root`; defaults `rounded` / `16` |
435
+ | `Avatar` | `AvatarStyled` | — | — | `constants.ts`; `root`; `avatarSizeConfig`; `$size`; `$color`; always circular |
436
+ | `Divider` | `DividerStyled` | — | — | `constants.ts`; `root`; `$type` (`default` Gray/100, `subtle` `border.subtle`); 1px; default `horizontal` |
437
+ | `Collapse` | `CollapseStyled` | — | — | `constants.ts`; `root`; defaults `unmountOnExit` / `300` ms |
438
+ | `Badge` | `BadgeStyled` | — | — | `constants.ts`; `root`; `$color` (legacy colours + Figma `active` / `inactive` / `highlight`); `$compact` / `$round` / `$pointer`; Micro/Bold; Space/2×Space/4; Radius/48 |
439
+ | `Backdrop` | `BackdropStyled` | — | portal in `.tsx` | `constants.ts`; `root`; `invisible` default `true` (popover/dropdown click-catcher, no fill/no blur, `zIndex` 999); `invisible={false}` → `#000` at 30% + `hasBlur` → `theme.v2.blur[6]`, `zIndex` 1; `open` optional (default `true`) + `onClose` |
440
+ | `CopyButton` | `CopyButtonStyled` | — | — | `constants.ts`; `root`; default `feedbackMs` 2000; `$copied` |
441
+ | `ProgressBar` | `ProgressBarStyled` | — | `fill`, `text` | `constants.ts`; `root`, `fill`, `text`; `$value`, `$isDecimal` |
442
+ | `Chip` | `ChipStyled` | — | `text`, `copyButton`, `icon` | `constants.ts`; `chipVariantStyles`; own copy Part — not `CopyButton` ([10](#10-dependency-isolation--enforcement)) |
443
+ | `Slider` | `SliderStyled` | — | — | `constants.ts`; `root`; `onChange: (value: number) => void` |
444
+ | `Input` | native `<input>` | `InputFieldStyled` | `label`, `input`, `loadingIcon`, `validatedIcon` | `constants.ts`; `root`, `label`, `input`, `loadingIcon`, `validatedIcon`; `inputFieldHeights`, Safari/number guards; `required` asterisk via MUI, `loading` needs label, `validated` works with/without label (`disabled` > `loading` > `validated`); `disableFocusRing` kills normal focus ring, `Mui-error`/`Mui-warning` rings always apply, error wins ties; `inputStateClasses` merged into `inputClassNames` (slots + states, separated by comment), selected via `:where()` |
445
+ | `Textarea` | native `<textarea>` | `TextareaFieldStyled` | `label`, `textarea` | `constants.ts`; `root`, `label`, `textarea`; static in-field label (no shrink); labelled min-height 96; Space/32 top + Space/8 sides; `required` asterisk |
446
+ | `Accordion` | `AccordionStyled` | — | `summary`, `content`, `labelStartAdornment`, `labelContent`, `labelEndAdornment`, `expandIcon` | `constants.ts`; `icons.tsx`; optional `expanded`/`onChange` (controlled) or `defaultExpanded` (uncontrolled); chevron separate from label adornments |
447
+ | `Menu` | `MenuStyled` | `MenuPanelStyled` | `popper`, `panel` | `constants.ts`; `popper`, `panel`; scroll-shadow after 12 items; see §9 |
448
+ | `Dialog` | `DialogStyled` | — | `title`, `divider`, `content`, `actions` | `constants.ts`; `root`, `title`, `divider`, `content`, `actions`; heading draws a local divider Part — not `Divider` ([10](#10-dependency-isolation--enforcement)); paper 394px unless `fullWidth` |
449
+ | `MenuItem` | `MenuItemStyled` | — | `indicator`, `content`, `disabled` | `constants.ts`; `menuItemIndicatorVariants`; `icons.tsx`; `startAdornment` overrides indicator |
450
+ | `ToggleButtons` | `ToggleButtonsStyled` | — | `button` | `constants.ts`; `root`, `button`; `$isActive`; `$type` (`text` / `icon`); `$size` (`md` 32px / `sm` 24px); Elevation/100 |
451
+ | `LazyImage` | native `<img>` via library | — | `root` (wrapper) | `constants.ts`; `lazyImageEffectAssets` |
452
+ | `FieldFeedback` | `FieldFeedbackStyled` | — | `icon`, `message` | `constants.ts`; `style.ts` `getFieldFeedbackColors` (v2 tokens `error[500]`, `warning[500]`, `success[700]`, `brand[500]`); `$feedbackType` |
453
+ | `ScrollLoader` | `ScrollLoaderStyled` | — | — | `constants.ts`; `root`; near-bottom offset 50; `loadMoreItems` + pagination props |
454
+
455
+ ---
456
+
457
+ ## 13. Storybook
458
+
459
+ Path: `.storybook/stories/components/primitives/<Name>.stories.tsx`
460
+
461
+ ```tsx
462
+ import { Avatar } from '@components/primitives'
463
+
464
+ const meta: Meta<typeof Avatar> = {
465
+ title: 'Primitives/Avatar',
466
+ component: Avatar,
467
+ tags: ['autodocs'],
468
+ parameters: { layout: 'padded', controls: { expanded: true } },
469
+ }
470
+ ```
471
+
472
+ Required stories:
473
+
474
+ - **`Default`** — driven by `args`
475
+ - **Variant/size matrix** — use `Story*` helpers from `@storybook-utils`
476
+ - **`{Name}Styled`** — `const ButtonStyled = styled(Button)({ border: '2px solid red' })` proof on the perceived root
477
+ - **`{Name}StyledSlots`** — `const ButtonStyledSlots = styled(Button)({ [\`& .\${buttonClassNames.startIcon}\`]: { … } })` proof
478
+ - **Meaningful states** — disabled, error, loading, indeterminate, etc.
479
+
480
+ ---
481
+
482
+ ## 14. PR checklist
483
+
484
+ **Structure**
485
+
486
+ - [ ] One concept, no shared hook module, no domain logic — otherwise Tier 2
487
+ - [ ] Five files (`constants.ts` required); optional `icons.tsx`; no `hooks/`, `utils.ts`, sub-`components/`
488
+ - [ ] Lookup maps and values imported from more than one file live in `constants.ts`; a value imported by one file is inlined there
489
+ - [ ] `<name>ClassNames` exported from `index.ts` + barrel
490
+ - [ ] Registered in [`index.ts`](./index.ts) with explicit exports (no `export *`)
491
+ - [ ] Zero imports from another primitive folder
492
+ - [ ] Consumers import via `@components/primitives` or `@components` (no per-primitive paths, no `@v2/components/primitives`)
493
+
494
+ **Naming & styling**
495
+
496
+ - [ ] Names match [6](#6-naming-conventions) — `PascalCase`, `<Name>Props`, `$` transient props, `os-shared-*` slot classes
497
+ - [ ] Slot classNames wired on every DOM node; root uses `joinClassNames`
498
+ - [ ] `ref` / `className` / `sx` routed correctly; red-border `styled()` test passes
499
+ - [ ] No inline `style={{}}`; theme tokens where available; lookup maps in `constants.ts`
500
+ - [ ] Tokens come from required `theme.v2` slots ([7.3](#73-required-theme-access)). Palette: `theme.v2.palette` ([7.3.1](#731-v2-palette-tokens)). Typography: `theme.v2.typography` ([7.3.6](#736-v2-typography-tokens)). Spacing: `theme.v2.spacing(n)` — `n` is pixels, not MUI units ([7.3.3](#733-v2-spacing-tokens)). Radius: `theme.v2.radius(n)` ([7.3.4](#734-v2-radius-tokens)). Blur: `theme.v2.blur[n]` ([7.3.5](#735-v2-blur-tokens))
501
+ - [ ] No MUI `name`/`slot` or slot-class maps
502
+
503
+ **Verification**
504
+
505
+ - [ ] Storybook updated (`Default`, `{Name}Styled`, `{Name}StyledSlots`, states)
506
+ - [ ] [Reference table](#12-reference--current-primitives) updated if roles changed
507
+ - [ ] `yarn lint && yarn build:types` pass