@inferencesh/ui 0.0.0-stage → 0.1.1
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/LICENSE +22 -0
- package/README.md +68 -2
- package/components.json +22 -0
- package/package.json +231 -4
- package/src/components/audio-player.tsx +549 -0
- package/src/components/button.tsx +13 -0
- package/src/components/chart.tsx +403 -0
- package/src/components/circuit.tsx +276 -0
- package/src/components/code-block/code-block.tsx +260 -0
- package/src/components/code-block/index.ts +18 -0
- package/src/components/code-block/languages.ts +311 -0
- package/src/components/code-block/styles.ts +39 -0
- package/src/components/code-block/tokenizer.ts +290 -0
- package/src/components/code-block/types.ts +38 -0
- package/src/components/code-block/utils.ts +39 -0
- package/src/components/code.tsx +48 -0
- package/src/components/cookie-consent.tsx +137 -0
- package/src/components/copy-button.tsx +44 -0
- package/src/components/country-dropdown.tsx +188 -0
- package/src/components/glow-loader-fast.tsx +32 -0
- package/src/components/glow-loader-mono.tsx +33 -0
- package/src/components/glow-loader-sm-fast.tsx +32 -0
- package/src/components/glow-loader-sm-mono.tsx +34 -0
- package/src/components/glow-loader-sm.tsx +32 -0
- package/src/components/glow-loader.tsx +32 -0
- package/src/components/glow-logo.tsx +32 -0
- package/src/components/gradient.tsx +80 -0
- package/src/components/index.ts +45 -0
- package/src/components/inline-sidebar.tsx +351 -0
- package/src/components/input-color.tsx +349 -0
- package/src/components/logo-base.tsx +213 -0
- package/src/components/logo-color.tsx +23 -0
- package/src/components/logo-full.tsx +71 -0
- package/src/components/logo-svg.tsx +23 -0
- package/src/components/menu-navigation.tsx +359 -0
- package/src/components/multi-select.tsx +721 -0
- package/src/components/pastel-gradient.tsx +25 -0
- package/src/components/pixel-symmetry.tsx +151 -0
- package/src/components/progress.tsx +28 -0
- package/src/components/scroll-more.tsx +131 -0
- package/src/components/select.tsx +85 -0
- package/src/components/shimmering-text.tsx +108 -0
- package/src/components/sidebar-light.tsx +102 -0
- package/src/components/sonner.tsx +38 -0
- package/src/components/theme-switcher-sidebar.tsx +56 -0
- package/src/components/theme-switcher-simple.tsx +110 -0
- package/src/components/theme-switcher.tsx +47 -0
- package/src/components/toggle-group.tsx +55 -0
- package/src/components/tooltip-button.tsx +91 -0
- package/src/components/tooltip.tsx +71 -0
- package/src/components/tree-view.tsx +499 -0
- package/src/components/unglow-loader.tsx +33 -0
- package/src/components/waveform.tsx +1632 -0
- package/src/components/zoomable-image.tsx +76 -0
- package/src/docs/api-docs.tsx +763 -0
- package/src/docs/callout.tsx +160 -0
- package/src/docs/code-group.tsx +215 -0
- package/src/docs/copy-menu.tsx +143 -0
- package/src/docs/doc-tabs.tsx +84 -0
- package/src/docs/index.ts +16 -0
- package/src/docs/steps.tsx +105 -0
- package/src/docs/table-of-contents.tsx +174 -0
- package/src/examples/guide-markdown.ts +129 -0
- package/src/examples/index.ts +1153 -0
- package/src/global.d.ts +1 -0
- package/src/hooks/use-copy-to-clipboard.ts +36 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/lib/display-name.ts +15 -0
- package/src/lib/example-input.ts +130 -0
- package/src/lib/plans.ts +44 -0
- package/src/lib/safe-content.ts +137 -0
- package/src/lib/schema.ts +320 -0
- package/src/lib/storage.ts +108 -0
- package/src/markdown/docs-markdown-renderer.tsx +518 -0
- package/src/markdown/helpers.ts +95 -0
- package/src/markdown/index.ts +24 -0
- package/src/markdown/markdown-renderer.tsx +232 -0
- package/src/markdown/youtube-embed.tsx +43 -0
- package/src/og/image.ts +312 -0
- package/src/og/index.tsx +430 -0
- package/src/og/query.ts +26 -0
- package/src/shadcn/accordion.tsx +71 -0
- package/src/shadcn/alert-dialog.tsx +187 -0
- package/src/shadcn/alert.tsx +75 -0
- package/src/shadcn/aspect-ratio.tsx +22 -0
- package/src/shadcn/attachment.tsx +207 -0
- package/src/shadcn/avatar.tsx +106 -0
- package/src/shadcn/badge.tsx +51 -0
- package/src/shadcn/breadcrumb.tsx +124 -0
- package/src/shadcn/bubble.tsx +127 -0
- package/src/shadcn/button-group.tsx +87 -0
- package/src/shadcn/button.tsx +57 -0
- package/src/shadcn/calendar.tsx +219 -0
- package/src/shadcn/card.tsx +102 -0
- package/src/shadcn/carousel.tsx +242 -0
- package/src/shadcn/chart.tsx +372 -0
- package/src/shadcn/checkbox.tsx +28 -0
- package/src/shadcn/collapsible.tsx +19 -0
- package/src/shadcn/combobox.tsx +297 -0
- package/src/shadcn/command.tsx +194 -0
- package/src/shadcn/context-menu.tsx +270 -0
- package/src/shadcn/dialog.tsx +158 -0
- package/src/shadcn/direction.tsx +4 -0
- package/src/shadcn/drawer.tsx +227 -0
- package/src/shadcn/dropdown-menu.tsx +265 -0
- package/src/shadcn/empty.tsx +103 -0
- package/src/shadcn/field.tsx +238 -0
- package/src/shadcn/form.tsx +173 -0
- package/src/shadcn/hover-card.tsx +50 -0
- package/src/shadcn/index.ts +53 -0
- package/src/shadcn/input-group.tsx +156 -0
- package/src/shadcn/input-otp.tsx +84 -0
- package/src/shadcn/input.tsx +19 -0
- package/src/shadcn/item.tsx +201 -0
- package/src/shadcn/kbd.tsx +26 -0
- package/src/shadcn/label.tsx +19 -0
- package/src/shadcn/marker.tsx +70 -0
- package/src/shadcn/menubar.tsx +278 -0
- package/src/shadcn/message-scroller.tsx +131 -0
- package/src/shadcn/message.tsx +91 -0
- package/src/shadcn/native-select.tsx +60 -0
- package/src/shadcn/navigation-menu.tsx +167 -0
- package/src/shadcn/pagination.tsx +130 -0
- package/src/shadcn/popover.tsx +87 -0
- package/src/shadcn/progress.tsx +82 -0
- package/src/shadcn/radio-group.tsx +35 -0
- package/src/shadcn/resizable.tsx +49 -0
- package/src/shadcn/scroll-area.tsx +52 -0
- package/src/shadcn/select.tsx +200 -0
- package/src/shadcn/separator.tsx +22 -0
- package/src/shadcn/sheet.tsx +136 -0
- package/src/shadcn/sidebar.tsx +721 -0
- package/src/shadcn/skeleton.tsx +13 -0
- package/src/shadcn/slider.tsx +51 -0
- package/src/shadcn/sonner.tsx +49 -0
- package/src/shadcn/spinner.tsx +10 -0
- package/src/shadcn/switch.tsx +29 -0
- package/src/shadcn/table.tsx +115 -0
- package/src/shadcn/tabs.tsx +79 -0
- package/src/shadcn/textarea.tsx +17 -0
- package/src/shadcn/toggle-group.tsx +89 -0
- package/src/shadcn/toggle.tsx +44 -0
- package/src/shadcn/tooltip.tsx +63 -0
- package/src/theme/config.ts +48 -0
- package/src/theme/css/base.css +92 -0
- package/src/theme/css/bitboy.css +75 -0
- package/src/theme/css/dark.css +47 -0
- package/src/theme/css/designer.css +122 -0
- package/src/theme/css/fonts.css +33 -0
- package/src/theme/css/globals.css +14 -0
- package/src/theme/css/hackintosh.css +57 -0
- package/src/theme/css/llama.css +103 -0
- package/src/theme/css/safelist.css +39 -0
- package/src/theme/css/shadcn.css +77 -0
- package/src/theme/css/theme.css +216 -0
- package/src/theme/embed-theme.ts +146 -0
- package/src/theme/fonts/FiraCode-VF.woff2 +0 -0
- package/src/theme/fonts/Hack-Regular.woff2 +0 -0
- package/src/theme/fonts/LICENSE-FiraCode.txt +93 -0
- package/src/theme/fonts/LICENSE-Hack.txt +45 -0
- package/src/theme/fonts/LICENSE-MonaSans.txt +93 -0
- package/src/theme/fonts/Mona-Sans.woff2 +0 -0
- package/src/theme/index.ts +15 -0
- package/src/utils/cn.ts +3 -0
- package/src/utils/index.ts +2 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema types and utilities for parsing and resolving references
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type SchemaTypeName =
|
|
6
|
+
| "string"
|
|
7
|
+
| "number"
|
|
8
|
+
| "integer"
|
|
9
|
+
| "boolean"
|
|
10
|
+
| "object"
|
|
11
|
+
| "array"
|
|
12
|
+
| "any"
|
|
13
|
+
| "null";
|
|
14
|
+
|
|
15
|
+
export type SchemaType =
|
|
16
|
+
| string
|
|
17
|
+
| number
|
|
18
|
+
| boolean
|
|
19
|
+
| Record<string, unknown>
|
|
20
|
+
| unknown[]
|
|
21
|
+
| null
|
|
22
|
+
| undefined;
|
|
23
|
+
|
|
24
|
+
export interface SchemaAlternative extends Omit<Schema, 'type'> {
|
|
25
|
+
type: SchemaTypeName;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Schema {
|
|
29
|
+
$id?: string;
|
|
30
|
+
$ref?: string;
|
|
31
|
+
$schema?: string;
|
|
32
|
+
$comment?: string;
|
|
33
|
+
$defs?: Record<string, Schema>;
|
|
34
|
+
|
|
35
|
+
type?: SchemaTypeName | SchemaTypeName[];
|
|
36
|
+
enum?: SchemaType[];
|
|
37
|
+
const?: SchemaType;
|
|
38
|
+
|
|
39
|
+
// String validations
|
|
40
|
+
minLength?: number;
|
|
41
|
+
maxLength?: number;
|
|
42
|
+
pattern?: string;
|
|
43
|
+
|
|
44
|
+
// Number validations
|
|
45
|
+
multipleOf?: number;
|
|
46
|
+
maximum?: number;
|
|
47
|
+
exclusiveMaximum?: number;
|
|
48
|
+
minimum?: number;
|
|
49
|
+
exclusiveMinimum?: number;
|
|
50
|
+
|
|
51
|
+
// Object validations
|
|
52
|
+
maxProperties?: number;
|
|
53
|
+
minProperties?: number;
|
|
54
|
+
required?: string[];
|
|
55
|
+
properties?: Record<string, Schema>;
|
|
56
|
+
patternProperties?: Record<string, Schema>;
|
|
57
|
+
additionalProperties?: Schema | boolean;
|
|
58
|
+
|
|
59
|
+
// Array validations
|
|
60
|
+
items?: Schema | Schema[];
|
|
61
|
+
additionalItems?: Schema;
|
|
62
|
+
maxItems?: number;
|
|
63
|
+
minItems?: number;
|
|
64
|
+
uniqueItems?: boolean;
|
|
65
|
+
contains?: Schema;
|
|
66
|
+
|
|
67
|
+
// Conditionals
|
|
68
|
+
if?: Schema;
|
|
69
|
+
then?: Schema;
|
|
70
|
+
else?: Schema;
|
|
71
|
+
|
|
72
|
+
// Combiners
|
|
73
|
+
allOf?: SchemaAlternative[];
|
|
74
|
+
anyOf?: SchemaAlternative[];
|
|
75
|
+
oneOf?: SchemaAlternative[];
|
|
76
|
+
not?: SchemaAlternative;
|
|
77
|
+
|
|
78
|
+
format?: string;
|
|
79
|
+
contentMediaType?: string;
|
|
80
|
+
contentEncoding?: string;
|
|
81
|
+
|
|
82
|
+
// Metadata
|
|
83
|
+
title?: string;
|
|
84
|
+
description?: string;
|
|
85
|
+
default?: SchemaType;
|
|
86
|
+
readOnly?: boolean;
|
|
87
|
+
writeOnly?: boolean;
|
|
88
|
+
examples?: SchemaType | SchemaType[];
|
|
89
|
+
definitions?: Record<string, Schema>;
|
|
90
|
+
|
|
91
|
+
// Set by the form layer when an `anyOf [T, null]` is collapsed to T, so a
|
|
92
|
+
// renderer can offer "unset" and default builders can leave it absent.
|
|
93
|
+
nullable?: boolean;
|
|
94
|
+
|
|
95
|
+
// Vendor extensions
|
|
96
|
+
'x-promoted'?: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A field parsed from a schema with resolved references.
|
|
101
|
+
* `displayType` holds the human-readable type string from `resolveDisplayType`
|
|
102
|
+
* (e.g. "object | string", "file | object") while `type` stays compatible
|
|
103
|
+
* with Schema for structural operations.
|
|
104
|
+
*/
|
|
105
|
+
export interface SchemaField extends Schema {
|
|
106
|
+
displayType: string;
|
|
107
|
+
key: string;
|
|
108
|
+
resolved?: Schema;
|
|
109
|
+
isRequired?: boolean;
|
|
110
|
+
promoted?: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Type guards
|
|
114
|
+
function hasRef(value: unknown): value is Schema & { $ref: string } {
|
|
115
|
+
return typeof value === 'object' && value !== null && '$ref' in value && typeof (value as Schema).$ref === 'string';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Deeply resolves all $ref references in a schema
|
|
120
|
+
*/
|
|
121
|
+
export function resolveSchemaReferences(schema: Schema, rootSchema: Schema): Schema {
|
|
122
|
+
if (!schema) return schema;
|
|
123
|
+
|
|
124
|
+
// Handle direct reference
|
|
125
|
+
if (hasRef(schema) && rootSchema.$defs) {
|
|
126
|
+
const refName = schema.$ref.replace('#/$defs/', '');
|
|
127
|
+
const resolvedSchema = rootSchema.$defs[refName];
|
|
128
|
+
if (resolvedSchema) {
|
|
129
|
+
const fullyResolved = resolveSchemaReferences(resolvedSchema, rootSchema);
|
|
130
|
+
return {
|
|
131
|
+
...fullyResolved,
|
|
132
|
+
...schema,
|
|
133
|
+
$ref: schema.$ref
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Handle nested references in properties
|
|
139
|
+
if (schema.properties) {
|
|
140
|
+
const resolvedProperties: Record<string, Schema> = {};
|
|
141
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
142
|
+
resolvedProperties[key] = resolveSchemaReferences(propSchema, rootSchema);
|
|
143
|
+
}
|
|
144
|
+
schema = { ...schema, properties: resolvedProperties };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Handle references in array items
|
|
148
|
+
if (schema.items) {
|
|
149
|
+
if (Array.isArray(schema.items)) {
|
|
150
|
+
schema = {
|
|
151
|
+
...schema,
|
|
152
|
+
items: schema.items.map(itemSchema => resolveSchemaReferences(itemSchema, rootSchema))
|
|
153
|
+
};
|
|
154
|
+
} else {
|
|
155
|
+
schema = {
|
|
156
|
+
...schema,
|
|
157
|
+
items: resolveSchemaReferences(schema.items, rootSchema)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Handle references in anyOf, oneOf, allOf
|
|
163
|
+
const combiners = ['anyOf', 'oneOf', 'allOf'] as const;
|
|
164
|
+
for (const key of combiners) {
|
|
165
|
+
const schemas = schema[key];
|
|
166
|
+
if (schemas) {
|
|
167
|
+
schema = {
|
|
168
|
+
...schema,
|
|
169
|
+
[key]: schemas.map((subSchema: Schema) => resolveSchemaReferences(subSchema, rootSchema))
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return schema;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Process anyOf to handle nullable fields
|
|
179
|
+
*/
|
|
180
|
+
function processAnyOf(schema: Schema): { processedSchema: Schema; isNullable: boolean } {
|
|
181
|
+
if (!schema.anyOf || schema.anyOf.length > 2) {
|
|
182
|
+
return { processedSchema: schema, isNullable: false };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const nullSchema = schema.anyOf.find(type => type.type === 'null');
|
|
186
|
+
const nonNullSchema = schema.anyOf.find(type => type.type !== 'null');
|
|
187
|
+
|
|
188
|
+
if (!nullSchema) {
|
|
189
|
+
return { processedSchema: schema, isNullable: false };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!nonNullSchema) {
|
|
193
|
+
return {
|
|
194
|
+
processedSchema: { ...schema, type: 'null', anyOf: undefined },
|
|
195
|
+
isNullable: true
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
processedSchema: {
|
|
201
|
+
...schema,
|
|
202
|
+
...nonNullSchema,
|
|
203
|
+
description: schema.description || nonNullSchema.description,
|
|
204
|
+
default: schema.default || nonNullSchema.default,
|
|
205
|
+
anyOf: undefined
|
|
206
|
+
},
|
|
207
|
+
isNullable: true
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Synthesize a display type from a oneOf schema.
|
|
213
|
+
* Special-cases the File schema ($id: /schemas/File) to show "file" instead of "object".
|
|
214
|
+
*/
|
|
215
|
+
function resolveOneOfType(schema: Schema): string | undefined {
|
|
216
|
+
if (!schema.oneOf || schema.oneOf.length === 0) return undefined;
|
|
217
|
+
|
|
218
|
+
const isFileSchema = schema.$id === '/schemas/File';
|
|
219
|
+
const types: string[] = [];
|
|
220
|
+
for (const alt of schema.oneOf) {
|
|
221
|
+
if (isFileSchema && alt.type === 'object') {
|
|
222
|
+
types.push('file');
|
|
223
|
+
} else if (typeof alt.type === 'string') {
|
|
224
|
+
types.push(alt.type);
|
|
225
|
+
} else {
|
|
226
|
+
types.push('any');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (types.length === 0) return undefined;
|
|
231
|
+
if (types.length === 1) return types[0];
|
|
232
|
+
return types.join(' | ');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Normalize a schema type to a single SchemaTypeName.
|
|
237
|
+
* JSON Schema allows type arrays (e.g. ["string", "null"]) — pick the
|
|
238
|
+
* first non-null entry so field logic can use simple === comparisons.
|
|
239
|
+
*/
|
|
240
|
+
export function normalizeType(schema: Schema): SchemaTypeName | undefined {
|
|
241
|
+
if (!schema.type) return undefined;
|
|
242
|
+
if (!Array.isArray(schema.type)) return schema.type;
|
|
243
|
+
return schema.type.find(t => t !== 'null') ?? schema.type[0];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Determine the display type for a resolved schema
|
|
248
|
+
*/
|
|
249
|
+
export function resolveDisplayType(schema: Schema): string {
|
|
250
|
+
if (schema.format) {
|
|
251
|
+
return schema.format;
|
|
252
|
+
}
|
|
253
|
+
if (schema.type) {
|
|
254
|
+
return Array.isArray(schema.type) ? schema.type.join(' | ') : schema.type;
|
|
255
|
+
}
|
|
256
|
+
const oneOfType = resolveOneOfType(schema);
|
|
257
|
+
if (oneOfType) return oneOfType;
|
|
258
|
+
if (schema.anyOf?.length) {
|
|
259
|
+
return schema.anyOf.map(alt => alt.type ?? 'any').join(' | ');
|
|
260
|
+
}
|
|
261
|
+
return 'any';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Create a field from a schema property
|
|
266
|
+
*/
|
|
267
|
+
function createFieldFromSchema(
|
|
268
|
+
schema: Schema,
|
|
269
|
+
key: string,
|
|
270
|
+
rootSchema: Schema,
|
|
271
|
+
required?: string[]
|
|
272
|
+
): SchemaField {
|
|
273
|
+
const resolvedSchema = resolveSchemaReferences(schema, rootSchema);
|
|
274
|
+
|
|
275
|
+
const promoted = schema['x-promoted'] === true || resolvedSchema['x-promoted'] === true;
|
|
276
|
+
|
|
277
|
+
const type = normalizeType(resolvedSchema);
|
|
278
|
+
|
|
279
|
+
if (hasRef(schema)) {
|
|
280
|
+
return {
|
|
281
|
+
...schema,
|
|
282
|
+
...resolvedSchema,
|
|
283
|
+
key,
|
|
284
|
+
type,
|
|
285
|
+
displayType: resolveDisplayType(resolvedSchema),
|
|
286
|
+
title: resolvedSchema.title || key,
|
|
287
|
+
isRequired: required?.includes(key),
|
|
288
|
+
...(promoted && { promoted }),
|
|
289
|
+
resolved: resolvedSchema,
|
|
290
|
+
$ref: schema.$ref
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
...resolvedSchema,
|
|
296
|
+
key,
|
|
297
|
+
type,
|
|
298
|
+
displayType: resolveDisplayType(resolvedSchema),
|
|
299
|
+
title: schema.title || key,
|
|
300
|
+
isRequired: required?.includes(key),
|
|
301
|
+
...(promoted && { promoted }),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Parse a JSON Schema into a list of fields with resolved references
|
|
307
|
+
*/
|
|
308
|
+
export function getFieldsFromSchema(schema: Schema): { fields: SchemaField[] } {
|
|
309
|
+
if (!schema || !schema.properties) {
|
|
310
|
+
return { fields: [] };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const fields = Object.entries(schema.properties).map(([key, fieldData]): SchemaField => {
|
|
314
|
+
const resolvedFieldData = resolveSchemaReferences(fieldData, schema);
|
|
315
|
+
const { processedSchema } = processAnyOf(resolvedFieldData);
|
|
316
|
+
return createFieldFromSchema(processedSchema, key, schema, schema.required);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return { fields };
|
|
320
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser's localStorage and sessionStorage, for a page that may not be
|
|
3
|
+
* allowed either. Framed by another site (the app inside ChatGPT, Claude or
|
|
4
|
+
* Codex) with third-party storage blocked, reading `window.localStorage`
|
|
5
|
+
* itself throws a SecurityError; elsewhere a write can be refused (a full
|
|
6
|
+
* quota, some private modes). Neither reaches the caller: what the browser
|
|
7
|
+
* will not keep is kept in memory until the page goes away, so a value reads
|
|
8
|
+
* back the same and simply does not outlive the page.
|
|
9
|
+
*
|
|
10
|
+
* Every use of web storage goes through `localStore` and `sessionStore`; lint
|
|
11
|
+
* rejects the browser globals outside this module. Both are also what
|
|
12
|
+
* zustand's `createJSONStorage` expects (a `StateStorage`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The part of the DOM `Storage` interface the stores provide. */
|
|
16
|
+
export interface WebStorage {
|
|
17
|
+
/** How many keys are stored. */
|
|
18
|
+
readonly length: number
|
|
19
|
+
/** The key at `index`, or null past the end. */
|
|
20
|
+
key(index: number): string | null
|
|
21
|
+
getItem(key: string): string | null
|
|
22
|
+
setItem(key: string, value: string): void
|
|
23
|
+
removeItem(key: string): void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A store over the Storage `resolve` returns. `resolve` runs once, on first
|
|
28
|
+
* use rather than when the module loads, and may throw or return nothing:
|
|
29
|
+
* the store is then memory only.
|
|
30
|
+
*/
|
|
31
|
+
export function createSafeStorage(resolve: () => Storage | null | undefined): WebStorage {
|
|
32
|
+
let browser: Storage | null | undefined
|
|
33
|
+
// What the browser does not hold: everything when there is no Storage, and
|
|
34
|
+
// otherwise each value it refused to write. A key here shadows the browser's.
|
|
35
|
+
const kept = new Map<string, string>()
|
|
36
|
+
|
|
37
|
+
const storage = (): Storage | null => {
|
|
38
|
+
if (browser === undefined) {
|
|
39
|
+
try {
|
|
40
|
+
browser = usable(resolve())
|
|
41
|
+
} catch {
|
|
42
|
+
browser = null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return browser
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const keys = (): string[] => {
|
|
49
|
+
const browser = storage()
|
|
50
|
+
const all = new Set<string>()
|
|
51
|
+
for (let i = 0; browser && i < browser.length; i++) {
|
|
52
|
+
const key = browser.key(i)
|
|
53
|
+
if (key !== null) all.add(key)
|
|
54
|
+
}
|
|
55
|
+
for (const key of kept.keys()) all.add(key)
|
|
56
|
+
return [...all]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
get length() {
|
|
61
|
+
return kept.size === 0 ? (storage()?.length ?? 0) : keys().length
|
|
62
|
+
},
|
|
63
|
+
key(index) {
|
|
64
|
+
return (kept.size === 0 ? storage()?.key(index) : keys()[index]) ?? null
|
|
65
|
+
},
|
|
66
|
+
getItem(key) {
|
|
67
|
+
return kept.get(key) ?? storage()?.getItem(key) ?? null
|
|
68
|
+
},
|
|
69
|
+
setItem(key, value) {
|
|
70
|
+
const browser = storage()
|
|
71
|
+
if (browser) {
|
|
72
|
+
try {
|
|
73
|
+
browser.setItem(key, value)
|
|
74
|
+
kept.delete(key)
|
|
75
|
+
return
|
|
76
|
+
} catch {
|
|
77
|
+
// refused this write: the value is kept below instead
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
kept.set(key, String(value))
|
|
81
|
+
},
|
|
82
|
+
removeItem(key) {
|
|
83
|
+
kept.delete(key)
|
|
84
|
+
storage()?.removeItem(key)
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// A Storage that answers the calls the store makes, else null. Node 25 gives
|
|
90
|
+
// every process a `localStorage` global without its methods unless it is
|
|
91
|
+
// started with --localstorage-file, and a DOM shim can hand that one back.
|
|
92
|
+
function usable(candidate: Storage | null | undefined): Storage | null {
|
|
93
|
+
if (!candidate) return null
|
|
94
|
+
const s = candidate as Partial<Storage>
|
|
95
|
+
return typeof s.getItem === 'function' && typeof s.setItem === 'function' && typeof s.removeItem === 'function' && typeof s.key === 'function'
|
|
96
|
+
? candidate
|
|
97
|
+
: null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Without a window (a server render, a test) there is no web storage to ask
|
|
101
|
+
// for. Node's own `localStorage` global is not it, and is left alone.
|
|
102
|
+
const browserWindow = (): Window | undefined => (typeof window === 'undefined' ? undefined : window)
|
|
103
|
+
|
|
104
|
+
/** localStorage: kept across visits when the browser allows it. */
|
|
105
|
+
export const localStore = createSafeStorage(() => browserWindow()?.localStorage)
|
|
106
|
+
|
|
107
|
+
/** sessionStorage: kept for the tab's lifetime when the browser allows it. */
|
|
108
|
+
export const sessionStore = createSafeStorage(() => browserWindow()?.sessionStorage)
|