fusee-framework 0.1.0-alpha.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,69 +1,78 @@
1
- // ─── Hash-based SPA Router ────────────────────────────────────────────────────
2
- // Routes are declared as: { path: '/about', component: AboutComponent }
3
- // Navigation via <a href="#/about"> or router.navigate('/about')
4
-
5
1
  let _routes = []
6
2
  let _outlet = null
7
3
  let _currentInstance = null
8
4
 
9
5
  export function createRouter(routes) {
10
- _routes = routes
11
-
12
- // Listen for hash changes (back/forward + manual URL edits)
13
- window.addEventListener('hashchange', () => _resolve())
14
-
15
- // Initial render on page load
16
- window.addEventListener('DOMContentLoaded', () => _resolve())
17
-
18
- return { navigate }
6
+ _routes = routes
7
+
8
+ const handler = () => _resolve()
9
+ window.addEventListener('hashchange', handler)
10
+
11
+ if (document.readyState === 'loading') {
12
+ window.addEventListener('DOMContentLoaded', handler)
13
+ } else {
14
+ _resolve()
15
+ }
16
+
17
+ return {
18
+ navigate,
19
+ destroy() {
20
+ window.removeEventListener('hashchange', handler)
21
+ window.removeEventListener('DOMContentLoaded', handler)
22
+ if (_currentInstance) {
23
+ try {
24
+ _currentInstance.unmount()
25
+ } catch {}
26
+ }
27
+ _routes = []
28
+ _outlet = null
29
+ _currentInstance = null
30
+ }
31
+ }
19
32
  }
20
33
 
21
34
  export function navigate(path) {
22
- window.location.hash = path
35
+ window.location.hash = path
23
36
  }
24
37
 
25
- // ── Mount outlet ──────────────────────────────────────────────────────────────
26
- // Call this with the DOM element that acts as <router-outlet>
27
38
  export function mountOutlet(el) {
28
- _outlet = el
29
- _resolve()
39
+ _outlet = el
40
+ _resolve()
30
41
  }
31
42
 
32
- // ── Internal resolution ───────────────────────────────────────────────────────
33
43
  function _resolve() {
34
- if (!_outlet) return
44
+ if (!_outlet) {
45
+ return
46
+ }
35
47
 
36
- // Parse current hash: '#/about' → '/about', fallback to '/'
37
- const hash = window.location.hash
38
- const path = hash.startsWith('#') ? hash.slice(1) || '/' : '/'
48
+ const hash = window.location.hash
49
+ const path = hash.startsWith('#') ? hash.slice(1) || '/' : '/'
39
50
 
40
- const matched = _routes.find(r => _matchPath(r.path, path))
51
+ const matched = _routes.find(r => _matchPath(r.path, path))
41
52
 
42
- if (!matched) {
43
- _outlet.innerHTML = `<p style="color:red">[framework] No route matched for "${path}"</p>`
44
- return
45
- }
53
+ if (!matched) {
54
+ _outlet.innerHTML = `<p style="color:red">[framework] No route matched for "${path}"</p>`
55
+ return
56
+ }
46
57
 
47
- // Unmount previous component to run cleanup hooks
48
- if (_currentInstance) {
49
- try { _currentInstance.unmount() } catch {}
50
- }
58
+ if (_currentInstance) {
59
+ try { _currentInstance.unmount() } catch {}
60
+ }
51
61
 
52
- _outlet.innerHTML = ''
62
+ _outlet.innerHTML = ''
53
63
 
54
- const componentFn = matched.component
55
- _currentInstance = componentFn()
56
- _currentInstance.render(_outlet)
64
+ const componentFn = matched.component
65
+ _currentInstance = componentFn()
66
+ _currentInstance.render(_outlet)
57
67
  }
58
68
 
59
- // Simple path matching — supports exact paths and :param segments
60
69
  function _matchPath(routePath, actualPath) {
61
- const routeParts = routePath.split('/').filter(Boolean)
62
- const actualParts = actualPath.split('/').filter(Boolean)
70
+ const routeParts = routePath.split('/').filter(Boolean)
71
+ const actualParts = actualPath.split('/').filter(Boolean)
63
72
 
64
- if (routeParts.length !== actualParts.length) return false
73
+ if (routeParts.length !== actualParts.length) return false
65
74
 
66
- return routeParts.every((part, i) =>
67
- part.startsWith(':') || part === actualParts[i]
68
- )
75
+ return routeParts.every((part, i) =>
76
+ part.startsWith(':') || part === actualParts[i]
77
+ )
69
78
  }
@@ -0,0 +1,25 @@
1
+ import type { ComponentResult, ComponentFactory } from './component'
2
+
3
+ export function mountTemplate(
4
+ template: string,
5
+ container: HTMLElement,
6
+ context: ComponentResult,
7
+ components: Record<string, ComponentFactory>
8
+ ): { effects: (() => void)[] }
9
+
10
+ export function compileNode(
11
+ node: HTMLElement,
12
+ context: ComponentResult,
13
+ components: Record<string, ComponentFactory>,
14
+ effects: (() => void)[]
15
+ ): void
16
+
17
+ function processTextNodes(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
18
+ function processAttrBindings(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
19
+
20
+ function bindComponents(
21
+ container: HTMLElement,
22
+ components: Record<string, ComponentFactory>,
23
+ context: ComponentResult,
24
+ effects: (() => void)[]
25
+ ): void
@@ -0,0 +1,44 @@
1
+ // ─── Component Types ─────────────────────────────────────────────────────────
2
+
3
+ export type ComponentProps = Record<string, any>
4
+
5
+ export type EmptyProps = Record<string, never>
6
+
7
+ export type PropSchema = string[] | Record<string, PropConfig>
8
+
9
+ export type PropConfig = {
10
+ type?: FunctionConstructor | StringConstructor | NumberConstructor | BooleanConstructor
11
+ default?: any | (() => any)
12
+ required?: boolean
13
+ }
14
+
15
+ export type ComponentInstance = {
16
+ props: ComponentProps
17
+ _mountHooks: (() => void)[]
18
+ _unmountHooks: (() => void)[]
19
+ _effects: import('./signal').EffectRunner[]
20
+ _element: HTMLElement | null
21
+ }
22
+
23
+ export type ComponentResult = {
24
+ template: string
25
+ [key: string]: any
26
+ }
27
+
28
+ export type ComponentOptions<TProps = ComponentProps> = {
29
+ props?: PropSchema
30
+ components?: Record<string, ComponentFactory<any>>
31
+ setup: (props: TProps) => ComponentResult
32
+ }
33
+
34
+ export type ComponentFactory<TProps = ComponentProps> = (props?: TProps) => ComponentApi
35
+
36
+ export type ComponentApi = {
37
+ render: (container: HTMLElement) => ComponentInstance
38
+ unmount: () => void
39
+ instance: ComponentInstance
40
+ }
41
+
42
+ export declare function onMount(fn: () => void): void
43
+ export declare function onUnmount(fn: () => void): void
44
+ export declare function defineComponent<TProps = EmptyProps>(options: ComponentOptions<TProps>): ComponentFactory<TProps>
@@ -0,0 +1,32 @@
1
+ import type { ComponentResult, ComponentFactory } from './component'
2
+
3
+ export function processDirectives(
4
+ root: HTMLElement,
5
+ context: ComponentResult,
6
+ components: Record<string, ComponentFactory>,
7
+ effects: (() => void)[]
8
+ ): void
9
+
10
+ export function processRefs(root: HTMLElement, context: ComponentResult): void
11
+
12
+ export function processModel(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
13
+
14
+ export function processFor(
15
+ root: HTMLElement,
16
+ context: ComponentResult,
17
+ components: Record<string, ComponentFactory>,
18
+ effects: (() => void)[]
19
+ ): void
20
+
21
+ export function processIf(
22
+ root: HTMLElement,
23
+ context: ComponentResult,
24
+ components: Record<string, ComponentFactory>,
25
+ effects: (() => void)[]
26
+ ): void
27
+
28
+ export function processShow(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
29
+ export function processHtml(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
30
+ export function processText(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
31
+ export function processEvents(root: HTMLElement, context: ComponentResult): void
32
+ export function processCloak(root: HTMLElement): void
@@ -0,0 +1,15 @@
1
+ import type { ComponentResult } from './component'
2
+
3
+ export type InterpolationPart = {
4
+ type: 'static' | 'dynamic'
5
+ value?: string
6
+ key?: string
7
+ }
8
+
9
+ export const MUSTACHE_RE: RegExp
10
+
11
+ export function evaluateExpression(expr: string, context: Record<string, any>, extraContext?: Record<string, any>): any
12
+
13
+ export function parseInterpolation(str: string): InterpolationPart[]
14
+
15
+ export function sanitizeAttr(name: string, value: string): string
@@ -0,0 +1,9 @@
1
+ // ─── Fusée Framework Types ───────────────────────────────────────────────────
2
+
3
+ // Public API
4
+ export * from './signal'
5
+ export * from './component'
6
+ export * from './router'
7
+
8
+ // Re-export as namespace for convenience
9
+ export as namespace Fusee
@@ -0,0 +1,17 @@
1
+ // ─── Router Types ─────────────────────────────────────────────────────────────
2
+
3
+ import type { ComponentApi } from './component'
4
+
5
+ export type Route = {
6
+ path: string
7
+ component: () => ComponentApi
8
+ }
9
+
10
+ export type Router = {
11
+ navigate: (path: string) => void
12
+ destroy: () => void
13
+ }
14
+
15
+ export declare function createRouter(routes: Route[]): Router
16
+ export declare function navigate(path: string): void
17
+ export declare function mountOutlet(el: HTMLElement): void
@@ -0,0 +1,30 @@
1
+ // ─── Signal Types ────────────────────────────────────────────────────────────
2
+
3
+ export type SignalAccessor<T> = {
4
+ (): T;
5
+ (newValue: T): void;
6
+ isSignal: boolean;
7
+ }
8
+
9
+ export type ComputedAccessor<T> = {
10
+ (): T;
11
+ isSignal: boolean;
12
+ readonly: true;
13
+ }
14
+
15
+ export type Signal<T = any> = SignalAccessor<T>
16
+ export type Computed<T = any> = ComputedAccessor<T>
17
+
18
+ type EffectRunner = {
19
+ (): void;
20
+ deps: Set<Set<EffectRunner>>;
21
+ }
22
+
23
+ export declare function signal<T>(initialValue: T): Signal<T>
24
+ export declare function effect(fn: () => void): () => void
25
+ export declare function computed<T>(fn: () => T): Computed<T>
26
+ export declare function batch<T>(fn: () => T): T
27
+ export declare function untrack<T>(fn: () => T): T
28
+
29
+ declare function setEffectHook(fn: (eff: any) => void): void
30
+
package/index.html CHANGED
@@ -1,11 +1,18 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="ro">
3
+
3
4
  <head>
4
5
  <meta charset="UTF-8" />
5
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
7
  <title>MyFramework — SPA Demo</title>
7
8
  <style>
8
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+ *,
10
+ *::before,
11
+ *::after {
12
+ box-sizing: border-box;
13
+ margin: 0;
14
+ padding: 0;
15
+ }
9
16
 
10
17
  body {
11
18
  font-family: system-ui, sans-serif;
@@ -23,12 +30,18 @@
23
30
  }
24
31
 
25
32
  nav a {
26
- color: #8b8bff;
33
+ color: #ff3333; /* Link-urile sunt acum roșii */
27
34
  text-decoration: none;
28
35
  font-weight: 500;
29
36
  }
30
37
 
31
- nav a:hover { color: #fff; }
38
+ /* Temă Red Fusée */
39
+ #app h1 { color: #ff3333; text-shadow: 0 0 20px rgba(255, 51, 51, 0.4); margin-bottom: 1rem; }
40
+ #app p { color: #ff9999; opacity: 0.9; }
41
+
42
+ nav a:hover {
43
+ color: #fff;
44
+ }
32
45
 
33
46
  #app {
34
47
  padding: 2rem;
@@ -36,8 +49,16 @@
36
49
  margin: 0 auto;
37
50
  }
38
51
 
39
- .page h1 { margin-bottom: 1rem; font-size: 1.8rem; }
40
- .page p { margin-bottom: 0.5rem; color: #aaa; line-height: 1.6; }
52
+ .page h1 {
53
+ margin-bottom: 1rem;
54
+ font-size: 1.8rem;
55
+ }
56
+
57
+ .page p {
58
+ margin-bottom: 0.5rem;
59
+ color: #aaa;
60
+ line-height: 1.6;
61
+ }
41
62
 
42
63
  .counter {
43
64
  margin-top: 2rem;
@@ -48,7 +69,13 @@
48
69
  display: inline-block;
49
70
  }
50
71
 
51
- .counter h2 { margin-bottom: 1rem; font-size: 1rem; color: #888; text-transform: uppercase; letter-spacing: 0.1em; }
72
+ .counter h2 {
73
+ margin-bottom: 1rem;
74
+ font-size: 1rem;
75
+ color: #888;
76
+ text-transform: uppercase;
77
+ letter-spacing: 0.1em;
78
+ }
52
79
 
53
80
  .value {
54
81
  font-size: 4rem !important;
@@ -83,15 +110,51 @@
83
110
  transition: background 0.15s;
84
111
  }
85
112
 
86
- .actions button:hover { background: #2a2a3a; }
113
+ .actions button:hover {
114
+ background: #2a2a3a;
115
+ }
116
+
117
+ .primary-btn {
118
+ padding: 0.6rem 1.25rem;
119
+ border: none;
120
+ background: #8b8bff;
121
+ color: #fff;
122
+ font-weight: 600;
123
+ border-radius: 6px;
124
+ cursor: pointer;
125
+ font-size: 1rem;
126
+ transition: background 0.15s, transform 0.1s;
127
+ }
128
+
129
+ .primary-btn:hover {
130
+ background: #7171e5;
131
+ }
132
+
133
+ .primary-btn:active {
134
+ transform: scale(0.97);
135
+ }
87
136
 
88
137
  .status {
89
138
  margin-top: 1rem;
90
139
  font-size: 0.8rem;
91
140
  color: #4caf50 !important;
92
141
  }
142
+
143
+ .positive {
144
+ color: #8b8bff !important;
145
+ }
146
+
147
+ .negative {
148
+ color: #ff6b6b !important;
149
+ }
150
+
151
+ .actions button:disabled {
152
+ opacity: 0.3;
153
+ cursor: not-allowed;
154
+ }
93
155
  </style>
94
156
  </head>
157
+
95
158
  <body>
96
159
  <nav>
97
160
  <a href="#/">Home</a>
@@ -99,5 +162,9 @@
99
162
  </nav>
100
163
  <div id="app"></div>
101
164
  <script type="module" src="./app/main.js"></script>
165
+ <!-- For TypeScript: compile with `npx tsc` then change to: -->
166
+ <!-- <script type="module" src="./dist/app/main.js"></script> -->
167
+
102
168
  </body>
103
- </html>
169
+
170
+ </html>
package/package.json CHANGED
@@ -1,20 +1,31 @@
1
1
  {
2
2
  "name": "fusee-framework",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "1.0.0",
4
4
  "description": "Fusée - Signals-first JS Framework | Peak Performance",
5
5
  "main": "framework/index.js",
6
+ "types": "framework/types/index.d.ts",
7
+ "bin": {
8
+ "create-fusee-app": "./framework/bin/create-fusee.js"
9
+ },
10
+ "type": "module",
6
11
  "scripts": {
12
+ "dev": "vite",
13
+ "build": "vite build",
14
+ "preview": "vite preview",
7
15
  "test": "echo \"Error: no test specified\" && exit 1"
8
16
  },
9
17
  "keywords": [],
10
18
  "author": "",
11
19
  "license": "MIT",
12
- "type": "commonjs",
13
20
  "repository": {
14
21
  "type": "git",
15
22
  "url": "git+https://github.com/SebiSomu/fusee.git"
16
23
  },
17
24
  "publishConfig": {
18
25
  "registry": "https://registry.npmjs.org/"
26
+ },
27
+ "devDependencies": {
28
+ "typescript": "^6.0.2",
29
+ "vite": "^5.0.0"
19
30
  }
20
- }
31
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ES2020",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+
8
+ "allowJs": true,
9
+ "checkJs": false,
10
+ "strict": true,
11
+ "noImplicitAny": false,
12
+ "strictNullChecks": true,
13
+
14
+ "outDir": "./dist",
15
+ "rootDir": ".",
16
+ "removeComments": true,
17
+ "sourceMap": true,
18
+
19
+ "esModuleInterop": true,
20
+ "skipLibCheck": true,
21
+ "forceConsistentCasingInFileNames": true,
22
+
23
+ "declaration": true,
24
+ "declarationDir": "./dist/types",
25
+
26
+ "resolveJsonModule": true
27
+ },
28
+ "include": [
29
+ "app/**/*",
30
+ "framework/**/*"
31
+ ],
32
+ "exclude": [
33
+ "node_modules",
34
+ "dist",
35
+ "**/*.test.ts",
36
+ "**/*.spec.ts"
37
+ ]
38
+ }
package/vite.config.js ADDED
@@ -0,0 +1,35 @@
1
+ import { defineConfig } from 'vite'
2
+ import path from 'path'
3
+
4
+ export default defineConfig({
5
+ // Directorul rădăcină al proiectului
6
+ root: '.',
7
+
8
+ // Alias-uri pentru import-uri mai curate
9
+ // În loc de '../../framework/index.js' poți scrie '@fusee'
10
+ resolve: {
11
+ alias: {
12
+ '@fusee': path.resolve(__dirname, './framework'),
13
+ '@app': path.resolve(__dirname, './app'),
14
+ }
15
+ },
16
+
17
+ // Config pentru server-ul de development
18
+ server: {
19
+ port: 3000,
20
+ open: true, // Deschide browserul automat
21
+ hmr: true, // Hot Module Replacement
22
+ },
23
+
24
+ // Config pentru build de producție
25
+ build: {
26
+ outDir: 'dist',
27
+ emptyOutDir: true,
28
+
29
+ rollupOptions: {
30
+ input: {
31
+ main: path.resolve(__dirname, 'index.html'),
32
+ }
33
+ }
34
+ }
35
+ })