From 58d1fdc3ae471bd62463498318fb93589229ffe0 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Thu, 2 Jul 2026 23:16:47 +0530 Subject: [PATCH 01/10] feat(json-schema): detect string formats during inference --- .../__tests__/json-schema-generator.test.ts | 33 ++++++++++++++++ apps/web/src/lib/json-schema-generator.ts | 38 +++++++++++++++++-- 2 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/lib/__tests__/json-schema-generator.test.ts diff --git a/apps/web/src/lib/__tests__/json-schema-generator.test.ts b/apps/web/src/lib/__tests__/json-schema-generator.test.ts new file mode 100644 index 00000000..bf95c412 --- /dev/null +++ b/apps/web/src/lib/__tests__/json-schema-generator.test.ts @@ -0,0 +1,33 @@ +import { detectFormat, inferSchema } from '../json-schema-generator'; + +describe('detectFormat', () => { + it('detects each format', () => { + expect(detectFormat('550e8400-e29b-41d4-a716-446655440000')).toBe('uuid'); + expect(detectFormat('2026-07-02T10:30:00Z')).toBe('date-time'); + expect(detectFormat('2026-07-02T10:30:00.5+05:30')).toBe('date-time'); + expect(detectFormat('2026-07-02')).toBe('date'); + expect(detectFormat('10:30:00')).toBe('time'); + expect(detectFormat('user@example.com')).toBe('email'); + expect(detectFormat('192.168.0.1')).toBe('ipv4'); + expect(detectFormat('https://example.com/x')).toBe('uri'); + }); + it('returns undefined for plain strings and near-misses', () => { + expect(detectFormat('hello world')).toBeUndefined(); + expect(detectFormat('2026-13-99')).toBeUndefined(); + expect(detectFormat('999.999.999.999')).toBeUndefined(); + expect(detectFormat('not-a-uuid-1234')).toBeUndefined(); + }); +}); + +describe('inferSchema format tagging', () => { + it('tags detected string formats', () => { + const node = inferSchema({ created: '2026-07-02T10:30:00Z', name: 'x' }); + expect(node).toMatchObject({ + kind: 'object', + fields: { + created: { node: { kind: 'scalar', t: 'string', format: 'date-time' } }, + name: { node: { kind: 'scalar', t: 'string' } }, + }, + }); + }); +}); diff --git a/apps/web/src/lib/json-schema-generator.ts b/apps/web/src/lib/json-schema-generator.ts index 6249e0e9..3fad0a7d 100644 --- a/apps/web/src/lib/json-schema-generator.ts +++ b/apps/web/src/lib/json-schema-generator.ts @@ -29,16 +29,43 @@ export const OUTPUT_LANGUAGE_ORDER: readonly OutputLanguage[] = [ type ScalarJson = 'string' | 'number' | 'integer' | 'boolean' | 'null'; +export type StringFormat = + | 'date-time' + | 'date' + | 'time' + | 'email' + | 'uri' + | 'uuid' + | 'ipv4'; + export type SchemaNode = - | { kind: 'scalar'; t: ScalarJson } + | { kind: 'scalar'; t: ScalarJson; format?: StringFormat } | { kind: 'array'; item: SchemaNode } | { kind: 'object'; fields: Record } | { kind: 'union'; variants: SchemaNode[] }; +// Anchored, specific -> general. First match wins. +const FORMAT_PATTERNS: ReadonlyArray = [ + ['uuid', /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/], + ['date-time', /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/], + ['date', /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/], + ['time', /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?$/], + ['email', /^[^\s@]+@[^\s@]+\.[^\s@]+$/], + ['ipv4', /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/], + ['uri', /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s]+$/], +]; + +export function detectFormat(value: string): StringFormat | undefined { + for (const [fmt, re] of FORMAT_PATTERNS) { + if (re.test(value)) return fmt; + } + return undefined; +} + function fingerprint(n: SchemaNode): string { switch (n.kind) { case 'scalar': - return `s:${n.t}`; + return `s:${n.t}${n.format ? ':' + n.format : ''}`; case 'array': return `a:${fingerprint(n.item)}`; case 'object': { @@ -132,7 +159,12 @@ export function inferSchema(value: unknown): SchemaNode { return { kind: 'array', item: acc }; } const ty = typeof value; - if (ty === 'string') return { kind: 'scalar', t: 'string' }; + if (ty === 'string') { + const format = detectFormat(value as string); + return format + ? { kind: 'scalar', t: 'string', format } + : { kind: 'scalar', t: 'string' }; + } if (ty === 'boolean') return { kind: 'scalar', t: 'boolean' }; if (ty === 'number') { const n = value as number; From 1c9b79218bb1a20fd2d82e3a8ec4277803a7f359 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Thu, 2 Jul 2026 23:22:47 +0530 Subject: [PATCH 02/10] feat(json-schema): drop format when merged strings disagree --- .../__tests__/json-schema-generator.test.ts | 22 ++++++++++++++++++- apps/web/src/lib/json-schema-generator.ts | 7 +++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/apps/web/src/lib/__tests__/json-schema-generator.test.ts b/apps/web/src/lib/__tests__/json-schema-generator.test.ts index bf95c412..12fc45f3 100644 --- a/apps/web/src/lib/__tests__/json-schema-generator.test.ts +++ b/apps/web/src/lib/__tests__/json-schema-generator.test.ts @@ -1,4 +1,4 @@ -import { detectFormat, inferSchema } from '../json-schema-generator'; +import { detectFormat, inferSchema, mergeNodes } from '../json-schema-generator'; describe('detectFormat', () => { it('detects each format', () => { @@ -31,3 +31,23 @@ describe('inferSchema format tagging', () => { }); }); }); + +describe('mergeNodes format handling', () => { + const strEmail = { kind: 'scalar', t: 'string', format: 'email' } as const; + const strDate = { kind: 'scalar', t: 'string', format: 'date' } as const; + const plainStr = { kind: 'scalar', t: 'string' } as const; + + it('keeps a shared format', () => { + expect(mergeNodes(strEmail, { ...strEmail })).toEqual(strEmail); + }); + it('drops format when two strings disagree', () => { + expect(mergeNodes(strEmail, strDate)).toEqual(plainStr); + }); + it('drops format when only one side has one', () => { + expect(mergeNodes(strEmail, plainStr)).toEqual(plainStr); + }); + it('array of mixed-format strings infers plain string item', () => { + const node = inferSchema(['a@b.com', '2026-07-02']); + expect(node).toEqual({ kind: 'array', item: plainStr }); + }); +}); diff --git a/apps/web/src/lib/json-schema-generator.ts b/apps/web/src/lib/json-schema-generator.ts index 3fad0a7d..031d6245 100644 --- a/apps/web/src/lib/json-schema-generator.ts +++ b/apps/web/src/lib/json-schema-generator.ts @@ -125,7 +125,12 @@ export function mergeNodes(a: SchemaNode, b: SchemaNode): SchemaNode { if (fingerprint(a) === fingerprint(b)) return a; if (a.kind === 'scalar' && b.kind === 'scalar') { - if (a.t === b.t) return a; + if (a.t === b.t) { + if (a.t === 'string' && a.format !== b.format) { + return { kind: 'scalar', t: 'string' }; + } + return a; + } if ( (a.t === 'integer' && b.t === 'number') || (a.t === 'number' && b.t === 'integer') From d809ccd1baf7ff1d104018c3e02310f9c45380cf Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Thu, 2 Jul 2026 23:28:09 +0530 Subject: [PATCH 03/10] feat(json-schema): emit format keyword in JSON Schema output --- .../src/lib/__tests__/json-schema-generator.test.ts | 12 +++++++++++- apps/web/src/lib/json-schema-generator.ts | 7 +++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/web/src/lib/__tests__/json-schema-generator.test.ts b/apps/web/src/lib/__tests__/json-schema-generator.test.ts index 12fc45f3..b4e947c5 100644 --- a/apps/web/src/lib/__tests__/json-schema-generator.test.ts +++ b/apps/web/src/lib/__tests__/json-schema-generator.test.ts @@ -1,4 +1,4 @@ -import { detectFormat, inferSchema, mergeNodes } from '../json-schema-generator'; +import { detectFormat, inferSchema, mergeNodes, toJsonSchemaDocument } from '../json-schema-generator'; describe('detectFormat', () => { it('detects each format', () => { @@ -51,3 +51,13 @@ describe('mergeNodes format handling', () => { expect(node).toEqual({ kind: 'array', item: plainStr }); }); }); + +describe('JSON Schema format output', () => { + it('emits format for detected strings', () => { + const doc = toJsonSchemaDocument( + inferSchema({ email: 'a@b.com', note: 'hi' }) + ) as any; + expect(doc.properties.email).toEqual({ type: 'string', format: 'email' }); + expect(doc.properties.note).toEqual({ type: 'string' }); + }); +}); diff --git a/apps/web/src/lib/json-schema-generator.ts b/apps/web/src/lib/json-schema-generator.ts index 031d6245..69c4b730 100644 --- a/apps/web/src/lib/json-schema-generator.ts +++ b/apps/web/src/lib/json-schema-generator.ts @@ -189,8 +189,11 @@ export function inferSchema(value: unknown): SchemaNode { function jsonSchemaTypeFragment(node: SchemaNode): Record { switch (node.kind) { - case 'scalar': - return { type: node.t }; + case 'scalar': { + const frag: Record = { type: node.t }; + if (node.t === 'string' && node.format) frag.format = node.format; + return frag; + } case 'array': return { type: 'array', items: toJsonSchemaFragment(node.item) }; case 'object': { From 457a68561a65738a64acfc1de37a45decca9aaf0 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Thu, 2 Jul 2026 23:32:21 +0530 Subject: [PATCH 04/10] feat(json-schema): add collectFormats tree walker --- .../__tests__/json-schema-generator.test.ts | 18 ++++++++++++++- apps/web/src/lib/json-schema-generator.ts | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/apps/web/src/lib/__tests__/json-schema-generator.test.ts b/apps/web/src/lib/__tests__/json-schema-generator.test.ts index b4e947c5..f0273c44 100644 --- a/apps/web/src/lib/__tests__/json-schema-generator.test.ts +++ b/apps/web/src/lib/__tests__/json-schema-generator.test.ts @@ -1,4 +1,4 @@ -import { detectFormat, inferSchema, mergeNodes, toJsonSchemaDocument } from '../json-schema-generator'; +import { collectFormats, detectFormat, inferSchema, mergeNodes, toJsonSchemaDocument } from '../json-schema-generator'; describe('detectFormat', () => { it('detects each format', () => { @@ -61,3 +61,19 @@ describe('JSON Schema format output', () => { expect(doc.properties.note).toEqual({ type: 'string' }); }); }); + +describe('collectFormats', () => { + it('counts formats across nested and array shapes', () => { + const node = inferSchema({ + id: '550e8400-e29b-41d4-a716-446655440000', + owner: { email: 'a@b.com' }, + contacts: [{ email: 'c@d.com' }, { email: 'e@f.com' }], + created: '2026-07-02T00:00:00Z', + }); + const m = collectFormats(node); + expect(m.get('uuid')).toBe(1); + expect(m.get('email')).toBe(2); + expect(m.get('date-time')).toBe(1); + expect(m.has('date')).toBe(false); + }); +}); diff --git a/apps/web/src/lib/json-schema-generator.ts b/apps/web/src/lib/json-schema-generator.ts index 69c4b730..3fef4d3f 100644 --- a/apps/web/src/lib/json-schema-generator.ts +++ b/apps/web/src/lib/json-schema-generator.ts @@ -187,6 +187,28 @@ export function inferSchema(value: unknown): SchemaNode { return { kind: 'scalar', t: 'null' }; } +export function collectFormats(node: SchemaNode): Map { + const counts = new Map(); + const walk = (n: SchemaNode): void => { + switch (n.kind) { + case 'scalar': + if (n.format) counts.set(n.format, (counts.get(n.format) ?? 0) + 1); + return; + case 'array': + walk(n.item); + return; + case 'object': + for (const { node: child } of Object.values(n.fields)) walk(child); + return; + case 'union': + n.variants.forEach(walk); + return; + } + }; + walk(node); + return counts; +} + function jsonSchemaTypeFragment(node: SchemaNode): Record { switch (node.kind) { case 'scalar': { From 11e5326075f138a20b05848ff767cc886d234dc0 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Thu, 2 Jul 2026 23:38:08 +0530 Subject: [PATCH 05/10] feat(json-schema): map formats to Python stdlib types with imports --- .../__tests__/json-schema-generator.test.ts | 23 +++++++++++++++- apps/web/src/lib/json-schema-generator.ts | 27 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/apps/web/src/lib/__tests__/json-schema-generator.test.ts b/apps/web/src/lib/__tests__/json-schema-generator.test.ts index f0273c44..454ccd66 100644 --- a/apps/web/src/lib/__tests__/json-schema-generator.test.ts +++ b/apps/web/src/lib/__tests__/json-schema-generator.test.ts @@ -1,4 +1,4 @@ -import { collectFormats, detectFormat, inferSchema, mergeNodes, toJsonSchemaDocument } from '../json-schema-generator'; +import { collectFormats, detectFormat, generateFromSchema, inferSchema, mergeNodes, toJsonSchemaDocument } from '../json-schema-generator'; describe('detectFormat', () => { it('detects each format', () => { @@ -77,3 +77,24 @@ describe('collectFormats', () => { expect(m.has('date')).toBe(false); }); }); + +describe('Python format mapping', () => { + const node = inferSchema({ + created: '2026-07-02T00:00:00Z', + id: '550e8400-e29b-41d4-a716-446655440000', + email: 'a@b.com', + }); + const py = generateFromSchema(node, 'python'); + + it('maps date-time and uuid to stdlib types', () => { + expect(py).toContain('created: datetime'); + expect(py).toContain('id: UUID'); + }); + it('leaves email as str', () => { + expect(py).toContain('email: str'); + }); + it('adds the needed imports', () => { + expect(py).toContain('from datetime import datetime'); + expect(py).toContain('from uuid import UUID'); + }); +}); diff --git a/apps/web/src/lib/json-schema-generator.ts b/apps/web/src/lib/json-schema-generator.ts index 3fef4d3f..4d78deba 100644 --- a/apps/web/src/lib/json-schema-generator.ts +++ b/apps/web/src/lib/json-schema-generator.ts @@ -329,6 +329,13 @@ function csharpSafeProp(jsonKey: string): string { return goFieldName(jsonKey); } +const PY_FORMAT_TYPES: Partial> = { + 'date-time': { type: 'datetime', module: 'datetime' }, + date: { type: 'date', module: 'datetime' }, + time: { type: 'time', module: 'datetime' }, + uuid: { type: 'UUID', module: 'uuid' }, +}; + function scalarToPython(node: SchemaNode): string | null { if (node.kind !== 'scalar') return null; switch (node.t) { @@ -369,6 +376,10 @@ function emitPythonType( path: string[], defs: Map ): string { + if (node.kind === 'scalar' && node.t === 'string' && node.format) { + const m = PY_FORMAT_TYPES[node.format]; + if (m) return m.type; + } const scalar = scalarToPython(node); if (scalar !== null) { if (node.kind === 'scalar' && node.t === 'null') return 'None'; @@ -431,8 +442,22 @@ export function generatePython(node: SchemaNode): string { .sort(([a], [b]) => a.localeCompare(b)) .map(([, code]) => code) .join('\n\n\n'); - const header = `from __future__ import annotations + // Collect stdlib imports for mapped formats actually present. + const formats = collectFormats(node); + const byModule = new Map>(); + for (const fmt of formats.keys()) { + const m = PY_FORMAT_TYPES[fmt]; + if (!m) continue; + if (!byModule.has(m.module)) byModule.set(m.module, new Set()); + byModule.get(m.module)!.add(m.type); + } + const stdlibImports = [...byModule.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([mod, names]) => `from ${mod} import ${[...names].sort().join(', ')}`) + .join('\n'); + const header = `from __future__ import annotations +${stdlibImports ? '\n' + stdlibImports + '\n' : ''} from typing import Any from pydantic import BaseModel, Field From 16cd06a7435a9cce25b8a941c27aec0298dd14e3 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Thu, 2 Jul 2026 23:45:38 +0530 Subject: [PATCH 06/10] feat(json-schema): map formats to Go/Java/C#/Dart/Swift stdlib types Co-Authored-By: Claude Opus 4.8 (1M context) --- .../__tests__/json-schema-generator.test.ts | 51 ++++++++++++++++ apps/web/src/lib/json-schema-generator.ts | 58 +++++++++++++++++-- 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/apps/web/src/lib/__tests__/json-schema-generator.test.ts b/apps/web/src/lib/__tests__/json-schema-generator.test.ts index 454ccd66..3c0284a4 100644 --- a/apps/web/src/lib/__tests__/json-schema-generator.test.ts +++ b/apps/web/src/lib/__tests__/json-schema-generator.test.ts @@ -98,3 +98,54 @@ describe('Python format mapping', () => { expect(py).toContain('from uuid import UUID'); }); }); + +describe('format mapping across languages', () => { + const node = inferSchema({ + created: '2026-07-02T00:00:00Z', + day: '2026-07-02', + id: '550e8400-e29b-41d4-a716-446655440000', + email: 'a@b.com', + }); + + it('Go maps date-time to time.Time and imports time', () => { + const go = generateFromSchema(node, 'go'); + expect(go).toMatch(/Created\s+time\.Time/); + expect(go).toContain('"time"'); + expect(go).toMatch(/Email\s+string/); + }); + + it('Java maps to java.time and UUID', () => { + const java = generateFromSchema(node, 'java'); + expect(java).toContain('OffsetDateTime'); + expect(java).toContain('LocalDate'); + expect(java).toContain('UUID'); + expect(java).toContain('import java.time.*;'); + }); + + it('C# maps to BCL date/guid types', () => { + const cs = generateFromSchema(node, 'csharp'); + expect(cs).toContain('DateTimeOffset'); + expect(cs).toContain('DateOnly'); + expect(cs).toContain('Guid'); + expect(cs).toContain('using System;'); + }); + + it('Dart maps date-time to DateTime and parses it', () => { + const dart = generateFromSchema(node, 'dart'); + expect(dart).toContain('DateTime created'); + expect(dart).toContain('DateTime.parse('); + }); + + it('Swift maps date-time to Date and uuid to UUID', () => { + const sw = generateFromSchema(node, 'swift'); + expect(sw).toContain('created: Date'); + expect(sw).toContain('id: UUID'); + expect(sw).toContain('.iso8601'); + }); + + it('TypeScript leaves formats as string', () => { + const ts = generateFromSchema(node, 'typescript'); + expect(ts).toMatch(/created: string/); + expect(ts).toMatch(/id: string/); + }); +}); diff --git a/apps/web/src/lib/json-schema-generator.ts b/apps/web/src/lib/json-schema-generator.ts index 4d78deba..2c26f879 100644 --- a/apps/web/src/lib/json-schema-generator.ts +++ b/apps/web/src/lib/json-schema-generator.ts @@ -541,6 +541,9 @@ export function generateGo(node: SchemaNode): string { typeStr: optional ? `*${t}` : t, extraDeps: [] as string[], }); + if (n.kind === 'scalar' && n.t === 'string' && n.format === 'date-time') { + return { typeStr: optional ? '*time.Time' : 'time.Time', extraDeps: [] }; + } if (n.kind === 'scalar') { switch (n.t) { case 'string': @@ -601,10 +604,14 @@ export function generateGo(node: SchemaNode): string { walk(node, []); + const importBlock = collectFormats(node).has('date-time') + ? 'import (\n\t"encoding/json"\n\t"time"\n)' + : 'import "encoding/json"'; + if (node.kind !== 'object') { return `package main -import "encoding/json" +${importBlock} // Root wraps non-object JSON root. type Root = json.RawMessage @@ -613,7 +620,7 @@ type Root = json.RawMessage return `package main -import "encoding/json" +${importBlock} ${structs.join('\n\n')} `; @@ -695,6 +702,16 @@ ${structs.join('\n\n')} export function generateJava(node: SchemaNode): string { function jType(n: SchemaNode, path: string[], optional: boolean): string { const wrap = (s: string) => (optional ? `Optional<${s}>` : s); + if (n.kind === 'scalar' && n.t === 'string' && n.format) { + const map: Partial> = { + 'date-time': 'OffsetDateTime', + date: 'LocalDate', + time: 'LocalTime', + uuid: 'UUID', + }; + const t = map[n.format]; + if (t) return wrap(t); + } if (n.kind === 'scalar') { switch (n.t) { case 'string': @@ -760,9 +777,12 @@ export function generateJava(node: SchemaNode): string { `; } + const needsTime = ['date-time', 'date', 'time'].some((f) => + collectFormats(node).has(f as StringFormat) + ); return `import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; -import java.util.*; +${needsTime ? 'import java.time.*;\n' : ''}import java.util.*; ${classes.join('\n\n')} `; @@ -771,6 +791,16 @@ ${classes.join('\n\n')} export function generateCSharp(node: SchemaNode): string { function csType(n: SchemaNode, path: string[], optional: boolean): string { const wrap = (s: string) => (optional ? `${s}?` : s); + if (n.kind === 'scalar' && n.t === 'string' && n.format) { + const map: Partial> = { + 'date-time': 'DateTimeOffset', + date: 'DateOnly', + time: 'TimeOnly', + uuid: 'Guid', + }; + const t = map[n.format]; + if (t) return wrap(t); + } if (n.kind === 'scalar') { switch (n.t) { case 'string': @@ -838,7 +868,10 @@ public record Root(JsonElement Value); `; } - return `using System.Collections.Generic; + const needsSystem = ['date-time', 'date', 'time', 'uuid'].some((f) => + collectFormats(node).has(f as StringFormat) + ); + return `${needsSystem ? 'using System;\n' : ''}using System.Collections.Generic; using System.Text.Json.Serialization; ${classes.join('\n\n')} @@ -847,6 +880,9 @@ ${classes.join('\n\n')} export function generateDart(node: SchemaNode): string { function dType(n: SchemaNode, path: string[]): string { + if (n.kind === 'scalar' && n.t === 'string' && n.format === 'date-time') { + return 'DateTime'; + } if (n.kind === 'scalar') { switch (n.t) { case 'string': @@ -880,6 +916,9 @@ export function generateDart(node: SchemaNode): string { jsonAccess: string ): string { if (child.kind === 'scalar') { + if (child.t === 'string' && child.format === 'date-time') { + return `DateTime.parse(${jsonAccess} as String)`; + } switch (child.t) { case 'string': return `${jsonAccess} as String`; @@ -972,6 +1011,10 @@ ${fromJsonLines.join('\n')} export function generateSwift(node: SchemaNode): string { function swType(n: SchemaNode, path: string[], optional: boolean): string { const opt = optional ? '?' : ''; + if (n.kind === 'scalar' && n.t === 'string' && n.format) { + if (n.format === 'date-time') return `Date${opt}`; + if (n.format === 'uuid') return `UUID${opt}`; + } if (n.kind === 'scalar') { switch (n.t) { case 'string': @@ -1038,9 +1081,14 @@ ${props.join('\n')} return `// Non-object JSON root: decode as Array, Dictionary, or a scalar using JSONSerialization.\n`; } + const hasDate = collectFormats(node).has('date-time'); return `import Foundation -// Note: fields inferred as mixed or null-only types use String as a placeholder — adjust before production use. +// Note: fields inferred as mixed or null-only types use String as a placeholder — adjust before production use.${ + hasDate + ? '\n// Date fields expect ISO 8601 — set JSONDecoder().dateDecodingStrategy = .iso8601.' + : '' + } ${structs.join('\n\n')} `; From ded397843c96f19604dca5bb74667fd5b8c05188 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Thu, 2 Jul 2026 23:52:55 +0530 Subject: [PATCH 07/10] feat(json-schema): show detected-format badges --- apps/web/messages/af.json | 4 +++- apps/web/messages/ar.json | 4 +++- apps/web/messages/ca.json | 4 +++- apps/web/messages/cs.json | 4 +++- apps/web/messages/da.json | 4 +++- apps/web/messages/de.json | 4 +++- apps/web/messages/el.json | 4 +++- apps/web/messages/en.json | 4 +++- apps/web/messages/es.json | 4 +++- apps/web/messages/fa.json | 4 +++- apps/web/messages/fr.json | 4 +++- apps/web/messages/id.json | 4 +++- apps/web/messages/it.json | 4 +++- apps/web/messages/ja.json | 4 +++- apps/web/messages/ko.json | 4 +++- apps/web/messages/ms.json | 4 +++- apps/web/messages/nb.json | 4 +++- apps/web/messages/nl.json | 4 +++- apps/web/messages/pl.json | 4 +++- apps/web/messages/pt-BR.json | 4 +++- apps/web/messages/pt.json | 4 +++- apps/web/messages/ru.json | 4 +++- apps/web/messages/sv.json | 4 +++- apps/web/messages/tr.json | 4 +++- apps/web/messages/uk.json | 4 +++- apps/web/messages/vi.json | 4 +++- apps/web/messages/zh.json | 4 +++- .../json-schema-generator-layout.tsx | 24 +++++++++++++++---- 28 files changed, 101 insertions(+), 31 deletions(-) diff --git a/apps/web/messages/af.json b/apps/web/messages/af.json index 2ccc468a..9c959f17 100644 --- a/apps/web/messages/af.json +++ b/apps/web/messages/af.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Laai redigeerder…" + "editorLoading": "Laai redigeerder…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Beeld na Base64", diff --git a/apps/web/messages/ar.json b/apps/web/messages/ar.json index c393e28f..decdef0c 100644 --- a/apps/web/messages/ar.json +++ b/apps/web/messages/ar.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "جاري تحميل المحرر…" + "editorLoading": "جاري تحميل المحرر…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "الصورة إلى Base64", diff --git a/apps/web/messages/ca.json b/apps/web/messages/ca.json index 63f39bd0..586f5fac 100644 --- a/apps/web/messages/ca.json +++ b/apps/web/messages/ca.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "S'està carregant l'editor…" + "editorLoading": "S'està carregant l'editor…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Imatge a Base64", diff --git a/apps/web/messages/cs.json b/apps/web/messages/cs.json index 66cb138d..a2ccf526 100644 --- a/apps/web/messages/cs.json +++ b/apps/web/messages/cs.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Načítání editoru…" + "editorLoading": "Načítání editoru…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Obrázek do Base64", diff --git a/apps/web/messages/da.json b/apps/web/messages/da.json index 7daba98f..a13ce452 100644 --- a/apps/web/messages/da.json +++ b/apps/web/messages/da.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Indlæser editor…" + "editorLoading": "Indlæser editor…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Billede til Base64", diff --git a/apps/web/messages/de.json b/apps/web/messages/de.json index 14b3b134..cd2f6eb5 100644 --- a/apps/web/messages/de.json +++ b/apps/web/messages/de.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Editor wird geladen…" + "editorLoading": "Editor wird geladen…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Bild zu Base64", diff --git a/apps/web/messages/el.json b/apps/web/messages/el.json index 85a3770d..c6e02d8f 100644 --- a/apps/web/messages/el.json +++ b/apps/web/messages/el.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Φόρτωση επεξεργαστή…" + "editorLoading": "Φόρτωση επεξεργαστή…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Εικόνα στο Base64", diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index f6ec24fc..7dcbd1ed 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -2390,7 +2390,9 @@ "csharp": "C# (System.Text.Json)", "dart": "Dart", "swift": "Swift (Codable)" - } + }, + "detectedFormats": "Detected formats", + "download": "Download" }, "SnippetManager": { "title": "Code Snippet Manager", diff --git a/apps/web/messages/es.json b/apps/web/messages/es.json index 6419427b..269ee10a 100644 --- a/apps/web/messages/es.json +++ b/apps/web/messages/es.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Cargando el editor…" + "editorLoading": "Cargando el editor…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Imagen a Base64", diff --git a/apps/web/messages/fa.json b/apps/web/messages/fa.json index 7e5fc04d..31a26e9c 100644 --- a/apps/web/messages/fa.json +++ b/apps/web/messages/fa.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "در حال بارگذاری ویرایشگر…" + "editorLoading": "در حال بارگذاری ویرایشگر…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "تصویر به Base64", diff --git a/apps/web/messages/fr.json b/apps/web/messages/fr.json index 5dfce7b9..b9056dda 100644 --- a/apps/web/messages/fr.json +++ b/apps/web/messages/fr.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Chargement de l’éditeur…" + "editorLoading": "Chargement de l’éditeur…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Image vers Base64", diff --git a/apps/web/messages/id.json b/apps/web/messages/id.json index 84fe890c..eeeae3b1 100644 --- a/apps/web/messages/id.json +++ b/apps/web/messages/id.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Memuat editor…" + "editorLoading": "Memuat editor…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Gambar ke Base64", diff --git a/apps/web/messages/it.json b/apps/web/messages/it.json index 58949a38..a6c52cd8 100644 --- a/apps/web/messages/it.json +++ b/apps/web/messages/it.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Caricamento editor…" + "editorLoading": "Caricamento editor…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Immagine su Base64", diff --git a/apps/web/messages/ja.json b/apps/web/messages/ja.json index 611039d7..c4002348 100644 --- a/apps/web/messages/ja.json +++ b/apps/web/messages/ja.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "エディタを読み込み中…" + "editorLoading": "エディタを読み込み中…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "画像をBase64に変換", diff --git a/apps/web/messages/ko.json b/apps/web/messages/ko.json index 51dfe6f0..cb2eac20 100644 --- a/apps/web/messages/ko.json +++ b/apps/web/messages/ko.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "편집기 로드 중…" + "editorLoading": "편집기 로드 중…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "이미지를 Base64로", diff --git a/apps/web/messages/ms.json b/apps/web/messages/ms.json index 96e17af7..4a9c050f 100644 --- a/apps/web/messages/ms.json +++ b/apps/web/messages/ms.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Memuatkan editor…" + "editorLoading": "Memuatkan editor…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Imej ke Base64", diff --git a/apps/web/messages/nb.json b/apps/web/messages/nb.json index b80f182b..c7266c35 100644 --- a/apps/web/messages/nb.json +++ b/apps/web/messages/nb.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Laster inn redigeringsprogrammet…" + "editorLoading": "Laster inn redigeringsprogrammet…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Bilde til Base64", diff --git a/apps/web/messages/nl.json b/apps/web/messages/nl.json index 48cfabe3..e8acc436 100644 --- a/apps/web/messages/nl.json +++ b/apps/web/messages/nl.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Editor laden…" + "editorLoading": "Editor laden…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Afbeelding naar Base64", diff --git a/apps/web/messages/pl.json b/apps/web/messages/pl.json index 0a90fcf0..9e95804c 100644 --- a/apps/web/messages/pl.json +++ b/apps/web/messages/pl.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Ładowanie edytora…" + "editorLoading": "Ładowanie edytora…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Obraz do Base64", diff --git a/apps/web/messages/pt-BR.json b/apps/web/messages/pt-BR.json index 7747d903..171d2cb8 100644 --- a/apps/web/messages/pt-BR.json +++ b/apps/web/messages/pt-BR.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Carregando o editor…" + "editorLoading": "Carregando o editor…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Imagem para Base64", diff --git a/apps/web/messages/pt.json b/apps/web/messages/pt.json index 18dc98d8..42fc9f2a 100644 --- a/apps/web/messages/pt.json +++ b/apps/web/messages/pt.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "A carregar o editor…" + "editorLoading": "A carregar o editor…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Imagem para Base64", diff --git a/apps/web/messages/ru.json b/apps/web/messages/ru.json index 05ad664e..9fc7de7b 100644 --- a/apps/web/messages/ru.json +++ b/apps/web/messages/ru.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Загрузка редактора…" + "editorLoading": "Загрузка редактора…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Изображение в Base64", diff --git a/apps/web/messages/sv.json b/apps/web/messages/sv.json index 6ab85c0a..bc97807a 100644 --- a/apps/web/messages/sv.json +++ b/apps/web/messages/sv.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Läser in redigeraren…" + "editorLoading": "Läser in redigeraren…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Bild till Base64", diff --git a/apps/web/messages/tr.json b/apps/web/messages/tr.json index a12da0f9..62f7bc40 100644 --- a/apps/web/messages/tr.json +++ b/apps/web/messages/tr.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Editör yükleniyor…" + "editorLoading": "Editör yükleniyor…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Base64'e görüntü", diff --git a/apps/web/messages/uk.json b/apps/web/messages/uk.json index f26e72de..9b2b7fc0 100644 --- a/apps/web/messages/uk.json +++ b/apps/web/messages/uk.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Завантаження редактора…" + "editorLoading": "Завантаження редактора…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Зображення до Base64", diff --git a/apps/web/messages/vi.json b/apps/web/messages/vi.json index 8f08b722..02028ffb 100644 --- a/apps/web/messages/vi.json +++ b/apps/web/messages/vi.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "Đang tải trình soạn thảo…" + "editorLoading": "Đang tải trình soạn thảo…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "Hình ảnh tới Base64", diff --git a/apps/web/messages/zh.json b/apps/web/messages/zh.json index 75ac18e6..de2d5b00 100644 --- a/apps/web/messages/zh.json +++ b/apps/web/messages/zh.json @@ -2644,7 +2644,9 @@ "dart": "Dart", "swift": "Swift (Codable)" }, - "editorLoading": "正在加载编辑器…" + "editorLoading": "正在加载编辑器…", + "detectedFormats": "Detected formats", + "download": "Download" }, "ImageToBase64": { "title": "图像转 Base64", diff --git a/apps/web/src/components/json-schema-generator/json-schema-generator-layout.tsx b/apps/web/src/components/json-schema-generator/json-schema-generator-layout.tsx index cfdbbd54..37469a79 100644 --- a/apps/web/src/components/json-schema-generator/json-schema-generator-layout.tsx +++ b/apps/web/src/components/json-schema-generator/json-schema-generator-layout.tsx @@ -19,11 +19,14 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { cn } from '@/lib/utils'; import { highlightSchemaOutput } from '@/lib/hljs-json-schema-output'; import { + collectFormats, generateFromSchema, inferSchema, OUTPUT_LANGUAGE_ORDER, type OutputLanguage, + type StringFormat, } from '@/lib/json-schema-generator'; +import { Badge } from '@/components/ui/badge'; import './json-schema-highlighter.css'; import { JsonSchemaInputEditor } from './json-schema-input-editor'; @@ -46,10 +49,10 @@ export function JsonSchemaGeneratorLayout() { const [language, setLanguage] = useState('python'); const { isCopied: copied, copyToClipboard } = useCopyToClipboard(); - const { output, error } = useMemo(() => { + const { output, error, formats } = useMemo(() => { const trimmed = input.trim(); if (!trimmed) { - return { output: '', error: null as string | null }; + return { output: '', error: null as string | null, formats: [] as [StringFormat, number][] }; } try { const parsed: unknown = JSON.parse(trimmed); @@ -57,10 +60,11 @@ export function JsonSchemaGeneratorLayout() { return { output: generateFromSchema(schema, language), error: null as string | null, + formats: [...collectFormats(schema).entries()], }; } catch (e) { const msg = e instanceof Error ? e.message : String(e); - return { output: '', error: msg }; + return { output: '', error: msg, formats: [] as [StringFormat, number][] }; } }, [input, language]); @@ -176,10 +180,22 @@ export function JsonSchemaGeneratorLayout() { error && 'border-destructive/40' )} > -
+
+ {!error && formats.length > 0 && ( +
+ + {t('detectedFormats')} + + {formats.map(([fmt, count]) => ( + + {fmt}{count > 1 ? ` ×${count}` : ''} + + ))} +
+ )}
{error ? ( From 0f77cdde0dc4f5ea56f5c39511921c4859f3c155 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Thu, 2 Jul 2026 23:57:25 +0530 Subject: [PATCH 08/10] feat(json-schema): add download button for generated output --- .../json-schema-generator-layout.tsx | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/json-schema-generator/json-schema-generator-layout.tsx b/apps/web/src/components/json-schema-generator/json-schema-generator-layout.tsx index 37469a79..7f87edad 100644 --- a/apps/web/src/components/json-schema-generator/json-schema-generator-layout.tsx +++ b/apps/web/src/components/json-schema-generator/json-schema-generator-layout.tsx @@ -4,7 +4,7 @@ import { useCallback, useMemo, useState } from 'react'; import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'; import { useIsMobile } from '@/components/hooks/use-mobile'; import { useTranslations } from 'next-intl'; -import { AlertCircle, Check, Copy, FileJson, Trash2 } from 'lucide-react'; +import { AlertCircle, Check, Copy, Download, FileJson, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; @@ -17,6 +17,7 @@ import { } from '@/components/ui/select'; import { ScrollArea } from '@/components/ui/scroll-area'; import { cn } from '@/lib/utils'; +import { triggerDownload } from '@/lib/csv-excel-json-utils'; import { highlightSchemaOutput } from '@/lib/hljs-json-schema-output'; import { collectFormats, @@ -41,6 +42,18 @@ const defaultSample = `{ "scores": [9.5, 8.0] }`; +const FILE_EXT: Record = { + jsonSchema: 'json', + python: 'py', + typescript: 'ts', + go: 'go', + rust: 'rs', + java: 'java', + csharp: 'cs', + dart: 'dart', + swift: 'swift', +}; + export function JsonSchemaGeneratorLayout() { const t = useTranslations('JsonSchemaGenerator'); const isMobile = useIsMobile(); @@ -81,6 +94,14 @@ export function JsonSchemaGeneratorLayout() { }); }, [output, t, copyToClipboard]); + const handleDownload = useCallback(() => { + if (!output || error) return; + const ext = FILE_EXT[language]; + const filename = language === 'jsonSchema' ? 'schema.json' : `generated.${ext}`; + const mime = language === 'jsonSchema' ? 'application/json' : 'text/plain'; + triggerDownload(new Blob([output], { type: `${mime};charset=utf-8` }), filename); + }, [output, error, language]); + const handleClear = () => { setInput(''); }; @@ -133,6 +154,15 @@ export function JsonSchemaGeneratorLayout() { )} {t('copy')} +
From 26a7eb4769a1d136c4ea509a0f3af51d1454c845 Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Fri, 3 Jul 2026 00:07:33 +0530 Subject: [PATCH 09/10] fix(json-schema): parse date-time in Dart arrays; drop unused Go time import --- .../__tests__/json-schema-generator.test.ts | 21 +++++++++++++++++++ apps/web/src/lib/json-schema-generator.ts | 10 ++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/apps/web/src/lib/__tests__/json-schema-generator.test.ts b/apps/web/src/lib/__tests__/json-schema-generator.test.ts index 3c0284a4..fc9b311b 100644 --- a/apps/web/src/lib/__tests__/json-schema-generator.test.ts +++ b/apps/web/src/lib/__tests__/json-schema-generator.test.ts @@ -149,3 +149,24 @@ describe('format mapping across languages', () => { expect(ts).toMatch(/id: string/); }); }); + +describe('format mapping in arrays and non-object roots', () => { + it('Dart parses date-time inside an array (no raw cast)', () => { + const dart = generateFromSchema(inferSchema({ days: ['2026-07-02T00:00:00Z'] }), 'dart'); + expect(dart).toContain('List'); + expect(dart).toContain('DateTime.parse(e as String)'); + expect(dart).not.toContain('e as DateTime'); + }); + + it('Go non-object root with a date-time does not import unused time', () => { + const go = generateFromSchema(inferSchema(['2026-07-02T00:00:00Z']), 'go'); + expect(go).toContain('type Root = json.RawMessage'); + expect(go).not.toContain('"time"'); + }); + + it('Go object root with a date-time still imports time', () => { + const go = generateFromSchema(inferSchema({ created: '2026-07-02T00:00:00Z' }), 'go'); + expect(go).toContain('"time"'); + expect(go).toMatch(/Created\s+time\.Time/); + }); +}); diff --git a/apps/web/src/lib/json-schema-generator.ts b/apps/web/src/lib/json-schema-generator.ts index 2c26f879..4631f57b 100644 --- a/apps/web/src/lib/json-schema-generator.ts +++ b/apps/web/src/lib/json-schema-generator.ts @@ -604,9 +604,10 @@ export function generateGo(node: SchemaNode): string { walk(node, []); - const importBlock = collectFormats(node).has('date-time') - ? 'import (\n\t"encoding/json"\n\t"time"\n)' - : 'import "encoding/json"'; + const importBlock = + node.kind === 'object' && collectFormats(node).has('date-time') + ? 'import (\n\t"encoding/json"\n\t"time"\n)' + : 'import "encoding/json"'; if (node.kind !== 'object') { return `package main @@ -940,8 +941,7 @@ export function generateDart(node: SchemaNode): string { const itemName = typeNameForPath(itemPath); return `(${jsonAccess} as List).map((e) => ${itemName}.fromJson(e as Map)).toList()`; } - const inner = dType(item, itemPath); - return `(${jsonAccess} as List).map((e) => e as ${inner}).toList()`; + return `(${jsonAccess} as List).map((e) => ${dartParseExpr(item, itemPath, 'e')}).toList()`; } if (child.kind === 'object') { const nm = typeNameForPath(fieldPath); From 7acdb11aa4e7cbf781b7d38b0e83a04796df4a0f Mon Sep 17 00:00:00 2001 From: itsmeakhil Date: Fri, 3 Jul 2026 00:14:31 +0530 Subject: [PATCH 10/10] Update environment-set-list.tsx --- .../environment-set-list.tsx | 55 ++++++++++++++----- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/apps/web/src/components/environment-manager/environment-set-list.tsx b/apps/web/src/components/environment-manager/environment-set-list.tsx index f3dbfb36..81724036 100644 --- a/apps/web/src/components/environment-manager/environment-set-list.tsx +++ b/apps/web/src/components/environment-manager/environment-set-list.tsx @@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" -import { Search, Copy, Pencil, Trash2, FileCode2, Eye, ArrowUpDown } from "lucide-react" +import { Search, Copy, Pencil, Trash2, FileCode2, Eye, ArrowUpDown, Check } from "lucide-react" import { toast } from "sonner" import { deleteEnvSetEntry } from "@/lib/environment-manager-api" import { formatDotEnv } from "@/lib/environment-manager-utils" @@ -60,6 +60,15 @@ import { ScrollArea } from "@/components/ui/scroll-area" type SortOrder = "updatedDesc" | "updatedAsc" | "nameAsc" +// ponytail: heuristic on free-text environment field, not an enum — false negatives just fall back to neutral styling +function envAccentClass(environment: string) { + const e = environment.toLowerCase() + if (/prod/.test(e)) return "border-l-destructive" + if (/stag|preprod|uat/.test(e)) return "border-l-amber-500" + if (/dev|local|test|qa/.test(e)) return "border-l-emerald-500" + return "border-l-border" +} + export function EnvironmentSetList() { const t = useTranslations("EnvironmentManager.list") const tToast = useTranslations("EnvironmentManager.toasts") @@ -166,9 +175,12 @@ export function EnvironmentSetList() { - setSortOrder("updatedDesc")}>{t("sort.updatedDesc")} - setSortOrder("updatedAsc")}>{t("sort.updatedAsc")} - setSortOrder("nameAsc")}>{t("sort.nameAsc")} + {(["updatedDesc", "updatedAsc", "nameAsc"] as const).map((order) => ( + setSortOrder(order)} className="justify-between gap-2"> + {t(`sort.${order}`)} + {sortOrder === order && } + + ))} @@ -191,7 +203,13 @@ export function EnvironmentSetList() {
{filtered.map((entry) => ( - + {entry.project} @@ -207,16 +225,19 @@ export function EnvironmentSetList() { {entry.tags.length > 0 && (
- {entry.tags.map((tag) => ( - setTagFilter(tag)} - > - {tag} - - ))} + {entry.tags.map((tag) => { + const active = tagFilter.toLowerCase().trim() === tag.toLowerCase() + return ( + setTagFilter(active ? "" : tag)} + > + {tag} + + ) + })}
)}

@@ -235,6 +256,7 @@ export function EnvironmentSetList() { variant="outline" size="icon" className="h-8 w-8" + aria-label={t("copyDotEnv")} onClick={() => copyToClipboard( formatDotEnv(entry.variables), @@ -254,6 +276,7 @@ export function EnvironmentSetList() { variant="outline" size="icon" className="h-8 w-8" + aria-label={t("view")} onClick={() => setViewingEntry(entry)} > @@ -268,6 +291,7 @@ export function EnvironmentSetList() { variant="ghost" size="icon" className="h-8 w-8" + aria-label={t("edit")} onClick={() => { setEditing(entry) setEditOpen(true) @@ -285,6 +309,7 @@ export function EnvironmentSetList() { variant="ghost" size="icon" className="h-8 w-8 text-destructive hover:text-destructive ml-auto" + aria-label={t("delete")} onClick={() => setDeleteEntry(entry)} >