@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,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Preferences Utility
|
|
3
|
+
*
|
|
4
|
+
* A comprehensive user preference management system for the Hellboy Design System.
|
|
5
|
+
* This utility provides a practical, opinionated solution for storing and retrieving
|
|
6
|
+
* user preferences using localStorage with optional IndexedDB fallback.
|
|
7
|
+
*
|
|
8
|
+
* Philosophy:
|
|
9
|
+
* The Hellboy Design System aims to maximize development velocity by providing
|
|
10
|
+
* complete, practical solutions rather than staying purely agnostic. While many
|
|
11
|
+
* design systems focus solely on visual components, we believe in delivering
|
|
12
|
+
* utilities that solve common real-world problems developers face.
|
|
13
|
+
*
|
|
14
|
+
* This preference system is designed to be:
|
|
15
|
+
* - Easy to use with minimal configuration
|
|
16
|
+
* - Type-safe with TypeScript
|
|
17
|
+
* - Persistent across sessions
|
|
18
|
+
* - Extensible for custom preferences
|
|
19
|
+
* - Framework-agnostic (works with React, Vue, vanilla JS, etc.)
|
|
20
|
+
*
|
|
21
|
+
* Supported preference categories:
|
|
22
|
+
* - Theme preferences (light/dark mode, custom colors)
|
|
23
|
+
* - Layout preferences (sidebar widths, panel sizes)
|
|
24
|
+
* - Component states (drawer positions, table column widths)
|
|
25
|
+
* - User-specific settings (language, timezone, etc.)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Storage backend types
|
|
30
|
+
*/
|
|
31
|
+
type StorageBackend = 'localStorage' | 'indexedDB';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Base preference key type for type safety
|
|
35
|
+
*/
|
|
36
|
+
type PreferenceKey = string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* User preferences interface
|
|
40
|
+
* Extend this interface to add custom preference types
|
|
41
|
+
*/
|
|
42
|
+
export interface UserPreferences {
|
|
43
|
+
// Theme preferences
|
|
44
|
+
theme?: 'light' | 'dark';
|
|
45
|
+
colorConfig?: {
|
|
46
|
+
primaryHue?: number;
|
|
47
|
+
primarySaturation?: number;
|
|
48
|
+
primaryLightness?: number;
|
|
49
|
+
secondaryHue?: number;
|
|
50
|
+
secondarySaturation?: number;
|
|
51
|
+
secondaryLightness?: number;
|
|
52
|
+
accentHue?: number;
|
|
53
|
+
accentSaturation?: number;
|
|
54
|
+
accentLightness?: number;
|
|
55
|
+
successHue?: number;
|
|
56
|
+
successSaturation?: number;
|
|
57
|
+
successLightness?: number;
|
|
58
|
+
warningHue?: number;
|
|
59
|
+
warningSaturation?: number;
|
|
60
|
+
warningLightness?: number;
|
|
61
|
+
errorHue?: number;
|
|
62
|
+
errorSaturation?: number;
|
|
63
|
+
errorLightness?: number;
|
|
64
|
+
infoHue?: number;
|
|
65
|
+
infoSaturation?: number;
|
|
66
|
+
infoLightness?: number;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Layout preferences
|
|
70
|
+
layout?: {
|
|
71
|
+
leftSidebarWidth?: number;
|
|
72
|
+
rightSidebarWidth?: number;
|
|
73
|
+
bottomBarHeight?: number;
|
|
74
|
+
navbarCollapsed?: boolean;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Component-specific preferences
|
|
78
|
+
components?: {
|
|
79
|
+
[componentName: string]: {
|
|
80
|
+
[key: string]: any;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Custom user settings
|
|
85
|
+
custom?: {
|
|
86
|
+
[key: string]: any;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Storage configuration
|
|
92
|
+
*/
|
|
93
|
+
interface StorageConfig {
|
|
94
|
+
backend: StorageBackend;
|
|
95
|
+
prefix: string;
|
|
96
|
+
dbName?: string;
|
|
97
|
+
storeName?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Default storage configuration
|
|
102
|
+
*/
|
|
103
|
+
const DEFAULT_CONFIG: StorageConfig = {
|
|
104
|
+
backend: 'localStorage',
|
|
105
|
+
prefix: 'hellboy-ds',
|
|
106
|
+
dbName: 'hellboy-ds-prefs',
|
|
107
|
+
storeName: 'preferences',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Current storage configuration
|
|
112
|
+
*/
|
|
113
|
+
let config: StorageConfig = { ...DEFAULT_CONFIG };
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Configure the preference storage system
|
|
117
|
+
* Call this once at app initialization if you need custom configuration
|
|
118
|
+
*
|
|
119
|
+
* @param userConfig - Custom storage configuration
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* import { configurePreferences } from '@hellboy/ds';
|
|
124
|
+
*
|
|
125
|
+
* configurePreferences({
|
|
126
|
+
* backend: 'localStorage',
|
|
127
|
+
* prefix: 'my-app'
|
|
128
|
+
* });
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export const configurePreferences = (userConfig: Partial<StorageConfig>): void => {
|
|
132
|
+
config = { ...config, ...userConfig };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get full storage key with prefix
|
|
137
|
+
*/
|
|
138
|
+
const getStorageKey = (key: PreferenceKey): string => {
|
|
139
|
+
return `${config.prefix}:${key}`;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if localStorage is available
|
|
144
|
+
*/
|
|
145
|
+
const isLocalStorageAvailable = (): boolean => {
|
|
146
|
+
try {
|
|
147
|
+
const test = '__storage_test__';
|
|
148
|
+
localStorage.setItem(test, test);
|
|
149
|
+
localStorage.removeItem(test);
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* IndexedDB helper (for future expansion)
|
|
158
|
+
* Currently a placeholder - localStorage is the primary backend
|
|
159
|
+
*/
|
|
160
|
+
class IndexedDBStore {
|
|
161
|
+
private db: IDBDatabase | null = null;
|
|
162
|
+
|
|
163
|
+
async init(): Promise<void> {
|
|
164
|
+
if (typeof window === 'undefined' || !window.indexedDB) {
|
|
165
|
+
throw new Error('IndexedDB not available');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const request = indexedDB.open(config.dbName!, 1);
|
|
170
|
+
|
|
171
|
+
request.onerror = () => reject(request.error);
|
|
172
|
+
request.onsuccess = () => {
|
|
173
|
+
this.db = request.result;
|
|
174
|
+
resolve();
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
request.onupgradeneeded = (event) => {
|
|
178
|
+
const db = (event.target as IDBOpenDBRequest).result;
|
|
179
|
+
if (!db.objectStoreNames.contains(config.storeName!)) {
|
|
180
|
+
db.createObjectStore(config.storeName!, { keyPath: 'key' });
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async get(key: string): Promise<any> {
|
|
187
|
+
if (!this.db) await this.init();
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
const transaction = this.db!.transaction([config.storeName!], 'readonly');
|
|
190
|
+
const store = transaction.objectStore(config.storeName!);
|
|
191
|
+
const request = store.get(key);
|
|
192
|
+
|
|
193
|
+
request.onsuccess = () => resolve(request.result?.value);
|
|
194
|
+
request.onerror = () => reject(request.error);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async set(key: string, value: any): Promise<void> {
|
|
199
|
+
if (!this.db) await this.init();
|
|
200
|
+
return new Promise((resolve, reject) => {
|
|
201
|
+
const transaction = this.db!.transaction([config.storeName!], 'readwrite');
|
|
202
|
+
const store = transaction.objectStore(config.storeName!);
|
|
203
|
+
const request = store.put({ key, value });
|
|
204
|
+
|
|
205
|
+
request.onsuccess = () => resolve();
|
|
206
|
+
request.onerror = () => reject(request.error);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async remove(key: string): Promise<void> {
|
|
211
|
+
if (!this.db) await this.init();
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
const transaction = this.db!.transaction([config.storeName!], 'readwrite');
|
|
214
|
+
const store = transaction.objectStore(config.storeName!);
|
|
215
|
+
const request = store.delete(key);
|
|
216
|
+
|
|
217
|
+
request.onsuccess = () => resolve();
|
|
218
|
+
request.onerror = () => reject(request.error);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async clear(): Promise<void> {
|
|
223
|
+
if (!this.db) await this.init();
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const transaction = this.db!.transaction([config.storeName!], 'readwrite');
|
|
226
|
+
const store = transaction.objectStore(config.storeName!);
|
|
227
|
+
const request = store.clear();
|
|
228
|
+
|
|
229
|
+
request.onsuccess = () => resolve();
|
|
230
|
+
request.onerror = () => reject(request.error);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Singleton IndexedDB instance
|
|
237
|
+
*/
|
|
238
|
+
const indexedDBStore = new IndexedDBStore();
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get a preference value
|
|
242
|
+
*
|
|
243
|
+
* @param key - Preference key
|
|
244
|
+
* @param defaultValue - Default value if preference doesn't exist
|
|
245
|
+
* @returns The preference value or default value
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```ts
|
|
249
|
+
* import { getPreference } from '@hellboy/ds';
|
|
250
|
+
*
|
|
251
|
+
* const theme = getPreference('theme', 'light');
|
|
252
|
+
* const sidebarWidth = getPreference('layout.leftSidebarWidth', 280);
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
export const getPreference = <T = any>(
|
|
256
|
+
key: PreferenceKey,
|
|
257
|
+
defaultValue?: T
|
|
258
|
+
): T | undefined => {
|
|
259
|
+
try {
|
|
260
|
+
if (config.backend === 'localStorage' && isLocalStorageAvailable()) {
|
|
261
|
+
const stored = localStorage.getItem(getStorageKey(key));
|
|
262
|
+
if (stored !== null) {
|
|
263
|
+
return JSON.parse(stored) as T;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return defaultValue;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.warn(`Failed to get preference "${key}":`, error);
|
|
269
|
+
return defaultValue;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Async version for IndexedDB (future use)
|
|
275
|
+
*/
|
|
276
|
+
export const getPreferenceAsync = async <T = any>(
|
|
277
|
+
key: PreferenceKey,
|
|
278
|
+
defaultValue?: T
|
|
279
|
+
): Promise<T | undefined> => {
|
|
280
|
+
try {
|
|
281
|
+
if (config.backend === 'indexedDB') {
|
|
282
|
+
const value = await indexedDBStore.get(getStorageKey(key));
|
|
283
|
+
return value !== undefined ? value : defaultValue;
|
|
284
|
+
}
|
|
285
|
+
return getPreference(key, defaultValue);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
console.warn(`Failed to get preference "${key}":`, error);
|
|
288
|
+
return defaultValue;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Set a preference value
|
|
294
|
+
*
|
|
295
|
+
* @param key - Preference key
|
|
296
|
+
* @param value - Preference value to store
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```ts
|
|
300
|
+
* import { setPreference } from '@hellboy/ds';
|
|
301
|
+
*
|
|
302
|
+
* setPreference('theme', 'dark');
|
|
303
|
+
* setPreference('layout.leftSidebarWidth', 320);
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
export const setPreference = <T = any>(key: PreferenceKey, value: T): void => {
|
|
307
|
+
try {
|
|
308
|
+
if (config.backend === 'localStorage' && isLocalStorageAvailable()) {
|
|
309
|
+
localStorage.setItem(getStorageKey(key), JSON.stringify(value));
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.warn(`Failed to set preference "${key}":`, error);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Async version for IndexedDB (future use)
|
|
318
|
+
*/
|
|
319
|
+
export const setPreferenceAsync = async <T = any>(
|
|
320
|
+
key: PreferenceKey,
|
|
321
|
+
value: T
|
|
322
|
+
): Promise<void> => {
|
|
323
|
+
try {
|
|
324
|
+
if (config.backend === 'indexedDB') {
|
|
325
|
+
await indexedDBStore.set(getStorageKey(key), value);
|
|
326
|
+
} else {
|
|
327
|
+
setPreference(key, value);
|
|
328
|
+
}
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.warn(`Failed to set preference "${key}":`, error);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Remove a preference
|
|
336
|
+
*
|
|
337
|
+
* @param key - Preference key to remove
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```ts
|
|
341
|
+
* import { removePreference } from '@hellboy/ds';
|
|
342
|
+
*
|
|
343
|
+
* removePreference('layout.leftSidebarWidth');
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
export const removePreference = (key: PreferenceKey): void => {
|
|
347
|
+
try {
|
|
348
|
+
if (config.backend === 'localStorage' && isLocalStorageAvailable()) {
|
|
349
|
+
localStorage.removeItem(getStorageKey(key));
|
|
350
|
+
}
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.warn(`Failed to remove preference "${key}":`, error);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Async version for IndexedDB (future use)
|
|
358
|
+
*/
|
|
359
|
+
export const removePreferenceAsync = async (key: PreferenceKey): Promise<void> => {
|
|
360
|
+
try {
|
|
361
|
+
if (config.backend === 'indexedDB') {
|
|
362
|
+
await indexedDBStore.remove(getStorageKey(key));
|
|
363
|
+
} else {
|
|
364
|
+
removePreference(key);
|
|
365
|
+
}
|
|
366
|
+
} catch (error) {
|
|
367
|
+
console.warn(`Failed to remove preference "${key}":`, error);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Clear all preferences
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```ts
|
|
376
|
+
* import { clearPreferences } from '@hellboy/ds';
|
|
377
|
+
*
|
|
378
|
+
* clearPreferences(); // Removes all hellboy-ds preferences
|
|
379
|
+
* ```
|
|
380
|
+
*/
|
|
381
|
+
export const clearPreferences = (): void => {
|
|
382
|
+
try {
|
|
383
|
+
if (config.backend === 'localStorage' && isLocalStorageAvailable()) {
|
|
384
|
+
const prefix = `${config.prefix}:`;
|
|
385
|
+
const keysToRemove: string[] = [];
|
|
386
|
+
|
|
387
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
388
|
+
const key = localStorage.key(i);
|
|
389
|
+
if (key?.startsWith(prefix)) {
|
|
390
|
+
keysToRemove.push(key);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
keysToRemove.forEach(key => localStorage.removeItem(key));
|
|
395
|
+
}
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.warn('Failed to clear preferences:', error);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Async version for IndexedDB (future use)
|
|
403
|
+
*/
|
|
404
|
+
export const clearPreferencesAsync = async (): Promise<void> => {
|
|
405
|
+
try {
|
|
406
|
+
if (config.backend === 'indexedDB') {
|
|
407
|
+
await indexedDBStore.clear();
|
|
408
|
+
} else {
|
|
409
|
+
clearPreferences();
|
|
410
|
+
}
|
|
411
|
+
} catch (error) {
|
|
412
|
+
console.warn('Failed to clear preferences:', error);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Get all preferences as an object
|
|
418
|
+
*
|
|
419
|
+
* @returns Object containing all stored preferences
|
|
420
|
+
*
|
|
421
|
+
* @example
|
|
422
|
+
* ```ts
|
|
423
|
+
* import { getAllPreferences } from '@hellboy/ds';
|
|
424
|
+
*
|
|
425
|
+
* const allPrefs = getAllPreferences();
|
|
426
|
+
* console.log(allPrefs);
|
|
427
|
+
* // { 'theme': 'dark', 'layout.leftSidebarWidth': 320, ... }
|
|
428
|
+
* ```
|
|
429
|
+
*/
|
|
430
|
+
export const getAllPreferences = (): Record<string, any> => {
|
|
431
|
+
const preferences: Record<string, any> = {};
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
if (config.backend === 'localStorage' && isLocalStorageAvailable()) {
|
|
435
|
+
const prefix = `${config.prefix}:`;
|
|
436
|
+
|
|
437
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
438
|
+
const key = localStorage.key(i);
|
|
439
|
+
if (key?.startsWith(prefix)) {
|
|
440
|
+
const prefKey = key.substring(prefix.length);
|
|
441
|
+
const value = localStorage.getItem(key);
|
|
442
|
+
if (value !== null) {
|
|
443
|
+
try {
|
|
444
|
+
preferences[prefKey] = JSON.parse(value);
|
|
445
|
+
} catch {
|
|
446
|
+
preferences[prefKey] = value;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} catch (error) {
|
|
453
|
+
console.warn('Failed to get all preferences:', error);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return preferences;
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Export preferences as JSON
|
|
461
|
+
* Useful for backup or migration
|
|
462
|
+
*
|
|
463
|
+
* @returns JSON string of all preferences
|
|
464
|
+
*
|
|
465
|
+
* @example
|
|
466
|
+
* ```ts
|
|
467
|
+
* import { exportPreferences } from '@hellboy/ds';
|
|
468
|
+
*
|
|
469
|
+
* const json = exportPreferences();
|
|
470
|
+
* // Download or save to file
|
|
471
|
+
* const blob = new Blob([json], { type: 'application/json' });
|
|
472
|
+
* const url = URL.createObjectURL(blob);
|
|
473
|
+
* ```
|
|
474
|
+
*/
|
|
475
|
+
export const exportPreferences = (): string => {
|
|
476
|
+
const prefs = getAllPreferences();
|
|
477
|
+
return JSON.stringify(prefs, null, 2);
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Import preferences from JSON
|
|
482
|
+
* Useful for restoring from backup
|
|
483
|
+
*
|
|
484
|
+
* @param json - JSON string of preferences to import
|
|
485
|
+
* @param merge - Whether to merge with existing preferences (default: false)
|
|
486
|
+
*
|
|
487
|
+
* @example
|
|
488
|
+
* ```ts
|
|
489
|
+
* import { importPreferences } from '@hellboy/ds';
|
|
490
|
+
*
|
|
491
|
+
* const json = '{"theme":"dark","layout.leftSidebarWidth":320}';
|
|
492
|
+
* importPreferences(json, true); // Merge with existing
|
|
493
|
+
* ```
|
|
494
|
+
*/
|
|
495
|
+
export const importPreferences = (json: string, merge: boolean = false): void => {
|
|
496
|
+
try {
|
|
497
|
+
const prefs = JSON.parse(json);
|
|
498
|
+
|
|
499
|
+
if (!merge) {
|
|
500
|
+
clearPreferences();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
Object.entries(prefs).forEach(([key, value]) => {
|
|
504
|
+
setPreference(key, value);
|
|
505
|
+
});
|
|
506
|
+
} catch (error) {
|
|
507
|
+
console.warn('Failed to import preferences:', error);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Create a preference namespace for better organization
|
|
513
|
+
* Returns scoped get/set functions
|
|
514
|
+
*
|
|
515
|
+
* @param namespace - Namespace prefix
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* ```ts
|
|
519
|
+
* import { createPreferenceNamespace } from '@hellboy/ds';
|
|
520
|
+
*
|
|
521
|
+
* const layoutPrefs = createPreferenceNamespace('layout');
|
|
522
|
+
* layoutPrefs.set('sidebarWidth', 300);
|
|
523
|
+
* const width = layoutPrefs.get('sidebarWidth', 280);
|
|
524
|
+
* ```
|
|
525
|
+
*/
|
|
526
|
+
export const createPreferenceNamespace = (namespace: string) => {
|
|
527
|
+
const getKey = (key: string) => `${namespace}.${key}`;
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
get: <T = any>(key: string, defaultValue?: T) =>
|
|
531
|
+
getPreference<T>(getKey(key), defaultValue),
|
|
532
|
+
set: <T = any>(key: string, value: T) =>
|
|
533
|
+
setPreference(getKey(key), value),
|
|
534
|
+
remove: (key: string) => removePreference(getKey(key)),
|
|
535
|
+
clear: () => {
|
|
536
|
+
const prefs = getAllPreferences();
|
|
537
|
+
const prefix = `${namespace}.`;
|
|
538
|
+
Object.keys(prefs)
|
|
539
|
+
.filter(key => key.startsWith(prefix))
|
|
540
|
+
.forEach(key => removePreference(key));
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* React hook for preference management (optional, for React users)
|
|
547
|
+
* Import this separately to avoid bundling React in non-React projects
|
|
548
|
+
*
|
|
549
|
+
* Note: This is a reference implementation. For production use in React,
|
|
550
|
+
* consider creating a dedicated hook file with proper React imports.
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* ```tsx
|
|
554
|
+
* // In a React component
|
|
555
|
+
* import { useState, useEffect } from 'react';
|
|
556
|
+
* import { getPreference, setPreference } from '@hellboy/ds';
|
|
557
|
+
*
|
|
558
|
+
* function usePreference<T>(key: string, defaultValue: T) {
|
|
559
|
+
* const [value, setValue] = useState<T>(() =>
|
|
560
|
+
* getPreference(key, defaultValue) ?? defaultValue
|
|
561
|
+
* );
|
|
562
|
+
*
|
|
563
|
+
* const updateValue = (newValue: T) => {
|
|
564
|
+
* setValue(newValue);
|
|
565
|
+
* setPreference(key, newValue);
|
|
566
|
+
* };
|
|
567
|
+
*
|
|
568
|
+
* return [value, updateValue] as const;
|
|
569
|
+
* }
|
|
570
|
+
*
|
|
571
|
+
* // Usage
|
|
572
|
+
* function MyComponent() {
|
|
573
|
+
* const [theme, setTheme] = usePreference('theme', 'light');
|
|
574
|
+
* return <button onClick={() => setTheme('dark')}>Switch to Dark</button>;
|
|
575
|
+
* }
|
|
576
|
+
* ```
|
|
577
|
+
*/
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noUnusedLocals": true,
|
|
12
|
+
"noUnusedParameters": true,
|
|
13
|
+
"noImplicitReturns": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"outDir": "./dist",
|
|
18
|
+
"rootDir": "./src",
|
|
19
|
+
"moduleResolution": "bundler",
|
|
20
|
+
"jsx": "react-jsx",
|
|
21
|
+
"resolveJsonModule": true,
|
|
22
|
+
"isolatedModules": true
|
|
23
|
+
},
|
|
24
|
+
"include": ["src"]
|
|
25
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/index.ts'],
|
|
5
|
+
format: ['esm', 'cjs'],
|
|
6
|
+
dts: true,
|
|
7
|
+
sourcemap: true,
|
|
8
|
+
clean: true,
|
|
9
|
+
external: ['react', 'react-dom', '@ariakit/react'],
|
|
10
|
+
// Copiar arquivos CSS e outros assets
|
|
11
|
+
// Copiar `theme.css` para `dist/theme.css` para que consumidores
|
|
12
|
+
// possam carregá-lo diretamente no browser como fonte da verdade.
|
|
13
|
+
asset: [],
|
|
14
|
+
onSuccess: async () => {
|
|
15
|
+
const fs = await import('fs');
|
|
16
|
+
const path = await import('path');
|
|
17
|
+
// Generate theme.css from theme.json (source of truth)
|
|
18
|
+
try {
|
|
19
|
+
const jsonPath = path.resolve(__dirname, 'src/style/themes/theme.json');
|
|
20
|
+
const dest = path.resolve(__dirname, 'dist/theme.css');
|
|
21
|
+
|
|
22
|
+
let themeObj: any = null;
|
|
23
|
+
if (fs.existsSync && fs.existsSync(jsonPath)) {
|
|
24
|
+
const raw = await fs.promises.readFile(jsonPath, 'utf8');
|
|
25
|
+
themeObj = JSON.parse(raw);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (themeObj) {
|
|
29
|
+
const css = `/**\n * Theme Configuration (generated from theme.json)\n * Generated during build from src/style/themes/theme.json\n */\n\n:root {\n /* Primary Color */\n --primary-hue: ${themeObj.primary.hue};\n --primary-saturation: ${themeObj.primary.saturation}%;\n --primary-lightness: ${themeObj.primary.lightness}%;\n --primary-lightness-hover: ${themeObj.primary.hover}%;\n --primary-lightness-active: ${themeObj.primary.active}%;\n --primary-lightness-disabled: ${themeObj.primary.disabled}%;\n\n /* Secondary Color */\n --secondary-hue: ${themeObj.secondary.hue};\n --secondary-saturation: ${themeObj.secondary.saturation}%;\n --secondary-lightness: ${themeObj.secondary.lightness}%;\n --secondary-lightness-hover: ${themeObj.secondary.hover}%;\n --secondary-lightness-active: ${themeObj.secondary.active}%;\n --secondary-lightness-disabled: ${themeObj.secondary.disabled}%;\n\n /* Accent Color */\n --accent-hue: ${themeObj.accent.hue};\n --accent-saturation: ${themeObj.accent.saturation}%;\n --accent-lightness: ${themeObj.accent.lightness}%;\n --accent-lightness-hover: ${themeObj.accent.hover}%;\n --accent-lightness-active: ${themeObj.accent.active}%;\n --accent-lightness-disabled: ${themeObj.accent.disabled}%;\n\n /* Status Colors */\n --success-hue: ${themeObj.success.hue};\n --success-saturation: ${themeObj.success.saturation}%;\n --success-lightness: ${themeObj.success.lightness}%;\n --success-lightness-hover: ${themeObj.success.hover}%;\n --success-lightness-active: ${themeObj.success.active}%;\n\n --warning-hue: ${themeObj.warning.hue};\n --warning-saturation: ${themeObj.warning.saturation}%;\n --warning-lightness: ${themeObj.warning.lightness}%;\n --warning-lightness-hover: ${themeObj.warning.hover}%;\n --warning-lightness-active: ${themeObj.warning.active}%;\n\n --error-hue: ${themeObj.error.hue};\n --error-saturation: ${themeObj.error.saturation}%;\n --error-lightness: ${themeObj.error.lightness}%;\n --error-lightness-hover: ${themeObj.error.hover}%;\n --error-lightness-active: ${themeObj.error.active}%;\n\n --info-hue: ${themeObj.info.hue};\n --info-saturation: ${themeObj.info.saturation}%;\n --info-lightness: ${themeObj.info.lightness}%;\n --info-lightness-hover: ${themeObj.info.hover}%;\n --info-lightness-active: ${themeObj.info.active}%;\n}\n`;
|
|
30
|
+
|
|
31
|
+
await fs.promises.writeFile(dest, css, 'utf8');
|
|
32
|
+
console.log('✓ Generated dist/theme.css from theme.json');
|
|
33
|
+
|
|
34
|
+
// Also copy into web/public if exists
|
|
35
|
+
const webPublicDest = path.resolve(__dirname, '..', 'web', 'public', 'theme.css');
|
|
36
|
+
const webPublicDir = path.dirname(webPublicDest);
|
|
37
|
+
if (fs.existsSync && fs.existsSync(webPublicDir)) {
|
|
38
|
+
await fs.promises.writeFile(webPublicDest, css, 'utf8');
|
|
39
|
+
console.log('✓ Wrote theme.css to ../web/public/theme.css');
|
|
40
|
+
} else {
|
|
41
|
+
console.log('→ ../web/public not found — skipped writing theme.css to web public');
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
console.warn('! theme.json not found; skipping theme generation');
|
|
45
|
+
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.warn('! Could not generate theme.css from theme.json:', err);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log('✓ Build completo!');
|
|
51
|
+
},
|
|
52
|
+
});
|