From 5f2963b49550db7b822b00e648f2ea47695371a2 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Fri, 22 May 2026 22:13:12 +0200 Subject: [PATCH 01/21] spec1: type system --- .../Settings/SettingDefinition.cs | 12 +++++ .../SimpleModule.Core/Settings/SettingType.cs | 7 +++ modules/Core/src/SimpleModule.Core/types.ts | 19 +++++++ .../SimpleModule.Settings/SettingsModule.cs | 50 +++++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 modules/Core/src/SimpleModule.Core/types.ts diff --git a/framework/SimpleModule.Core/Settings/SettingDefinition.cs b/framework/SimpleModule.Core/Settings/SettingDefinition.cs index 62046dde..67c690cb 100644 --- a/framework/SimpleModule.Core/Settings/SettingDefinition.cs +++ b/framework/SimpleModule.Core/Settings/SettingDefinition.cs @@ -1,5 +1,9 @@ +using System.Collections.Generic; +using SimpleModule.Core; + namespace SimpleModule.Core.Settings; +[Dto] public class SettingDefinition { public string Key { get; set; } = string.Empty; @@ -9,4 +13,12 @@ public class SettingDefinition public SettingScope Scope { get; set; } public string? DefaultValue { get; set; } public SettingType Type { get; set; } + public IReadOnlyList? AllowedValues { get; set; } + public double? Min { get; set; } + public double? Max { get; set; } + public string? Pattern { get; set; } + public bool Required { get; set; } + public bool Sensitive { get; set; } + public int Order { get; set; } + public string? Placeholder { get; set; } } diff --git a/framework/SimpleModule.Core/Settings/SettingType.cs b/framework/SimpleModule.Core/Settings/SettingType.cs index 8f6e29bb..d3722d85 100644 --- a/framework/SimpleModule.Core/Settings/SettingType.cs +++ b/framework/SimpleModule.Core/Settings/SettingType.cs @@ -6,4 +6,11 @@ public enum SettingType Number = 1, Bool = 2, Json = 3, + Select = 4, + Color = 5, + Url = 6, + Email = 7, + Password = 8, + MultilineText = 9, + DateTime = 10, } diff --git a/modules/Core/src/SimpleModule.Core/types.ts b/modules/Core/src/SimpleModule.Core/types.ts new file mode 100644 index 00000000..8e9fb0a0 --- /dev/null +++ b/modules/Core/src/SimpleModule.Core/types.ts @@ -0,0 +1,19 @@ +// Auto-generated from [Dto] types — do not edit +export interface SettingDefinition { + key: string; + displayName: string; + description: string; + group: string; + scope: any; + defaultValue: string; + type: any; + allowedValues: string[]; + min: number | null; + max: number | null; + pattern: string; + required: boolean; + sensitive: boolean; + order: number; + placeholder: string; +} + diff --git a/modules/Settings/src/SimpleModule.Settings/SettingsModule.cs b/modules/Settings/src/SimpleModule.Settings/SettingsModule.cs index 11e14e73..f04f20c8 100644 --- a/modules/Settings/src/SimpleModule.Settings/SettingsModule.cs +++ b/modules/Settings/src/SimpleModule.Settings/SettingsModule.cs @@ -117,6 +117,56 @@ public void ConfigureSettings(ISettingsBuilder settings) DefaultValue = "true", Type = SettingType.Bool, } + ) + .Add( + new SettingDefinition + { + Key = "app.primary_color", + DisplayName = "Primary Color", + Description = "Brand color used throughout the application", + Group = "Appearance", + Scope = SettingScope.Application, + DefaultValue = "\"#3b82f6\"", + Type = SettingType.Color, + } + ) + .Add( + new SettingDefinition + { + Key = "app.support_email", + DisplayName = "Support Email", + Description = "Email address displayed on support pages", + Group = "General", + Scope = SettingScope.Application, + Type = SettingType.Email, + Pattern = @"^[^@\s]+@[^@\s]+\.[^@\s]+$", + Placeholder = "support@example.com", + } + ) + .Add( + new SettingDefinition + { + Key = "app.welcome_message", + DisplayName = "Welcome Message", + Description = "Message shown to users on the dashboard", + Group = "General", + Scope = SettingScope.Application, + Type = SettingType.MultilineText, + Placeholder = "Welcome to the application!", + } + ) + .Add( + new SettingDefinition + { + Key = "user.preferred_density", + DisplayName = "Display Density", + Description = "Controls spacing and size of UI elements", + Group = "Appearance", + Scope = SettingScope.User, + DefaultValue = "\"comfortable\"", + Type = SettingType.Select, + AllowedValues = ["compact", "comfortable", "spacious"], + } ); } } From d7d564664705fbe51d92a4a97e1a52fc7b4fe6e8 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Fri, 22 May 2026 22:14:37 +0200 Subject: [PATCH 02/21] spec3: field components Rewrite SettingField.tsx as thin dispatcher and add per-type field components under components/fields/ with clean decoded-value contract (no JSON encode/decode). --- .../components/SettingField.tsx | 186 ++++-------------- .../components/fields/BoolField.tsx | 33 ++++ .../components/fields/ColorField.tsx | 96 +++++++++ .../components/fields/DateTimeField.tsx | 110 +++++++++++ .../components/fields/EmailField.tsx | 86 ++++++++ .../components/fields/JsonField.tsx | 112 +++++++++++ .../components/fields/MultilineTextField.tsx | 83 ++++++++ .../components/fields/NumberField.tsx | 111 +++++++++++ .../components/fields/PasswordField.tsx | 129 ++++++++++++ .../components/fields/SelectField.tsx | 38 ++++ .../components/fields/TextField.tsx | 91 +++++++++ .../components/fields/UrlField.tsx | 105 ++++++++++ .../components/fields/common.ts | 68 +++++++ .../components/fields/index.ts | 12 ++ .../components/fields/types.ts | 41 ++++ 15 files changed, 1155 insertions(+), 146 deletions(-) create mode 100644 modules/Settings/src/SimpleModule.Settings/components/fields/BoolField.tsx create mode 100644 modules/Settings/src/SimpleModule.Settings/components/fields/ColorField.tsx create mode 100644 modules/Settings/src/SimpleModule.Settings/components/fields/DateTimeField.tsx create mode 100644 modules/Settings/src/SimpleModule.Settings/components/fields/EmailField.tsx create mode 100644 modules/Settings/src/SimpleModule.Settings/components/fields/JsonField.tsx create mode 100644 modules/Settings/src/SimpleModule.Settings/components/fields/MultilineTextField.tsx create mode 100644 modules/Settings/src/SimpleModule.Settings/components/fields/NumberField.tsx create mode 100644 modules/Settings/src/SimpleModule.Settings/components/fields/PasswordField.tsx create mode 100644 modules/Settings/src/SimpleModule.Settings/components/fields/SelectField.tsx create mode 100644 modules/Settings/src/SimpleModule.Settings/components/fields/TextField.tsx create mode 100644 modules/Settings/src/SimpleModule.Settings/components/fields/UrlField.tsx create mode 100644 modules/Settings/src/SimpleModule.Settings/components/fields/common.ts create mode 100644 modules/Settings/src/SimpleModule.Settings/components/fields/index.ts create mode 100644 modules/Settings/src/SimpleModule.Settings/components/fields/types.ts diff --git a/modules/Settings/src/SimpleModule.Settings/components/SettingField.tsx b/modules/Settings/src/SimpleModule.Settings/components/SettingField.tsx index ea4e0ae2..fea56be1 100644 --- a/modules/Settings/src/SimpleModule.Settings/components/SettingField.tsx +++ b/modules/Settings/src/SimpleModule.Settings/components/SettingField.tsx @@ -1,156 +1,50 @@ -import { Button, Input, Switch, Textarea } from '@simplemodule/ui'; -import { useMemo, useState } from 'react'; - -const SettingTypes = { - Text: 0, - Number: 1, - Bool: 2, - Json: 3, -} as const; - -type SettingType = (typeof SettingTypes)[keyof typeof SettingTypes]; - -interface SettingDefinition { - key: string; - displayName: string; - description?: string; - group?: string; - scope: number; - defaultValue?: string; - type: SettingType; -} - -interface SettingFieldProps { +import type React from 'react'; +import type { SettingDefinition } from './fields'; +import { + BoolField, + ColorField, + DateTimeField, + EmailField, + JsonField, + MultilineTextField, + NumberField, + PasswordField, + SelectField, + TextField, + UrlField, +} from './fields'; + +export interface SettingFieldProps { definition: SettingDefinition; - currentValue?: string | null; - onSave: (key: string, value: string, scope: number) => Promise; -} - -function decodeForDisplay(stored: string, type: SettingType): string { - if (stored === '') return ''; - try { - const parsed = JSON.parse(stored); - switch (type) { - case SettingTypes.Text: - return typeof parsed === 'string' ? parsed : stored; - case SettingTypes.Number: - return typeof parsed === 'number' ? String(parsed) : stored; - case SettingTypes.Bool: - return typeof parsed === 'boolean' ? String(parsed) : stored; - case SettingTypes.Json: - return JSON.stringify(parsed, null, 2); - default: - return stored; - } - } catch { - return stored; - } + value: unknown; + onSave: (value: unknown) => Promise; + onDirty?: (isDirty: boolean) => void; + disabled?: boolean; + autoFocus?: boolean; } -function encodeForStorage(input: string, type: SettingType): string { - switch (type) { - case SettingTypes.Text: - return JSON.stringify(input); - case SettingTypes.Number: { - const num = Number(input); - return Number.isFinite(num) && input.trim() !== '' ? String(num) : JSON.stringify(input); - } - case SettingTypes.Bool: - return input === 'true' ? 'true' : 'false'; - case SettingTypes.Json: - return input; - default: - return input; - } -} - -export default function SettingField({ definition, currentValue, onSave }: SettingFieldProps) { - const storedRaw = currentValue ?? definition.defaultValue ?? ''; - const initial = useMemo( - () => decodeForDisplay(storedRaw, definition.type), - [storedRaw, definition.type], - ); - const [value, setValue] = useState(initial); - const [saving, setSaving] = useState(false); - const [jsonError, setJsonError] = useState(null); - const hasChanged = value !== initial; - - const performSave = async (rawInput: string) => { - if (definition.type === SettingTypes.Json) { - try { - JSON.parse(rawInput); - setJsonError(null); - } catch (err) { - setJsonError(err instanceof Error ? err.message : 'Invalid JSON'); - return; - } - } - setSaving(true); - try { - await onSave(definition.key, encodeForStorage(rawInput, definition.type), definition.scope); - } finally { - setSaving(false); - } - }; - - const renderInput = () => { - switch (definition.type) { - case SettingTypes.Text: - return ( - setValue(e.target.value)} /> - ); - case SettingTypes.Number: - return ( - setValue(e.target.value)} - /> - ); - case SettingTypes.Bool: - return ( - { - const newVal = String(checked); - setValue(newVal); - void performSave(newVal); - }} - /> - ); - case SettingTypes.Json: - return ( -