@specbook/ui 0.0.0-stage → 0.1.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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/dist-viewer/apple-touch-icon.png +0 -0
  4. package/dist-viewer/assets/Host-DdTTHeXU.js +37 -0
  5. package/dist-viewer/assets/faker-B3c03STC.js +11 -0
  6. package/dist-viewer/assets/index-DnYdlDXQ.js +1 -0
  7. package/dist-viewer/assets/index-Ds6DZpGP.css +1 -0
  8. package/dist-viewer/assets/main-uV4JzQPd.js +14 -0
  9. package/dist-viewer/assets/mount-B-qFOcWp.js +2 -0
  10. package/dist-viewer/assets/panel-CoQ6iQpX.js +1 -0
  11. package/dist-viewer/assets/panel-Coi9H1nv.css +1 -0
  12. package/dist-viewer/assets/registry-DI74Wtj8.js +2 -0
  13. package/dist-viewer/assets/worker-Dw3Ps3hk.js +181 -0
  14. package/dist-viewer/favicon.svg +1 -0
  15. package/dist-viewer/index.html +19 -0
  16. package/dist-viewer/specbook.js +1 -0
  17. package/index.html +12 -0
  18. package/package.json +60 -3
  19. package/src/app.tsx +363 -0
  20. package/src/assets/mark.svg +1 -0
  21. package/src/badge.ts +14 -0
  22. package/src/branding.ts +92 -0
  23. package/src/components/Catalog.tsx +324 -0
  24. package/src/components/EnvBadge.tsx +46 -0
  25. package/src/components/EnvironmentMenu.tsx +262 -0
  26. package/src/components/Fields.tsx +220 -0
  27. package/src/components/GitHubStar.tsx +22 -0
  28. package/src/components/ListResizer.tsx +70 -0
  29. package/src/components/ModeHint.tsx +17 -0
  30. package/src/components/Palette.tsx +85 -0
  31. package/src/components/PoweredBy.tsx +15 -0
  32. package/src/components/ShareMenu.tsx +94 -0
  33. package/src/components/ThemeToggle.tsx +52 -0
  34. package/src/embed/panel.tsx +114 -0
  35. package/src/embed/styles.ts +27 -0
  36. package/src/embed.ts +65 -0
  37. package/src/engine.ts +432 -0
  38. package/src/extension.ts +101 -0
  39. package/src/fielddoc.ts +83 -0
  40. package/src/main.tsx +16 -0
  41. package/src/markdown.ts +133 -0
  42. package/src/nav.ts +118 -0
  43. package/src/panel/Body.tsx +182 -0
  44. package/src/panel/BodyEditor.tsx +92 -0
  45. package/src/panel/CodeSample.tsx +237 -0
  46. package/src/panel/Combo.tsx +124 -0
  47. package/src/panel/ControlsGrid.tsx +189 -0
  48. package/src/panel/FieldDoc.tsx +42 -0
  49. package/src/panel/FieldTable.tsx +113 -0
  50. package/src/panel/Host.tsx +100 -0
  51. package/src/panel/OperationDocs.tsx +181 -0
  52. package/src/panel/Panel.tsx +193 -0
  53. package/src/panel/ParamGrid.tsx +396 -0
  54. package/src/panel/RequestPanel.tsx +625 -0
  55. package/src/panel/RequestView.tsx +108 -0
  56. package/src/panel/ResponseView.tsx +182 -0
  57. package/src/panel/SecretsBar.tsx +156 -0
  58. package/src/panel/SequenceDocs.tsx +132 -0
  59. package/src/panel/SequencePanel.tsx +247 -0
  60. package/src/panel/StepExchange.tsx +118 -0
  61. package/src/panel/completions.ts +114 -0
  62. package/src/panel/draft.ts +106 -0
  63. package/src/panel/fieldnotes-split.ts +55 -0
  64. package/src/panel/fieldnotes.ts +40 -0
  65. package/src/panel/graphql.ts +31 -0
  66. package/src/panel/icons.tsx +85 -0
  67. package/src/panel/jsonCursor.ts +115 -0
  68. package/src/panel/stepnotes-split.ts +38 -0
  69. package/src/panel/stepnotes.ts +58 -0
  70. package/src/routing.ts +120 -0
  71. package/src/runtime.ts +32 -0
  72. package/src/schema.ts +359 -0
  73. package/src/secrets.ts +61 -0
  74. package/src/status.ts +27 -0
  75. package/src/store.ts +10 -0
  76. package/src/styles/app.css +821 -0
  77. package/src/styles/panel.css +1897 -0
  78. package/src/styles/tokens.css +122 -0
  79. package/src/theme.ts +37 -0
  80. package/src/viewer/Opening.tsx +43 -0
  81. package/src/viewer/bundles.ts +74 -0
  82. package/src/viewer/client.ts +54 -0
  83. package/src/viewer/embed.ts +78 -0
  84. package/src/viewer/faker-en.ts +3 -0
  85. package/src/viewer/main.tsx +285 -0
  86. package/src/viewer/mount.ts +52 -0
  87. package/src/viewer/protocol.ts +51 -0
  88. package/src/viewer/recent.ts +22 -0
  89. package/src/viewer/registry-data.ts +4 -0
  90. package/src/viewer/registry.ts +154 -0
  91. package/src/viewer/share.ts +134 -0
  92. package/src/viewer/site.ts +3 -0
  93. package/src/viewer/uploads.ts +53 -0
  94. package/src/viewer/viewer.css +184 -0
  95. package/src/viewer/worker-factory.ts +14 -0
  96. package/src/viewer/worker.ts +171 -0
  97. package/src/virtual.d.ts +104 -0
package/src/schema.ts ADDED
@@ -0,0 +1,359 @@
1
+ // JSON Schema helpers for the annotated request/response views: resolve refs, unwrap the
2
+ // "{{variable}}" placeholder alternative, describe nodes, and validate a value against a schema.
3
+
4
+ export type JsonSchema = Record<string, any>
5
+ export type Defs = Record<string, any> | undefined
6
+
7
+ export type Kind =
8
+ | 'object'
9
+ | 'array'
10
+ | 'string'
11
+ | 'integer'
12
+ | 'number'
13
+ | 'boolean'
14
+ | 'null'
15
+ | 'unknown'
16
+
17
+ export const TEMPLATE = /^\s*\{\{[^}]+\}\}\s*$/
18
+
19
+ export const isTemplate = (v: unknown): v is string => typeof v === 'string' && TEMPLATE.test(v)
20
+
21
+ function isPlaceholderSchema(s: JsonSchema): boolean {
22
+ return s?.type === 'string' && typeof s.pattern === 'string' && s.pattern.includes('\\{\\{')
23
+ }
24
+
25
+ const META = ['title', 'description', 'default', 'examples', 'deprecated']
26
+
27
+ function withMeta(target: JsonSchema, outer: JsonSchema): JsonSchema {
28
+ const out = { ...target }
29
+ for (const k of META) if (outer[k] !== undefined && out[k] === undefined) out[k] = outer[k]
30
+ return out
31
+ }
32
+
33
+ /** Follows `$ref` (into the service definitions) until a concrete schema is reached. */
34
+ export function resolveRef(schema: JsonSchema | undefined, defs: Defs): JsonSchema {
35
+ let s: JsonSchema = schema ?? {}
36
+ for (let i = 0; i < 12 && s.$ref; i++) {
37
+ const name = String(s.$ref).split('/').pop() as string
38
+ const target = defs?.$defs?.[name]
39
+ if (!target) return { ...s, type: undefined }
40
+ s = withMeta(target, s)
41
+ }
42
+ return s
43
+ }
44
+
45
+ /**
46
+ * Resolves refs, drops the `"{{variable}}"` alternative, and flattens `allOf`.
47
+ * For `oneOf`/`anyOf` it picks the first branch (callers that have a value use `pickBranch`).
48
+ */
49
+ export function unwrap(schema: JsonSchema | undefined, defs: Defs, depth = 0): JsonSchema {
50
+ let s = resolveRef(schema, defs)
51
+ if (depth > 10) return s
52
+ if (Array.isArray(s.anyOf)) {
53
+ const branches = s.anyOf.filter((b: JsonSchema) => !isPlaceholderSchema(b))
54
+ if (branches.length === 1) return unwrap(withMeta(branches[0], s), defs, depth + 1)
55
+ if (branches.length > 1) return unwrap(withMeta(branches[0], s), defs, depth + 1)
56
+ }
57
+ if (Array.isArray(s.oneOf) && s.oneOf.length)
58
+ return unwrap(withMeta(s.oneOf[0], s), defs, depth + 1)
59
+ if (Array.isArray(s.allOf)) {
60
+ const merged: JsonSchema = { ...s }
61
+ Reflect.deleteProperty(merged, 'allOf')
62
+ const props: Record<string, any> = { ...(merged.properties ?? {}) }
63
+ const required = new Set<string>(merged.required ?? [])
64
+ for (const part of s.allOf as JsonSchema[]) {
65
+ const u = unwrap(part, defs, depth + 1)
66
+ Object.assign(props, u.properties ?? {})
67
+ for (const r of u.required ?? []) required.add(r)
68
+ for (const k of META) if (u[k] !== undefined && merged[k] === undefined) merged[k] = u[k]
69
+ if (u.type && !merged.type) merged.type = u.type
70
+ if (u.items && !merged.items) merged.items = u.items
71
+ if (u.enum && !merged.enum) merged.enum = u.enum
72
+ }
73
+ merged.properties = props
74
+ merged.required = [...required]
75
+ if (!merged.type && Object.keys(props).length) merged.type = 'object'
76
+ s = merged
77
+ }
78
+ return s
79
+ }
80
+
81
+ export function kindOf(schema: JsonSchema): Kind {
82
+ const t = Array.isArray(schema.type) ? schema.type.find((x: string) => x !== 'null') : schema.type
83
+ if (
84
+ t === 'object' ||
85
+ t === 'array' ||
86
+ t === 'string' ||
87
+ t === 'integer' ||
88
+ t === 'number' ||
89
+ t === 'boolean'
90
+ ) {
91
+ return t
92
+ }
93
+ if (schema.properties) return 'object'
94
+ if (schema.items) return 'array'
95
+ if (schema.enum) return typeof schema.enum[0] === 'number' ? 'number' : 'string'
96
+ return t === 'null' ? 'null' : 'unknown'
97
+ }
98
+
99
+ export function describe(schema: JsonSchema): string | undefined {
100
+ const d = schema.description ?? schema.title
101
+ return typeof d === 'string' && d.trim() ? d.trim() : undefined
102
+ }
103
+
104
+ /** First sentence/line of a description, for compact inline comments. */
105
+ export function summary(desc: string | undefined, max = 120): string | undefined {
106
+ if (!desc) return undefined
107
+ const line = desc.split(/\n+/)[0] as string
108
+ const sentence = line.match(/^.*?[.!?](\s|$)/)?.[0]?.trim() ?? line
109
+ const text = sentence.replace(/\s+/g, ' ')
110
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text
111
+ }
112
+
113
+ export interface Prop {
114
+ name: string
115
+ schema: JsonSchema
116
+ required: boolean
117
+ }
118
+
119
+ export function propsOf(schema: JsonSchema, defs: Defs): Prop[] {
120
+ const s = unwrap(schema, defs)
121
+ const required = new Set<string>(s.required ?? [])
122
+ return Object.entries<JsonSchema>(s.properties ?? {}).map(([name, sch]) => ({
123
+ name,
124
+ schema: sch,
125
+ required: required.has(name),
126
+ }))
127
+ }
128
+
129
+ export function childSchema(
130
+ schema: JsonSchema,
131
+ key: string | number,
132
+ defs: Defs,
133
+ ): JsonSchema | undefined {
134
+ const s = unwrap(schema, defs)
135
+ if (typeof key === 'number') return s.items
136
+ if (s.properties?.[key]) return s.properties[key]
137
+ if (s.additionalProperties && typeof s.additionalProperties === 'object')
138
+ return s.additionalProperties
139
+ return undefined
140
+ }
141
+
142
+ export interface Issue {
143
+ path: string
144
+ kind: 'type' | 'enum' | 'required' | 'extra' | 'range'
145
+ message: string
146
+ }
147
+
148
+ const typeName = (v: unknown) => (v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v)
149
+
150
+ /** Pick the oneOf/anyOf branch whose primary type matches the value (falls back to the first). */
151
+ function pickBranch(schema: JsonSchema, value: unknown, defs: Defs): JsonSchema {
152
+ const s = resolveRef(schema, defs)
153
+ const branches = ((s.oneOf ?? s.anyOf) as JsonSchema[] | undefined)?.filter(
154
+ (b) => !isPlaceholderSchema(b),
155
+ )
156
+ if (!branches?.length) return unwrap(s, defs)
157
+ const want = typeName(value)
158
+ const match = branches.find((b) => {
159
+ const k = kindOf(unwrap(b, defs))
160
+ return (
161
+ k === want || (k === 'integer' && want === 'number') || (k === 'number' && want === 'number')
162
+ )
163
+ })
164
+ return unwrap(withMeta(match ?? (branches[0] as JsonSchema), s), defs)
165
+ }
166
+
167
+ export interface ValidateOptions {
168
+ /** Report object keys that the schema does not declare (response conformance). */
169
+ reportExtra?: boolean
170
+ }
171
+
172
+ export function validate(
173
+ value: unknown,
174
+ schema: JsonSchema | undefined,
175
+ defs: Defs,
176
+ opts: ValidateOptions = {},
177
+ path = '',
178
+ depth = 0,
179
+ ): Issue[] {
180
+ if (!schema || depth > 12) return []
181
+ if (isTemplate(value)) return [] // a {{variable}} stands for any value
182
+ const s = pickBranch(schema, value, defs)
183
+ const issues: Issue[] = []
184
+ const at = path || '(root)'
185
+ const nullable = Array.isArray(s.type) && s.type.includes('null')
186
+ if (value === null) {
187
+ if (!nullable && kindOf(s) !== 'null' && kindOf(s) !== 'unknown' && s.nullable !== true) {
188
+ issues.push({ path: at, kind: 'type', message: `expected ${kindOf(s)}, got null` })
189
+ }
190
+ return issues
191
+ }
192
+ const kind = kindOf(s)
193
+ const got = typeName(value)
194
+ switch (kind) {
195
+ case 'object': {
196
+ if (got !== 'object')
197
+ return [{ path: at, kind: 'type', message: `expected object, got ${got}` }]
198
+ const obj = value as Record<string, unknown>
199
+ const props = (s.properties ?? {}) as Record<string, JsonSchema>
200
+ for (const r of (s.required ?? []) as string[]) {
201
+ if (!(r in obj))
202
+ issues.push({
203
+ path: path ? `${path}.${r}` : r,
204
+ kind: 'required',
205
+ message: 'required field is missing',
206
+ })
207
+ }
208
+ for (const [k, v] of Object.entries(obj)) {
209
+ const p = path ? `${path}.${k}` : k
210
+ if (props[k]) issues.push(...validate(v, props[k], defs, opts, p, depth + 1))
211
+ else if (s.additionalProperties && typeof s.additionalProperties === 'object')
212
+ issues.push(...validate(v, s.additionalProperties, defs, opts, p, depth + 1))
213
+ else if (
214
+ opts.reportExtra &&
215
+ s.additionalProperties !== true &&
216
+ Object.keys(props).length > 0
217
+ )
218
+ issues.push({ path: p, kind: 'extra', message: 'not in the spec' })
219
+ }
220
+ return issues
221
+ }
222
+ case 'array': {
223
+ if (got !== 'array')
224
+ return [{ path: at, kind: 'type', message: `expected array, got ${got}` }]
225
+ ;(value as unknown[]).forEach((item, i) => {
226
+ issues.push(...validate(item, s.items, defs, opts, `${path}[${i}]`, depth + 1))
227
+ })
228
+ return issues
229
+ }
230
+ case 'string': {
231
+ if (got !== 'string')
232
+ return [{ path: at, kind: 'type', message: `expected string, got ${got}` }]
233
+ if (s.enum && !s.enum.includes(value)) {
234
+ issues.push({ path: at, kind: 'enum', message: `must be one of: ${s.enum.join(' · ')}` })
235
+ }
236
+ return issues
237
+ }
238
+ case 'integer':
239
+ case 'number': {
240
+ if (got !== 'number')
241
+ return [{ path: at, kind: 'type', message: `expected ${kind}, got ${got}` }]
242
+ const n = value as number
243
+ if (kind === 'integer' && !Number.isInteger(n))
244
+ issues.push({ path: at, kind: 'type', message: 'expected an integer' })
245
+ if (s.minimum !== undefined && n < s.minimum)
246
+ issues.push({ path: at, kind: 'range', message: `must be ≥ ${s.minimum}` })
247
+ if (s.maximum !== undefined && n > s.maximum)
248
+ issues.push({ path: at, kind: 'range', message: `must be ≤ ${s.maximum}` })
249
+ if (s.enum && !s.enum.includes(n))
250
+ issues.push({ path: at, kind: 'enum', message: `must be one of: ${s.enum.join(' · ')}` })
251
+ return issues
252
+ }
253
+ case 'boolean':
254
+ return got === 'boolean'
255
+ ? []
256
+ : [{ path: at, kind: 'type', message: `expected boolean, got ${got}` }]
257
+ default:
258
+ return []
259
+ }
260
+ }
261
+
262
+ export type KeyPath = (string | number)[]
263
+
264
+ /** A path such as "data.tags[0].id", the same format `validate` uses for issue paths. */
265
+ export const pathString = (p: KeyPath): string =>
266
+ p.reduce<string>(
267
+ (acc, k) => (typeof k === 'number' ? `${acc}[${k}]` : acc ? `${acc}.${k}` : String(k)),
268
+ '',
269
+ )
270
+
271
+ /**
272
+ * For every line of a JSON document, the path of the key/element that starts on it (or undefined).
273
+ * Tolerates incomplete or invalid JSON, so annotations keep working while the user types.
274
+ */
275
+ export function linePaths(text: string): (KeyPath | undefined)[] {
276
+ interface Box {
277
+ type: 'obj' | 'arr'
278
+ path: KeyPath
279
+ key?: string
280
+ idx: number
281
+ }
282
+ const out: (KeyPath | undefined)[] = [undefined]
283
+ const stack: Box[] = []
284
+ let line = 0
285
+ const set = (p: KeyPath) => {
286
+ if (out[line] === undefined) out[line] = p
287
+ }
288
+ const top = () => stack[stack.length - 1]
289
+ const childPath = (b: Box | undefined): KeyPath | undefined =>
290
+ !b
291
+ ? []
292
+ : b.type === 'arr'
293
+ ? [...b.path, b.idx]
294
+ : b.key !== undefined
295
+ ? [...b.path, b.key]
296
+ : undefined
297
+
298
+ for (let i = 0; i < text.length; i++) {
299
+ const c = text[i] as string
300
+ if (c === '\n') {
301
+ line++
302
+ out[line] = undefined
303
+ } else if (c === '"') {
304
+ let j = i + 1
305
+ let str = ''
306
+ while (j < text.length && text[j] !== '"') {
307
+ if (text[j] === '\\') {
308
+ str += text[j + 1] ?? ''
309
+ j += 2
310
+ } else str += text[j++]
311
+ }
312
+ let k = j + 1
313
+ while (k < text.length && /\s/.test(text[k] as string) && text[k] !== '\n') k++
314
+ const t = top()
315
+ if (t?.type === 'obj' && text[k] === ':') {
316
+ t.key = str
317
+ set([...t.path, str])
318
+ } else if (t?.type === 'arr') set([...t.path, t.idx])
319
+ i = j
320
+ } else if (c === '{' || c === '[') {
321
+ const t = top()
322
+ const p = childPath(t)
323
+ if (p) set(p)
324
+ stack.push({ type: c === '{' ? 'obj' : 'arr', path: p ?? [], idx: 0 })
325
+ } else if (c === '}' || c === ']') {
326
+ stack.pop()
327
+ const t = top()
328
+ if (t?.type === 'arr') t.idx++
329
+ } else if (c === ',') {
330
+ const t = top()
331
+ // object commas need no bookkeeping; scalar array elements advance the index where they end
332
+ void t
333
+ } else if (/[-\d]|[tfn]/.test(c)) {
334
+ const t = top()
335
+ if (t?.type === 'arr') {
336
+ set([...t.path, t.idx])
337
+ // scalar element: advance the index at the next comma
338
+ let j = i
339
+ while (j < text.length && !/[,\]\n]/.test(text[j] as string)) j++
340
+ if (text[j] === ',') t.idx++
341
+ i = j - 1
342
+ }
343
+ }
344
+ }
345
+ return out
346
+ }
347
+
348
+ export function schemaAt(
349
+ root: JsonSchema | undefined,
350
+ path: KeyPath,
351
+ defs: Defs,
352
+ ): JsonSchema | undefined {
353
+ let s = root
354
+ for (const k of path) {
355
+ if (!s) return undefined
356
+ s = childSchema(s, k, defs)
357
+ }
358
+ return s
359
+ }
package/src/secrets.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Where a static page keeps the credentials a person types in.
3
+ *
4
+ * Values are keyed by (API origin, secret name): a key typed for one API is never sent to another
5
+ * host, whatever a spec's `servers` says. The backend is `sessionStorage` (this tab only, gone when it
6
+ * closes) with an in-memory fallback; another backend (an encrypted store, say) can sit behind the same
7
+ * interface. Requests carry `{{secrets.NAME}}` placeholders until the transport sends them.
8
+ */
9
+ const KEY = 'specbook.secrets.v1'
10
+
11
+ type Table = Record<string, Record<string, string>>
12
+
13
+ export interface SecretStore {
14
+ get(origin: string, name: string): string | undefined
15
+ set(origin: string, name: string, value: string): void
16
+ remove(origin: string, name: string): void
17
+ }
18
+
19
+ let memory: Table = {}
20
+
21
+ const read = (): Table => {
22
+ try {
23
+ const raw = sessionStorage.getItem(KEY)
24
+ return raw ? (JSON.parse(raw) as Table) : memory
25
+ } catch {
26
+ return memory
27
+ }
28
+ }
29
+
30
+ const write = (t: Table) => {
31
+ memory = t
32
+ try {
33
+ sessionStorage.setItem(KEY, JSON.stringify(t))
34
+ } catch {
35
+ /* private mode or blocked storage: the in-memory copy still serves this page */
36
+ }
37
+ }
38
+
39
+ export const secretStore: SecretStore = {
40
+ get: (origin, name) => read()[origin]?.[name],
41
+ set(origin, name, value) {
42
+ const t = read()
43
+ write({ ...t, [origin]: { ...t[origin], [name]: value } })
44
+ },
45
+ remove(origin, name) {
46
+ const t = read()
47
+ const { [name]: _gone, ...rest } = t[origin] ?? {}
48
+ write({ ...t, [origin]: rest })
49
+ },
50
+ }
51
+
52
+ export const originOf = (url: string | undefined): string | undefined => {
53
+ try {
54
+ return url ? new URL(url).origin : undefined
55
+ } catch {
56
+ return undefined
57
+ }
58
+ }
59
+
60
+ /** `{{secrets.NAME}}`, the same placeholder the dev server's proxy substitutes. */
61
+ export const SECRET_PLACEHOLDER = /\{\{\s*secrets\.([\w$-]+)\s*\}\}/g
package/src/status.ts ADDED
@@ -0,0 +1,27 @@
1
+ const NAMES: Record<number, string> = {
2
+ 200: 'OK',
3
+ 201: 'Created',
4
+ 202: 'Accepted',
5
+ 204: 'No content',
6
+ 301: 'Moved',
7
+ 304: 'Not modified',
8
+ 400: 'Bad request',
9
+ 401: 'Unauthorized',
10
+ 402: 'Payment required',
11
+ 403: 'Forbidden',
12
+ 404: 'Not found',
13
+ 405: 'Invalid input',
14
+ 409: 'Conflict',
15
+ 410: 'Gone',
16
+ 412: 'Precondition failed',
17
+ 422: 'Invalid input',
18
+ 429: 'Too many requests',
19
+ 500: 'Server error',
20
+ 502: 'Bad gateway',
21
+ 503: 'Unavailable',
22
+ }
23
+
24
+ export const statusName = (s: number) =>
25
+ NAMES[s] ?? (s >= 500 ? 'Server error' : s >= 400 ? 'Client error' : 'Response')
26
+ export const statusClass = (s: number) =>
27
+ s >= 500 ? 's5' : s >= 400 ? 's4' : s >= 300 ? 's3' : 's2'
package/src/store.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { signal } from '@preact/signals'
2
+
3
+ /** Bumped whenever sources may have changed; used to bust dynamic-import caches. */
4
+ export const version = signal(Date.now())
5
+ export const env = signal('mock')
6
+ export const environments = signal<string[]>(['mock'])
7
+ /** Environments the config marks `production` or `sandbox`; others are judged by their name. */
8
+ export const environmentRisk = signal<Record<string, 'production' | 'sandbox'>>({})
9
+ export const locale = signal<string | undefined>(undefined)
10
+ export const seed = signal<number | undefined>(undefined)