rich-react-component 0.2.0 → 0.3.1
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 +21 -21
- package/README.md +314 -120
- package/dist/base/Appearance.d.ts +116 -0
- package/dist/base/AppearanceMenu.d.ts +65 -0
- package/dist/base/Badge.d.ts +8 -1
- package/dist/base/Button.d.ts +9 -1
- package/dist/base/Sidebar.d.ts +111 -21
- package/dist/base/Tag.d.ts +4 -2
- package/dist/base/Tooltip.d.ts +8 -1
- package/dist/base/appearanceSchemes.d.ts +92 -0
- package/dist/base/index.d.ts +4 -0
- package/dist/base/sidebarModel.d.ts +171 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +0 -15
- package/dist/index.js +2211 -1333
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -0
- package/package.json +6 -2
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,120 +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
|
-
[](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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
[](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;
|