rich-react-component 0.1.0 → 0.3.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.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 suleymangulle
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 suleymangulle
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,118 +1,314 @@
1
- # rich-react-component
2
-
3
- Repository: [github.com/suleymangulle/rich-react-component](https://github.com/suleymangulle/rich-react-component) (private) · [npm](https://www.npmjs.com/package/rich-react-component)
4
-
5
- [![npm version](https://img.shields.io/npm/v/rich-react-component.svg)](https://www.npmjs.com/package/rich-react-component)
6
-
7
- Enterprise React + TypeScript component library built on **Bootstrap + Metronic** conventions, organized into three capability layers:
8
-
9
- ```text
10
- Base — pure UI behavior, no HTTP, no backend knowledge
11
- Remote — adds remote-data fetching to Base components by composing them
12
- Smart — metadata-driven configuration layer (Phases 1-5 complete)
13
- ```
14
-
15
- Each layer **reuses** the one below it instead of reimplementing it — `RemoteComboBox` composes `ComboBox`, it does not contain a second combobox implementation. See [`react-components-initial.md`](./react-components-initial.md) for the original architecture brief this library follows.
16
-
17
- ## Install
18
-
19
- ```bash
20
- npm install rich-react-component bootstrap react react-dom
21
- ```
22
-
23
- `bootstrap`, `react`, and `react-dom` are peer dependencies — the library ships no CSS of its own beyond what Bootstrap classes provide.
24
-
25
- ```tsx
26
- import "bootstrap/dist/css/bootstrap.min.css";
27
- ```
28
-
29
- ## Quick example
30
-
31
- ```tsx
32
- import { ComboBox, SmartField } from "rich-react-component";
33
- import { RemoteComboBox } from "rich-react-component";
34
-
35
- // Base — you own the data
36
- <ComboBox
37
- options={[{ value: "tr", label: "Turkey" }]}
38
- value={countryId}
39
- onChange={setCountryId}
40
- />
41
-
42
- // Remote the component owns fetching, cancellation, and stale-response
43
- // protection; you only describe the endpoint and the field mapping
44
- <RemoteComboBox
45
- url="/api/countries"
46
- valueMember="id"
47
- displayMember="name"
48
- value={countryId}
49
- onChange={setCountryId}
50
- />
51
-
52
- // Smart the server describes the field, SmartField resolves it onto
53
- // RemoteComboBox for you; visible/readOnly/required/validation all come
54
- // from metadata instead of being written by hand at every call site
55
- <SmartField
56
- metadata={{
57
- name: "countryId", component: "ComboBox",
58
- valueMember: "id", displayMember: "name",
59
- dataSource: { id: "Countries" },
60
- }}
61
- ctx={ctx} // { values, onFieldChange, registry, dataSources, actions }
62
- />
63
- ```
64
-
65
- ## What's in each layer
66
-
67
- **Base** (49 components) — form fields (`Input`, `NumberInput`, `PasswordInput`, `FileInput`, `CheckBox`, `Switch`, `RadioGroup`, `Select`, `MultiSelect`, `DatePicker`, `TimePicker`, `DateTimePicker`, `ComboBox`, `AutoComplete`, `FormField`), feedback (`Modal`, `Toast`, `Confirm`, `Alert`, `Spinner`, `Badge`, `Skeleton`), data display (`DataGrid`, `Pagination`, `Avatar`, `Tag`, `Rating`, `ProgressBar`), navigation (`Breadcrumb`, `Menu`, `Stepper`, `Tabs`, `Navbar`, `Sidebar`, `PageHeader`), layout (`Card`, `Divider`, `Stack`, `Container`, `Row`, `Col`), and more (`Button`, `Tooltip`, `Popover`, `Accordion`, `Icon`).
68
-
69
- **Remote** — `RemoteComboBox`, `RemoteAutoComplete`, `RemoteSelect`, `RemoteMultiSelect`, `RemoteDataGrid`, backed by a shared `HttpClient` abstraction and a `useRemoteData` hook that handles request cancellation, stale-response protection, and a consistent `idle/loading/success/empty/error` state model.
70
-
71
- **Smart***Phases 1-5 complete, the full approved plan.* A metadata-driven layer: the server describes a field or action, Smart resolves that description onto the exact Base/Remote component that already exists — it never reimplements one.
72
-
73
- - **Metadata contract** a discriminated union per component type (`InputMetadata | ComboBoxMetadata | DatePickerMetadata | DataGridMetadata`), plus `SmartActionMetadata` for buttons. Declarative only: no expressions, no serialized functions.
74
- - **Registries** — `SmartComponentRegistry` (keyed by the `component` discriminant, not a switch), `SmartDataSourceRegistry` and `SmartActionRegistry` (id-based indirection — metadata never carries a raw URL or a function).
75
- - **Built-in resolvers** — Input, ComboBox, DatePicker, and DataGrid, registered via `registerBuiltInResolvers()` through the exact same call a custom application component would use. DataGrid's column metadata (`format: "text" | "date" | "currency" | "boolean"`) maps to a small, Smart-owned set of formatters — the server requests a formatter, never ships one.
76
- - **Headless engine** — `useSmartField` / `useSmartAction` resolve one field/action's metadata (visibility, validation, localized label, resolved request params); `useSmartDependencies` tracks `dependsOn` and reports which field needs fresh metadata when a value it depends on changes.
77
- - **Components** — `SmartField` / `SmartAction` resolve one piece of metadata into UI; `SmartForm` / `SmartActions` are thin iteration helpers over a whole `SmartFormMetadata` — no field-value ownership, no business-rule evaluation, no layout decisions.
78
-
79
- ## Demo pages
80
-
81
- Two dev-only pages exercise the library live — not part of the published package:
82
-
83
- ```bash
84
- npm run dev
85
- ```
86
-
87
- - `/index.html` — every Base/Remote component in one manual-test page
88
- - `/smart-demo.html` the full Phase 1-5 Smart engine end to end: a cascading Country/City form and server-driven field visibility/validation rendered through one `<SmartForm>` call, a paginated DataGrid through one `<SmartField>` call, `<SmartActions>` with a real fail-safe-disabled button, a dependency-refresh log, and EN/TR localization (including the DataGrid's boolean formatter) with in-page documentation popups
89
-
90
- ## Scripts
91
-
92
- | Command | Description |
93
- | --- | --- |
94
- | `npm run dev` | Start the demo pages with Vite |
95
- | `npm run build` | Type-check + build the library to `dist/` (ESM, CJS, `.d.ts`) |
96
- | `npm test` | Run the test suite once (Vitest) |
97
- | `npm run test:watch` | Run tests in watch mode |
98
- | `npm run typecheck` | Type-check the whole project without emitting |
99
-
100
- ## Project structure
101
-
102
- ```text
103
- src/
104
- ├── base/ # Base layer no dependency on remote/ or smart/
105
- ├── remote/ # Remote layer — may depend on base/, not on smart/
106
- ├── smart/ # Smart layer (Phases 1-5) — may depend on base/ and remote/
107
- └── index.ts # public entry point
108
-
109
- demo/ # dev-only manual-test pages (not published)
110
- ```
111
-
112
- ## Testing
113
-
114
- Behavioral tests over snapshots. Every capability layer is tested against its own contract — Base components never call `fetch`, Remote components reuse (not duplicate) their Base counterpart's UI, dependency-driven param changes reload correctly without stale-response races, and the Smart registry falls back safely for an unresolved component type instead of rendering the wrong thing.
115
-
116
- ## License
117
-
118
- [MIT](./LICENSE) © suleymangulle
1
+ # rich-react-component
2
+
3
+ Repository: [github.com/suleymangulle/rich-react-component](https://github.com/suleymangulle/rich-react-component) (private) · [npm](https://www.npmjs.com/package/rich-react-component)
4
+
5
+ [![npm version](https://img.shields.io/npm/v/rich-react-component.svg)](https://www.npmjs.com/package/rich-react-component)
6
+
7
+ Enterprise React + TypeScript component library built on **Bootstrap + Metronic** conventions, organized into three capability layers:
8
+
9
+ ```text
10
+ Base — pure UI behavior, no HTTP, no backend knowledge
11
+ Remote — adds remote-data fetching to Base components by composing them
12
+ Smart — metadata-driven configuration layer (Phases 1-5 complete)
13
+ ```
14
+
15
+ Each layer **reuses** the one below it instead of reimplementing it — `RemoteComboBox` composes `ComboBox`, it does not contain a second combobox implementation. See [`react-components-initial.md`](./react-components-initial.md) for the original architecture brief this library follows.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install rich-react-component bootstrap react react-dom
21
+ ```
22
+
23
+ `bootstrap`, `react`, and `react-dom` are peer dependencies — the library ships no CSS of its own beyond what Bootstrap classes provide.
24
+
25
+ ```tsx
26
+ import "bootstrap/dist/css/bootstrap.min.css";
27
+ ```
28
+
29
+ The library ships one opt-in stylesheet: a repository-owned **Metronic Bootstrap
30
+ visual skin** covering every public component. It is emitted as a separate
31
+ `dist/style.css` asset, so importing the package entry point stays
32
+ side-effect-free for SSR. Load it after Bootstrap and before your own overrides:
33
+
34
+ ```tsx
35
+ import "bootstrap/dist/css/bootstrap.min.css";
36
+ import "rich-react-component/style.css";
37
+ // ...then your application's own overrides
38
+ ```
39
+
40
+ Every rule in it is anchored on a library-owned `rrc-*` class, so importing it
41
+ cannot repaint your own Bootstrap markup — components emit both their Bootstrap
42
+ class and their `rrc-*` class, and a consumer who skips the import keeps plain
43
+ Bootstrap. It is **not** the official Metronic stylesheet: no licensed Metronic
44
+ or KeenIcons asset is used. Typography prefers Inter but the package never
45
+ fetches a font; supply Inter from your application and it is picked up, or opt
46
+ the whole page in with `body { font-family: var(--rrc-font-sans); }`.
47
+
48
+ ## Quick example
49
+
50
+ ```tsx
51
+ import { ComboBox, SmartField } from "rich-react-component";
52
+ import { RemoteComboBox } from "rich-react-component";
53
+
54
+ // Base you own the data
55
+ <ComboBox
56
+ options={[{ value: "tr", label: "Turkey" }]}
57
+ value={countryId}
58
+ onChange={setCountryId}
59
+ />
60
+
61
+ // Remote the component owns fetching, cancellation, and stale-response
62
+ // protection; you only describe the endpoint and the field mapping
63
+ <RemoteComboBox
64
+ url="/api/countries"
65
+ valueMember="id"
66
+ displayMember="name"
67
+ value={countryId}
68
+ onChange={setCountryId}
69
+ />
70
+
71
+ // Smart — the server describes the field, SmartField resolves it onto
72
+ // RemoteComboBox for you; visible/readOnly/required/validation all come
73
+ // from metadata instead of being written by hand at every call site
74
+ <SmartField
75
+ metadata={{
76
+ name: "countryId", component: "ComboBox",
77
+ valueMember: "id", displayMember: "name",
78
+ dataSource: { id: "Countries" },
79
+ }}
80
+ ctx={ctx} // { values, onFieldChange, registry, dataSources, actions }
81
+ />
82
+ ```
83
+
84
+ ## What's in each layer
85
+
86
+ **Base** (55 components) — form fields (`Input`, `NumberInput`, `PasswordInput`, `FileInput`, `CheckBox`, `Switch`, `RadioGroup`, `Select`, `MultiSelect`, `DatePicker`, `TimePicker`, `DateTimePicker`, `ComboBox`, `AutoComplete`, `FormField`), feedback (`Modal`, `Toast`, `Confirm`, `Alert`, `Spinner`, `Badge`, `Skeleton`), data display (`DataGrid`, `Pagination`, `Avatar`, `Tag`, `Rating`, `ProgressBar`, `ListItem`, `Sparkline`, `Statistic`), navigation (`Breadcrumb`, `Menu`, `Stepper`, `Tabs`, `Navbar`, `Sidebar`, `PageHeader`), layout (`Card`, `Divider`, `Stack`, `Flex`, `Container`, `Row`, `Col`), typography (`Text`), and more (`Button`, `IconButton`, `Tooltip`, `Popover`, `Accordion`, `Icon`).
87
+
88
+ `DataGrid` columns are declarative by default (`{ field: "conversion", header: "CONV.", align: "end" }`) `render` stays available as the escape hatch for genuinely custom cells, and `format`/`formatter` cover the common presentation cases (`dataGridFormatters` exports the built-in `text`/`date`/`currency`/`boolean` set). `Card` accepts `title`/`subtitle`/`icon`/`avatar`/`actions`/`loading` directly, with `header` remaining as the raw escape hatch. `Tabs` supports `variant` (`default`/`underline`/`pill`/`card`/`button`), `orientation`, `size`, `stretch`, and per-item `icon`/`badge`, with roving-tabindex keyboard navigation.
89
+
90
+ **Remote** — `RemoteComboBox`, `RemoteAutoComplete`, `RemoteSelect`, `RemoteMultiSelect`, `RemoteDataGrid`, backed by a shared `HttpClient` abstraction and a `useRemoteData` hook that handles request cancellation, stale-response protection, and a consistent `idle/loading/success/empty/error` state model.
91
+
92
+ **Smart** *Phases 1-5 complete, the full approved plan.* A metadata-driven layer: the server describes a field or action, Smart resolves that description onto the exact Base/Remote component that already exists — it never reimplements one.
93
+
94
+ - **Metadata contract** — a discriminated union per component type (`InputMetadata | ComboBoxMetadata | DatePickerMetadata | DataGridMetadata`), plus `SmartActionMetadata` for buttons. Declarative only: no expressions, no serialized functions.
95
+ - **Registries** `SmartComponentRegistry` (keyed by the `component` discriminant, not a switch), `SmartDataSourceRegistry` and `SmartActionRegistry` (id-based indirection — metadata never carries a raw URL or a function).
96
+ - **Built-in resolvers** — Input, ComboBox, DatePicker, and DataGrid, registered via `registerBuiltInResolvers()` through the exact same call a custom application component would use. DataGrid's column metadata (`format: "text" | "date" | "currency" | "boolean"`) maps onto Base DataGrid's own `dataGridFormatters` — Smart requests a formatter, it doesn't maintain a second rendering system for it. The one exception is the boolean formatter's localized text, which Smart still resolves itself (via the `formatter` escape hatch) since that's a translation-catalog concern Base can't own.
97
+ - **Headless engine** — `useSmartField` / `useSmartAction` resolve one field/action's metadata (visibility, validation, localized label, resolved request params); `useSmartDependencies` tracks `dependsOn` and reports which field needs fresh metadata when a value it depends on changes.
98
+ - **Components** — `SmartField` / `SmartAction` resolve one piece of metadata into UI; `SmartForm` / `SmartActions` are thin iteration helpers over a whole `SmartFormMetadata` no field-value ownership, no business-rule evaluation, no layout decisions.
99
+
100
+ ## Sidebar
101
+
102
+ One recursive navigation tree with a single authoritative expansion state and a
103
+ single authoritative `activeKey` — no per-group `Accordion` instance, and no
104
+ per-item `active` boolean to keep in sync.
105
+
106
+ ```tsx
107
+ <Sidebar
108
+ variant="compact" // "grouped" (default) | "compact"
109
+ items={[
110
+ { type: "link", key: "overview", label: "Overview", href: "/overview" },
111
+ { type: "divider", key: "sep" },
112
+ {
113
+ type: "group", key: "workspace", label: "Workspace",
114
+ children: [
115
+ {
116
+ type: "collapsible", key: "catalog", label: "Catalog",
117
+ children: [
118
+ { type: "link", key: "items", label: "Items", href: "/items" },
119
+ { type: "action", key: "sync", label: "Sync now", onSelect: (item, event) => reload() },
120
+ ],
121
+ },
122
+ ],
123
+ },
124
+ ]}
125
+ activeKey="items" // ancestors are derived, never passed in
126
+ expandActivePath // opens the active branch on mount and on
127
+ // every activeKey change — and only then,
128
+ // so the user can close it in between.
129
+ // `activePathExpansion="always"` restores
130
+ // the older force-open behaviour.
131
+ expandMode="multiple" // "single" closes the competing branch
132
+ collapsible
133
+ header={<Brand />}
134
+ footer={<OrganizationSelector />} // tenant behavior belongs to the app
135
+ navLabel="Primary"
136
+ />
137
+ ```
138
+
139
+ **Item kinds.** `link` (a real `href`), `action` (a command, rendered as a
140
+ native button), `collapsible` (a branch with an explicit toggle), `group` (a
141
+ visual section — never navigable, and expandable only with `collapsible: true`)
142
+ and `divider`. Each supports `key`, `label`, `icon`, `badge`, `disabled`,
143
+ `className` and `ariaLabel`.
144
+
145
+ **Semantics.** A destination renders as an anchor, or through `renderLink` so
146
+ your router's own link can be swapped in without this package importing a
147
+ router. A command renders as a button; a placeholder `href="#"` is never
148
+ generated. Enabled links keep their default navigation, so modified clicks
149
+ still open a new tab; disabled links are prevented from navigating and disabled
150
+ actions use the native `disabled` attribute. Only the actual active destination
151
+ gets `aria-current="page"` — its ancestors get `data-active-ancestor="true"`.
152
+
153
+ **State.** `expandedKeys` / `defaultExpandedKeys` / `onExpandedKeysChange`
154
+ (keys are always emitted in tree order), `collapsed` / `defaultCollapsed` /
155
+ `onCollapsedChange`, and `mobileOpen` / `defaultMobileOpen` /
156
+ `onMobileOpenChange` are three separate concerns that never share a value.
157
+ Collapsing the aside leaves the expanded keys intact; opening the mobile drawer
158
+ leaves the desktop collapse choice intact.
159
+
160
+ **Collapsed and mobile.** A collapsed row keeps its accessible name on the
161
+ control itself and surfaces it through the existing `Tooltip`. A nested branch
162
+ opens as a flyout built from the existing `Popup` primitive, with its own local
163
+ open state, so `expandedKeys` are never touched. The mobile drawer is a modal
164
+ dialog with a backdrop, Escape-to-close, and focus restoration to whatever
165
+ opened it.
166
+
167
+ **LTR and RTL.** Indentation, flyout placement, the collapse control and the
168
+ drawer all use logical CSS, so `dir="rtl"` mirrors without a second stylesheet.
169
+
170
+ ### Migrating from 0.2.0
171
+
172
+ `sections`, `SidebarLeafItem`, `SidebarGroup` and `SidebarSection` still work.
173
+ They are normalized onto the recursive model internally, so there is exactly one
174
+ rendering path. Two behavior notes:
175
+
176
+ - A legacy leaf **without** an `href` used to render as `<a href="#">`. It now
177
+ renders as a `<button>`; its props are unchanged and `onClick` still fires.
178
+ This is the one intentional DOM change, and it is what removes the
179
+ placeholder anchor.
180
+ - A legacy `active: true` flag is folded into the authoritative `activeKey`.
181
+ When several items are flagged, the first in declaration order wins.
182
+
183
+ `variant` defaults to `"grouped"`, the presentation this component has always
184
+ rendered. `compact` is opt-in.
185
+
186
+ ## Appearance
187
+
188
+ An opt-in controller for four strictly independent settings. It is **not** a
189
+ Metronic Layout Builder: there is no demo/template switching, and no
190
+ page-loader, fixed-header or icon-style option.
191
+
192
+ ```tsx
193
+ <AppearanceProvider
194
+ defaultMode="system" // "light" | "dark" | "system"
195
+ defaultSidebarPresentation="compact" // "compact" | "grouped"
196
+ defaultSidebarTone="auto" // "light" | "dark" | "auto"
197
+ defaultColorSchemeId="blue"
198
+ storage={window.localStorage} // persistence is opt-in...
199
+ storageKey="my-app:appearance" // ...and needs your own key
200
+ >
201
+ <App />
202
+ </AppearanceProvider>
203
+ ```
204
+
205
+ `useAppearance()` returns the current settings, the `resolvedTheme`, the live
206
+ `systemTheme`, the setters, and `appearanceProps` to spread onto your own
207
+ layout root. By default the provider mutates **no** global DOM; pass
208
+ `applyTo="documentElement"` if you want it to write the theme attribute and the
209
+ scheme variables for you. A `Sidebar` with no explicit `variant`/`tone` follows
210
+ the provider, and still works with no provider at all.
211
+
212
+ Importing the package is SSR-safe: `window`, `document`, `localStorage` and
213
+ `matchMedia` are never touched during module evaluation or during render, so
214
+ the server and the first client render agree. To also avoid the first-paint
215
+ theme flash, inline the initialization snippet yourself — it is returned as a
216
+ string and never injected automatically:
217
+
218
+ ```tsx
219
+ <script dangerouslySetInnerHTML={{ __html: createAppearanceInitScript({ storageKey: "my-app:appearance" }) }} />
220
+ ```
221
+
222
+ `AppearanceMenu` is the composable selector — drop it into a `Navbar` slot, a
223
+ `Popover`, or a settings drawer. Every section is individually hideable, and
224
+ every visible string is supplied by the caller, so it ships no copy in any
225
+ language:
226
+
227
+ ```tsx
228
+ <AppearanceMenu labels={{ presentation, mode, tone, colorScheme }} sections={{ tone: false }} />
229
+ ```
230
+
231
+ ### Color schemes
232
+
233
+ A scheme is a complete coordinated token family, never one raw color: changing
234
+ only the primary hue leaves buttons, active components and menu states
235
+ inconsistent, so an incomplete family is rejected at registration.
236
+
237
+ ```tsx
238
+ registerColorScheme(createColorScheme({
239
+ id: "brand",
240
+ primary: "#7b1fa2", primaryActive: "#5c1379", primaryLight: "#f0dcf7",
241
+ primaryInverse: "#ffffff", primaryRgb: "123, 31, 162",
242
+ }));
243
+ ```
244
+
245
+ Applying a scheme emits three groups of CSS custom properties at once:
246
+
247
+ | Group | Variables | Status |
248
+ | --- | --- | --- |
249
+ | Bootstrap 5.3 | `--bs-primary`, `--bs-primary-rgb` | genuinely exist in the peer dependency, written directly |
250
+ | Metronic-compatible | `--bs-primary-active` / `-light` / `-inverse`, `--bs-component-*`, `--bs-menu-link-color-*` | not in Bootstrap; emitted so a real Metronic build gets a coordinated result instead of a half-recolored theme |
251
+ | Repository-owned | `--rrc-*` | what this library's stylesheet actually consumes; each falls back to the Bootstrap name, then the Metronic name, then a literal — so it works with Metronic, with plain Bootstrap, or with neither |
252
+
253
+ On dark surfaces the `--rrc-menu-link-color-*` tokens are derived from the
254
+ scheme's primary rather than chained to the `--bs-*` value: neither Bootstrap
255
+ nor Metronic ships a dark variant of those variables, and the light-mode shades
256
+ measure about 2:1 on a dark neutral. An application with its own dark palette
257
+ overrides the `--rrc-*` tokens, which are the documented integration point.
258
+
259
+ ## Component showcase
260
+
261
+ A dev-only application exercises the library live — not part of the published package:
262
+
263
+ ```bash
264
+ npm run dev
265
+ ```
266
+
267
+ The showcase is a routed single-page application: a persistent shell (categorized
268
+ Sidebar, header, appearance drawer) with one route per public visual component,
269
+ each page showing a live Preview and the source that produced it.
270
+
271
+ - `/overview/introduction` — the landing route
272
+ - `/components/<category>/<component>` — one route per Base component
273
+ - `/remote/<component>` and `/smart/<component>` — the Remote and Smart layers
274
+
275
+ The demo consumes the library through its real public specifier
276
+ (`rich-react-component`), never through private source paths, so the package
277
+ boundary is exercised the way a consumer exercises it.
278
+
279
+ ## Scripts
280
+
281
+ | Command | Description |
282
+ | --- | --- |
283
+ | `npm run dev` | Start the showcase with Vite |
284
+ | `npm run build:demo` | Build the showcase SPA to `demo-dist/` |
285
+ | `npm run preview:demo` | Preview the production showcase build |
286
+ | `npm run build` | Type-check + build the library to `dist/` (ESM, CJS, `.d.ts`) |
287
+ | `npm test` | Run the test suite once (Vitest) |
288
+ | `npm run test:watch` | Run tests in watch mode |
289
+ | `npm run typecheck` | Type-check the whole project without emitting |
290
+
291
+ ## Project structure
292
+
293
+ ```text
294
+ src/
295
+ ├── base/ # Base layer — no dependency on remote/ or smart/
296
+ ├── remote/ # Remote layer — may depend on base/, not on smart/
297
+ ├── smart/ # Smart layer (Phases 1-5) — may depend on base/ and remote/
298
+ └── index.ts # public entry point
299
+
300
+ demo/ # dev-only routed showcase application (not published)
301
+ registry/ # authoritative route/category metadata + public-export inventory
302
+ shell/ # persistent app shell: sidebar, header, drawers, preferences
303
+ pages/ # one lazily loaded module per category
304
+ ui/ # page standard and the Preview/Code example viewer
305
+ mocks/ # deterministic in-memory HttpClient for the Remote demos
306
+ ```
307
+
308
+ ## Testing
309
+
310
+ Behavioral tests over snapshots. Every capability layer is tested against its own contract — Base components never call `fetch`, Remote components reuse (not duplicate) their Base counterpart's UI, dependency-driven param changes reload correctly without stale-response races, and the Smart registry falls back safely for an unresolved component type instead of rendering the wrong thing.
311
+
312
+ ## License
313
+
314
+ [MIT](./LICENSE) © suleymangulle
@@ -0,0 +1,116 @@
1
+ import { CSSProperties, ReactNode } from 'react';
2
+ import { ColorScheme } from './appearanceSchemes';
3
+ /** What the user picked. */
4
+ export type ThemeMode = "light" | "dark" | "system";
5
+ /** What that actually resolves to right now. Never "system". */
6
+ export type ResolvedTheme = "light" | "dark";
7
+ /** Which Sidebar presentation the application uses. Not a Metronic demo/template switch. */
8
+ export type SidebarPresentation = "compact" | "grouped";
9
+ /** The Sidebar's own surface tone, independent of the resolved page theme. */
10
+ export type SidebarTone = "light" | "dark" | "auto";
11
+ export declare const THEME_MODES: readonly ThemeMode[];
12
+ export declare const SIDEBAR_PRESENTATIONS: readonly SidebarPresentation[];
13
+ export declare const SIDEBAR_TONES: readonly SidebarTone[];
14
+ /** The four settings, kept strictly separate so one can never overwrite another. */
15
+ export interface AppearanceSettings {
16
+ mode: ThemeMode;
17
+ sidebarPresentation: SidebarPresentation;
18
+ sidebarTone: SidebarTone;
19
+ colorSchemeId: string;
20
+ }
21
+ export interface AppearanceContextValue extends AppearanceSettings {
22
+ /** `mode`, with "system" already resolved through `prefers-color-scheme`. */
23
+ resolvedTheme: ResolvedTheme;
24
+ /** The operating-system preference, observed live. */
25
+ systemTheme: ResolvedTheme;
26
+ colorScheme: ColorScheme;
27
+ setMode: (mode: ThemeMode) => void;
28
+ setSidebarPresentation: (presentation: SidebarPresentation) => void;
29
+ setSidebarTone: (tone: SidebarTone) => void;
30
+ setColorSchemeId: (id: string) => void;
31
+ /**
32
+ * Spread onto your own layout root when the provider is not configured to
33
+ * touch the DOM itself (the default). Carries the resolved theme attribute
34
+ * and the selected scheme's CSS custom properties.
35
+ */
36
+ appearanceProps: {
37
+ "data-bs-theme": ResolvedTheme;
38
+ style: CSSProperties;
39
+ };
40
+ }
41
+ /** Minimal `localStorage`-compatible surface — supply your own for SSR or tests. */
42
+ export interface AppearanceStorage {
43
+ getItem: (key: string) => string | null;
44
+ setItem: (key: string, value: string) => void;
45
+ }
46
+ export interface AppearanceProviderProps {
47
+ children: ReactNode;
48
+ mode?: ThemeMode;
49
+ defaultMode?: ThemeMode;
50
+ onModeChange?: (mode: ThemeMode) => void;
51
+ sidebarPresentation?: SidebarPresentation;
52
+ defaultSidebarPresentation?: SidebarPresentation;
53
+ onSidebarPresentationChange?: (presentation: SidebarPresentation) => void;
54
+ sidebarTone?: SidebarTone;
55
+ defaultSidebarTone?: SidebarTone;
56
+ onSidebarToneChange?: (tone: SidebarTone) => void;
57
+ colorSchemeId?: string;
58
+ defaultColorSchemeId?: string;
59
+ onColorSchemeIdChange?: (id: string) => void;
60
+ /**
61
+ * Opt-in persistence. Both `storage` and `storageKey` are required before
62
+ * anything is read or written — this library never invents an application
63
+ * storage key.
64
+ */
65
+ storage?: AppearanceStorage;
66
+ storageKey?: string;
67
+ /**
68
+ * Where to write the resolved theme attribute and the scheme variables.
69
+ * Defaults to "none": the provider mutates no global DOM unless asked to.
70
+ */
71
+ applyTo?: "none" | "documentElement" | "body";
72
+ /** Attribute used for the resolved theme. Bootstrap 5.3's own mechanism by default. */
73
+ themeAttribute?: string;
74
+ }
75
+ /**
76
+ * Opt-in appearance controller. Deliberately NOT a Metronic Layout Builder:
77
+ * it owns four independent settings and nothing else — no demo/template
78
+ * switching, no page-loader, header or icon-style options.
79
+ *
80
+ * SSR-safe by construction: no `window`, `document`, `localStorage` or
81
+ * `matchMedia` is touched during module evaluation or during render. The
82
+ * system preference and any persisted values are read in effects, so the
83
+ * server and the first client render agree. Use
84
+ * `createAppearanceInitScript()` if you also need to avoid the initial
85
+ * theme flash.
86
+ */
87
+ export declare function AppearanceProvider({ children, mode, defaultMode, onModeChange, sidebarPresentation, defaultSidebarPresentation, onSidebarPresentationChange, sidebarTone, defaultSidebarTone, onSidebarToneChange, colorSchemeId, defaultColorSchemeId, onColorSchemeIdChange, storage, storageKey, applyTo, themeAttribute, }: AppearanceProviderProps): import("react").JSX.Element;
88
+ /** Throws outside an `AppearanceProvider`, so a missing provider fails loudly instead of silently. */
89
+ export declare function useAppearance(): AppearanceContextValue;
90
+ /**
91
+ * Internal: components that merely *follow* the appearance when a provider
92
+ * happens to be present (Sidebar) must keep working without one. Not exported
93
+ * from the package entry point.
94
+ */
95
+ export declare function useOptionalAppearance(): AppearanceContextValue | undefined;
96
+ export interface AppearanceInitScriptOptions {
97
+ storageKey: string;
98
+ themeAttribute?: string;
99
+ target?: "documentElement" | "body";
100
+ /** Must match the provider's `defaultMode`. */
101
+ defaultMode?: ThemeMode;
102
+ }
103
+ /**
104
+ * Returns the source of a tiny synchronous snippet that applies the persisted
105
+ * resolved theme before first paint, removing the theme flash.
106
+ *
107
+ * It is returned as a string and never injected automatically — the consuming
108
+ * application decides where and whether to inline it (for example in a
109
+ * Next.js `<Script strategy="beforeInteractive">` or a raw `<script>` in the
110
+ * document head).
111
+ *
112
+ * The caller-supplied values are embedded as JSON string literals and their
113
+ * `<` characters are escaped, so a key can neither break out of the literal
114
+ * nor terminate a surrounding `<script>` element.
115
+ */
116
+ export declare function createAppearanceInitScript({ storageKey, themeAttribute, target, defaultMode, }: AppearanceInitScriptOptions): string;