@hellboy/ds 0.1.2
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/README.md +111 -0
- package/dist/index.css +3699 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +1087 -0
- package/dist/index.d.ts +1087 -0
- package/dist/index.js +3391 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3287 -0
- package/dist/index.mjs.map +1 -0
- package/dist/theme.css +55 -0
- package/hellboy-ds-0.1.2.tgz +0 -0
- package/package.json +42 -0
- package/src/components/badge/Badge.tsx +29 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/banner/Banner.tsx +48 -0
- package/src/components/banner/banner.css +44 -0
- package/src/components/banner/index.ts +1 -0
- package/src/components/button/button.tsx +127 -0
- package/src/components/button/index.ts +1 -0
- package/src/components/card/card.tsx +57 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/checkbox/Checkbox.tsx +98 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/code-block/code-block.tsx +44 -0
- package/src/components/code-block/index.ts +1 -0
- package/src/components/color-control/color-control.tsx +322 -0
- package/src/components/color-control/index.ts +1 -0
- package/src/components/drag-handle/DragHandle.tsx +78 -0
- package/src/components/drag-handle/index.ts +1 -0
- package/src/components/drawer/drawer.tsx +82 -0
- package/src/components/drawer/index.ts +1 -0
- package/src/components/floating-bar/floating-bar.tsx +52 -0
- package/src/components/floating-bar/index.ts +2 -0
- package/src/components/footer/footer.tsx +28 -0
- package/src/components/footer/index.ts +1 -0
- package/src/components/grid/Grid.tsx +53 -0
- package/src/components/grid/index.ts +1 -0
- package/src/components/header/header.tsx +57 -0
- package/src/components/header/index.ts +1 -0
- package/src/components/icons/icons.tsx +44 -0
- package/src/components/icons/index.ts +1 -0
- package/src/components/index.ts +29 -0
- package/src/components/input/DatePicker.tsx +133 -0
- package/src/components/input/Input.tsx +220 -0
- package/src/components/input/InputDate.tsx +10 -0
- package/src/components/input/InputDateTime.tsx +10 -0
- package/src/components/input/InputEmail.tsx +10 -0
- package/src/components/input/InputField.tsx +137 -0
- package/src/components/input/InputNumber.tsx +10 -0
- package/src/components/input/InputPassword.tsx +10 -0
- package/src/components/input/InputSearch.tsx +10 -0
- package/src/components/input/InputTel.tsx +10 -0
- package/src/components/input/InputText.tsx +10 -0
- package/src/components/input/InputTime.tsx +10 -0
- package/src/components/input/InputUrl.tsx +10 -0
- package/src/components/input/TimePicker.tsx +151 -0
- package/src/components/input/index.ts +11 -0
- package/src/components/layout/Layout.tsx +244 -0
- package/src/components/layout/index.ts +1 -0
- package/src/components/list/List.tsx +159 -0
- package/src/components/list/index.ts +1 -0
- package/src/components/navbar/MenuCategory.tsx +20 -0
- package/src/components/navbar/MenuGroup.tsx +288 -0
- package/src/components/navbar/MenuItem.tsx +65 -0
- package/src/components/navbar/Navbar.tsx +23 -0
- package/src/components/navbar/index.ts +4 -0
- package/src/components/page/index.ts +1 -0
- package/src/components/page/page.tsx +46 -0
- package/src/components/page-index/PageIndex.tsx +275 -0
- package/src/components/page-index/index.ts +1 -0
- package/src/components/popover/index.ts +1 -0
- package/src/components/popover/popover.tsx +199 -0
- package/src/components/radio/Radio.tsx +176 -0
- package/src/components/radio/index.ts +1 -0
- package/src/components/section/index.ts +1 -0
- package/src/components/section/section.tsx +66 -0
- package/src/components/select/Select.tsx +212 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/slider/Slider.tsx +267 -0
- package/src/components/slider/index.ts +1 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.tsx +99 -0
- package/src/components/table/Table.tsx +147 -0
- package/src/components/table/index.ts +1 -0
- package/src/components/theme-control/index.ts +1 -0
- package/src/components/theme-control/theme-control.tsx +78 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip/tooltip.tsx +207 -0
- package/src/contexts/NavbarTooltipContext.tsx +48 -0
- package/src/contexts/index.ts +1 -0
- package/src/foundations/motion.md +136 -0
- package/src/index.ts +40 -0
- package/src/style/_shared/field.css +69 -0
- package/src/style/components/badge/badge.css +74 -0
- package/src/style/components/button/button.css +244 -0
- package/src/style/components/card/card.css +69 -0
- package/src/style/components/checkbox.css +142 -0
- package/src/style/components/code-block/code-block.css +34 -0
- package/src/style/components/color-control/color-control.css +126 -0
- package/src/style/components/drag-handle/drag-handle.css +68 -0
- package/src/style/components/drawer/drawer.css +210 -0
- package/src/style/components/floating-bar/floating-bar.css +39 -0
- package/src/style/components/footer/footer.css +108 -0
- package/src/style/components/grid/grid.css +33 -0
- package/src/style/components/header/header.css +44 -0
- package/src/style/components/icons/icons.css +44 -0
- package/src/style/components/input/input.css +393 -0
- package/src/style/components/layout/layout.css +205 -0
- package/src/style/components/list/list.css +140 -0
- package/src/style/components/navbar/navbar.css +342 -0
- package/src/style/components/page/page.css +46 -0
- package/src/style/components/page-index/page-index.css +158 -0
- package/src/style/components/popover/popover.css +44 -0
- package/src/style/components/radio.css +178 -0
- package/src/style/components/section/section.css +67 -0
- package/src/style/components/select/select.css +143 -0
- package/src/style/components/slider/slider.css +159 -0
- package/src/style/components/switch/switch.css +267 -0
- package/src/style/components/table/table.css +108 -0
- package/src/style/components/theme-control/theme-control.css +35 -0
- package/src/style/components/tooltip/tooltip.css +52 -0
- package/src/style/foundations/global.css +316 -0
- package/src/style/foundations/motion.css +164 -0
- package/src/style/foundations/spacing.css +51 -0
- package/src/style/foundations/typography.css +39 -0
- package/src/style/foundations/z-index.css +81 -0
- package/src/style/modes/dark.css +146 -0
- package/src/style/modes/light.css +147 -0
- package/src/style/semantic.css +52 -0
- package/src/style/styles.css +51 -0
- package/src/style/themes/theme.json +37 -0
- package/src/utils/README.md +305 -0
- package/src/utils/USER_PREFERENCES.md +558 -0
- package/src/utils/theme.ts +127 -0
- package/src/utils/user-preferences.ts +577 -0
- package/tsconfig.json +25 -0
- package/tsup.config.ts +52 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
# User Preferences Utility
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The **User Preferences** utility is a comprehensive preference management system for the Hellboy Design System. It provides a practical, type-safe solution for storing and retrieving user preferences across sessions using localStorage with optional IndexedDB support.
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
The Hellboy Design System aims to **maximize development velocity** by providing complete, practical solutions rather than staying purely agnostic. While many design systems focus solely on visual components, we believe in delivering utilities that solve common real-world problems developers face when building modern web applications.
|
|
10
|
+
|
|
11
|
+
This preference system is designed to be:
|
|
12
|
+
|
|
13
|
+
- ✅ **Easy to use** with minimal configuration
|
|
14
|
+
- ✅ **Type-safe** with TypeScript
|
|
15
|
+
- ✅ **Persistent** across browser sessions
|
|
16
|
+
- ✅ **Extensible** for custom preferences
|
|
17
|
+
- ✅ **Framework-agnostic** (works with React, Vue, Svelte, vanilla JS, etc.)
|
|
18
|
+
|
|
19
|
+
## Why This Utility Exists
|
|
20
|
+
|
|
21
|
+
When building applications with the Hellboy Design System, you'll commonly need to:
|
|
22
|
+
|
|
23
|
+
- Remember user's theme preference (light/dark mode)
|
|
24
|
+
- Persist custom color configurations
|
|
25
|
+
- Save layout preferences (sidebar widths, panel sizes)
|
|
26
|
+
- Store component states (drawer positions, table column preferences)
|
|
27
|
+
- Maintain user-specific settings across sessions
|
|
28
|
+
|
|
29
|
+
Instead of implementing this functionality from scratch in every project, the User Preferences utility provides a **ready-to-use, production-ready solution** that integrates seamlessly with the design system.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
The utility is included in the core `@hellboy/ds` package:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @hellboy/ds
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Basic Usage
|
|
40
|
+
|
|
41
|
+
### Importing
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import {
|
|
45
|
+
getPreference,
|
|
46
|
+
setPreference,
|
|
47
|
+
removePreference,
|
|
48
|
+
clearPreferences
|
|
49
|
+
} from '@hellboy/ds';
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Simple Get/Set
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// Set a preference
|
|
56
|
+
setPreference('theme', 'dark');
|
|
57
|
+
setPreference('sidebarWidth', 320);
|
|
58
|
+
|
|
59
|
+
// Get a preference with optional default
|
|
60
|
+
const theme = getPreference('theme', 'light'); // 'dark'
|
|
61
|
+
const width = getPreference('sidebarWidth', 280); // 320
|
|
62
|
+
|
|
63
|
+
// Remove a preference
|
|
64
|
+
removePreference('sidebarWidth');
|
|
65
|
+
|
|
66
|
+
// Clear all preferences
|
|
67
|
+
clearPreferences();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Supported Preference Types
|
|
71
|
+
|
|
72
|
+
The utility comes with built-in TypeScript interfaces for common preference categories:
|
|
73
|
+
|
|
74
|
+
### Theme Preferences
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { setPreference, getPreference } from '@hellboy/ds';
|
|
78
|
+
|
|
79
|
+
// Theme mode
|
|
80
|
+
setPreference('theme', 'dark');
|
|
81
|
+
|
|
82
|
+
// Custom color configuration
|
|
83
|
+
setPreference('colorConfig', {
|
|
84
|
+
primaryHue: 220,
|
|
85
|
+
primarySaturation: 80,
|
|
86
|
+
primaryLightness: 50,
|
|
87
|
+
// ... other color properties
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Layout Preferences
|
|
92
|
+
|
|
93
|
+
Perfect for remembering resizable panel dimensions:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// Save sidebar widths
|
|
97
|
+
setPreference('layout.leftSidebarWidth', 280);
|
|
98
|
+
setPreference('layout.rightSidebarWidth', 320);
|
|
99
|
+
setPreference('layout.navbarCollapsed', false);
|
|
100
|
+
|
|
101
|
+
// Restore on load
|
|
102
|
+
const leftWidth = getPreference('layout.leftSidebarWidth', 280);
|
|
103
|
+
const rightWidth = getPreference('layout.rightSidebarWidth', 300);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Component-Specific Preferences
|
|
107
|
+
|
|
108
|
+
Store state for individual components:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// Save drawer state
|
|
112
|
+
setPreference('components.drawer.position', 'right');
|
|
113
|
+
setPreference('components.drawer.width', 400);
|
|
114
|
+
|
|
115
|
+
// Save table preferences
|
|
116
|
+
setPreference('components.userTable.sortColumn', 'name');
|
|
117
|
+
setPreference('components.userTable.sortDirection', 'asc');
|
|
118
|
+
setPreference('components.userTable.pageSize', 50);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Custom Preferences
|
|
122
|
+
|
|
123
|
+
Store any custom data:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
setPreference('custom.userLanguage', 'pt-BR');
|
|
127
|
+
setPreference('custom.timezone', 'America/Sao_Paulo');
|
|
128
|
+
setPreference('custom.notifications', {
|
|
129
|
+
email: true,
|
|
130
|
+
push: false,
|
|
131
|
+
sms: true
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Advanced Features
|
|
136
|
+
|
|
137
|
+
### Namespaces
|
|
138
|
+
|
|
139
|
+
Organize preferences with namespaces:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { createPreferenceNamespace } from '@hellboy/ds';
|
|
143
|
+
|
|
144
|
+
// Create a namespace for layout preferences
|
|
145
|
+
const layoutPrefs = createPreferenceNamespace('layout');
|
|
146
|
+
|
|
147
|
+
// Use scoped methods
|
|
148
|
+
layoutPrefs.set('sidebarWidth', 300);
|
|
149
|
+
layoutPrefs.set('navbarHeight', 64);
|
|
150
|
+
|
|
151
|
+
const width = layoutPrefs.get('sidebarWidth', 280); // 300
|
|
152
|
+
|
|
153
|
+
// Clear only layout preferences
|
|
154
|
+
layoutPrefs.clear();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Export/Import
|
|
158
|
+
|
|
159
|
+
Backup and restore preferences:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { exportPreferences, importPreferences } from '@hellboy/ds';
|
|
163
|
+
|
|
164
|
+
// Export all preferences as JSON
|
|
165
|
+
const json = exportPreferences();
|
|
166
|
+
|
|
167
|
+
// Download as file
|
|
168
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
169
|
+
const url = URL.createObjectURL(blob);
|
|
170
|
+
const a = document.createElement('a');
|
|
171
|
+
a.href = url;
|
|
172
|
+
a.download = 'user-preferences.json';
|
|
173
|
+
a.click();
|
|
174
|
+
|
|
175
|
+
// Import preferences (merge or replace)
|
|
176
|
+
importPreferences(json, true); // Merge with existing
|
|
177
|
+
importPreferences(json, false); // Replace all
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Get All Preferences
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { getAllPreferences } from '@hellboy/ds';
|
|
184
|
+
|
|
185
|
+
const allPrefs = getAllPreferences();
|
|
186
|
+
console.log(allPrefs);
|
|
187
|
+
// {
|
|
188
|
+
// 'theme': 'dark',
|
|
189
|
+
// 'layout.leftSidebarWidth': 320,
|
|
190
|
+
// 'custom.language': 'pt-BR',
|
|
191
|
+
// ...
|
|
192
|
+
// }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Configuration
|
|
196
|
+
|
|
197
|
+
Customize the storage backend and key prefix:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { configurePreferences } from '@hellboy/ds';
|
|
201
|
+
|
|
202
|
+
// Configure at app initialization
|
|
203
|
+
configurePreferences({
|
|
204
|
+
backend: 'localStorage', // or 'indexedDB' (future)
|
|
205
|
+
prefix: 'my-app',
|
|
206
|
+
dbName: 'my-app-prefs',
|
|
207
|
+
storeName: 'preferences'
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Framework Integration
|
|
212
|
+
|
|
213
|
+
### React Hook Pattern
|
|
214
|
+
|
|
215
|
+
Here's a recommended pattern for React applications:
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
// hooks/usePreference.ts
|
|
219
|
+
import { useState, useEffect } from 'react';
|
|
220
|
+
import { getPreference, setPreference } from '@hellboy/ds';
|
|
221
|
+
|
|
222
|
+
export function usePreference<T>(key: string, defaultValue: T) {
|
|
223
|
+
const [value, setValue] = useState<T>(() =>
|
|
224
|
+
getPreference(key, defaultValue) ?? defaultValue
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const updateValue = (newValue: T | ((prev: T) => T)) => {
|
|
228
|
+
const resolvedValue = typeof newValue === 'function'
|
|
229
|
+
? (newValue as (prev: T) => T)(value)
|
|
230
|
+
: newValue;
|
|
231
|
+
|
|
232
|
+
setValue(resolvedValue);
|
|
233
|
+
setPreference(key, resolvedValue);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return [value, updateValue] as const;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Usage in components
|
|
240
|
+
function MyComponent() {
|
|
241
|
+
const [theme, setTheme] = usePreference('theme', 'light');
|
|
242
|
+
const [sidebarWidth, setSidebarWidth] = usePreference('layout.sidebarWidth', 280);
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<div>
|
|
246
|
+
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
|
|
247
|
+
Toggle Theme
|
|
248
|
+
</button>
|
|
249
|
+
<button onClick={() => setSidebarWidth(w => w + 20)}>
|
|
250
|
+
Increase Sidebar
|
|
251
|
+
</button>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Vue Composition API
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// composables/usePreference.ts
|
|
261
|
+
import { ref, watch } from 'vue';
|
|
262
|
+
import { getPreference, setPreference } from '@hellboy/ds';
|
|
263
|
+
|
|
264
|
+
export function usePreference<T>(key: string, defaultValue: T) {
|
|
265
|
+
const value = ref<T>(getPreference(key, defaultValue) ?? defaultValue);
|
|
266
|
+
|
|
267
|
+
watch(value, (newValue) => {
|
|
268
|
+
setPreference(key, newValue);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Usage in components
|
|
275
|
+
import { usePreference } from '@/composables/usePreference';
|
|
276
|
+
|
|
277
|
+
export default {
|
|
278
|
+
setup() {
|
|
279
|
+
const theme = usePreference('theme', 'light');
|
|
280
|
+
const sidebarWidth = usePreference('layout.sidebarWidth', 280);
|
|
281
|
+
|
|
282
|
+
return { theme, sidebarWidth };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Vanilla JavaScript
|
|
288
|
+
|
|
289
|
+
```javascript
|
|
290
|
+
import { getPreference, setPreference } from '@hellboy/ds';
|
|
291
|
+
|
|
292
|
+
// On page load
|
|
293
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
294
|
+
const theme = getPreference('theme', 'light');
|
|
295
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
296
|
+
|
|
297
|
+
const sidebarWidth = getPreference('layout.sidebarWidth', 280);
|
|
298
|
+
document.querySelector('.sidebar').style.width = `${sidebarWidth}px`;
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// On user interaction
|
|
302
|
+
document.querySelector('#theme-toggle').addEventListener('click', () => {
|
|
303
|
+
const current = getPreference('theme', 'light');
|
|
304
|
+
const next = current === 'light' ? 'dark' : 'light';
|
|
305
|
+
setPreference('theme', next);
|
|
306
|
+
document.documentElement.setAttribute('data-theme', next);
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Real-World Examples
|
|
311
|
+
|
|
312
|
+
### Example 1: Persistent Resizable Sidebar
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
import { useState, useCallback } from 'react';
|
|
316
|
+
import { getPreference, setPreference } from '@hellboy/ds';
|
|
317
|
+
import { Layout } from '@hellboy/ds';
|
|
318
|
+
|
|
319
|
+
function App() {
|
|
320
|
+
const [leftWidth, setLeftWidth] = useState(() =>
|
|
321
|
+
getPreference('layout.leftSidebarWidth', 280)
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const handleResize = useCallback((delta: number) => {
|
|
325
|
+
setLeftWidth(prev => {
|
|
326
|
+
const newWidth = Math.max(200, Math.min(600, prev + delta));
|
|
327
|
+
setPreference('layout.leftSidebarWidth', newWidth);
|
|
328
|
+
return newWidth;
|
|
329
|
+
});
|
|
330
|
+
}, []);
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<Layout
|
|
334
|
+
variant="sidebar-main-sidebar"
|
|
335
|
+
resizable
|
|
336
|
+
sidebarLeft={<Sidebar width={leftWidth} />}
|
|
337
|
+
onLeftResize={handleResize}
|
|
338
|
+
>
|
|
339
|
+
<MainContent />
|
|
340
|
+
</Layout>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Example 2: Theme Persistence with Color Customization
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { useEffect } from 'react';
|
|
349
|
+
import {
|
|
350
|
+
getPreference,
|
|
351
|
+
setPreference,
|
|
352
|
+
applyColorConfig,
|
|
353
|
+
setTheme
|
|
354
|
+
} from '@hellboy/ds';
|
|
355
|
+
|
|
356
|
+
function ThemeProvider({ children }) {
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
// Restore theme mode
|
|
359
|
+
const theme = getPreference('theme', 'light');
|
|
360
|
+
setTheme(theme);
|
|
361
|
+
|
|
362
|
+
// Restore custom colors
|
|
363
|
+
const colorConfig = getPreference('colorConfig');
|
|
364
|
+
if (colorConfig) {
|
|
365
|
+
applyColorConfig(colorConfig);
|
|
366
|
+
}
|
|
367
|
+
}, []);
|
|
368
|
+
|
|
369
|
+
const updateTheme = (newTheme) => {
|
|
370
|
+
setPreference('theme', newTheme);
|
|
371
|
+
setTheme(newTheme);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const updateColors = (colorConfig) => {
|
|
375
|
+
setPreference('colorConfig', colorConfig);
|
|
376
|
+
applyColorConfig(colorConfig);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
return children;
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Example 3: Table Preferences
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
function DataTable() {
|
|
387
|
+
const [sortColumn, setSortColumn] = usePreference('table.sortColumn', 'name');
|
|
388
|
+
const [sortDirection, setSortDirection] = usePreference('table.sortDirection', 'asc');
|
|
389
|
+
const [pageSize, setPageSize] = usePreference('table.pageSize', 25);
|
|
390
|
+
const [visibleColumns, setVisibleColumns] = usePreference('table.visibleColumns', [
|
|
391
|
+
'name', 'email', 'role', 'status'
|
|
392
|
+
]);
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<Table
|
|
396
|
+
sortColumn={sortColumn}
|
|
397
|
+
sortDirection={sortDirection}
|
|
398
|
+
pageSize={pageSize}
|
|
399
|
+
visibleColumns={visibleColumns}
|
|
400
|
+
onSortChange={(col, dir) => {
|
|
401
|
+
setSortColumn(col);
|
|
402
|
+
setSortDirection(dir);
|
|
403
|
+
}}
|
|
404
|
+
onPageSizeChange={setPageSize}
|
|
405
|
+
onVisibleColumnsChange={setVisibleColumns}
|
|
406
|
+
/>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## API Reference
|
|
412
|
+
|
|
413
|
+
### Core Functions
|
|
414
|
+
|
|
415
|
+
| Function | Description |
|
|
416
|
+
|----------|-------------|
|
|
417
|
+
| `getPreference<T>(key, defaultValue?)` | Get a preference value |
|
|
418
|
+
| `setPreference<T>(key, value)` | Set a preference value |
|
|
419
|
+
| `removePreference(key)` | Remove a preference |
|
|
420
|
+
| `clearPreferences()` | Clear all preferences |
|
|
421
|
+
| `getAllPreferences()` | Get all preferences as object |
|
|
422
|
+
|
|
423
|
+
### Async Functions (IndexedDB)
|
|
424
|
+
|
|
425
|
+
| Function | Description |
|
|
426
|
+
|----------|-------------|
|
|
427
|
+
| `getPreferenceAsync<T>(key, defaultValue?)` | Async get for IndexedDB |
|
|
428
|
+
| `setPreferenceAsync<T>(key, value)` | Async set for IndexedDB |
|
|
429
|
+
| `removePreferenceAsync(key)` | Async remove for IndexedDB |
|
|
430
|
+
| `clearPreferencesAsync()` | Async clear for IndexedDB |
|
|
431
|
+
|
|
432
|
+
### Utility Functions
|
|
433
|
+
|
|
434
|
+
| Function | Description |
|
|
435
|
+
|----------|-------------|
|
|
436
|
+
| `configurePreferences(config)` | Configure storage backend |
|
|
437
|
+
| `createPreferenceNamespace(namespace)` | Create scoped preferences |
|
|
438
|
+
| `exportPreferences()` | Export preferences as JSON |
|
|
439
|
+
| `importPreferences(json, merge?)` | Import preferences from JSON |
|
|
440
|
+
|
|
441
|
+
## TypeScript Support
|
|
442
|
+
|
|
443
|
+
The utility is fully typed with TypeScript:
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
import type { UserPreferences } from '@hellboy/ds';
|
|
447
|
+
|
|
448
|
+
// Type-safe preference access
|
|
449
|
+
const theme = getPreference<'light' | 'dark'>('theme', 'light');
|
|
450
|
+
const width = getPreference<number>('layout.sidebarWidth', 280);
|
|
451
|
+
|
|
452
|
+
// Extend UserPreferences interface for custom types
|
|
453
|
+
declare module '@hellboy/ds' {
|
|
454
|
+
interface UserPreferences {
|
|
455
|
+
custom?: {
|
|
456
|
+
myCustomSetting?: string;
|
|
457
|
+
myFeatureFlags?: {
|
|
458
|
+
newUI: boolean;
|
|
459
|
+
betaFeatures: boolean;
|
|
460
|
+
};
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## Storage Details
|
|
467
|
+
|
|
468
|
+
### localStorage (Default)
|
|
469
|
+
|
|
470
|
+
- **Prefix**: `hellboy-ds:` (configurable)
|
|
471
|
+
- **Format**: JSON serialization
|
|
472
|
+
- **Capacity**: ~5-10MB (browser-dependent)
|
|
473
|
+
- **Sync**: Synchronous operations
|
|
474
|
+
- **Browser Support**: All modern browsers
|
|
475
|
+
|
|
476
|
+
### IndexedDB (Future)
|
|
477
|
+
|
|
478
|
+
- **Database**: `hellboy-ds-prefs` (configurable)
|
|
479
|
+
- **Store**: `preferences` (configurable)
|
|
480
|
+
- **Format**: Native JSON
|
|
481
|
+
- **Capacity**: Much larger than localStorage
|
|
482
|
+
- **Sync**: Asynchronous operations
|
|
483
|
+
- **Browser Support**: All modern browsers
|
|
484
|
+
|
|
485
|
+
## Best Practices
|
|
486
|
+
|
|
487
|
+
1. **Use meaningful keys**: Use dot notation for hierarchical organization
|
|
488
|
+
```typescript
|
|
489
|
+
setPreference('components.drawer.width', 400);
|
|
490
|
+
setPreference('layout.sidebar.collapsed', true);
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
2. **Always provide defaults**: Ensures app works on first load
|
|
494
|
+
```typescript
|
|
495
|
+
const width = getPreference('sidebarWidth', 280);
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
3. **Namespace related preferences**: Keep things organized
|
|
499
|
+
```typescript
|
|
500
|
+
const layoutPrefs = createPreferenceNamespace('layout');
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
4. **Export for backups**: Allow users to backup their settings
|
|
504
|
+
```typescript
|
|
505
|
+
const json = exportPreferences();
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
5. **Type your preferences**: Use TypeScript for type safety
|
|
509
|
+
```typescript
|
|
510
|
+
const theme = getPreference<'light' | 'dark'>('theme', 'light');
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
## Browser Compatibility
|
|
514
|
+
|
|
515
|
+
- ✅ Chrome 4+
|
|
516
|
+
- ✅ Firefox 3.5+
|
|
517
|
+
- ✅ Safari 4+
|
|
518
|
+
- ✅ Edge (all versions)
|
|
519
|
+
- ✅ Opera 10.50+
|
|
520
|
+
- ✅ iOS Safari 3.2+
|
|
521
|
+
- ✅ Android Browser 2.1+
|
|
522
|
+
|
|
523
|
+
## Performance Considerations
|
|
524
|
+
|
|
525
|
+
- localStorage is **synchronous** - operations block the main thread
|
|
526
|
+
- For **large datasets** (>1MB), consider using IndexedDB backend (when available)
|
|
527
|
+
- **Batch updates** when possible to minimize localStorage writes
|
|
528
|
+
- Use **namespaces** to organize and clear related preferences efficiently
|
|
529
|
+
|
|
530
|
+
## Security Notes
|
|
531
|
+
|
|
532
|
+
- localStorage is **not encrypted** - don't store sensitive data
|
|
533
|
+
- Data is **domain-specific** - accessible to all scripts on the same origin
|
|
534
|
+
- Data persists **indefinitely** until explicitly cleared or browser storage is cleared
|
|
535
|
+
- Consider **user privacy** - allow users to export/clear their data
|
|
536
|
+
|
|
537
|
+
## Migration from Legacy Storage
|
|
538
|
+
|
|
539
|
+
If you're migrating from direct localStorage usage:
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
// Before
|
|
543
|
+
localStorage.setItem('myTheme', 'dark');
|
|
544
|
+
const theme = localStorage.getItem('myTheme') || 'light';
|
|
545
|
+
|
|
546
|
+
// After
|
|
547
|
+
import { setPreference, getPreference } from '@hellboy/ds';
|
|
548
|
+
setPreference('theme', 'dark');
|
|
549
|
+
const theme = getPreference('theme', 'light');
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
## Support & Contributing
|
|
553
|
+
|
|
554
|
+
This utility is part of the Hellboy Design System. For issues, feature requests, or contributions, please visit the [GitHub repository](#).
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
**Design System Philosophy**: We believe in providing **practical, opinionated solutions** that accelerate development. While staying framework-agnostic where possible, we're not afraid to be opinionated when it delivers real value to developers. The User Preferences utility embodies this philosophy by solving a common problem with a production-ready, batteries-included solution.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Utility
|
|
3
|
+
* Manages data-theme attribute on <html>, localStorage persistence, and prefers-color-scheme fallback
|
|
4
|
+
*
|
|
5
|
+
* Priority: data-theme (explicit) > prefers-color-scheme (OS preference) > light (default)
|
|
6
|
+
*
|
|
7
|
+
* This utility now uses the User Preferences system for storage.
|
|
8
|
+
* See USER_PREFERENCES.md for more details on the underlying storage system.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getPreference, setPreference } from './user-preferences';
|
|
12
|
+
|
|
13
|
+
type Theme = 'light' | 'dark';
|
|
14
|
+
|
|
15
|
+
// Preference key for storing the chosen mode only
|
|
16
|
+
const THEME_KEY = 'theme';
|
|
17
|
+
|
|
18
|
+
const ATTR_NAME = 'data-theme';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Apply theme to document element
|
|
22
|
+
*/
|
|
23
|
+
export const applyTheme = (theme: Theme) => {
|
|
24
|
+
document.documentElement.setAttribute(ATTR_NAME, theme);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get current theme.
|
|
29
|
+
* Sources of truth (simplified):
|
|
30
|
+
* - `data-theme` attribute on <html>
|
|
31
|
+
* - OS `prefers-color-scheme`
|
|
32
|
+
* - default: 'light'
|
|
33
|
+
*/
|
|
34
|
+
export const getTheme = (): Theme => {
|
|
35
|
+
// 1. If `data-theme` is explicitly set on <html>, that wins
|
|
36
|
+
const dataTheme = document.documentElement.getAttribute(ATTR_NAME);
|
|
37
|
+
if (dataTheme === 'light' || dataTheme === 'dark') return dataTheme;
|
|
38
|
+
|
|
39
|
+
// 2. If the user previously selected a mode, use stored preference
|
|
40
|
+
try {
|
|
41
|
+
const stored = getPreference<Theme>(THEME_KEY);
|
|
42
|
+
if (stored === 'light' || stored === 'dark') return stored;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// ignore storage errors and fallback to system/default
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Fall back to OS preference
|
|
48
|
+
if (typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
49
|
+
return 'dark';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 4. Default
|
|
53
|
+
return 'light';
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Backwards-compatible alias
|
|
58
|
+
*/
|
|
59
|
+
export const getSavedTheme = (): Theme => getTheme();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set theme on document (no persistence)
|
|
63
|
+
*/
|
|
64
|
+
export const setTheme = (theme: Theme) => {
|
|
65
|
+
if (!['light', 'dark'].includes(theme)) {
|
|
66
|
+
console.warn(`Invalid theme: ${theme}. Using 'light' as fallback.`);
|
|
67
|
+
theme = 'light';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Persist only the mode (light/dark)
|
|
71
|
+
try {
|
|
72
|
+
setPreference(THEME_KEY, theme);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// ignore storage errors
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
applyTheme(theme);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Initialize theme on startup (mode-only)
|
|
82
|
+
*/
|
|
83
|
+
export const initializeTheme = () => {
|
|
84
|
+
const theme = getTheme();
|
|
85
|
+
applyTheme(theme);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Listen to system theme changes. Only calls callback when OS preference
|
|
90
|
+
* changes. Consumers decide whether to apply it.
|
|
91
|
+
*/
|
|
92
|
+
export const onSystemThemeChange = (callback: (theme: Theme) => void): (() => void) => {
|
|
93
|
+
if (typeof window === 'undefined') return () => {};
|
|
94
|
+
|
|
95
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
96
|
+
const listener = (e: MediaQueryListEvent | MediaQueryList) => {
|
|
97
|
+
const theme = e.matches ? 'dark' : 'light';
|
|
98
|
+
// Only notify consumers if the user has NOT explicitly chosen a mode
|
|
99
|
+
const hasStored = (() => {
|
|
100
|
+
try {
|
|
101
|
+
const stored = getPreference<Theme>(THEME_KEY);
|
|
102
|
+
return stored === 'light' || stored === 'dark';
|
|
103
|
+
} catch { return false; }
|
|
104
|
+
})();
|
|
105
|
+
|
|
106
|
+
const dataTheme = document.documentElement.getAttribute(ATTR_NAME);
|
|
107
|
+
if (!hasStored && !dataTheme) callback(theme);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (mediaQuery.addEventListener) {
|
|
111
|
+
mediaQuery.addEventListener('change', listener);
|
|
112
|
+
return () => mediaQuery.removeEventListener('change', listener);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
mediaQuery.addListener(listener as any);
|
|
116
|
+
return () => mediaQuery.removeListener(listener as any);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Toggle between light and dark themes (no persistence)
|
|
121
|
+
*/
|
|
122
|
+
export const toggleTheme = (): Theme => {
|
|
123
|
+
const current = getTheme();
|
|
124
|
+
const next = current === 'light' ? 'dark' : 'light';
|
|
125
|
+
setTheme(next);
|
|
126
|
+
return next;
|
|
127
|
+
};
|