working on problem opening projet

This commit is contained in:
Tara West
2026-01-12 13:27:19 +01:00
parent c1cc4b9b98
commit 188d993420
18 changed files with 2151 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
/**
* Default Style Tokens (Minimal Set for MVP)
*
* This file defines the minimal set of CSS custom properties (design tokens)
* that will be available in every Noodl project.
*
* These tokens can be used in any CSS property that accepts the relevant value type.
* Example: style="background: var(--primary); padding: var(--space-md);"
*
* @module StyleTokens
*/
export interface StyleToken {
name: string;
value: string;
category: TokenCategory;
description: string;
}
export type TokenCategory = 'color' | 'spacing' | 'border' | 'shadow';
/**
* Minimal set of design tokens for MVP
* Following modern design system conventions (similar to Tailwind/shadcn)
*/
export const DEFAULT_TOKENS: Record<string, StyleToken> = {
// ===== COLORS =====
'--primary': {
name: '--primary',
value: '#3b82f6', // Blue
category: 'color',
description: 'Primary brand color for main actions and highlights'
},
'--background': {
name: '--background',
value: '#ffffff',
category: 'color',
description: 'Main background color'
},
'--foreground': {
name: '--foreground',
value: '#0f172a', // Near black
category: 'color',
description: 'Main text color'
},
'--border': {
name: '--border',
value: '#e2e8f0', // Light gray
category: 'color',
description: 'Default border color'
},
// ===== SPACING =====
'--space-sm': {
name: '--space-sm',
value: '8px',
category: 'spacing',
description: 'Small spacing (padding, margin, gap)'
},
'--space-md': {
name: '--space-md',
value: '16px',
category: 'spacing',
description: 'Medium spacing (padding, margin, gap)'
},
'--space-lg': {
name: '--space-lg',
value: '24px',
category: 'spacing',
description: 'Large spacing (padding, margin, gap)'
},
// ===== BORDERS =====
'--radius-md': {
name: '--radius-md',
value: '8px',
category: 'border',
description: 'Medium border radius for rounded corners'
},
// ===== SHADOWS =====
'--shadow-sm': {
name: '--shadow-sm',
value: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
category: 'shadow',
description: 'Small shadow for subtle elevation'
},
'--shadow-md': {
name: '--shadow-md',
value: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
category: 'shadow',
description: 'Medium shadow for moderate elevation'
}
};
/**
* Get all default tokens as a simple key-value map
* Useful for CSS injection
*/
export function getDefaultTokenValues(): Record<string, string> {
const values: Record<string, string> = {};
for (const [key, token] of Object.entries(DEFAULT_TOKENS)) {
values[key] = token.value;
}
return values;
}
/**
* Get tokens by category
*/
export function getTokensByCategory(category: TokenCategory): StyleToken[] {
return Object.values(DEFAULT_TOKENS).filter((token) => token.category === category);
}

View File

@@ -0,0 +1,211 @@
/**
* Style Tokens Model
*
* Manages CSS custom properties (design tokens) for a Noodl project.
* Tokens are stored in project metadata and can be customized per project.
*
* @module StyleTokens
*/
import Model from '../../../../shared/model';
import { EventDispatcher } from '../../../../shared/utils/EventDispatcher';
import { ProjectModel } from '../projectmodel';
import { getDefaultTokenValues, DEFAULT_TOKENS, StyleToken, TokenCategory } from './DefaultTokens';
export class StyleTokensModel extends Model {
/** Custom token values (overrides defaults) */
private customTokens: Record<string, string>;
constructor() {
super();
this.customTokens = {};
this.loadFromProject();
this.bindListeners();
}
/**
* Bind to project events to stay in sync
*/
private bindListeners() {
const onProjectChanged = () => {
this.loadFromProject();
this.notifyListeners('tokensChanged');
};
EventDispatcher.instance.on(
['ProjectModel.importComplete', 'ProjectModel.instanceHasChanged'],
() => {
if (ProjectModel.instance) {
onProjectChanged();
}
},
this
);
EventDispatcher.instance.on(
'ProjectModel.metadataChanged',
({ key }) => {
if (key === 'styleTokens') {
onProjectChanged();
}
},
this
);
}
/**
* Unbind listeners
*/
private unbindListeners() {
EventDispatcher.instance.off(this);
}
/**
* Load tokens from current project
*/
private loadFromProject() {
if (ProjectModel.instance) {
this.customTokens = ProjectModel.instance.getMetaData('styleTokens') || {};
} else {
this.customTokens = {};
}
}
/**
* Save tokens to current project
*/
private saveToProject() {
if (ProjectModel.instance) {
this.unbindListeners();
ProjectModel.instance.setMetaData('styleTokens', this.customTokens);
this.bindListeners();
}
}
/**
* Get all tokens (defaults + custom overrides)
*/
getAllTokens(): Record<string, string> {
const defaults = getDefaultTokenValues();
return {
...defaults,
...this.customTokens
};
}
/**
* Get a specific token value
* @param name Token name (e.g., '--primary')
* @returns Token value or undefined if not found
*/
getToken(name: string): string | undefined {
// Check custom tokens first
if (this.customTokens[name] !== undefined) {
return this.customTokens[name];
}
// Fall back to default
const defaultToken = DEFAULT_TOKENS[name];
return defaultToken?.value;
}
/**
* Set a custom token value
* @param name Token name (e.g., '--primary')
* @param value Token value (e.g., '#ff0000')
*/
setToken(name: string, value: string) {
// Validate token name starts with --
if (!name.startsWith('--')) {
console.warn(`Token name must start with -- : ${name}`);
return;
}
this.customTokens[name] = value;
this.saveToProject();
this.notifyListeners('tokensChanged');
this.notifyListeners('tokenChanged', { name, value });
}
/**
* Reset a token to its default value
* @param name Token name (e.g., '--primary')
*/
resetToken(name: string) {
if (this.customTokens[name] !== undefined) {
delete this.customTokens[name];
this.saveToProject();
this.notifyListeners('tokensChanged');
this.notifyListeners('tokenReset', { name });
}
}
/**
* Reset all tokens to defaults
*/
resetAllTokens() {
this.customTokens = {};
this.saveToProject();
this.notifyListeners('tokensChanged');
this.notifyListeners('allTokensReset');
}
/**
* Check if a token has been customized
*/
isTokenCustomized(name: string): boolean {
return this.customTokens[name] !== undefined;
}
/**
* Get token metadata
*/
getTokenInfo(name: string): StyleToken | undefined {
return DEFAULT_TOKENS[name];
}
/**
* Get all tokens by category
*/
getTokensByCategory(category: TokenCategory): Record<string, string> {
const tokens: Record<string, string> = {};
for (const [name, tokenInfo] of Object.entries(DEFAULT_TOKENS)) {
if (tokenInfo.category === category) {
tokens[name] = this.getToken(name) || tokenInfo.value;
}
}
return tokens;
}
/**
* Generate CSS string for injection
* @returns CSS custom properties as a string
*/
generateCSS(): string {
const allTokens = this.getAllTokens();
const entries = Object.entries(allTokens);
if (entries.length === 0) {
return '';
}
const declarations = entries.map(([name, value]) => ` ${name}: ${value};`).join('\n');
return `:root {\n${declarations}\n}`;
}
/**
* Cleanup
*/
dispose() {
this.unbindListeners();
this.removeAllListeners();
}
}
/**
* Singleton instance
*/
export const StyleTokens = new StyleTokensModel();

View File

@@ -0,0 +1,11 @@
/**
* Style Tokens System
*
* Exports for the Style Tokens system
*
* @module StyleTokens
*/
export { StyleTokensModel, StyleTokens } from './StyleTokensModel';
export { DEFAULT_TOKENS, getDefaultTokenValues, getTokensByCategory } from './DefaultTokens';
export type { StyleToken, TokenCategory } from './DefaultTokens';

View File

@@ -169,6 +169,14 @@ export class ProjectModel extends Model {
if (json.rootNodeId) _this.rootNode = _this.findNodeWithId(json.rootNodeId);
// Handle rootComponent from templates (name of component instead of node ID)
if (json.rootComponent && !_this.rootNode) {
const rootComponent = _this.getComponentWithName(json.rootComponent);
if (rootComponent) {
_this.setRootComponent(rootComponent);
}
}
// Upgrade project if necessary
ProjectModel.upgrade(_this);

View File

@@ -40,6 +40,9 @@ export interface ProjectContent {
/** Project name (will be overridden by user input) */
name: string;
/** Name of the root component that serves as the entry point */
rootComponent?: string;
/** Array of component definitions */
components: ComponentDefinition[];

View File

@@ -36,6 +36,7 @@ export const helloWorldTemplate: ProjectTemplate = {
content: {
name: 'Hello World Project',
rootComponent: 'App',
components: [
// App component (root)
{

View File

@@ -59,6 +59,7 @@ const RouterNode = {
displayNodeName: 'Page Router',
category: 'Visuals',
docs: 'https://docs.noodl.net/nodes/navigation/page-router',
allowAsExportRoot: true,
useVariants: false,
connectionPanel: {
groupPriority: ['General', 'Actions', 'Events', 'Mounted']

View File

@@ -0,0 +1,163 @@
/**
* Style Tokens Injector
*
* Injects CSS custom properties (design tokens) into the DOM
* for use in Noodl projects.
*
* This class is responsible for:
* - Injecting default tokens into the page
* - Updating tokens when project settings change
* - Cleaning up on unmount
*/
interface GraphModel {
getMetaData(): Record<string, unknown> | undefined;
on(event: string, handler: (data: unknown) => void): void;
off(event: string): void;
}
interface StyleTokensInjectorOptions {
graphModel: GraphModel;
}
export class StyleTokensInjector {
private styleElement: HTMLStyleElement | null = null;
private graphModel: GraphModel;
private tokens: Record<string, string> = {};
constructor(options: StyleTokensInjectorOptions) {
this.graphModel = options.graphModel;
// Load tokens from project metadata
this.loadTokens();
// Inject tokens into DOM
this.injectTokens();
// Listen for project changes
this.bindListeners();
}
/**
* Load tokens from project metadata
*/
private loadTokens() {
try {
const metadata = this.graphModel.getMetaData();
const styleTokens = metadata?.styleTokens;
// Validate that styleTokens is a proper object
if (styleTokens && typeof styleTokens === 'object' && !Array.isArray(styleTokens)) {
this.tokens = styleTokens as Record<string, string>;
} else {
this.tokens = this.getDefaultTokens();
}
} catch (error) {
console.warn('Failed to load style tokens, using defaults:', error);
this.tokens = this.getDefaultTokens();
}
}
/**
* Get default tokens (fallback if project doesn't have custom tokens)
*/
private getDefaultTokens(): Record<string, string> {
return {
// Colors
'--primary': '#3b82f6',
'--background': '#ffffff',
'--foreground': '#0f172a',
'--border': '#e2e8f0',
// Spacing
'--space-sm': '8px',
'--space-md': '16px',
'--space-lg': '24px',
// Borders
'--radius-md': '8px',
// Shadows
'--shadow-sm': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
'--shadow-md': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)'
};
}
/**
* Inject tokens as CSS custom properties
*/
private injectTokens() {
// Support SSR
if (typeof document === 'undefined') return;
// Remove existing style element if any
this.removeStyleElement();
// Create new style element
this.styleElement = document.createElement('style');
this.styleElement.id = 'noodl-style-tokens';
this.styleElement.textContent = this.generateCSS();
// Inject into head
document.head.appendChild(this.styleElement);
}
/**
* Generate CSS string from tokens
*/
private generateCSS(): string {
const entries = Object.entries(this.tokens);
if (entries.length === 0) {
return '';
}
const declarations = entries.map(([name, value]) => ` ${name}: ${value};`).join('\n');
return `:root {\n${declarations}\n}`;
}
/**
* Update tokens and re-inject
*/
updateTokens(newTokens: Record<string, string>) {
this.tokens = { ...this.getDefaultTokens(), ...newTokens };
this.injectTokens();
}
/**
* Bind to graph model events
*/
private bindListeners() {
if (!this.graphModel) return;
// Listen for metadata changes
this.graphModel.on('metadataChanged', (metadata: unknown) => {
if (metadata && typeof metadata === 'object' && 'styleTokens' in metadata) {
const data = metadata as Record<string, unknown>;
if (data.styleTokens && typeof data.styleTokens === 'object') {
this.updateTokens(data.styleTokens as Record<string, string>);
}
}
});
}
/**
* Remove style element from DOM
*/
private removeStyleElement() {
if (this.styleElement && this.styleElement.parentNode) {
this.styleElement.parentNode.removeChild(this.styleElement);
this.styleElement = null;
}
}
/**
* Cleanup
*/
dispose() {
this.removeStyleElement();
if (this.graphModel) {
this.graphModel.off('metadataChanged');
}
}
}
export default StyleTokensInjector;

View File

@@ -8,6 +8,7 @@ import NoodlJSAPI from './noodl-js-api';
import projectSettings from './project-settings';
import { createNodeFromReactComponent } from './react-component-node';
import registerNodes from './register-nodes';
import { StyleTokensInjector } from './style-tokens-injector';
import Styles from './styles';
if (typeof window !== 'undefined' && window.NoodlEditor) {
@@ -189,6 +190,11 @@ export default class Viewer extends React.Component {
//make the styles available to all nodes via `this.context.styles`
noodlRuntime.context.styles = this.styles;
// Initialize style tokens injector
this.styleTokensInjector = new StyleTokensInjector({
graphModel: noodlRuntime.graphModel
});
this.state.waitingForExport = !this.runningDeployed;
if (this.runningDeployed) {