kmod-cli 1.0.10
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 +53 -0
- package/bin/gen-components.js +68 -0
- package/bin/index.js +153 -0
- package/component-templates/components/access-denied.tsx +130 -0
- package/component-templates/components/breadcumb.tsx +42 -0
- package/component-templates/components/count-down.tsx +94 -0
- package/component-templates/components/count-input.tsx +221 -0
- package/component-templates/components/date-range-calendar/button.tsx +61 -0
- package/component-templates/components/date-range-calendar/calendar.tsx +132 -0
- package/component-templates/components/date-range-calendar/date-input.tsx +259 -0
- package/component-templates/components/date-range-calendar/date-range-picker.tsx +594 -0
- package/component-templates/components/date-range-calendar/label.tsx +31 -0
- package/component-templates/components/date-range-calendar/popover.tsx +32 -0
- package/component-templates/components/date-range-calendar/select.tsx +125 -0
- package/component-templates/components/date-range-calendar/switch.tsx +30 -0
- package/component-templates/components/datetime-picker/button.tsx +61 -0
- package/component-templates/components/datetime-picker/calendar.tsx +156 -0
- package/component-templates/components/datetime-picker/datetime-picker.tsx +75 -0
- package/component-templates/components/datetime-picker/input.tsx +20 -0
- package/component-templates/components/datetime-picker/label.tsx +18 -0
- package/component-templates/components/datetime-picker/period-input.tsx +62 -0
- package/component-templates/components/datetime-picker/popover.tsx +32 -0
- package/component-templates/components/datetime-picker/select.tsx +125 -0
- package/component-templates/components/datetime-picker/time-picker-input.tsx +131 -0
- package/component-templates/components/datetime-picker/time-picker-utils.tsx +204 -0
- package/component-templates/components/datetime-picker/time-picker.tsx +59 -0
- package/component-templates/components/gradient-outline.tsx +233 -0
- package/component-templates/components/gradient-svg.tsx +157 -0
- package/component-templates/components/grid-layout.tsx +69 -0
- package/component-templates/components/hydrate-guard.tsx +40 -0
- package/component-templates/components/image.tsx +92 -0
- package/component-templates/components/loader-slash-gradient.tsx +85 -0
- package/component-templates/components/masonry-gallery.tsx +221 -0
- package/component-templates/components/modal.tsx +110 -0
- package/component-templates/components/multi-select.tsx +447 -0
- package/component-templates/components/non-hydration.tsx +27 -0
- package/component-templates/components/portal.tsx +34 -0
- package/component-templates/components/segments-circle.tsx +235 -0
- package/component-templates/components/single-select.tsx +248 -0
- package/component-templates/components/stroke-circle.tsx +57 -0
- package/component-templates/components/table/column-table.tsx +15 -0
- package/component-templates/components/table/data-table.tsx +339 -0
- package/component-templates/components/table/readme.tsx +95 -0
- package/component-templates/components/table/table.tsx +60 -0
- package/component-templates/components/text-hover-effect.tsx +120 -0
- package/component-templates/components/timout-loader.tsx +52 -0
- package/component-templates/components/toast.tsx +994 -0
- package/component-templates/configs/config.ts +33 -0
- package/component-templates/configs/feature-config.tsx +432 -0
- package/component-templates/configs/keys.ts +7 -0
- package/component-templates/core/api-service.ts +202 -0
- package/component-templates/core/calculate.ts +18 -0
- package/component-templates/core/idb.ts +166 -0
- package/component-templates/core/storage.ts +213 -0
- package/component-templates/hooks/count-down.ts +38 -0
- package/component-templates/hooks/fade-on-scroll.ts +52 -0
- package/component-templates/hooks/safe-action.ts +59 -0
- package/component-templates/hooks/spam-guard.ts +31 -0
- package/component-templates/lib/utils.ts +6 -0
- package/component-templates/providers/feature-guard.tsx +432 -0
- package/component-templates/queries/query.tsx +775 -0
- package/component-templates/utils/colors/color-by-text.ts +307 -0
- package/component-templates/utils/colors/stripe-effect.ts +100 -0
- package/component-templates/utils/hash/hash-aes.ts +35 -0
- package/components.json +348 -0
- package/package.json +60 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// A reusable, library-style Feature Flags system for React/TypeScript
|
|
2
|
+
// ---------------------------------------------------------------
|
|
3
|
+
// Quick start:
|
|
4
|
+
//
|
|
5
|
+
// import { FeatureConfig } from "./feature-config";
|
|
6
|
+
// import { FeatureKey } from "./feature-keys";
|
|
7
|
+
//
|
|
8
|
+
// export const {
|
|
9
|
+
// FeatureProvider,
|
|
10
|
+
// Feature,
|
|
11
|
+
// FeatureLock,
|
|
12
|
+
// useFeature,
|
|
13
|
+
// useFlags,
|
|
14
|
+
// refresh,
|
|
15
|
+
// setFlags,
|
|
16
|
+
// sources,
|
|
17
|
+
// } = FeatureConfig({
|
|
18
|
+
// keys: FeatureKey, // enum or keys array
|
|
19
|
+
// sources: [
|
|
20
|
+
// sources.local({
|
|
21
|
+
// flags: {
|
|
22
|
+
// [FeatureKey.NewDashboard]: false,
|
|
23
|
+
// [FeatureKey.BetaButton]: true,
|
|
24
|
+
// },
|
|
25
|
+
// priority: 0,
|
|
26
|
+
// }),
|
|
27
|
+
// sources.storage({ storageKey: "feature_flags", priority: 5 }),
|
|
28
|
+
// sources.remote({
|
|
29
|
+
// priority: 10, // remote override local
|
|
30
|
+
// fetch: async () => {
|
|
31
|
+
// const res = await fetch("/api/feature-flags");
|
|
32
|
+
// return (await res.json()) as Record<string, boolean>;
|
|
33
|
+
// },
|
|
34
|
+
// transform: (raw) => raw, // optional map
|
|
35
|
+
// }),
|
|
36
|
+
// ],
|
|
37
|
+
// strategy: "last-wins", // "any-true" | "all-true" | custom reducer
|
|
38
|
+
// strict: "warn", // "error" | "warn" | "silent"
|
|
39
|
+
// });
|
|
40
|
+
//
|
|
41
|
+
// // App root
|
|
42
|
+
// <FeatureProvider>{children}</FeatureProvider>
|
|
43
|
+
//
|
|
44
|
+
// // Usage
|
|
45
|
+
// <Feature feature={FeatureKey.NewDashboard}>New UI</Feature>
|
|
46
|
+
// <FeatureLock feature={FeatureKey.BetaButton}>Beta locked</FeatureLock>
|
|
47
|
+
// ---------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
"use client";
|
|
50
|
+
import React, {
|
|
51
|
+
createContext,
|
|
52
|
+
ReactNode,
|
|
53
|
+
useContext,
|
|
54
|
+
useEffect,
|
|
55
|
+
useMemo,
|
|
56
|
+
useState,
|
|
57
|
+
} from 'react';
|
|
58
|
+
|
|
59
|
+
// -----------------------------
|
|
60
|
+
// Types
|
|
61
|
+
// -----------------------------
|
|
62
|
+
export type EnumLike = Record<string, string | number> | readonly string[];
|
|
63
|
+
|
|
64
|
+
type InferKeys<E extends EnumLike> = E extends readonly string[]
|
|
65
|
+
? E[number]
|
|
66
|
+
: E extends Record<string, infer V>
|
|
67
|
+
? Extract<V, string> | Extract<V, number> extends infer U
|
|
68
|
+
? Extract<U, string> // we only accept string keys for flags
|
|
69
|
+
: never
|
|
70
|
+
: never;
|
|
71
|
+
|
|
72
|
+
export type FlagMap<K extends string> = Partial<Record<K, boolean>>;
|
|
73
|
+
|
|
74
|
+
export type FeatureSource<K extends string> = {
|
|
75
|
+
name?: string;
|
|
76
|
+
priority?: number; // higher runs later if last-wins
|
|
77
|
+
load: (ctx: {
|
|
78
|
+
abortSignal?: AbortSignal;
|
|
79
|
+
}) => Promise<FlagMap<K>> | FlagMap<K>;
|
|
80
|
+
save?: (flags: FlagMap<K>) => Promise<void> | void; // optional, for writable stores
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type MergeStrategy<K extends string> =
|
|
84
|
+
| "last-wins"
|
|
85
|
+
| "any-true"
|
|
86
|
+
| "all-true"
|
|
87
|
+
| ((entries: { name: string; flags: FlagMap<K> }[]) => FlagMap<K>);
|
|
88
|
+
|
|
89
|
+
export type StrictMode = "error" | "warn" | "silent";
|
|
90
|
+
|
|
91
|
+
export interface FeatureOptions<
|
|
92
|
+
E extends EnumLike,
|
|
93
|
+
K extends string = InferKeys<E> & string
|
|
94
|
+
> {
|
|
95
|
+
keys: E; // enum object or readonly string[]
|
|
96
|
+
sources?: FeatureSource<K>[];
|
|
97
|
+
strategy?: MergeStrategy<K>;
|
|
98
|
+
strict?: StrictMode;
|
|
99
|
+
initialFlags?: FlagMap<K>; // SSR hydration or preloaded
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// -----------------------------
|
|
103
|
+
// Helpers
|
|
104
|
+
// -----------------------------
|
|
105
|
+
function normalizeEnum<E extends EnumLike>(e: E): string[] {
|
|
106
|
+
if (Array.isArray(e)) return e as string[];
|
|
107
|
+
// TS enum has both keys and reverse mapping for numeric enums –
|
|
108
|
+
// we're interested in string values only.
|
|
109
|
+
return Object.values(e).filter((v): v is string => typeof v === "string");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function filterUnknownKeys<K extends string>(
|
|
113
|
+
keys: readonly string[],
|
|
114
|
+
flags: FlagMap<K>,
|
|
115
|
+
strict: StrictMode,
|
|
116
|
+
sourceName: string
|
|
117
|
+
): FlagMap<K> {
|
|
118
|
+
const allowed = new Set(keys);
|
|
119
|
+
const out: FlagMap<K> = {};
|
|
120
|
+
for (const [k, v] of Object.entries(flags)) {
|
|
121
|
+
if (allowed.has(k)) {
|
|
122
|
+
(out as any)[k] = !!v;
|
|
123
|
+
} else if (strict !== "silent") {
|
|
124
|
+
const msg = `[FeatureConfig] Unknown key "${k}" from ${sourceName}`;
|
|
125
|
+
strict === "error" ? console.error(msg) : console.warn(msg);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function mergeFlags<K extends string>(
|
|
132
|
+
entries: { name: string; flags: FlagMap<K> }[],
|
|
133
|
+
strategy: MergeStrategy<K>
|
|
134
|
+
): FlagMap<K> {
|
|
135
|
+
if (typeof strategy === "function") return strategy(entries);
|
|
136
|
+
|
|
137
|
+
if (strategy === "any-true") {
|
|
138
|
+
const out: FlagMap<K> = {};
|
|
139
|
+
for (const e of entries) {
|
|
140
|
+
for (const [k, v] of Object.entries(e.flags)) {
|
|
141
|
+
(out as any)[k] = Boolean((out as any)[k]) || Boolean(v);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (strategy === "all-true") {
|
|
148
|
+
// Start with union of keys, then AND them across sources
|
|
149
|
+
const out: Record<string, boolean> = {};
|
|
150
|
+
const allKeys = new Set<string>();
|
|
151
|
+
entries.forEach((e) => Object.keys(e.flags).forEach((k) => allKeys.add(k)));
|
|
152
|
+
allKeys.forEach((k) => {
|
|
153
|
+
out[k] = entries.every((e) => e.flags[k as K] === true);
|
|
154
|
+
});
|
|
155
|
+
return out as FlagMap<K>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// default: last-wins (based on order of entries)
|
|
159
|
+
const out: FlagMap<K> = {};
|
|
160
|
+
for (const e of entries) Object.assign(out, e.flags);
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// -----------------------------
|
|
165
|
+
// Built-in Sources (adapters)
|
|
166
|
+
// -----------------------------
|
|
167
|
+
function localSource<K extends string>(opts: {
|
|
168
|
+
flags: FlagMap<K>;
|
|
169
|
+
priority?: number;
|
|
170
|
+
name?: string;
|
|
171
|
+
}): FeatureSource<K> {
|
|
172
|
+
const { flags, priority = 0, name = "local" } = opts;
|
|
173
|
+
return {
|
|
174
|
+
name,
|
|
175
|
+
priority,
|
|
176
|
+
load: () => flags,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function storageSource<K extends string>(opts: {
|
|
181
|
+
storageKey: string;
|
|
182
|
+
priority?: number;
|
|
183
|
+
name?: string;
|
|
184
|
+
storage?: Storage | null; // default localStorage if available
|
|
185
|
+
}): FeatureSource<K> {
|
|
186
|
+
const { storageKey, priority = 0, name = "storage", storage } = opts;
|
|
187
|
+
return {
|
|
188
|
+
name,
|
|
189
|
+
priority,
|
|
190
|
+
load: () => {
|
|
191
|
+
try {
|
|
192
|
+
const s =
|
|
193
|
+
storage ??
|
|
194
|
+
(typeof window !== "undefined" ? window.localStorage : null);
|
|
195
|
+
const raw = s?.getItem(storageKey);
|
|
196
|
+
return raw ? (JSON.parse(raw) as FlagMap<K>) : {};
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.warn(`[FeatureConfig] storageSource load error`, e);
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function remoteSource<K extends string>(opts: {
|
|
206
|
+
fetch: (ctx: {
|
|
207
|
+
abortSignal?: AbortSignal;
|
|
208
|
+
}) => Promise<Record<string, boolean>>;
|
|
209
|
+
transform?: (raw: Record<string, boolean>) => FlagMap<K>;
|
|
210
|
+
priority?: number;
|
|
211
|
+
name?: string;
|
|
212
|
+
}): FeatureSource<K> {
|
|
213
|
+
const { fetch, transform, priority = 10, name = "remote" } = opts;
|
|
214
|
+
return {
|
|
215
|
+
name,
|
|
216
|
+
priority,
|
|
217
|
+
load: async ({ abortSignal }) => {
|
|
218
|
+
const raw = await fetch({ abortSignal });
|
|
219
|
+
return transform ? transform(raw) : (raw as FlagMap<K>);
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// -----------------------------
|
|
225
|
+
// Factory – FeatureConfig
|
|
226
|
+
// -----------------------------
|
|
227
|
+
export function FeatureConfig<
|
|
228
|
+
E extends EnumLike,
|
|
229
|
+
K extends string = InferKeys<E> & string
|
|
230
|
+
>(options: FeatureOptions<E, K>) {
|
|
231
|
+
const allKeys = normalizeEnum(options.keys);
|
|
232
|
+
const strict: StrictMode = options.strict ?? "warn";
|
|
233
|
+
const strategy: MergeStrategy<K> = options.strategy ?? "last-wins";
|
|
234
|
+
|
|
235
|
+
// Context shape
|
|
236
|
+
type Ctx = {
|
|
237
|
+
flags: FlagMap<K>;
|
|
238
|
+
loading: boolean;
|
|
239
|
+
refresh: () => Promise<void>;
|
|
240
|
+
setFlags: (patch: FlagMap<K> | ((prev: FlagMap<K>) => FlagMap<K>)) => void;
|
|
241
|
+
isEnabled: (k: K) => boolean;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const FeatureContext = createContext<Ctx | null>(null);
|
|
245
|
+
|
|
246
|
+
function FeatureProvider({ children }: { children: ReactNode }) {
|
|
247
|
+
const [flags, _setFlags] = useState<FlagMap<K>>(options.initialFlags ?? {});
|
|
248
|
+
const [loading, setLoading] = useState<boolean>(true);
|
|
249
|
+
const sourcesSorted = useMemo(
|
|
250
|
+
() =>
|
|
251
|
+
(options.sources ?? [])
|
|
252
|
+
.map((s, i) => ({
|
|
253
|
+
i,
|
|
254
|
+
s,
|
|
255
|
+
priority: s.priority ?? 0,
|
|
256
|
+
name: s.name ?? `source_${i}`,
|
|
257
|
+
}))
|
|
258
|
+
.sort((a, b) => a.priority - b.priority),
|
|
259
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
260
|
+
[]
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const safeSetFlags = (
|
|
264
|
+
next: FlagMap<K> | ((prev: FlagMap<K>) => FlagMap<K>)
|
|
265
|
+
) => {
|
|
266
|
+
_setFlags((prev) => {
|
|
267
|
+
const nextFlags =
|
|
268
|
+
typeof next === "function" ? (next as any)(prev) : next;
|
|
269
|
+
return nextFlags;
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const refresh = async (): Promise<void> => {
|
|
274
|
+
if (!sourcesSorted.length) {
|
|
275
|
+
// even without sources, enforce key filtering on initial flags
|
|
276
|
+
setLoading(false);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
setLoading(true);
|
|
280
|
+
const ctrl = new AbortController();
|
|
281
|
+
const collected: { name: string; flags: FlagMap<K> }[] = [];
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
for (const { s, name } of sourcesSorted) {
|
|
285
|
+
try {
|
|
286
|
+
const data = await s.load({ abortSignal: ctrl.signal });
|
|
287
|
+
const filtered = filterUnknownKeys<K>(allKeys, data, strict, name);
|
|
288
|
+
collected.push({ name, flags: filtered });
|
|
289
|
+
} catch (e) {
|
|
290
|
+
console.warn(`[FeatureConfig] Source "${name}" load failed`, e);
|
|
291
|
+
collected.push({ name, flags: {} });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const merged = mergeFlags<K>(collected, strategy);
|
|
295
|
+
// also filter initial flags/options
|
|
296
|
+
const initialFiltered = filterUnknownKeys<K>(
|
|
297
|
+
allKeys,
|
|
298
|
+
flags,
|
|
299
|
+
strict,
|
|
300
|
+
"initial"
|
|
301
|
+
);
|
|
302
|
+
_setFlags({ ...initialFiltered, ...merged });
|
|
303
|
+
} finally {
|
|
304
|
+
setLoading(false);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
// On mount: validate initial flags & then fetch from sources
|
|
310
|
+
const initialFiltered = filterUnknownKeys<K>(
|
|
311
|
+
allKeys,
|
|
312
|
+
options.initialFlags ?? {},
|
|
313
|
+
strict,
|
|
314
|
+
"initial"
|
|
315
|
+
);
|
|
316
|
+
if (Object.keys(initialFiltered).length) _setFlags(initialFiltered);
|
|
317
|
+
// Fetch async sources
|
|
318
|
+
refresh();
|
|
319
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
320
|
+
}, []);
|
|
321
|
+
|
|
322
|
+
const isEnabled = (k: K) => Boolean(flags[k]);
|
|
323
|
+
|
|
324
|
+
const ctx: Ctx = {
|
|
325
|
+
flags,
|
|
326
|
+
loading,
|
|
327
|
+
refresh,
|
|
328
|
+
setFlags: safeSetFlags,
|
|
329
|
+
isEnabled,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<FeatureContext.Provider value={ctx}>{children}</FeatureContext.Provider>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Hooks & Components
|
|
338
|
+
function useFlags() {
|
|
339
|
+
const ctx = useContext(FeatureContext);
|
|
340
|
+
if (!ctx) throw new Error("useFlags must be used inside FeatureProvider");
|
|
341
|
+
return ctx;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function useFeature(key: K) {
|
|
345
|
+
const { isEnabled, loading } = useFlags();
|
|
346
|
+
return { enabled: isEnabled(key), loading };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
type GateProps = {
|
|
350
|
+
feature: K;
|
|
351
|
+
fallback?: ReactNode;
|
|
352
|
+
loadingFallback?: ReactNode;
|
|
353
|
+
children: ReactNode;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
function Feature({ feature, fallback = null, loadingFallback = null, children }: GateProps) {
|
|
357
|
+
const { enabled, loading } = useFeature(feature);
|
|
358
|
+
if (loading) return <>{loadingFallback}</>;
|
|
359
|
+
return <>{enabled ? children : fallback}</>;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function FeatureLock({ feature, fallback = null, loadingFallback = null, children }: GateProps) {
|
|
363
|
+
const { enabled, loading } = useFeature(feature);
|
|
364
|
+
if (loading) return <>{loadingFallback}</>;
|
|
365
|
+
return <>{enabled ? fallback : children}</>;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// public helpers (instance-scoped)
|
|
369
|
+
const api = {
|
|
370
|
+
FeatureProvider,
|
|
371
|
+
useFeature,
|
|
372
|
+
useFlags,
|
|
373
|
+
Feature,
|
|
374
|
+
FeatureLock,
|
|
375
|
+
refresh: () => {
|
|
376
|
+
const ctx = (FeatureContext as any)._currentValue as ReturnType<
|
|
377
|
+
typeof useFlags
|
|
378
|
+
> | null;
|
|
379
|
+
// Note: in strict React this isn't guaranteed across roots; prefer calling useFlags() in components.
|
|
380
|
+
if (ctx?.refresh) return ctx.refresh();
|
|
381
|
+
return Promise.resolve();
|
|
382
|
+
},
|
|
383
|
+
setFlags: (patch: FlagMap<K> | ((prev: FlagMap<K>) => FlagMap<K>)) => {
|
|
384
|
+
const ctx = (FeatureContext as any)._currentValue as ReturnType<
|
|
385
|
+
typeof useFlags
|
|
386
|
+
> | null;
|
|
387
|
+
if (ctx?.setFlags) ctx.setFlags(patch);
|
|
388
|
+
},
|
|
389
|
+
sources: {
|
|
390
|
+
local: localSource<K>,
|
|
391
|
+
storage: storageSource<K>,
|
|
392
|
+
remote: remoteSource<K>,
|
|
393
|
+
},
|
|
394
|
+
} as const;
|
|
395
|
+
|
|
396
|
+
return api;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// feature-config-builder
|
|
400
|
+
/**
|
|
401
|
+
* automatically create feature defaults from enum
|
|
402
|
+
* @param keysEnum - enum of feature keys
|
|
403
|
+
* @param manualDefaults - manual overrides for defaults
|
|
404
|
+
* @param globalDefault - global default for all features (default: true)
|
|
405
|
+
* @returns - feature defaults object
|
|
406
|
+
*/
|
|
407
|
+
export function createFeatureDefaults<
|
|
408
|
+
T extends Record<string, string> | Record<string, number>
|
|
409
|
+
>(
|
|
410
|
+
keysEnum: T,
|
|
411
|
+
manualDefaults: Partial<Record<T[keyof T], boolean>> = {},
|
|
412
|
+
globalDefault: boolean = true
|
|
413
|
+
): Record<T[keyof T], boolean> {
|
|
414
|
+
const keys = Object.values(keysEnum) as T[keyof T][];
|
|
415
|
+
const defaults: Record<T[keyof T], boolean> = {} as any;
|
|
416
|
+
|
|
417
|
+
for (const key of keys) {
|
|
418
|
+
defaults[key] = globalDefault;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// merge manual override
|
|
422
|
+
return { ...defaults, ...manualDefaults };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// -----------------------------
|
|
426
|
+
// Example enum (you can delete this from your build and place in a separate file)
|
|
427
|
+
// -----------------------------
|
|
428
|
+
// export enum FeatureKey {
|
|
429
|
+
// NewDashboard = "newDashboard",
|
|
430
|
+
// BetaButton = "betaButton",
|
|
431
|
+
// AiAssistant = "aiAssistant",
|
|
432
|
+
// }
|