mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-13 15:52:56 +01:00
working on problem opening projet
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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[];
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ export const helloWorldTemplate: ProjectTemplate = {
|
||||
|
||||
content: {
|
||||
name: 'Hello World Project',
|
||||
rootComponent: 'App',
|
||||
components: [
|
||||
// App component (root)
|
||||
{
|
||||
|
||||
@@ -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']
|
||||
|
||||
163
packages/noodl-viewer-react/src/style-tokens-injector.ts
Normal file
163
packages/noodl-viewer-react/src/style-tokens-injector.ts
Normal 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;
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user