@hivemindhq/core 0.4.0 → 0.5.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/README.md +16 -16
- package/dist/{chunk-2RGM3KJL.js → chunk-K2544PJ5.js} +42 -20
- package/dist/chunk-K2544PJ5.js.map +1 -0
- package/dist/{chunk-P5E2XNDI.js → chunk-K4XDMY2V.js} +3 -3
- package/dist/{chunk-P5E2XNDI.js.map → chunk-K4XDMY2V.js.map} +1 -1
- package/dist/{chunk-ERZSVDIB.js → chunk-RW4JXOAM.js} +11 -3
- package/dist/chunk-RW4JXOAM.js.map +1 -0
- package/dist/chunk-VU3OPG32.js +907 -0
- package/dist/chunk-VU3OPG32.js.map +1 -0
- package/dist/components/index.d.ts +28 -3
- package/dist/components/index.js +2 -2
- package/dist/components/ui/index.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -6
- package/dist/utils/index.d.ts +312 -6
- package/dist/utils/index.js +2 -2
- package/package.json +15 -11
- package/src/components/AtomIcon.tsx +21 -0
- package/src/components/CryptoAmount.tsx +447 -0
- package/src/components/ErrorBanner.tsx +35 -0
- package/src/components/IpfsImage.tsx +21 -0
- package/src/components/LoadingDots.tsx +55 -0
- package/src/components/TripleAreaChart.tsx +108 -0
- package/src/components/TriplePositionsTornadoMinGraph.tsx +71 -0
- package/src/components/UnknownImage.tsx +55 -0
- package/src/components/index.ts +24 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/avatar.tsx +47 -0
- package/src/components/ui/badge.tsx +35 -0
- package/src/components/ui/breadcrumb.tsx +108 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/carousel.tsx +239 -0
- package/src/components/ui/chart.tsx +350 -0
- package/src/components/ui/checkbox.tsx +28 -0
- package/src/components/ui/collapsible.tsx +10 -0
- package/src/components/ui/command.tsx +177 -0
- package/src/components/ui/dialog.tsx +119 -0
- package/src/components/ui/dropdown-menu.tsx +202 -0
- package/src/components/ui/form.tsx +175 -0
- package/src/components/ui/index.ts +183 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +25 -0
- package/src/components/ui/loader.tsx +20 -0
- package/src/components/ui/pagination.tsx +104 -0
- package/src/components/ui/popover.tsx +45 -0
- package/src/components/ui/progress.tsx +25 -0
- package/src/components/ui/radio-group.tsx +42 -0
- package/src/components/ui/scroll-area.tsx +45 -0
- package/src/components/ui/select.tsx +178 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +139 -0
- package/src/components/ui/sidebar.tsx +723 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +27 -0
- package/src/components/ui/spinner.tsx +67 -0
- package/src/components/ui/switch.tsx +26 -0
- package/src/components/ui/table.tsx +113 -0
- package/src/components/ui/tabs.tsx +63 -0
- package/src/components/ui/textarea.tsx +21 -0
- package/src/components/ui/toast.tsx +146 -0
- package/src/components/ui/toaster.tsx +33 -0
- package/src/components/ui/toggle-group.tsx +58 -0
- package/src/components/ui/toggle.tsx +44 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/use-mobile.ts +20 -0
- package/src/hooks/use-toast.ts +190 -0
- package/src/index.ts +25 -0
- package/src/types/index.ts +17 -0
- package/src/utils/atom-label-detection.ts +689 -0
- package/src/utils/atom.ts +279 -0
- package/src/utils/cn.ts +18 -0
- package/src/utils/formatting.ts +624 -0
- package/src/utils/index.ts +11 -0
- package/src/utils/multivault-errors.ts +581 -0
- package/src/utils/search/formatting.tsx +95 -0
- package/src/utils/search/index.ts +28 -0
- package/src/utils/search/ranking.ts +203 -0
- package/src/utils/search/types.ts +114 -0
- package/tailwind.config.js +3 -3
- package/dist/chunk-2RGM3KJL.js.map +0 -1
- package/dist/chunk-ERZSVDIB.js.map +0 -1
- package/dist/chunk-H4RMZQ2Z.js +0 -213
- package/dist/chunk-H4RMZQ2Z.js.map +0 -1
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting utilities for display purposes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Crypto Amount Formatting
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options for formatting crypto amounts.
|
|
11
|
+
*/
|
|
12
|
+
export interface FormatCryptoAmountOptions {
|
|
13
|
+
/** Maximum significant digits before truncation (default: 10) */
|
|
14
|
+
maxSignificantDigits?: number
|
|
15
|
+
/** Use compact notation for large numbers (default: false) */
|
|
16
|
+
compact?: boolean
|
|
17
|
+
/** Locale for number formatting (default: 'en-US') */
|
|
18
|
+
locale?: string
|
|
19
|
+
/** Minimum decimal places to show (default: 0) */
|
|
20
|
+
minDecimals?: number
|
|
21
|
+
/** Maximum decimal places to show (default: 6) */
|
|
22
|
+
maxDecimals?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Result of formatting a crypto amount.
|
|
27
|
+
*/
|
|
28
|
+
export interface FormattedCryptoAmount {
|
|
29
|
+
/** The display string (may be truncated) */
|
|
30
|
+
display: string
|
|
31
|
+
/** The full value string (for tooltips) */
|
|
32
|
+
fullValue: string
|
|
33
|
+
/** Whether the display was truncated */
|
|
34
|
+
isTruncated: boolean
|
|
35
|
+
/** The numeric value (for calculations) */
|
|
36
|
+
numericValue: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Formats a raw crypto amount (in wei/smallest unit) for display.
|
|
41
|
+
*
|
|
42
|
+
* Handles:
|
|
43
|
+
* - Conversion from smallest unit to readable format
|
|
44
|
+
* - Truncation of excessive precision with tooltip support
|
|
45
|
+
* - Compact notation for large amounts (1.5M, 2.3B)
|
|
46
|
+
* - Locale-aware number formatting
|
|
47
|
+
*
|
|
48
|
+
* @param value - Raw amount as string or bigint (e.g., "1500000000000000000" for 1.5 tokens)
|
|
49
|
+
* @param decimals - Token decimals (default: 18)
|
|
50
|
+
* @param options - Formatting options
|
|
51
|
+
* @returns Formatted amount with display string and metadata
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* formatCryptoAmount("1500000000000000000", 18)
|
|
55
|
+
* // Returns: { display: "1.5", fullValue: "1.5", isTruncated: false, numericValue: 1.5 }
|
|
56
|
+
*
|
|
57
|
+
* formatCryptoAmount("12345678901234567890123", 18, { maxSignificantDigits: 6 })
|
|
58
|
+
* // Returns: { display: "12345.6...", fullValue: "12345.678901234567890123", isTruncated: true, ... }
|
|
59
|
+
*/
|
|
60
|
+
export function formatCryptoAmount(
|
|
61
|
+
value: string | bigint,
|
|
62
|
+
decimals: number = 18,
|
|
63
|
+
options: FormatCryptoAmountOptions = {}
|
|
64
|
+
): FormattedCryptoAmount {
|
|
65
|
+
const {
|
|
66
|
+
maxSignificantDigits = 10,
|
|
67
|
+
compact = false,
|
|
68
|
+
locale = 'en-US',
|
|
69
|
+
minDecimals = 0,
|
|
70
|
+
maxDecimals = 6,
|
|
71
|
+
} = options
|
|
72
|
+
|
|
73
|
+
// Convert to bigint if string
|
|
74
|
+
const bigIntValue = typeof value === 'string' ? BigInt(value) : value
|
|
75
|
+
|
|
76
|
+
// Handle zero case
|
|
77
|
+
if (bigIntValue === 0n) {
|
|
78
|
+
return {
|
|
79
|
+
display: '0',
|
|
80
|
+
fullValue: '0',
|
|
81
|
+
isTruncated: false,
|
|
82
|
+
numericValue: 0,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Convert from smallest unit to readable format
|
|
87
|
+
const divisor = 10n ** BigInt(decimals)
|
|
88
|
+
const wholePart = bigIntValue / divisor
|
|
89
|
+
const fractionalPart = bigIntValue % divisor
|
|
90
|
+
|
|
91
|
+
// Build the full decimal string
|
|
92
|
+
const fractionalStr = fractionalPart.toString().padStart(decimals, '0')
|
|
93
|
+
const trimmedFractional = fractionalStr.replace(/0+$/, '') // Remove trailing zeros
|
|
94
|
+
|
|
95
|
+
const fullValue = trimmedFractional
|
|
96
|
+
? `${wholePart}.${trimmedFractional}`
|
|
97
|
+
: wholePart.toString()
|
|
98
|
+
|
|
99
|
+
const numericValue = Number(fullValue)
|
|
100
|
+
|
|
101
|
+
// Handle compact notation
|
|
102
|
+
if (compact && Math.abs(numericValue) >= 1000) {
|
|
103
|
+
const compactDisplay = formatCompact(numericValue)
|
|
104
|
+
return {
|
|
105
|
+
display: compactDisplay,
|
|
106
|
+
fullValue,
|
|
107
|
+
isTruncated: true, // Compact is always "truncated" conceptually
|
|
108
|
+
numericValue,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Step 1: Check if decimals will be truncated (for tooltip purposes)
|
|
113
|
+
const originalDecimalCount = trimmedFractional.length
|
|
114
|
+
const decimalsWillBeTruncated = originalDecimalCount > maxDecimals
|
|
115
|
+
|
|
116
|
+
// Step 2: Apply maxDecimals limit FIRST (this is the primary display constraint)
|
|
117
|
+
let decimalLimitedValue: string
|
|
118
|
+
try {
|
|
119
|
+
decimalLimitedValue = new Intl.NumberFormat(locale, {
|
|
120
|
+
minimumFractionDigits: minDecimals,
|
|
121
|
+
maximumFractionDigits: maxDecimals,
|
|
122
|
+
useGrouping: false, // Disable commas for significant digit counting
|
|
123
|
+
}).format(numericValue)
|
|
124
|
+
} catch {
|
|
125
|
+
decimalLimitedValue = fullValue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Step 3: Check if significant digits truncation is STILL needed after decimal limiting
|
|
129
|
+
const { truncated, isTruncated: sigDigitsTruncated } = truncateSignificantDigits(
|
|
130
|
+
decimalLimitedValue,
|
|
131
|
+
maxSignificantDigits
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
// isTruncated is true if EITHER decimals were limited OR significant digits were truncated
|
|
135
|
+
const isTruncated = decimalsWillBeTruncated || sigDigitsTruncated
|
|
136
|
+
|
|
137
|
+
// Step 4: Format for display
|
|
138
|
+
let display: string
|
|
139
|
+
if (sigDigitsTruncated) {
|
|
140
|
+
// Extract numeric part (without ellipsis) and apply locale formatting for grouping separators
|
|
141
|
+
const numericPart = truncated.replace('…', '')
|
|
142
|
+
try {
|
|
143
|
+
const formattedPart = new Intl.NumberFormat(locale, {
|
|
144
|
+
maximumFractionDigits: maxDecimals,
|
|
145
|
+
}).format(parseFloat(numericPart))
|
|
146
|
+
display = `${formattedPart}…`
|
|
147
|
+
} catch {
|
|
148
|
+
display = truncated
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
// Re-apply locale formatting with grouping (commas) for final display
|
|
152
|
+
try {
|
|
153
|
+
display = new Intl.NumberFormat(locale, {
|
|
154
|
+
minimumFractionDigits: minDecimals,
|
|
155
|
+
maximumFractionDigits: maxDecimals,
|
|
156
|
+
}).format(numericValue)
|
|
157
|
+
} catch {
|
|
158
|
+
display = decimalLimitedValue
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
display,
|
|
164
|
+
fullValue,
|
|
165
|
+
isTruncated,
|
|
166
|
+
numericValue,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Truncates a numeric string to a maximum number of significant digits.
|
|
172
|
+
* Leading zeros in decimals don't count toward the limit.
|
|
173
|
+
*
|
|
174
|
+
* @param value - The numeric string to truncate
|
|
175
|
+
* @param maxDigits - Maximum significant digits to keep
|
|
176
|
+
* @returns Object with truncated string and whether truncation occurred
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* truncateSignificantDigits("123456789012", 10)
|
|
180
|
+
* // Returns: { truncated: "1234567890…", isTruncated: true }
|
|
181
|
+
*
|
|
182
|
+
* truncateSignificantDigits("0.0000123456789", 10)
|
|
183
|
+
* // Returns: { truncated: "0.0000123456789", isTruncated: false }
|
|
184
|
+
* // (leading zeros don't count, only 9 significant digits)
|
|
185
|
+
*/
|
|
186
|
+
export function truncateSignificantDigits(
|
|
187
|
+
value: string,
|
|
188
|
+
maxDigits: number
|
|
189
|
+
): { truncated: string; isTruncated: boolean } {
|
|
190
|
+
if (!value) {
|
|
191
|
+
return { truncated: '0', isTruncated: false }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Handle negative numbers
|
|
195
|
+
const isNegative = value.startsWith('-')
|
|
196
|
+
const absValue = isNegative ? value.slice(1) : value
|
|
197
|
+
|
|
198
|
+
// Split into whole and fractional parts
|
|
199
|
+
const [wholePart, fractionalPart] = absValue.split('.')
|
|
200
|
+
|
|
201
|
+
// Count significant digits (skip leading zeros in fractional part)
|
|
202
|
+
let significantCount = 0
|
|
203
|
+
let truncatedWhole = ''
|
|
204
|
+
let truncatedFractional = ''
|
|
205
|
+
let isTruncated = false
|
|
206
|
+
|
|
207
|
+
// Process whole part (all digits are significant unless the whole thing is "0")
|
|
208
|
+
if (wholePart !== '0') {
|
|
209
|
+
for (const char of wholePart) {
|
|
210
|
+
if (significantCount >= maxDigits) {
|
|
211
|
+
isTruncated = true
|
|
212
|
+
break
|
|
213
|
+
}
|
|
214
|
+
truncatedWhole += char
|
|
215
|
+
significantCount++
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// If we truncated in whole part, we're done (no fractional)
|
|
219
|
+
if (isTruncated) {
|
|
220
|
+
const result = isNegative ? `-${truncatedWhole}` : truncatedWhole
|
|
221
|
+
return { truncated: `${result}…`, isTruncated: true }
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
truncatedWhole = '0'
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Process fractional part if exists
|
|
228
|
+
if (fractionalPart) {
|
|
229
|
+
let foundNonZero = wholePart !== '0' // If whole is non-zero, all fractional digits count
|
|
230
|
+
|
|
231
|
+
for (const char of fractionalPart) {
|
|
232
|
+
if (!foundNonZero && char === '0') {
|
|
233
|
+
// Leading zeros in fractional don't count
|
|
234
|
+
truncatedFractional += char
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
foundNonZero = true
|
|
239
|
+
|
|
240
|
+
if (significantCount >= maxDigits) {
|
|
241
|
+
isTruncated = true
|
|
242
|
+
break
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
truncatedFractional += char
|
|
246
|
+
significantCount++
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Build final result
|
|
251
|
+
let result = truncatedWhole
|
|
252
|
+
if (truncatedFractional) {
|
|
253
|
+
result += `.${truncatedFractional}`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (isNegative) {
|
|
257
|
+
result = `-${result}`
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { truncated: result, isTruncated }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Truncates an address or ID for display.
|
|
265
|
+
* Shows first N characters after optional prefix.
|
|
266
|
+
*
|
|
267
|
+
* @param value - The full address or ID string
|
|
268
|
+
* @param chars - Number of characters to show (default: 5)
|
|
269
|
+
* @param prefix - Prefix to skip (default: '0x')
|
|
270
|
+
* @returns Truncated string
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* truncateId('0x123456789098765432') // Returns: '12345'
|
|
274
|
+
* truncateId('0x123456789098765432', 8) // Returns: '12345678'
|
|
275
|
+
*/
|
|
276
|
+
export function truncateId(
|
|
277
|
+
value: string,
|
|
278
|
+
chars: number = 5,
|
|
279
|
+
prefix: string = '0x'
|
|
280
|
+
): string {
|
|
281
|
+
if (!value) return ''
|
|
282
|
+
const start = value.startsWith(prefix) ? prefix.length : 0
|
|
283
|
+
return value.slice(start, start + chars)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Converts an IPFS URI to an HTTP gateway URL.
|
|
288
|
+
*
|
|
289
|
+
* @param uri - The IPFS URI (ipfs://... format)
|
|
290
|
+
* @param gateway - The gateway to use (default: ipfs.io)
|
|
291
|
+
* @returns HTTP URL for the content
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ipfsToHttp('ipfs://QmHash123') // Returns: 'https://ipfs.io/ipfs/QmHash123'
|
|
295
|
+
*/
|
|
296
|
+
export function ipfsToHttp(
|
|
297
|
+
uri: string | undefined | null,
|
|
298
|
+
gateway: string = 'https://ipfs.io/ipfs/'
|
|
299
|
+
): string | undefined {
|
|
300
|
+
if (!uri) return undefined
|
|
301
|
+
if (uri.startsWith('ipfs://')) {
|
|
302
|
+
return uri.replace('ipfs://', gateway)
|
|
303
|
+
}
|
|
304
|
+
return uri
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Formats a number with compact notation (K, M, B, etc.)
|
|
309
|
+
*
|
|
310
|
+
* @param value - The number to format
|
|
311
|
+
* @param decimals - Maximum decimal places (default: 2)
|
|
312
|
+
* @returns Formatted string
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* formatCompact(1500) // Returns: '1.5K'
|
|
316
|
+
* formatCompact(2500000) // Returns: '2.5M'
|
|
317
|
+
*/
|
|
318
|
+
export function formatCompact(
|
|
319
|
+
value: number,
|
|
320
|
+
decimals: number = 2
|
|
321
|
+
): string {
|
|
322
|
+
if (value === 0) return '0'
|
|
323
|
+
|
|
324
|
+
const formatter = new Intl.NumberFormat('en-US', {
|
|
325
|
+
notation: 'compact',
|
|
326
|
+
maximumFractionDigits: decimals,
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
return formatter.format(value)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Strengthens/normalizes an image URL to ensure it's displayable.
|
|
334
|
+
* Handles IPFS URIs, raw IPFS hashes, and validates HTTP URLs.
|
|
335
|
+
*
|
|
336
|
+
* @param url - The image URL to process
|
|
337
|
+
* @returns A normalized HTTP(S) URL
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* strengthenImageUrl('ipfs://QmHash123') // Returns: 'https://ipfs.io/ipfs/QmHash123'
|
|
341
|
+
* strengthenImageUrl('QmHash123') // Returns: 'https://ipfs.io/ipfs/QmHash123'
|
|
342
|
+
* strengthenImageUrl('https://example.com/img.png') // Returns: 'https://example.com/img.png'
|
|
343
|
+
*/
|
|
344
|
+
export function strengthenImageUrl(url: string = ''): string {
|
|
345
|
+
// Validate input
|
|
346
|
+
if (!url || typeof url !== 'string' || url.trim() === '') {
|
|
347
|
+
return url
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const trimmedUrl = url.trim()
|
|
351
|
+
|
|
352
|
+
// Handle IPFS protocol
|
|
353
|
+
if (trimmedUrl.startsWith('ipfs://')) {
|
|
354
|
+
return trimmedUrl.replace('ipfs://', 'https://ipfs.io/ipfs/')
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Already a valid HTTP(S) URL
|
|
358
|
+
if (trimmedUrl.startsWith('https://') || trimmedUrl.startsWith('http://')) {
|
|
359
|
+
return trimmedUrl
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Handle IPFS hashes without protocol (Qm... or baf...)
|
|
363
|
+
if (trimmedUrl.startsWith('Qm') || trimmedUrl.startsWith('baf')) {
|
|
364
|
+
return 'https://ipfs.io/ipfs/' + trimmedUrl
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return trimmedUrl
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ============================================================================
|
|
371
|
+
// String Manipulation Utilities
|
|
372
|
+
// ============================================================================
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Ellipsizes a string by showing characters from start and end with '...' in middle.
|
|
376
|
+
*
|
|
377
|
+
* @param str - The string to ellipsize
|
|
378
|
+
* @param length - Total visible characters (split between start and end)
|
|
379
|
+
* @returns Ellipsized string or original if shorter than length
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* ellipsizeString('0x1234567890abcdef', 8) // Returns: '0x12...cdef'
|
|
383
|
+
*/
|
|
384
|
+
export function ellipsizeString(
|
|
385
|
+
str: string | undefined | null,
|
|
386
|
+
length: number
|
|
387
|
+
): string {
|
|
388
|
+
if (!str) return ''
|
|
389
|
+
if (str.length < length) return str
|
|
390
|
+
const first = str.substring(0, length / 2)
|
|
391
|
+
const second = str.substring(str.length - length / 2, str.length)
|
|
392
|
+
return [first, second].join('...')
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Ellipsizes a hex string (0x-prefixed) preserving the prefix.
|
|
397
|
+
*
|
|
398
|
+
* @param hex - The hex string to ellipsize (must start with 0x)
|
|
399
|
+
* @param length - Visible characters in the payload (default: 12)
|
|
400
|
+
* @returns Ellipsized hex string
|
|
401
|
+
* @throws Error if hex doesn't start with 0x
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* ellipsizeHex('0x1234567890abcdef1234567890abcdef') // Returns: '0x123456...abcdef'
|
|
405
|
+
*/
|
|
406
|
+
export function ellipsizeHex(hex: string, length: number = 12): string {
|
|
407
|
+
if (!hex || !hex.startsWith('0x')) throw new Error('Invalid hex')
|
|
408
|
+
const prefix = '0x'
|
|
409
|
+
const payload = hex.replace('0x', '')
|
|
410
|
+
const shortenedPayload = ellipsizeString(payload, length)
|
|
411
|
+
return `${prefix}${shortenedPayload}`
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Truncates a string to a specified length, adding '...' at the end.
|
|
416
|
+
*
|
|
417
|
+
* @param str - The string to truncate
|
|
418
|
+
* @param length - Maximum length before truncation
|
|
419
|
+
* @returns Truncated string with '...' or original if shorter
|
|
420
|
+
*
|
|
421
|
+
* @example
|
|
422
|
+
* truncateString('Hello World', 5) // Returns: 'Hello...'
|
|
423
|
+
*/
|
|
424
|
+
export function truncateString(
|
|
425
|
+
str: string | undefined | null = '',
|
|
426
|
+
length: number
|
|
427
|
+
): string {
|
|
428
|
+
if (!str) return ''
|
|
429
|
+
if (str.length < length) return str
|
|
430
|
+
return `${str.substring(0, length)}...`
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Intelligently shortens a label based on its type (hex, URL, IPFS, DID, etc.)
|
|
435
|
+
* This is the main utility for displaying atom/entity labels in the UI.
|
|
436
|
+
*
|
|
437
|
+
* @param label - The label to shorten
|
|
438
|
+
* @param length - Maximum length for generic strings (default: 50)
|
|
439
|
+
* @returns Shortened label appropriate for its type
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* shortenLabel('0x1234567890abcdef...') // Returns: '0x1234...cdef'
|
|
443
|
+
* shortenLabel('https://example.com/page') // Returns: 'example.com/page'
|
|
444
|
+
* shortenLabel('ipfs://bafkre...') // Returns: 'bafkre...xyz'
|
|
445
|
+
*/
|
|
446
|
+
export function shortenLabel(
|
|
447
|
+
label: string | null | undefined,
|
|
448
|
+
length: number = 50
|
|
449
|
+
): string {
|
|
450
|
+
if (!label) return ''
|
|
451
|
+
|
|
452
|
+
// Handle hex addresses
|
|
453
|
+
if (label.startsWith('0x')) {
|
|
454
|
+
return ellipsizeHex(label, 12)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Handle IPFS
|
|
458
|
+
if (label.startsWith('bafkre')) {
|
|
459
|
+
return ellipsizeString(label, 12)
|
|
460
|
+
}
|
|
461
|
+
if (label.startsWith('ipfs://bafkre')) {
|
|
462
|
+
return ellipsizeString(label.replace('ipfs://', ''), 12)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Handle URLs - remove protocol and truncate if needed
|
|
466
|
+
if (label.startsWith('https://') || label.startsWith('http://')) {
|
|
467
|
+
const cleanUrl = label.replace(/^https?:\/\//, '')
|
|
468
|
+
return cleanUrl.length > length ? cleanUrl.slice(0, length) + '...' : cleanUrl
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Handle DID URIs
|
|
472
|
+
if (label.startsWith('did:')) {
|
|
473
|
+
// Extract meaningful parts of DID
|
|
474
|
+
if (label.includes('eip155:') && label.includes('|discord:')) {
|
|
475
|
+
const parts = label.split('|')
|
|
476
|
+
const ethPart = parts[0] // did:eip155:84532:0x...
|
|
477
|
+
const discordPart = parts[1] // discord:391378559945670667
|
|
478
|
+
|
|
479
|
+
// Extract just the address from the first part
|
|
480
|
+
const addressMatch = ethPart.match(/0x[a-fA-F0-9]+/)
|
|
481
|
+
const address = addressMatch ? ellipsizeHex(addressMatch[0], 8) : ethPart
|
|
482
|
+
|
|
483
|
+
return `${address} | ${discordPart}`
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Fallback for other DID formats
|
|
487
|
+
return ellipsizeString(label, length)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Handle CAIP-10 identifiers
|
|
491
|
+
if (label.startsWith('caip10:eip155:')) {
|
|
492
|
+
const parsedLabel = label.split(':')
|
|
493
|
+
const addressIndex = parsedLabel.findIndex(part => part.startsWith('0x'))
|
|
494
|
+
if (addressIndex !== -1) {
|
|
495
|
+
const address = parsedLabel[addressIndex]
|
|
496
|
+
parsedLabel[addressIndex] = ellipsizeHex(address, 12)
|
|
497
|
+
}
|
|
498
|
+
return parsedLabel.join(':')
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Default: truncate to specified length
|
|
502
|
+
return label.length > length ? label.slice(0, length) + '...' : label
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ============================================================================
|
|
506
|
+
// Capitalization Utilities
|
|
507
|
+
// ============================================================================
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Capitalizes the first letter of a string.
|
|
511
|
+
*
|
|
512
|
+
* @param str - The string to capitalize
|
|
513
|
+
* @returns String with first letter capitalized
|
|
514
|
+
*
|
|
515
|
+
* @example
|
|
516
|
+
* capitalizeFirstLetter('hello') // Returns: 'Hello'
|
|
517
|
+
*/
|
|
518
|
+
export function capitalizeFirstLetter(str: string): string {
|
|
519
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Capitalizes the first letter of each word in a phrase.
|
|
524
|
+
*
|
|
525
|
+
* @param phrase - The phrase to capitalize
|
|
526
|
+
* @returns Phrase with each word capitalized
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* capitalize('hello world') // Returns: 'Hello World'
|
|
530
|
+
*/
|
|
531
|
+
export function capitalize(phrase: string): string {
|
|
532
|
+
if (!phrase) return ''
|
|
533
|
+
return phrase
|
|
534
|
+
.split(' ')
|
|
535
|
+
.map(word => word ? word.charAt(0).toUpperCase() + word.slice(1) : '')
|
|
536
|
+
.join(' ')
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ============================================================================
|
|
540
|
+
// Time Utilities
|
|
541
|
+
// ============================================================================
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Converts seconds to a compact human-readable format (e.g., "5d", "3h", "10m").
|
|
545
|
+
*
|
|
546
|
+
* @param seconds - Number of seconds
|
|
547
|
+
* @returns Compact time string
|
|
548
|
+
*
|
|
549
|
+
* @example
|
|
550
|
+
* secondsToHms(3700) // Returns: '1h'
|
|
551
|
+
* secondsToHms(90) // Returns: '1m'
|
|
552
|
+
*/
|
|
553
|
+
export function secondsToHms(seconds: number): string {
|
|
554
|
+
seconds = Number(seconds)
|
|
555
|
+
const d = Math.floor(seconds / (3600 * 24))
|
|
556
|
+
if (d > 0) return `${d}d`
|
|
557
|
+
const h = Math.floor(seconds / 3600)
|
|
558
|
+
if (h > 0) return `${h}h`
|
|
559
|
+
const m = Math.floor((seconds % 3600) / 60)
|
|
560
|
+
if (m > 0) return `${m}m`
|
|
561
|
+
const s = Math.floor((seconds % 3600) % 60)
|
|
562
|
+
if (s > 0) return `${s}s`
|
|
563
|
+
return ''
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Formats a date as a human-readable "time ago" string.
|
|
568
|
+
* Handles various date input formats including Unix timestamps.
|
|
569
|
+
*
|
|
570
|
+
* @param date - Date object, ISO string, or Unix timestamp (10 or 13 digits)
|
|
571
|
+
* @param ago - Whether to append "ago" suffix (default: true)
|
|
572
|
+
* @returns Human-readable time difference string
|
|
573
|
+
*
|
|
574
|
+
* @example
|
|
575
|
+
* timeAgo(new Date(Date.now() - 3600000)) // Returns: '1h ago'
|
|
576
|
+
* timeAgo('1703001600', false) // Returns: '5d'
|
|
577
|
+
*/
|
|
578
|
+
export function timeAgo(date: Date | string, ago: boolean = true): string {
|
|
579
|
+
let output: Date
|
|
580
|
+
if (typeof date === 'string') {
|
|
581
|
+
if (date.length === 10) {
|
|
582
|
+
// Unix timestamp in seconds
|
|
583
|
+
output = new Date(parseInt(date) * 1000)
|
|
584
|
+
} else if (date.length === 13) {
|
|
585
|
+
// Unix timestamp in milliseconds
|
|
586
|
+
output = new Date(parseInt(date))
|
|
587
|
+
} else {
|
|
588
|
+
// ISO string or other format
|
|
589
|
+
output = new Date(date)
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
output = date
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const dateTimestamp = output.getTime() / 1000
|
|
596
|
+
const nowTimestamp = Date.now() / 1000
|
|
597
|
+
const timeGap = secondsToHms(nowTimestamp - dateTimestamp)
|
|
598
|
+
|
|
599
|
+
return ago ? `${timeGap} ago` : timeGap
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Converts seconds to a descriptive duration string (e.g., "5 d", "3 h").
|
|
604
|
+
*
|
|
605
|
+
* @param inputVar - Seconds as number or string
|
|
606
|
+
* @returns Duration string with space before unit
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* secondsToDhms(90000) // Returns: '1 d'
|
|
610
|
+
*/
|
|
611
|
+
export function secondsToDhms(inputVar: string | number): string {
|
|
612
|
+
const inputNumber = Number(inputVar)
|
|
613
|
+
const days = Math.floor(inputNumber / (3600 * 24))
|
|
614
|
+
const hours = Math.floor((inputNumber % (3600 * 24)) / 3600)
|
|
615
|
+
const minutes = Math.floor(((inputNumber % (3600 * 24)) % 3600) / 60)
|
|
616
|
+
const seconds = Math.floor(((inputNumber % (3600 * 24)) % 3600) % 60)
|
|
617
|
+
|
|
618
|
+
if (days) return `${days} d`
|
|
619
|
+
if (hours) return `${hours} h`
|
|
620
|
+
if (minutes) return `${minutes} m`
|
|
621
|
+
if (seconds) return `${seconds} s`
|
|
622
|
+
return ''
|
|
623
|
+
}
|
|
624
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions shared across Hive Mind applications
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { cn } from './cn'
|
|
6
|
+
export * from './formatting'
|
|
7
|
+
export * from './atom'
|
|
8
|
+
export * from './atom-label-detection'
|
|
9
|
+
export * from './search'
|
|
10
|
+
export * from './multivault-errors'
|
|
11
|
+
|