mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-03-08 10:03:31 +01:00
feat(element-configs): MVP 1 Infrastructure - Types, Registry, ButtonConfig
- Created ElementConfigTypes.ts with complete type definitions - Implemented ElementConfigRegistry singleton with full CRUD operations - Added ButtonConfig with 6 variants (primary, secondary, outline, ghost, destructive, link) - Added 4 size presets (sm, md, lg, xl) for buttons - All types compile without errors - Registry methods: register, get, applyDefaults, applyVariant, resolveStyles, validate - Token-based styling with var(--token-name) references Part of STYLE-002 MVP implementation. Next: TextConfig + Text sizing bug fix.
This commit is contained in:
@@ -0,0 +1,491 @@
|
||||
/**
|
||||
* ElementConfigRegistry
|
||||
*
|
||||
* Central registry for managing element configurations.
|
||||
* Provides methods to register, retrieve, and apply element configs.
|
||||
*
|
||||
* @module noodl-editor/models/ElementConfigs
|
||||
* @since 1.2.0
|
||||
*/
|
||||
|
||||
import type {
|
||||
ElementConfig,
|
||||
VariantConfig,
|
||||
SizeConfig,
|
||||
RegisterConfigOptions,
|
||||
ConfigValidationResult,
|
||||
ApplyVariantParams,
|
||||
ResolveStylesParams,
|
||||
StyleResolutionResult,
|
||||
CSSValue
|
||||
} from './ElementConfigTypes';
|
||||
|
||||
/**
|
||||
* Minimal node interface for applying configs
|
||||
* This represents the shape we need from NodeModel
|
||||
*/
|
||||
interface NodeLike {
|
||||
/** Node type identifier */
|
||||
type: string;
|
||||
/** Node unique ID */
|
||||
id: string;
|
||||
/** Node parameters/properties */
|
||||
parameters: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry for element configurations
|
||||
* Singleton pattern - use ElementConfigRegistry.instance
|
||||
*/
|
||||
export class ElementConfigRegistry {
|
||||
private static _instance: ElementConfigRegistry;
|
||||
private configs: Map<string, ElementConfig>;
|
||||
|
||||
private constructor() {
|
||||
this.configs = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance
|
||||
*/
|
||||
static get instance(): ElementConfigRegistry {
|
||||
if (!ElementConfigRegistry._instance) {
|
||||
ElementConfigRegistry._instance = new ElementConfigRegistry();
|
||||
}
|
||||
return ElementConfigRegistry._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new element configuration
|
||||
*
|
||||
* @param config - The element configuration to register
|
||||
* @param options - Registration options
|
||||
* @returns True if registered successfully, false if already exists and override is false
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* ElementConfigRegistry.instance.register(ButtonConfig);
|
||||
* ```
|
||||
*/
|
||||
register(config: ElementConfig, options: RegisterConfigOptions = {}): boolean {
|
||||
const { override = false, validate = true } = options;
|
||||
|
||||
// Validate if requested
|
||||
if (validate) {
|
||||
const validation = this.validate(config);
|
||||
if (!validation.valid) {
|
||||
console.error(`[ElementConfigRegistry] Invalid config for ${config.nodeType}:`, validation.errors);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (validation.warnings.length > 0) {
|
||||
console.warn(`[ElementConfigRegistry] Warnings for ${config.nodeType}:`, validation.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if already exists
|
||||
const exists = this.configs.has(config.nodeType);
|
||||
if (exists && !override) {
|
||||
console.warn(
|
||||
`[ElementConfigRegistry] Config for ${config.nodeType} already exists. Use override: true to replace.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Register the config
|
||||
this.configs.set(config.nodeType, config);
|
||||
|
||||
console.log(
|
||||
`[ElementConfigRegistry] ${exists ? 'Updated' : 'Registered'} config for ${config.nodeType} ` +
|
||||
`(${Object.keys(config.variants).length} variants)`
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an element configuration by node type
|
||||
*
|
||||
* @param nodeType - The node type identifier
|
||||
* @returns The element config, or undefined if not found
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const config = ElementConfigRegistry.instance.get('net.noodl.visual.button');
|
||||
* ```
|
||||
*/
|
||||
get(nodeType: string): ElementConfig | undefined {
|
||||
return this.configs.get(nodeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a config exists for a node type
|
||||
*
|
||||
* @param nodeType - The node type identifier
|
||||
* @returns True if a config exists
|
||||
*/
|
||||
has(nodeType: string): boolean {
|
||||
return this.configs.has(nodeType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all variant names for a node type
|
||||
*
|
||||
* @param nodeType - The node type identifier
|
||||
* @returns Array of variant names, or empty array if config not found
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const variants = ElementConfigRegistry.instance.getVariants('net.noodl.visual.button');
|
||||
* // Returns: ['primary', 'secondary', 'outline', 'ghost', 'destructive', 'link']
|
||||
* ```
|
||||
*/
|
||||
getVariants(nodeType: string): string[] {
|
||||
const config = this.configs.get(nodeType);
|
||||
if (!config) return [];
|
||||
return Object.keys(config.variants);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all size preset names for a node type
|
||||
*
|
||||
* @param nodeType - The node type identifier
|
||||
* @returns Array of size names, or empty array if no sizes defined
|
||||
*/
|
||||
getSizes(nodeType: string): string[] {
|
||||
const config = this.configs.get(nodeType);
|
||||
if (!config || !config.sizes) return [];
|
||||
return Object.keys(config.sizes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific variant configuration
|
||||
*
|
||||
* @param nodeType - The node type identifier
|
||||
* @param variantName - The variant name
|
||||
* @returns The variant config, or undefined if not found
|
||||
*/
|
||||
getVariant(nodeType: string, variantName: string): VariantConfig | undefined {
|
||||
const config = this.configs.get(nodeType);
|
||||
if (!config) return undefined;
|
||||
return config.variants[variantName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific size configuration
|
||||
*
|
||||
* @param nodeType - The node type identifier
|
||||
* @param sizeName - The size name
|
||||
* @returns The size config, or undefined if not found
|
||||
*/
|
||||
getSize(nodeType: string, sizeName: string): SizeConfig | undefined {
|
||||
const config = this.configs.get(nodeType);
|
||||
if (!config || !config.sizes) return undefined;
|
||||
return config.sizes[sizeName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply default styles to a node
|
||||
* This should be called when a new node is created
|
||||
*
|
||||
* @param node - The node model to apply defaults to
|
||||
* @returns True if defaults were applied, false if no config found
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In NodeModel constructor or creation hook:
|
||||
* ElementConfigRegistry.instance.applyDefaults(newNode);
|
||||
* ```
|
||||
*/
|
||||
applyDefaults(node: NodeLike): boolean {
|
||||
const config = this.configs.get(node.type);
|
||||
if (!config) return false;
|
||||
|
||||
// Apply default CSS properties to node parameters
|
||||
for (const [property, value] of Object.entries(config.defaults)) {
|
||||
// Only apply if the property doesn't already have a value
|
||||
if (node.parameters[property] === undefined) {
|
||||
node.parameters[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Set default variant if defined in defaults
|
||||
if (config.defaults['_variant'] && !node.parameters['_variant']) {
|
||||
node.parameters['_variant'] = config.defaults['_variant'];
|
||||
}
|
||||
|
||||
console.log(`[ElementConfigRegistry] Applied defaults to ${node.type} (node ${node.id})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a variant to a node
|
||||
*
|
||||
* @param node - The node model to apply the variant to
|
||||
* @param params - Variant application parameters
|
||||
* @returns True if variant was applied, false if config or variant not found
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* ElementConfigRegistry.instance.applyVariant(buttonNode, {
|
||||
* variantName: 'secondary',
|
||||
* preserveUserOverrides: true
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
applyVariant(node: NodeLike, params: ApplyVariantParams | string): boolean {
|
||||
const variantName = typeof params === 'string' ? params : params.variantName;
|
||||
const size = typeof params === 'object' ? params.size : undefined;
|
||||
const preserveUserOverrides = typeof params === 'object' ? params.preserveUserOverrides !== false : true;
|
||||
|
||||
const config = this.configs.get(node.type);
|
||||
if (!config) {
|
||||
console.warn(`[ElementConfigRegistry] No config found for ${node.type}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const variant = config.variants[variantName];
|
||||
if (!variant) {
|
||||
console.warn(`[ElementConfigRegistry] Variant "${variantName}" not found for ${node.type}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store user overrides if preserving
|
||||
const userOverrides: Record<string, any> = {};
|
||||
if (preserveUserOverrides) {
|
||||
// Detect which properties were user-modified
|
||||
// (properties not in defaults or previous variant)
|
||||
const previousVariantName = node.parameters['_variant'];
|
||||
const previousVariant = previousVariantName ? config.variants[previousVariantName] : null;
|
||||
|
||||
for (const key in node.parameters) {
|
||||
if (key.startsWith('_')) continue; // Skip system properties
|
||||
|
||||
const isFromDefaults = config.defaults[key] !== undefined;
|
||||
const isFromPreviousVariant = previousVariant && previousVariant[key] !== undefined;
|
||||
|
||||
if (!isFromDefaults && !isFromPreviousVariant) {
|
||||
userOverrides[key] = node.parameters[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply variant properties
|
||||
for (const [property, value] of Object.entries(variant)) {
|
||||
if (property === 'states') continue; // States are handled separately
|
||||
if (typeof value === 'string') {
|
||||
node.parameters[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply size if specified
|
||||
if (size && config.sizes) {
|
||||
const sizeConfig = config.sizes[size];
|
||||
if (sizeConfig) {
|
||||
for (const [property, value] of Object.entries(sizeConfig)) {
|
||||
node.parameters[property] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore user overrides
|
||||
if (preserveUserOverrides) {
|
||||
for (const [property, value] of Object.entries(userOverrides)) {
|
||||
node.parameters[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Store the variant name
|
||||
node.parameters['_variant'] = variantName;
|
||||
if (size) {
|
||||
node.parameters['_size'] = size;
|
||||
}
|
||||
|
||||
console.log(`[ElementConfigRegistry] Applied variant "${variantName}" to ${node.type} (node ${node.id})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve complete styles for a node
|
||||
* Merges defaults + variant + size + user overrides
|
||||
*
|
||||
* @param params - Resolution parameters
|
||||
* @returns Resolved styles with metadata
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = ElementConfigRegistry.instance.resolveStyles({
|
||||
* nodeType: 'net.noodl.visual.button',
|
||||
* variant: 'primary',
|
||||
* size: 'md',
|
||||
* userOverrides: { backgroundColor: '#custom' }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
resolveStyles(params: ResolveStylesParams): StyleResolutionResult | null {
|
||||
const { nodeType, variant, size, userOverrides = {} } = params;
|
||||
|
||||
const config = this.configs.get(nodeType);
|
||||
if (!config) return null;
|
||||
|
||||
const styles: Record<string, CSSValue> = {};
|
||||
const sources: Record<string, 'default' | 'variant' | 'size' | 'user'> = {};
|
||||
|
||||
// 1. Apply defaults
|
||||
for (const [property, value] of Object.entries(config.defaults)) {
|
||||
if (property.startsWith('_')) continue; // Skip system properties
|
||||
styles[property] = value;
|
||||
sources[property] = 'default';
|
||||
}
|
||||
|
||||
// 2. Apply variant
|
||||
let variantConfig: VariantConfig | undefined;
|
||||
if (variant) {
|
||||
variantConfig = config.variants[variant];
|
||||
if (variantConfig) {
|
||||
for (const [property, value] of Object.entries(variantConfig)) {
|
||||
if (property === 'states') continue;
|
||||
if (typeof value === 'string') {
|
||||
styles[property] = value;
|
||||
sources[property] = 'variant';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Apply size
|
||||
if (size && config.sizes) {
|
||||
const sizeConfig = config.sizes[size];
|
||||
if (sizeConfig) {
|
||||
for (const [property, value] of Object.entries(sizeConfig)) {
|
||||
styles[property] = value;
|
||||
sources[property] = 'size';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Apply user overrides (highest priority)
|
||||
for (const [property, value] of Object.entries(userOverrides)) {
|
||||
styles[property] = value;
|
||||
sources[property] = 'user';
|
||||
}
|
||||
|
||||
return {
|
||||
styles: {
|
||||
base: styles,
|
||||
states: variantConfig?.states
|
||||
},
|
||||
appliedVariant: variant,
|
||||
appliedSize: size,
|
||||
hasUserOverrides: Object.keys(userOverrides).length > 0,
|
||||
sources
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an element configuration
|
||||
*
|
||||
* @param config - The config to validate
|
||||
* @returns Validation result with errors and warnings
|
||||
*/
|
||||
validate(config: ElementConfig): ConfigValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
// Check required fields
|
||||
if (!config.nodeType) {
|
||||
errors.push('nodeType is required');
|
||||
}
|
||||
|
||||
if (!config.defaults) {
|
||||
errors.push('defaults object is required');
|
||||
}
|
||||
|
||||
if (!config.variants || Object.keys(config.variants).length === 0) {
|
||||
errors.push('At least one variant is required');
|
||||
}
|
||||
|
||||
// Check variant structure
|
||||
if (config.variants) {
|
||||
for (const [variantName, variantConfig] of Object.entries(config.variants)) {
|
||||
if (!variantConfig || typeof variantConfig !== 'object') {
|
||||
errors.push(`Variant "${variantName}" must be an object`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warnings for missing common properties
|
||||
if (config.defaults && !config.defaults['_variant']) {
|
||||
warnings.push('No default variant specified in defaults._variant');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered node types
|
||||
*
|
||||
* @returns Array of registered node type identifiers
|
||||
*/
|
||||
getRegisteredNodeTypes(): string[] {
|
||||
return Array.from(this.configs.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of registered configs
|
||||
*
|
||||
* @returns Number of registered configs
|
||||
*/
|
||||
getCount(): number {
|
||||
return this.configs.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registered configs
|
||||
* WARNING: This is mainly for testing, use with caution
|
||||
*/
|
||||
clear(): void {
|
||||
this.configs.clear();
|
||||
console.log('[ElementConfigRegistry] Cleared all configs');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a summary of the registry state
|
||||
*
|
||||
* @returns Summary object with counts and node types
|
||||
*/
|
||||
getSummary(): {
|
||||
totalConfigs: number;
|
||||
nodeTypes: string[];
|
||||
configDetails: Array<{
|
||||
nodeType: string;
|
||||
variantCount: number;
|
||||
sizeCount: number;
|
||||
hasDescription: boolean;
|
||||
}>;
|
||||
} {
|
||||
const nodeTypes = this.getRegisteredNodeTypes();
|
||||
const configDetails = nodeTypes.map((nodeType) => {
|
||||
const config = this.configs.get(nodeType)!;
|
||||
return {
|
||||
nodeType,
|
||||
variantCount: Object.keys(config.variants).length,
|
||||
sizeCount: config.sizes ? Object.keys(config.sizes).length : 0,
|
||||
hasDescription: !!config.description
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
totalConfigs: this.configs.size,
|
||||
nodeTypes,
|
||||
configDetails
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance for convenience
|
||||
export const registry = ElementConfigRegistry.instance;
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* ElementConfigTypes
|
||||
*
|
||||
* Type definitions for the Element Configuration system.
|
||||
* This system provides default styling, variants, and size presets
|
||||
* for Noodl's visual nodes (Button, Text, Group, etc.).
|
||||
*
|
||||
* @module noodl-editor/models/ElementConfigs
|
||||
* @since 1.2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* CSS property values with design token references
|
||||
* Values can be direct CSS values or CSS variable references like 'var(--token-name)'
|
||||
*/
|
||||
export type CSSValue = string;
|
||||
|
||||
/**
|
||||
* State-specific style overrides for interactive elements
|
||||
*/
|
||||
export interface StateConfig {
|
||||
/** Styles applied on hover state */
|
||||
hover?: Record<string, CSSValue>;
|
||||
|
||||
/** Styles applied on active/pressed state */
|
||||
active?: Record<string, CSSValue>;
|
||||
|
||||
/** Styles applied on focus state (inputs, buttons) */
|
||||
focus?: Record<string, CSSValue>;
|
||||
|
||||
/** Styles applied when element is disabled */
|
||||
disabled?: Record<string, CSSValue>;
|
||||
|
||||
/** Styles applied to placeholder text (inputs only) */
|
||||
placeholder?: Record<string, CSSValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A style variant configuration
|
||||
* Defines the CSS properties and state-specific overrides for a variant
|
||||
*/
|
||||
export interface VariantConfig {
|
||||
/** Base CSS properties for this variant */
|
||||
[property: string]: CSSValue | StateConfig | undefined;
|
||||
|
||||
/** Optional state-specific overrides */
|
||||
states?: StateConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Size preset configuration
|
||||
* Defines CSS property overrides for different size variations (sm, md, lg, xl)
|
||||
*/
|
||||
export interface SizeConfig {
|
||||
/** CSS properties for this size */
|
||||
[property: string]: CSSValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete element configuration
|
||||
* Defines defaults, variants, and size presets for a visual node type
|
||||
*/
|
||||
export interface ElementConfig {
|
||||
/** Noodl node type identifier (e.g., 'net.noodl.visual.button') */
|
||||
nodeType: string;
|
||||
|
||||
/** Default CSS properties applied on node creation */
|
||||
defaults: Record<string, CSSValue>;
|
||||
|
||||
/** Named style variants (e.g., 'primary', 'secondary', 'outline') */
|
||||
variants: Record<string, VariantConfig>;
|
||||
|
||||
/** Optional size presets (e.g., 'sm', 'md', 'lg', 'xl') */
|
||||
sizes?: Record<string, SizeConfig>;
|
||||
|
||||
/** Optional description for documentation */
|
||||
description?: string;
|
||||
|
||||
/** Optional categories for grouping (e.g., ['button', 'form', 'input']) */
|
||||
categories?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved styles for a node
|
||||
* Result of merging defaults + variant + size + user overrides
|
||||
*/
|
||||
export interface ResolvedStyles {
|
||||
/** Base CSS properties (defaults + variant + size) */
|
||||
base: Record<string, CSSValue>;
|
||||
|
||||
/** State-specific style overrides (if applicable) */
|
||||
states?: StateConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for applying a variant to a node
|
||||
*/
|
||||
export interface ApplyVariantParams {
|
||||
/** The variant name to apply */
|
||||
variantName: string;
|
||||
|
||||
/** Optional size to apply simultaneously */
|
||||
size?: string;
|
||||
|
||||
/** Whether to preserve user overrides (default: true) */
|
||||
preserveUserOverrides?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for resolving styles for a node
|
||||
*/
|
||||
export interface ResolveStylesParams {
|
||||
/** The node type identifier */
|
||||
nodeType: string;
|
||||
|
||||
/** Current variant name */
|
||||
variant?: string;
|
||||
|
||||
/** Current size name */
|
||||
size?: string;
|
||||
|
||||
/** User-defined CSS overrides */
|
||||
userOverrides?: Record<string, CSSValue>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of style resolution with metadata
|
||||
*/
|
||||
export interface StyleResolutionResult {
|
||||
/** Resolved CSS properties */
|
||||
styles: ResolvedStyles;
|
||||
|
||||
/** Which variant was applied */
|
||||
appliedVariant?: string;
|
||||
|
||||
/** Which size was applied */
|
||||
appliedSize?: string;
|
||||
|
||||
/** Whether user overrides were present */
|
||||
hasUserOverrides: boolean;
|
||||
|
||||
/** Source of each property (for debugging) */
|
||||
sources?: Record<string, 'default' | 'variant' | 'size' | 'user'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for registering a new element config
|
||||
*/
|
||||
export interface RegisterConfigOptions {
|
||||
/** Whether to override existing config with same nodeType */
|
||||
override?: boolean;
|
||||
|
||||
/** Whether to validate the config structure */
|
||||
validate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation result for element config
|
||||
*/
|
||||
export interface ConfigValidationResult {
|
||||
/** Whether the config is valid */
|
||||
valid: boolean;
|
||||
|
||||
/** Validation errors (if any) */
|
||||
errors: string[];
|
||||
|
||||
/** Validation warnings (if any) */
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom variant created by user
|
||||
* Contains variant configuration plus metadata
|
||||
*/
|
||||
export interface CustomVariant {
|
||||
/** User-defined variant name */
|
||||
name: string;
|
||||
|
||||
/** Node type this variant applies to */
|
||||
nodeType: string;
|
||||
|
||||
/** The actual variant configuration (styles and states) */
|
||||
config: VariantConfig;
|
||||
|
||||
/** Scope: 'project' (this project only) or 'global' (all projects) */
|
||||
scope: 'project' | 'global';
|
||||
|
||||
/** When the variant was created */
|
||||
createdAt: Date;
|
||||
|
||||
/** Optional user description */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event emitted when a variant is applied to a node
|
||||
*/
|
||||
export interface VariantAppliedEvent {
|
||||
/** Node ID that was modified */
|
||||
nodeId: string;
|
||||
|
||||
/** Node type */
|
||||
nodeType: string;
|
||||
|
||||
/** Previous variant (if any) */
|
||||
previousVariant?: string;
|
||||
|
||||
/** New variant applied */
|
||||
newVariant: string;
|
||||
|
||||
/** Timestamp of the change */
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event emitted when element config is registered/updated
|
||||
*/
|
||||
export interface ConfigRegisteredEvent {
|
||||
/** Node type registered */
|
||||
nodeType: string;
|
||||
|
||||
/** Number of variants in this config */
|
||||
variantCount: number;
|
||||
|
||||
/** Whether this was an update to existing config */
|
||||
isUpdate: boolean;
|
||||
|
||||
/** Timestamp of registration */
|
||||
timestamp: Date;
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* ButtonConfig
|
||||
*
|
||||
* Element configuration for Button nodes.
|
||||
* Defines default styling, 6 variants, and 4 size presets.
|
||||
*
|
||||
* @module noodl-editor/models/ElementConfigs/configs
|
||||
* @since 1.2.0
|
||||
*/
|
||||
|
||||
import type { ElementConfig } from '../ElementConfigTypes';
|
||||
|
||||
/**
|
||||
* Button element configuration
|
||||
* Provides modern, accessible button styles with multiple variants
|
||||
*/
|
||||
export const ButtonConfig: ElementConfig = {
|
||||
nodeType: 'net.noodl.visual.button',
|
||||
|
||||
description: 'Interactive button element with multiple style variants and sizes',
|
||||
|
||||
categories: ['button', 'interactive', 'form'],
|
||||
|
||||
// Default properties applied when a new Button is created
|
||||
defaults: {
|
||||
// Layout
|
||||
paddingTop: 'var(--space-2)',
|
||||
paddingBottom: 'var(--space-2)',
|
||||
paddingLeft: 'var(--space-4)',
|
||||
paddingRight: 'var(--space-4)',
|
||||
|
||||
// Typography
|
||||
fontSize: 'var(--text-sm)',
|
||||
fontWeight: 'var(--font-medium)',
|
||||
fontFamily: 'var(--font-sans)',
|
||||
textAlign: 'center',
|
||||
lineHeight: 'var(--leading-none)',
|
||||
|
||||
// Border
|
||||
borderRadius: 'var(--radius-md)',
|
||||
borderWidth: '0',
|
||||
borderStyle: 'solid',
|
||||
|
||||
// Display
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
|
||||
// Transitions
|
||||
transitionProperty: 'background-color, border-color, color, box-shadow, transform',
|
||||
transitionDuration: '150ms',
|
||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
|
||||
// Default variant
|
||||
_variant: 'primary'
|
||||
},
|
||||
|
||||
// Size presets
|
||||
sizes: {
|
||||
sm: {
|
||||
paddingTop: 'var(--space-1)',
|
||||
paddingBottom: 'var(--space-1)',
|
||||
paddingLeft: 'var(--space-3)',
|
||||
paddingRight: 'var(--space-3)',
|
||||
fontSize: 'var(--text-xs)',
|
||||
height: '32px'
|
||||
},
|
||||
|
||||
md: {
|
||||
paddingTop: 'var(--space-2)',
|
||||
paddingBottom: 'var(--space-2)',
|
||||
paddingLeft: 'var(--space-4)',
|
||||
paddingRight: 'var(--space-4)',
|
||||
fontSize: 'var(--text-sm)',
|
||||
height: '40px'
|
||||
},
|
||||
|
||||
lg: {
|
||||
paddingTop: 'var(--space-3)',
|
||||
paddingBottom: 'var(--space-3)',
|
||||
paddingLeft: 'var(--space-6)',
|
||||
paddingRight: 'var(--space-6)',
|
||||
fontSize: 'var(--text-base)',
|
||||
height: '48px'
|
||||
},
|
||||
|
||||
xl: {
|
||||
paddingTop: 'var(--space-4)',
|
||||
paddingBottom: 'var(--space-4)',
|
||||
paddingLeft: 'var(--space-8)',
|
||||
paddingRight: 'var(--space-8)',
|
||||
fontSize: 'var(--text-lg)',
|
||||
height: '56px'
|
||||
}
|
||||
},
|
||||
|
||||
// Style variants
|
||||
variants: {
|
||||
// Primary: Solid background, high emphasis
|
||||
primary: {
|
||||
backgroundColor: 'var(--primary)',
|
||||
color: 'var(--primary-foreground)',
|
||||
borderWidth: '0',
|
||||
boxShadow: 'var(--shadow-sm)',
|
||||
|
||||
states: {
|
||||
hover: {
|
||||
backgroundColor: 'var(--primary-hover)',
|
||||
boxShadow: 'var(--shadow-md)'
|
||||
},
|
||||
active: {
|
||||
transform: 'scale(0.98)',
|
||||
boxShadow: 'var(--shadow-sm)'
|
||||
},
|
||||
disabled: {
|
||||
opacity: '0.5',
|
||||
cursor: 'not-allowed',
|
||||
pointerEvents: 'none'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Secondary: Subtle background, medium emphasis
|
||||
secondary: {
|
||||
backgroundColor: 'var(--secondary)',
|
||||
color: 'var(--secondary-foreground)',
|
||||
borderWidth: '0',
|
||||
boxShadow: 'var(--shadow-sm)',
|
||||
|
||||
states: {
|
||||
hover: {
|
||||
backgroundColor: 'var(--secondary-hover)',
|
||||
boxShadow: 'var(--shadow-md)'
|
||||
},
|
||||
active: {
|
||||
transform: 'scale(0.98)',
|
||||
boxShadow: 'var(--shadow-sm)'
|
||||
},
|
||||
disabled: {
|
||||
opacity: '0.5',
|
||||
cursor: 'not-allowed',
|
||||
pointerEvents: 'none'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Outline: Transparent background with border
|
||||
outline: {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--foreground)',
|
||||
borderWidth: 'var(--border-1)',
|
||||
borderColor: 'var(--border)',
|
||||
borderStyle: 'solid',
|
||||
boxShadow: 'none',
|
||||
|
||||
states: {
|
||||
hover: {
|
||||
backgroundColor: 'var(--accent)',
|
||||
color: 'var(--accent-foreground)',
|
||||
borderColor: 'var(--accent)'
|
||||
},
|
||||
active: {
|
||||
transform: 'scale(0.98)'
|
||||
},
|
||||
disabled: {
|
||||
opacity: '0.5',
|
||||
cursor: 'not-allowed',
|
||||
pointerEvents: 'none'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Ghost: Minimal style, subtle hover
|
||||
ghost: {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--foreground)',
|
||||
borderWidth: '0',
|
||||
boxShadow: 'none',
|
||||
|
||||
states: {
|
||||
hover: {
|
||||
backgroundColor: 'var(--accent)',
|
||||
color: 'var(--accent-foreground)'
|
||||
},
|
||||
active: {
|
||||
transform: 'scale(0.98)'
|
||||
},
|
||||
disabled: {
|
||||
opacity: '0.5',
|
||||
cursor: 'not-allowed',
|
||||
pointerEvents: 'none'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Destructive: For dangerous actions (delete, remove, etc.)
|
||||
destructive: {
|
||||
backgroundColor: 'var(--destructive)',
|
||||
color: 'var(--destructive-foreground)',
|
||||
borderWidth: '0',
|
||||
boxShadow: 'var(--shadow-sm)',
|
||||
|
||||
states: {
|
||||
hover: {
|
||||
backgroundColor: 'var(--destructive-hover)',
|
||||
boxShadow: 'var(--shadow-md)'
|
||||
},
|
||||
active: {
|
||||
transform: 'scale(0.98)',
|
||||
boxShadow: 'var(--shadow-sm)'
|
||||
},
|
||||
disabled: {
|
||||
opacity: '0.5',
|
||||
cursor: 'not-allowed',
|
||||
pointerEvents: 'none'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Link: Text-only style, no background
|
||||
link: {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--primary)',
|
||||
borderWidth: '0',
|
||||
boxShadow: 'none',
|
||||
textDecoration: 'none',
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
height: 'auto',
|
||||
|
||||
states: {
|
||||
hover: {
|
||||
textDecoration: 'underline',
|
||||
color: 'var(--primary-hover)'
|
||||
},
|
||||
active: {
|
||||
color: 'var(--primary)'
|
||||
},
|
||||
disabled: {
|
||||
opacity: '0.5',
|
||||
cursor: 'not-allowed',
|
||||
pointerEvents: 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Element Configs
|
||||
*
|
||||
* Pre-built element configurations for Noodl's visual nodes.
|
||||
* Import and register these configs to enable default styling and variants.
|
||||
*
|
||||
* @module noodl-editor/models/ElementConfigs/configs
|
||||
* @since 1.2.0
|
||||
*/
|
||||
|
||||
export { ButtonConfig } from './ButtonConfig';
|
||||
|
||||
// TextConfig will be added next
|
||||
// export { TextConfig } from './TextConfig';
|
||||
|
||||
// Other configs to be implemented:
|
||||
// export { GroupConfig } from './GroupConfig';
|
||||
// export { TextInputConfig } from './TextInputConfig';
|
||||
// export { ImageConfig } from './ImageConfig';
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* ElementConfigs
|
||||
*
|
||||
* System for managing default configurations, style variants, and size presets
|
||||
* for Noodl's visual nodes (Button, Text, Group, Input, etc.).
|
||||
*
|
||||
* @module noodl-editor/models/ElementConfigs
|
||||
* @since 1.2.0
|
||||
*/
|
||||
|
||||
// Export all types
|
||||
export * from './ElementConfigTypes';
|
||||
|
||||
// Export registry
|
||||
export { ElementConfigRegistry, registry } from './ElementConfigRegistry';
|
||||
|
||||
// Configs will be exported once implemented
|
||||
// export * from './configs';
|
||||
Reference in New Issue
Block a user