@codeleap/utils 6.3.0 → 6.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/array.ts CHANGED
@@ -2,19 +2,26 @@ import { FunctionType } from '@codeleap/types'
2
2
 
3
3
  type GetterFunction<T> = FunctionType<[T, number], string | number> | keyof T
4
4
 
5
+ /**
6
+ * Indexes an array into an object keyed by a derived value.
7
+ *
8
+ * `keyAccessor` may be a property name string or a callback that returns a
9
+ * key. When omitted, array indices are used as keys. Duplicate keys silently
10
+ * overwrite earlier entries.
11
+ */
5
12
  export function objectFromArray<T, Getter extends GetterFunction<T>>(
6
13
  arr: T[],
7
14
  keyAccessor?: Getter,
8
15
  ): Record<string, T> {
9
- let getObjectKey = (_, idx) => idx
16
+ let getObjectKey: (value: T, idx: number) => string | number = (_, idx) => idx
10
17
 
11
18
  if (keyAccessor) {
12
19
  switch (typeof keyAccessor) {
13
20
  case 'string':
14
- getObjectKey = (value) => value[keyAccessor]
21
+ getObjectKey = (value: T) => value[keyAccessor as keyof T] as unknown as string | number
15
22
  break
16
23
  case 'function':
17
- getObjectKey = keyAccessor
24
+ getObjectKey = keyAccessor as (value: T, idx: number) => string | number
18
25
  break
19
26
  }
20
27
  }
@@ -24,6 +31,13 @@ export function objectFromArray<T, Getter extends GetterFunction<T>>(
24
31
  return Object.fromEntries(indexedMap)
25
32
  }
26
33
 
34
+ /**
35
+ * Returns a deduplicated copy of `array` where uniqueness is determined by
36
+ * the value returned from `getProperty`.
37
+ *
38
+ * When duplicates exist, the **last** occurrence wins because `objectFromArray`
39
+ * overwrites earlier entries with the same key.
40
+ */
27
41
  export function uniqueArrayByProperty<T, G extends GetterFunction<T>>(
28
42
  array: T[],
29
43
  getProperty: G,
@@ -31,6 +45,12 @@ export function uniqueArrayByProperty<T, G extends GetterFunction<T>>(
31
45
  return Object.values(objectFromArray(array, getProperty))
32
46
  }
33
47
 
48
+ /**
49
+ * Recursively flattens a nested array of arbitrary depth into a single-level array.
50
+ *
51
+ * Unlike `Array.prototype.flat(Infinity)`, this implementation does not rely on
52
+ * native flat support and works identically across all environments.
53
+ */
34
54
  export function flatten<T extends unknown>(arr: T[]) {
35
55
  let newArr = [] as T[]
36
56
 
@@ -45,6 +65,11 @@ export function flatten<T extends unknown>(arr: T[]) {
45
65
  return newArr
46
66
  }
47
67
 
68
+ /**
69
+ * Returns an inclusive integer array from `start` to `end`.
70
+ *
71
+ * Both bounds are included: `range(1, 3)` → `[1, 2, 3]`.
72
+ */
48
73
  export function range(start: number, end: number) {
49
74
  const length = end - start + 1
50
75
  return Array.from({ length }, (_, index) => index + start)
package/src/cloneDeep.ts CHANGED
@@ -1,14 +1,22 @@
1
- export function cloneDeep(value) {
1
+ /**
2
+ * Produces a full recursive clone of `value` without any external dependencies.
3
+ *
4
+ * Supported types: primitives (returned as-is), plain objects, arrays, `Date`,
5
+ * `Map`, `Set`, and `RegExp`. Class instances with prototype methods beyond
6
+ * these built-ins are cloned as plain objects — prototype chain is not preserved.
7
+ * Circular references will cause a stack overflow.
8
+ */
9
+ export function cloneDeep<T = any>(value: T): T {
2
10
  if (value === null || typeof value !== 'object') {
3
11
  return value
4
12
  }
5
13
 
6
14
  if (Array.isArray(value)) {
7
- return value.map(cloneDeep)
15
+ return value.map(cloneDeep) as unknown as T
8
16
  }
9
17
 
10
18
  if (value instanceof Date) {
11
- return new Date(value.getTime())
19
+ return new Date(value.getTime()) as unknown as T
12
20
  }
13
21
 
14
22
  if (value instanceof Map) {
@@ -16,7 +24,7 @@ export function cloneDeep(value) {
16
24
  value.forEach((v, k) => {
17
25
  clonedMap.set(k, cloneDeep(v))
18
26
  })
19
- return clonedMap
27
+ return clonedMap as unknown as T
20
28
  }
21
29
 
22
30
  if (value instanceof Set) {
@@ -24,20 +32,20 @@ export function cloneDeep(value) {
24
32
  value.forEach((v) => {
25
33
  clonedSet.add(cloneDeep(v))
26
34
  })
27
- return clonedSet
35
+ return clonedSet as unknown as T
28
36
  }
29
37
 
30
38
  if (value instanceof RegExp) {
31
- return new RegExp(value.source, value.flags)
39
+ return new RegExp(value.source, value.flags) as unknown as T
32
40
  }
33
41
 
34
- const clonedObj = {}
42
+ const clonedObj: Record<string, unknown> = {}
35
43
 
36
44
  for (const key in value) {
37
45
  if (Object.prototype.hasOwnProperty.call(value, key)) {
38
- clonedObj[key] = cloneDeep(value[key])
46
+ clonedObj[key] = cloneDeep((value as Record<string, unknown>)[key])
39
47
  }
40
48
  }
41
49
 
42
- return clonedObj
50
+ return clonedObj as unknown as T
43
51
  }
package/src/colors.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import tinycolor from 'tinycolor2'
2
2
  import { TypeGuards } from '@codeleap/types'
3
3
 
4
- export function hexToRgb(hex) {
4
+ /**
5
+ * Parses a 6-digit hex colour string (with or without leading `#`) into its
6
+ * `{ r, g, b }` components.
7
+ *
8
+ * Returns `null` for invalid or short (3-digit) hex values.
9
+ */
10
+ export function hexToRgb(hex: string) {
5
11
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
6
12
  return result
7
13
  ? {
@@ -12,8 +18,18 @@ export function hexToRgb(hex) {
12
18
  : null
13
19
  }
14
20
 
15
- const shadeColorCache = {}
16
- export function shadeColor(color: string, percent = 0, opacity = null) {
21
+ const shadeColorCache: Record<string, string> = {}
22
+
23
+ /**
24
+ * Lightens or darkens `color` by `percent` and optionally applies `opacity`,
25
+ * returning an `rgba(...)` string.
26
+ *
27
+ * - Positive `percent` → lighten; negative → darken (magnitude is used).
28
+ * - `opacity` must be in the `0–1` range (passed directly to tinycolor's `setAlpha`).
29
+ * - Results are memoised in a module-level cache keyed by the serialised parameters,
30
+ * so repeated calls with identical arguments are effectively free.
31
+ */
32
+ export function shadeColor(color: string, percent = 0, opacity: number | null = null) {
17
33
  const _color = color.trim()
18
34
  const serialParams = [_color, percent.toString()]
19
35
  if (TypeGuards.isNumber(opacity)) {
package/src/date.ts CHANGED
@@ -14,6 +14,15 @@ const removeTimezoneAndFormat = (date: any, format = 'YYYY-MM-DD') => {
14
14
  return dayjs(normalizedDate).startOf('day').format(format)
15
15
  }
16
16
 
17
+ /**
18
+ * Date helpers that normalise timezone-sensitive values before formatting.
19
+ *
20
+ * - `removeTimezoneAndFormat(date, format?)` — strips the time component from
21
+ * a `Date` or ISO string, anchors it to noon local time, then formats with
22
+ * dayjs. This prevents off-by-one-day errors that occur when UTC midnight
23
+ * falls on a different calendar day in the user's timezone. Returns `''` for
24
+ * falsy input.
25
+ */
17
26
  export const dateUtils = {
18
27
  removeTimezoneAndFormat,
19
28
  }
package/src/faker.ts CHANGED
@@ -25,6 +25,19 @@ function number(min: number = 0, max: number = 100) {
25
25
  return Math.floor(Math.random() * (max - min + 1)) + min
26
26
  }
27
27
 
28
+ /**
29
+ * Lightweight in-house fixture-data generator with no external dependencies.
30
+ *
31
+ * Provided methods:
32
+ * - `firstName()` / `lastName()` — random entries from hard-coded name lists.
33
+ * - `name()` — space-joined first + last name.
34
+ * - `animal()` — random animal name.
35
+ * - `number(min?, max?)` — random integer in `[min, max]` (default 0–100).
36
+ *
37
+ * All selections use `Math.random`, so results are not reproducible across calls.
38
+ * Use this instead of heavy faker libraries in tests or seed scripts where the
39
+ * small vocabulary is sufficient.
40
+ */
28
41
  export const faker = {
29
42
  lastName: () => getRandom(surnames),
30
43
  firstName: () => getRandom(names),
package/src/file.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  const separators = /[\\\/]+/
2
2
 
3
+ /**
4
+ * Splits a file system path into its directory, base name, and extension.
5
+ *
6
+ * Both `/` and `\` are treated as separators, so Windows and POSIX paths are
7
+ * handled uniformly. Files without an extension return an empty string for
8
+ * `extension`. The returned `path` is always joined with `/` regardless of the
9
+ * original separator.
10
+ */
3
11
  export function parseFilePathData(path: string) {
4
12
  const parts = path.split(separators)
5
13
 
package/src/locale.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import { AnyRecord } from '@codeleap/types'
2
2
 
3
+ /**
4
+ * Finds the closest matching key in `languageDictionary` for a given `locale`.
5
+ *
6
+ * Matching uses only the first two characters (the language subtag), so `'en-AU'`
7
+ * will match an `'en-US'` dictionary key. Returns `defaultLocale` when no
8
+ * partial match exists.
9
+ */
3
10
  export function getSimilarLocale(
4
11
  locale: string,
5
12
  defaultLocale: string,
package/src/misc.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import { capitalize } from './string'
2
2
  import { StylesOf } from '@codeleap/types'
3
3
 
4
+ /**
5
+ * Converts a remote image URL into a file-upload-compatible object
6
+ * (`{ uri, name, type }`).
7
+ *
8
+ * The `type` is inferred from the URL's file extension. Returns `null` when
9
+ * `imagePath` is falsy, making it safe to pass directly to optional file-input APIs.
10
+ */
4
11
  export function imagePathToFileObject(imagePath: string | null) {
5
12
  const parts = imagePath ? imagePath.split('.') : ''
6
13
 
@@ -17,7 +24,7 @@ export function imagePathToFileObject(imagePath: string | null) {
17
24
  return fileValue
18
25
  }
19
26
 
20
- const letterToColorMap = {
27
+ const letterToColorMap: Record<string, string> = {
21
28
  a: '#7CB9E8',
22
29
  b: '#3a9e77',
23
30
  c: '#A3C1AD',
@@ -46,12 +53,20 @@ const letterToColorMap = {
46
53
  z: '#ff8295',
47
54
  }
48
55
 
56
+ /**
57
+ * Maps the first character of `anyString` to a deterministic colour for use in
58
+ * avatar placeholders.
59
+ *
60
+ * Lookup is case-insensitive. Returns `'#999999'` for empty, undefined, or
61
+ * characters not in the `a–z` range.
62
+ */
49
63
  export function matchInitialToColor(anyString?: string) {
50
64
  if (!anyString) return '#999999'
51
65
  return letterToColorMap[anyString.toLowerCase().charAt(0)] || '#999999'
52
66
  }
53
67
 
54
- export function waitFor(ms) {
68
+ /** Returns a Promise that resolves after `ms` milliseconds. */
69
+ export function waitFor(ms: number) {
55
70
  return new Promise<void>((resolve) => {
56
71
  setTimeout(() => {
57
72
  resolve()
@@ -64,15 +79,23 @@ type ParseSourceUrlArg = {
64
79
  src?: string
65
80
  }
66
81
 
82
+ /**
83
+ * Resolves a media URL from either a raw string or a `{ source?, src? }` object.
84
+ *
85
+ * - Paths starting with `/media/` are prefixed with `Settings.BaseURL`.
86
+ * - Absolute URLs are returned unchanged.
87
+ * - Empty/missing addresses fall back to a random `picsum.photos` placeholder.
88
+ * - Returns `null` for a falsy `args` argument.
89
+ */
67
90
  export function parseSourceUrl(args: string, Settings?: any): string
68
91
  export function parseSourceUrl(
69
92
  args: ParseSourceUrlArg,
70
93
  Settings?: any
71
- ): string
94
+ ): string | null
72
95
  export function parseSourceUrl(
73
96
  args: ParseSourceUrlArg | string,
74
97
  Settings?: any,
75
- ): string {
98
+ ): string | null {
76
99
  if (!args) return null
77
100
 
78
101
  let res = ''
@@ -94,8 +117,16 @@ export function parseSourceUrl(
94
117
  return res
95
118
  }
96
119
 
120
+ /**
121
+ * Extracts style entries from a variant styles map whose keys begin with `match`,
122
+ * returning them as a new object with the prefix stripped and the first
123
+ * remaining character lowercased.
124
+ *
125
+ * Used internally by component style systems to pull out sub-part styles
126
+ * (e.g. `inputLabel`, `inputWrapper`) into their own namespaced objects.
127
+ */
97
128
  export function getNestedStylesByKey<T extends StylesOf<any>>(match:string, variantStyles: T) {
98
- const styles = {}
129
+ const styles: Record<string, unknown> = {}
99
130
 
100
131
  for (const [key, value] of Object.entries(variantStyles)) {
101
132
 
@@ -108,6 +139,15 @@ export function getNestedStylesByKey<T extends StylesOf<any>>(match:string, vari
108
139
  return styles
109
140
  }
110
141
 
142
+ /**
143
+ * Heuristically detects whether the app has been React Fast Refreshed since
144
+ * initial launch.
145
+ *
146
+ * Compares the elapsed time since `Settings.Environment.InitTime` against a
147
+ * 1-second threshold. Fast Refresh typically completes well under 1 second from
148
+ * app start; anything longer suggests a subsequent refresh rather than cold boot.
149
+ * Returns `undefined` and logs a warning when `InitTime` is not set.
150
+ */
111
151
  export function hasFastRefreshed(Settings: any) {
112
152
  if (Settings?.Environment?.InitTime) {
113
153
  const timeFromStartup = (new Date()).getTime() - Settings.Environment.InitTime.getTime()
@@ -120,9 +160,17 @@ export function hasFastRefreshed(Settings: any) {
120
160
  }
121
161
  }
122
162
 
123
- const throttleTimerId = []
124
-
125
- export function throttle(func, ref, delay) {
163
+ const throttleTimerId: Record<string | number, ReturnType<typeof setTimeout> | undefined> = {}
164
+
165
+ /**
166
+ * Leading-edge throttle keyed by a `ref` identifier rather than by function reference.
167
+ *
168
+ * `func` is called immediately on the first invocation for a given `ref`; subsequent
169
+ * calls with the same `ref` are dropped until `delay` milliseconds have elapsed.
170
+ * Using a string/number `ref` allows multiple independent throttle timers to coexist
171
+ * in a single module without needing to hold separate timer handles.
172
+ */
173
+ export function throttle(func: () => void, ref: string | number, delay: number) {
126
174
  if (throttleTimerId[ref]) {
127
175
  return
128
176
  }
package/src/object.ts CHANGED
@@ -1,10 +1,16 @@
1
1
  import { FunctionType, AppSettings } from '@codeleap/types'
2
2
 
3
- export function deepMerge(base = {}, changes = {}): any {
4
- const obj = {
3
+ /**
4
+ * Recursively merges `changes` into `base`, deep-merging nested plain objects.
5
+ *
6
+ * Arrays and `Date` instances are treated as scalar values — they replace rather
7
+ * than merge. If `changes` is not iterable, it is returned as-is.
8
+ */
9
+ export function deepMerge(base: Record<string, any> = {}, changes: Record<string, any> = {}): any {
10
+ const obj: Record<string, any> = {
5
11
  ...base,
6
12
  }
7
- let changeEntries = []
13
+ let changeEntries: [string, any][] = []
8
14
  try {
9
15
  changeEntries = Object.entries(changes)
10
16
  } catch (e) {
@@ -21,30 +27,46 @@ export function deepMerge(base = {}, changes = {}): any {
21
27
  return obj
22
28
  }
23
29
 
30
+ /**
31
+ * Like `Array.prototype.map` but over an object's entries.
32
+ *
33
+ * Returns an array — not a new object — so it is useful when you need to
34
+ * transform key-value pairs into arbitrary values (e.g. JSX elements).
35
+ */
24
36
  export function mapObject<T>(
25
37
  obj: T,
26
38
  callback: FunctionType<[[keyof T, T[keyof T]]], any>,
27
39
  ) {
28
- return Object.entries(obj).map((args) => callback(args as [keyof T, T[keyof T]]),
40
+ return Object.entries(obj as Record<string, unknown>).map((args) => callback(args as [keyof T, T[keyof T]]),
29
41
  )
30
42
  }
31
43
 
32
- export const deepSet = ([path, value]) => {
33
- const parts = path.split('.')
34
- const newObj = Array.isArray(value) ? [] : {}
35
-
36
- if (parts.length === 1) {
37
- newObj[parts[0]] = value
38
- } else {
39
- newObj[parts[0]] = deepSet([parts.slice(1).join('.'), value])
44
+ /**
45
+ * Sets a value at an arbitrarily deep dot-separated `path`, mutating `base` in place
46
+ * and returning it.
47
+ *
48
+ * Numeric path segments are coerced to array indices when the current node is an array.
49
+ */
50
+ export const deepSet = (base: any = {}, path: string, value: any): any => {
51
+ const keys = path.split('.')
52
+ const obj = base
53
+ let thisKey: string | number = keys[0]
54
+ if (Array.isArray(base)) {
55
+ thisKey = Number(thisKey)
40
56
  }
41
-
42
- return newObj
57
+ obj[thisKey] = keys.length === 1 ? value : deepSet(obj[thisKey], keys.slice(1).join('.'), value)
58
+ return obj
43
59
  }
44
60
 
45
- export const deepGet = (path, obj) => {
61
+ /**
62
+ * Reads a value from `obj` at a dot-separated `path`.
63
+ *
64
+ * Returns `undefined` (without throwing) when any intermediate node is absent,
65
+ * because property access on `undefined` propagates silently through the loop.
66
+ */
67
+ export const deepGet = (path: string, obj: Record<string, any>) => {
46
68
  const parts = path.split('.')
47
- let newObj = { ...obj }
69
+ let newObj: Record<string, any> = { ...obj }
48
70
 
49
71
  for (const prop of parts) {
50
72
  newObj = newObj[prop]
@@ -53,8 +75,13 @@ export const deepGet = (path, obj) => {
53
75
  return newObj
54
76
  }
55
77
 
56
- export function objectPaths(obj) {
57
- let paths = []
78
+ /**
79
+ * Returns every dot-separated leaf path in a nested object.
80
+ *
81
+ * Array values are treated as leaves — their indices are not traversed.
82
+ */
83
+ export function objectPaths(obj: Record<string, any>): string[] {
84
+ let paths: string[] = []
58
85
 
59
86
  Object.entries(obj).forEach(([key, value]) => {
60
87
  if (!Array.isArray(value) && typeof value === 'object') {
@@ -67,10 +94,17 @@ export function objectPaths(obj) {
67
94
  return paths
68
95
  }
69
96
 
97
+ /** Returns `true` only for `string`, `number`, and `boolean` — `null`, `undefined`, and objects are excluded. */
70
98
  export function isValuePrimitive(a:any) {
71
99
  return ['string', 'number', 'boolean'].includes(typeof a)
72
100
  }
73
101
 
102
+ /**
103
+ * Inline ternary helper for spread-merging optional style or prop objects.
104
+ *
105
+ * Intended for use inside object spreads where the ternary would otherwise
106
+ * require wrapping: `{ ...optionalObject(flag, trueProps, {}) }`.
107
+ */
74
108
  export function optionalObject(condition: boolean, ifTrue: any, ifFalse: any) {
75
109
  return condition ? ifTrue : ifFalse
76
110
  }
@@ -79,7 +113,16 @@ type TraverseRecArgs = {path:string[]; value: any; depth: number; key: string; t
79
113
 
80
114
  type TraverseCallback = (args?: TraverseRecArgs) => {stop?: boolean } | void
81
115
 
82
- export function traverse(obj = {}, callback:TraverseCallback, args?: TraverseRecArgs) {
116
+ /**
117
+ * Depth-first walk over a nested object, invoking `callback` at each node.
118
+ *
119
+ * The callback receives metadata about each visited node (`path`, `depth`,
120
+ * `key`, `type`, `primitive`). Primitive leaves fire the callback once; object
121
+ * nodes fire it for each child before recursing into it. Returning `{ stop: true }`
122
+ * from the callback is accepted by the type but **does not halt traversal** —
123
+ * the implementation does not check the return value.
124
+ */
125
+ export function traverse(obj: any = {}, callback:TraverseCallback, args?: TraverseRecArgs) {
83
126
  const isPrimitive = isValuePrimitive(obj)
84
127
 
85
128
  const info = {
@@ -127,16 +170,34 @@ export function traverse(obj = {}, callback:TraverseCallback, args?: TraverseRec
127
170
  }
128
171
  }
129
172
 
173
+ /**
174
+ * Identity helper that returns its argument unchanged, typed as `AppSettings`.
175
+ *
176
+ * Exists solely to give TypeScript a typed entry point for authoring settings
177
+ * objects so that editors provide autocompletion and type errors at the
178
+ * definition site rather than at the point of use.
179
+ */
130
180
  export function createSettings<T extends AppSettings>(a:T): T {
131
181
  return a
132
182
  }
133
183
 
184
+ /**
185
+ * Returns `obj.id` if it exists, otherwise `undefined`.
186
+ *
187
+ * Intended as a default `keyExtractor` in list utilities where records are
188
+ * expected to carry an `id` field.
189
+ */
134
190
  export function extractKey(obj:any) {
135
191
  if (obj.id) {
136
192
  return obj.id
137
193
  }
138
194
  }
139
195
 
196
+ /**
197
+ * Returns a new object containing only the entries for which `predicate` returns `true`.
198
+ *
199
+ * Only own enumerable keys are visited; prototype-chain properties are skipped.
200
+ */
140
201
  export function objectPickBy<K extends string = string, P = any>(obj: Record<K, P>, predicate: (valueKey: P, key: K) => boolean) {
141
202
  const result = {} as Record<K, P>
142
203
 
@@ -149,8 +210,15 @@ export function objectPickBy<K extends string = string, P = any>(obj: Record<K,
149
210
  return result
150
211
  }
151
212
 
213
+ /**
214
+ * Rebuilds an object by running every entry through `predicate`, which must
215
+ * return a `[newKey, newValue]` tuple.
216
+ *
217
+ * Keys returned by `predicate` need not be unique — later entries silently
218
+ * overwrite earlier ones if they resolve to the same key.
219
+ */
152
220
  export function transformObject<K extends string = string, T extends string = string>(obj: Record<K, T>, predicate: (value: T, key: K) => [K, T]): Record<string, any> {
153
- const result = {}
221
+ const result: Record<string, any> = {}
154
222
 
155
223
  for (const key in obj) {
156
224
  const [newKey, newValue] = predicate?.(obj?.[key], key)
@@ -160,3 +228,37 @@ export function transformObject<K extends string = string, T extends string = st
160
228
 
161
229
  return result
162
230
  }
231
+ type LowercaseFirst<S extends string> = S extends `${infer F}${infer Rest}` ? `${Lowercase<F>}${Rest}` : S
232
+
233
+ /**
234
+ * Mapped type that extracts keys of `T` starting with `P`, strips the prefix,
235
+ * and lowercases the first character of the remainder.
236
+ *
237
+ * Used to derive the return type of {@link filterObjectByPrefix} at compile time.
238
+ * For example, `FilterByPrefix<{ onPress: () => void }, 'on'>` yields `{ press: () => void }`.
239
+ */
240
+ export type FilterByPrefix<T, P extends string> = {
241
+ [K in Extract<keyof T, `${P}${string}`> as K extends `${P}${infer Rest}` ? LowercaseFirst<Rest> : never]: T[K]
242
+ }
243
+
244
+ /**
245
+ * Returns a new object containing only keys that start with `prefix`, with the
246
+ * prefix stripped and the first remaining character lowercased.
247
+ *
248
+ * Useful for splitting a flat props object into logical groups, e.g. separating
249
+ * all `inputXxx` props from a combined component interface.
250
+ */
251
+ export function filterObjectByPrefix<T extends Record<string, any>, P extends string>(
252
+ obj: T,
253
+ prefix: P,
254
+ ): FilterByPrefix<T, P> {
255
+ const result = {} as any
256
+ for (const key in obj) {
257
+ if (key.startsWith(prefix)) {
258
+ const newKey = key.slice(prefix.length)
259
+ const formattedKey = newKey.charAt(0).toLowerCase() + newKey.slice(1)
260
+ result[formattedKey] = obj[key]
261
+ }
262
+ }
263
+ return result
264
+ }