@nuraly/lumenjs 0.1.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.
Files changed (76) hide show
  1. package/README.md +297 -0
  2. package/dist/build/build.d.ts +5 -0
  3. package/dist/build/build.js +172 -0
  4. package/dist/build/error-page.d.ts +1 -0
  5. package/dist/build/error-page.js +74 -0
  6. package/dist/build/scan.d.ts +21 -0
  7. package/dist/build/scan.js +93 -0
  8. package/dist/build/serve-api.d.ts +3 -0
  9. package/dist/build/serve-api.js +56 -0
  10. package/dist/build/serve-loaders.d.ts +4 -0
  11. package/dist/build/serve-loaders.js +115 -0
  12. package/dist/build/serve-ssr.d.ts +7 -0
  13. package/dist/build/serve-ssr.js +121 -0
  14. package/dist/build/serve-static.d.ts +6 -0
  15. package/dist/build/serve-static.js +80 -0
  16. package/dist/build/serve.d.ts +5 -0
  17. package/dist/build/serve.js +79 -0
  18. package/dist/cli.d.ts +2 -0
  19. package/dist/cli.js +65 -0
  20. package/dist/dev-server/config.d.ts +25 -0
  21. package/dist/dev-server/config.js +55 -0
  22. package/dist/dev-server/index-html.d.ts +16 -0
  23. package/dist/dev-server/index-html.js +46 -0
  24. package/dist/dev-server/nuralyui-aliases.d.ts +16 -0
  25. package/dist/dev-server/nuralyui-aliases.js +164 -0
  26. package/dist/dev-server/plugins/vite-plugin-api-routes.d.ts +23 -0
  27. package/dist/dev-server/plugins/vite-plugin-api-routes.js +250 -0
  28. package/dist/dev-server/plugins/vite-plugin-auto-import.d.ts +5 -0
  29. package/dist/dev-server/plugins/vite-plugin-auto-import.js +47 -0
  30. package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +5 -0
  31. package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +62 -0
  32. package/dist/dev-server/plugins/vite-plugin-lit-hmr.d.ts +5 -0
  33. package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +46 -0
  34. package/dist/dev-server/plugins/vite-plugin-loaders.d.ts +38 -0
  35. package/dist/dev-server/plugins/vite-plugin-loaders.js +320 -0
  36. package/dist/dev-server/plugins/vite-plugin-routes.d.ts +21 -0
  37. package/dist/dev-server/plugins/vite-plugin-routes.js +157 -0
  38. package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +5 -0
  39. package/dist/dev-server/plugins/vite-plugin-source-annotator.js +39 -0
  40. package/dist/dev-server/plugins/vite-plugin-virtual-modules.d.ts +5 -0
  41. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +38 -0
  42. package/dist/dev-server/server.d.ts +23 -0
  43. package/dist/dev-server/server.js +155 -0
  44. package/dist/dev-server/ssr-render.d.ts +20 -0
  45. package/dist/dev-server/ssr-render.js +170 -0
  46. package/dist/editor/click-select.d.ts +1 -0
  47. package/dist/editor/click-select.js +46 -0
  48. package/dist/editor/editor-bridge.d.ts +17 -0
  49. package/dist/editor/editor-bridge.js +101 -0
  50. package/dist/editor/element-annotator.d.ts +33 -0
  51. package/dist/editor/element-annotator.js +83 -0
  52. package/dist/editor/hover-detect.d.ts +1 -0
  53. package/dist/editor/hover-detect.js +36 -0
  54. package/dist/editor/inline-text-edit.d.ts +1 -0
  55. package/dist/editor/inline-text-edit.js +114 -0
  56. package/dist/integrations/add.d.ts +1 -0
  57. package/dist/integrations/add.js +89 -0
  58. package/dist/runtime/app-shell.d.ts +1 -0
  59. package/dist/runtime/app-shell.js +22 -0
  60. package/dist/runtime/response.d.ts +15 -0
  61. package/dist/runtime/response.js +13 -0
  62. package/dist/runtime/router-data.d.ts +3 -0
  63. package/dist/runtime/router-data.js +40 -0
  64. package/dist/runtime/router-hydration.d.ts +10 -0
  65. package/dist/runtime/router-hydration.js +68 -0
  66. package/dist/runtime/router.d.ts +35 -0
  67. package/dist/runtime/router.js +202 -0
  68. package/dist/shared/dom-shims.d.ts +5 -0
  69. package/dist/shared/dom-shims.js +63 -0
  70. package/dist/shared/route-matching.d.ts +6 -0
  71. package/dist/shared/route-matching.js +44 -0
  72. package/dist/shared/types.d.ts +16 -0
  73. package/dist/shared/types.js +1 -0
  74. package/dist/shared/utils.d.ts +42 -0
  75. package/dist/shared/utils.js +109 -0
  76. package/package.json +53 -0
@@ -0,0 +1,23 @@
1
+ import { ViteDevServer, UserConfig, Plugin } from 'vite';
2
+ export { readProjectConfig, readProjectTitle, getLumenJSNodeModules, getLumenJSDirs } from './config.js';
3
+ export type { ProjectConfig } from './config.js';
4
+ export { getNuralyUIAliases, resolveNuralyUIPaths } from './nuralyui-aliases.js';
5
+ export interface DevServerOptions {
6
+ projectDir: string;
7
+ port: number;
8
+ editorMode?: boolean;
9
+ base?: string;
10
+ }
11
+ /**
12
+ * Returns shared Vite config used by both dev and production builds.
13
+ * Includes NuralyUI aliases, lit dedup, loaders strip, auto-import, and virtual modules.
14
+ */
15
+ export declare function getSharedViteConfig(projectDir: string, options?: {
16
+ mode?: 'development' | 'production';
17
+ integrations?: string[];
18
+ }): {
19
+ resolve: UserConfig['resolve'];
20
+ esbuild: UserConfig['esbuild'];
21
+ plugins: Plugin[];
22
+ };
23
+ export declare function createDevServer(options: DevServerOptions): Promise<ViteDevServer>;
@@ -0,0 +1,155 @@
1
+ import { createServer as createViteServer } from 'vite';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { createRequire } from 'module';
5
+ import { pathToFileURL } from 'url';
6
+ import { lumenRoutesPlugin } from './plugins/vite-plugin-routes.js';
7
+ import { lumenApiRoutesPlugin } from './plugins/vite-plugin-api-routes.js';
8
+ import { lumenLoadersPlugin } from './plugins/vite-plugin-loaders.js';
9
+ import { generateIndexHtml } from './index-html.js';
10
+ import { ssrRenderPage } from './ssr-render.js';
11
+ import { readProjectConfig, getLumenJSNodeModules, getLumenJSDirs } from './config.js';
12
+ import { getNuralyUIAliases, resolveNuralyUIPaths } from './nuralyui-aliases.js';
13
+ import { litDedupPlugin } from './plugins/vite-plugin-lit-dedup.js';
14
+ import { autoImportPlugin } from './plugins/vite-plugin-auto-import.js';
15
+ import { litHmrPlugin } from './plugins/vite-plugin-lit-hmr.js';
16
+ import { sourceAnnotatorPlugin } from './plugins/vite-plugin-source-annotator.js';
17
+ import { virtualModulesPlugin } from './plugins/vite-plugin-virtual-modules.js';
18
+ // Re-export for backwards compatibility
19
+ export { readProjectConfig, readProjectTitle, getLumenJSNodeModules, getLumenJSDirs } from './config.js';
20
+ export { getNuralyUIAliases, resolveNuralyUIPaths } from './nuralyui-aliases.js';
21
+ /**
22
+ * Returns shared Vite config used by both dev and production builds.
23
+ * Includes NuralyUI aliases, lit dedup, loaders strip, auto-import, and virtual modules.
24
+ */
25
+ export function getSharedViteConfig(projectDir, options) {
26
+ const mode = options?.mode || 'development';
27
+ const isDev = mode === 'development';
28
+ const pagesDir = path.join(projectDir, 'pages');
29
+ const lumenNodeModules = getLumenJSNodeModules();
30
+ const { runtimeDir, editorDir } = getLumenJSDirs();
31
+ // Resolve NuralyUI paths for aliases
32
+ const nuralyUIPaths = resolveNuralyUIPaths(projectDir);
33
+ const aliases = {};
34
+ if (nuralyUIPaths) {
35
+ Object.assign(aliases, getNuralyUIAliases(nuralyUIPaths.componentsPath, nuralyUIPaths.commonPath));
36
+ }
37
+ const resolve = {
38
+ alias: { ...aliases },
39
+ conditions: isDev ? ['development', 'browser'] : ['browser'],
40
+ dedupe: ['lit', 'lit-html', 'lit-element', '@lit/reactive-element'],
41
+ };
42
+ const esbuild = {
43
+ tsconfigRaw: {
44
+ compilerOptions: {
45
+ experimentalDecorators: true,
46
+ useDefineForClassFields: false,
47
+ }
48
+ }
49
+ };
50
+ const plugins = [
51
+ lumenRoutesPlugin(pagesDir),
52
+ lumenLoadersPlugin(pagesDir),
53
+ litDedupPlugin(lumenNodeModules, isDev),
54
+ autoImportPlugin(projectDir),
55
+ virtualModulesPlugin(runtimeDir, editorDir),
56
+ ];
57
+ // Conditionally add Tailwind plugin from the project's node_modules
58
+ if (options?.integrations?.includes('tailwind')) {
59
+ try {
60
+ const projectRequire = createRequire(pathToFileURL(path.join(projectDir, 'package.json')).href);
61
+ const tailwindMod = projectRequire('@tailwindcss/vite');
62
+ const tailwindPlugin = tailwindMod.default || tailwindMod;
63
+ plugins.unshift(tailwindPlugin());
64
+ }
65
+ catch {
66
+ console.warn('[LumenJS] Tailwind integration enabled but @tailwindcss/vite not found. Run: lumenjs add tailwind');
67
+ }
68
+ }
69
+ return { resolve, esbuild, plugins };
70
+ }
71
+ export async function createDevServer(options) {
72
+ const { projectDir, port, editorMode = false, base = '/' } = options;
73
+ const pagesDir = path.join(projectDir, 'pages');
74
+ const apiDir = path.join(projectDir, 'api');
75
+ const publicDir = path.join(projectDir, 'public');
76
+ const config = readProjectConfig(projectDir);
77
+ const { title, integrations } = config;
78
+ const shared = getSharedViteConfig(projectDir, { integrations });
79
+ const server = await createViteServer({
80
+ root: projectDir,
81
+ publicDir: fs.existsSync(publicDir) ? publicDir : undefined,
82
+ server: {
83
+ port,
84
+ host: true,
85
+ strictPort: false,
86
+ allowedHosts: true,
87
+ cors: true,
88
+ hmr: true,
89
+ },
90
+ resolve: shared.resolve,
91
+ plugins: [
92
+ ...shared.plugins,
93
+ lumenApiRoutesPlugin(apiDir, projectDir),
94
+ litHmrPlugin(projectDir),
95
+ ...(editorMode ? [sourceAnnotatorPlugin(projectDir)] : []),
96
+ {
97
+ name: 'lumenjs-index-html',
98
+ configureServer(server) {
99
+ server.middlewares.use((req, res, next) => {
100
+ if (req.url && !req.url.startsWith('/@') && !req.url.startsWith('/node_modules') &&
101
+ !req.url.startsWith('/api/') && !req.url.startsWith('/__nk_loader/') &&
102
+ !req.url.includes('.') && req.method === 'GET') {
103
+ const pathname = req.url.split('?')[0];
104
+ const SSR_PLACEHOLDER = '<!--__NK_SSR_CONTENT__-->';
105
+ ssrRenderPage(server, pagesDir, pathname, req.headers).then(async (ssrResult) => {
106
+ if (ssrResult?.redirect) {
107
+ res.writeHead(ssrResult.redirect.status, { Location: ssrResult.redirect.location });
108
+ res.end();
109
+ return;
110
+ }
111
+ const shellHtml = generateIndexHtml({
112
+ title,
113
+ editorMode,
114
+ ssrContent: ssrResult ? SSR_PLACEHOLDER : undefined,
115
+ loaderData: ssrResult?.loaderData,
116
+ layoutsData: ssrResult?.layoutsData,
117
+ integrations,
118
+ });
119
+ const transformed = await server.transformIndexHtml(req.url, shellHtml);
120
+ const finalHtml = ssrResult
121
+ ? transformed.replace(SSR_PLACEHOLDER, ssrResult.html)
122
+ : transformed;
123
+ res.setHeader('Content-Type', 'text/html');
124
+ res.setHeader('Cache-Control', 'no-store');
125
+ res.end(finalHtml);
126
+ }).catch(err => {
127
+ console.error('[LumenJS] SSR/HTML generation error:', err);
128
+ const html = generateIndexHtml({ title, editorMode, integrations });
129
+ server.transformIndexHtml(req.url, html).then(transformed => {
130
+ res.setHeader('Content-Type', 'text/html');
131
+ res.setHeader('Cache-Control', 'no-store');
132
+ res.end(transformed);
133
+ }).catch(next);
134
+ });
135
+ return;
136
+ }
137
+ next();
138
+ });
139
+ }
140
+ }
141
+ ],
142
+ esbuild: shared.esbuild,
143
+ optimizeDeps: {
144
+ include: ['lit', 'lit/decorators.js', 'lit/directive.js', 'lit/directive-helpers.js', 'lit/async-directive.js', 'lit-html', 'lit-element', '@lit/reactive-element'],
145
+ },
146
+ ssr: {
147
+ noExternal: true,
148
+ external: ['node-domexception'],
149
+ resolve: {
150
+ conditions: ['node', 'import'],
151
+ },
152
+ },
153
+ });
154
+ return server;
155
+ }
@@ -0,0 +1,20 @@
1
+ import { ViteDevServer } from 'vite';
2
+ export interface LayoutSSRData {
3
+ loaderPath: string;
4
+ data: any;
5
+ }
6
+ /**
7
+ * Server-side render a LumenJS page using @lit-labs/ssr.
8
+ * Wraps the page in its layout chain if layouts exist.
9
+ *
10
+ * Returns pre-rendered HTML and loader data, or null on failure (falls back to CSR).
11
+ */
12
+ export declare function ssrRenderPage(server: ViteDevServer, pagesDir: string, pathname: string, headers?: Record<string, string | string[] | undefined>): Promise<{
13
+ html: string;
14
+ loaderData: any;
15
+ layoutsData?: LayoutSSRData[];
16
+ redirect?: {
17
+ location: string;
18
+ status: number;
19
+ };
20
+ } | null>;
@@ -0,0 +1,170 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { resolvePageFile, extractRouteParams } from './plugins/vite-plugin-loaders.js';
4
+ import { stripOuterLitMarkers, dirToLayoutTagName, findTagName } from '../shared/utils.js';
5
+ import { installDomShims } from '../shared/dom-shims.js';
6
+ /**
7
+ * Server-side render a LumenJS page using @lit-labs/ssr.
8
+ * Wraps the page in its layout chain if layouts exist.
9
+ *
10
+ * Returns pre-rendered HTML and loader data, or null on failure (falls back to CSR).
11
+ */
12
+ export async function ssrRenderPage(server, pagesDir, pathname, headers) {
13
+ try {
14
+ const filePath = resolvePageFile(pagesDir, pathname);
15
+ if (!filePath)
16
+ return null;
17
+ const params = extractRouteParams(pagesDir, pathname, filePath);
18
+ // Install @lit-labs/ssr DOM shim via Vite's SSR module loader
19
+ await server.ssrLoadModule('@lit-labs/ssr/lib/install-global-dom-shim.js');
20
+ // Patch missing DOM APIs that NuralyUI components may use during SSR
21
+ installDomShims();
22
+ // Invalidate SSR module cache so we always get fresh content after file edits.
23
+ // Also clear the custom element from the SSR registry so the new class is used.
24
+ const g = globalThis;
25
+ invalidateSsrModule(server, filePath);
26
+ clearSsrCustomElement(g);
27
+ // Load the page module via Vite (registers the custom element, applies transforms)
28
+ const mod = await server.ssrLoadModule(filePath);
29
+ // Run loader if present
30
+ let loaderData = undefined;
31
+ if (mod.loader && typeof mod.loader === 'function') {
32
+ loaderData = await mod.loader({ params, query: {}, url: pathname, headers: headers || {} });
33
+ if (loaderData && typeof loaderData === 'object' && loaderData.__nk_redirect) {
34
+ return { html: '', loaderData: null, redirect: { location: loaderData.location, status: loaderData.status || 302 } };
35
+ }
36
+ }
37
+ // Determine the custom element tag name
38
+ const tagName = findTagName(mod);
39
+ if (!tagName)
40
+ return null;
41
+ // Discover layout chain for this page
42
+ const layoutChain = discoverLayoutChain(pagesDir, filePath);
43
+ const layoutsData = [];
44
+ // Load layout modules and run their loaders
45
+ const layoutModules = [];
46
+ for (const layout of layoutChain) {
47
+ // Invalidate layout module cache and clear SSR element registry
48
+ invalidateSsrModule(server, layout.filePath);
49
+ clearSsrCustomElement(g);
50
+ const layoutMod = await server.ssrLoadModule(layout.filePath);
51
+ let layoutLoaderData = undefined;
52
+ if (layoutMod.loader && typeof layoutMod.loader === 'function') {
53
+ layoutLoaderData = await layoutMod.loader({ params: {}, query: {}, url: pathname, headers: headers || {} });
54
+ if (layoutLoaderData && typeof layoutLoaderData === 'object' && layoutLoaderData.__nk_redirect) {
55
+ return { html: '', loaderData: null, redirect: { location: layoutLoaderData.location, status: layoutLoaderData.status || 302 } };
56
+ }
57
+ }
58
+ const layoutTagName = dirToLayoutTagName(layout.dir);
59
+ layoutModules.push({ tagName: layoutTagName, loaderData: layoutLoaderData });
60
+ layoutsData.push({ loaderPath: layout.dir, data: layoutLoaderData });
61
+ }
62
+ // Load SSR render + lit/static-html.js through Vite (same module registry as page)
63
+ const { render } = await server.ssrLoadModule('@lit-labs/ssr');
64
+ const { html, unsafeStatic } = await server.ssrLoadModule('lit/static-html.js');
65
+ // Render each element separately to avoid nesting Lit template markers.
66
+ const pageTag = unsafeStatic(tagName);
67
+ const pageTemplate = html `<${pageTag} .loaderData=${loaderData}></${pageTag}>`;
68
+ let pageHtml = '';
69
+ for (const chunk of render(pageTemplate)) {
70
+ pageHtml += typeof chunk === 'string' ? chunk : String(chunk);
71
+ }
72
+ pageHtml = stripOuterLitMarkers(pageHtml);
73
+ let htmlStr = pageHtml;
74
+ for (let i = layoutModules.length - 1; i >= 0; i--) {
75
+ const layoutTag = unsafeStatic(layoutModules[i].tagName);
76
+ const layoutData = layoutModules[i].loaderData;
77
+ const layoutTemplate = html `<${layoutTag} .loaderData=${layoutData}></${layoutTag}>`;
78
+ let layoutHtml = '';
79
+ for (const chunk of render(layoutTemplate)) {
80
+ layoutHtml += typeof chunk === 'string' ? chunk : String(chunk);
81
+ }
82
+ if (i > 0) {
83
+ layoutHtml = stripOuterLitMarkers(layoutHtml);
84
+ }
85
+ const closingTag = `</${layoutModules[i].tagName}>`;
86
+ const closingIdx = layoutHtml.lastIndexOf(closingTag);
87
+ if (closingIdx !== -1) {
88
+ htmlStr = layoutHtml.slice(0, closingIdx) + htmlStr + layoutHtml.slice(closingIdx);
89
+ }
90
+ else {
91
+ htmlStr = layoutHtml + htmlStr;
92
+ }
93
+ }
94
+ return { html: htmlStr, loaderData, layoutsData: layoutsData.length > 0 ? layoutsData : undefined };
95
+ }
96
+ catch (err) {
97
+ console.error('[LumenJS] SSR render failed, falling back to CSR:', err);
98
+ return null;
99
+ }
100
+ }
101
+ /**
102
+ * Discover layout files for a given page, from root → deepest directory.
103
+ */
104
+ function discoverLayoutChain(pagesDir, pageFilePath) {
105
+ const relativeToPages = path.relative(pagesDir, pageFilePath).replace(/\\/g, '/');
106
+ const dirParts = path.dirname(relativeToPages).split('/').filter(p => p && p !== '.');
107
+ const chain = [];
108
+ // Check root layout
109
+ const rootLayout = findLayoutFile(pagesDir);
110
+ if (rootLayout)
111
+ chain.push({ dir: '', filePath: rootLayout });
112
+ // Check each directory level
113
+ let currentDir = pagesDir;
114
+ let relDir = '';
115
+ for (const part of dirParts) {
116
+ currentDir = path.join(currentDir, part);
117
+ relDir = relDir ? `${relDir}/${part}` : part;
118
+ const layoutFile = findLayoutFile(currentDir);
119
+ if (layoutFile)
120
+ chain.push({ dir: relDir, filePath: layoutFile });
121
+ }
122
+ return chain;
123
+ }
124
+ function findLayoutFile(dir) {
125
+ for (const ext of ['.ts', '.js']) {
126
+ const p = path.join(dir, `_layout${ext}`);
127
+ if (fs.existsSync(p))
128
+ return p;
129
+ }
130
+ return null;
131
+ }
132
+ /**
133
+ * Aggressively invalidate a module for SSR re-execution.
134
+ */
135
+ function invalidateSsrModule(server, filePath) {
136
+ const byFile = server.moduleGraph.getModulesByFile(filePath);
137
+ if (byFile) {
138
+ for (const m of byFile) {
139
+ server.moduleGraph.invalidateModule(m);
140
+ m.ssrModule = null;
141
+ m.ssrTransformResult = null;
142
+ }
143
+ }
144
+ const urlMod = server.moduleGraph.getModuleById(filePath);
145
+ if (urlMod) {
146
+ server.moduleGraph.invalidateModule(urlMod);
147
+ urlMod.ssrModule = null;
148
+ urlMod.ssrTransformResult = null;
149
+ }
150
+ }
151
+ /**
152
+ * Patch the SSR customElements registry to allow re-registration.
153
+ */
154
+ function clearSsrCustomElement(g) {
155
+ const registry = g.customElements;
156
+ if (!registry || registry.__nk_patched)
157
+ return;
158
+ registry.__nk_patched = true;
159
+ const origDefine = registry.define.bind(registry);
160
+ registry.define = (name, ctor) => {
161
+ if (registry.__definitions && registry.__definitions.has(name)) {
162
+ const oldCtor = registry.__definitions.get(name)?.ctor;
163
+ registry.__definitions.delete(name);
164
+ if (oldCtor && registry.__reverseDefinitions) {
165
+ registry.__reverseDefinitions.delete(oldCtor);
166
+ }
167
+ }
168
+ return origDefine(name, ctor);
169
+ };
170
+ }
@@ -0,0 +1 @@
1
+ export declare function setupClickToSelect(): void;
@@ -0,0 +1,46 @@
1
+ import { findAnnotatedElement } from './element-annotator.js';
2
+ import { sendToHost, getElementAttributes, getDynamicTexts, serializeRect, isPreviewMode } from './editor-bridge.js';
3
+ export function setupClickToSelect() {
4
+ let clickTimer = null;
5
+ let lastClickResult = null;
6
+ document.addEventListener('click', (event) => {
7
+ if (isPreviewMode())
8
+ return;
9
+ const result = findAnnotatedElement(event);
10
+ if (!result)
11
+ return;
12
+ event.preventDefault();
13
+ event.stopPropagation();
14
+ // Delay dispatch to distinguish single-click from double-click
15
+ lastClickResult = result;
16
+ if (clickTimer)
17
+ clearTimeout(clickTimer);
18
+ clickTimer = setTimeout(() => {
19
+ if (!lastClickResult)
20
+ return;
21
+ sendToHost({
22
+ type: 'NK_ELEMENT_CLICKED',
23
+ payload: {
24
+ sourceFile: lastClickResult.source.file,
25
+ line: lastClickResult.source.line,
26
+ tag: lastClickResult.source.tag,
27
+ attributes: getElementAttributes(lastClickResult.element),
28
+ nkId: lastClickResult.element.getAttribute('data-nk-id'),
29
+ rect: serializeRect(lastClickResult.element.getBoundingClientRect()),
30
+ dynamicTexts: getDynamicTexts(lastClickResult.element),
31
+ }
32
+ });
33
+ lastClickResult = null;
34
+ }, 250);
35
+ }, true);
36
+ document.addEventListener('dblclick', (event) => {
37
+ if (isPreviewMode())
38
+ return;
39
+ // Cancel the pending single-click
40
+ if (clickTimer) {
41
+ clearTimeout(clickTimer);
42
+ clickTimer = null;
43
+ }
44
+ lastClickResult = null;
45
+ }, true);
46
+ }
@@ -0,0 +1,17 @@
1
+ export interface NkEditorMessage {
2
+ type: 'NK_READY' | 'NK_ELEMENT_CLICKED' | 'NK_ELEMENT_HOVERED' | 'NK_SELECT_ELEMENT' | 'NK_HIGHLIGHT_ELEMENT' | 'NK_TEXT_CHANGED' | 'NK_SET_PREVIEW_MODE';
3
+ payload?: any;
4
+ }
5
+ export declare function isPreviewMode(): boolean;
6
+ export declare function sendToHost(message: NkEditorMessage): void;
7
+ export declare function getElementAttributes(el: HTMLElement): Record<string, string>;
8
+ export declare function getDynamicTexts(el: HTMLElement): Array<{
9
+ tag: string;
10
+ expression: string;
11
+ }>;
12
+ export declare function serializeRect(rect: DOMRect): {
13
+ top: number;
14
+ left: number;
15
+ width: number;
16
+ height: number;
17
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * LumenJS Editor Bridge — injected in editor mode.
3
+ *
4
+ * Handles click/hover detection on elements and communicates with the
5
+ * Studio host via postMessage. Follows the pattern from preview-iframe-bridge.ts.
6
+ */
7
+ import { startAnnotator } from './element-annotator.js';
8
+ import { setupClickToSelect } from './click-select.js';
9
+ import { setupHoverDetection } from './hover-detect.js';
10
+ import { setupInlineTextEdit } from './inline-text-edit.js';
11
+ let previewMode = false;
12
+ export function isPreviewMode() {
13
+ return previewMode;
14
+ }
15
+ export function sendToHost(message) {
16
+ if (window.parent && window.parent !== window) {
17
+ window.parent.postMessage(message, '*');
18
+ }
19
+ }
20
+ export function getElementAttributes(el) {
21
+ const attrs = {};
22
+ for (const attr of Array.from(el.attributes)) {
23
+ if (!attr.name.startsWith('data-nk-')) {
24
+ attrs[attr.name] = attr.value;
25
+ }
26
+ }
27
+ return attrs;
28
+ }
29
+ export function getDynamicTexts(el) {
30
+ const results = [];
31
+ const roots = [el];
32
+ if (el.shadowRoot)
33
+ roots.push(el.shadowRoot);
34
+ for (const root of roots) {
35
+ root.querySelectorAll('[data-nk-dynamic]').forEach((child) => {
36
+ const raw = child.getAttribute('data-nk-dynamic') || '';
37
+ if (raw) {
38
+ const expression = raw.replace(/__NK_EXPR__/g, '${');
39
+ results.push({ tag: child.tagName.toLowerCase(), expression });
40
+ }
41
+ });
42
+ }
43
+ return results;
44
+ }
45
+ export function serializeRect(rect) {
46
+ return { top: rect.top, left: rect.left, width: rect.width, height: rect.height };
47
+ }
48
+ function handleHostMessage(event) {
49
+ const message = event.data;
50
+ if (!message || typeof message !== 'object' || !message.type)
51
+ return;
52
+ switch (message.type) {
53
+ case 'NK_SELECT_ELEMENT': {
54
+ const { sourceFile, line } = message.payload || {};
55
+ const el = document.querySelector(`[data-nk-source="${sourceFile}:${line}"]`);
56
+ if (el instanceof HTMLElement) {
57
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
58
+ el.style.outline = '2px solid #3b82f6';
59
+ setTimeout(() => { el.style.outline = ''; }, 2000);
60
+ }
61
+ break;
62
+ }
63
+ case 'NK_HIGHLIGHT_ELEMENT': {
64
+ document.querySelectorAll('[data-nk-highlight]').forEach(el => {
65
+ el.removeAttribute('data-nk-highlight');
66
+ el.style.outline = '';
67
+ });
68
+ if (message.payload) {
69
+ const { sourceFile, line } = message.payload;
70
+ const el = document.querySelector(`[data-nk-source="${sourceFile}:${line}"]`);
71
+ if (el instanceof HTMLElement) {
72
+ el.setAttribute('data-nk-highlight', 'true');
73
+ el.style.outline = '1px dashed #3b82f6';
74
+ }
75
+ }
76
+ break;
77
+ }
78
+ case 'NK_SET_PREVIEW_MODE': {
79
+ previewMode = !!message.payload;
80
+ document.body.style.cursor = previewMode ? '' : 'default';
81
+ break;
82
+ }
83
+ }
84
+ }
85
+ function initEditorBridge() {
86
+ if (window.self === window.top)
87
+ return; // Not in iframe
88
+ startAnnotator();
89
+ setupClickToSelect();
90
+ setupHoverDetection();
91
+ setupInlineTextEdit();
92
+ window.addEventListener('message', handleHostMessage);
93
+ sendToHost({ type: 'NK_READY' });
94
+ }
95
+ // Auto-init
96
+ if (document.readyState === 'loading') {
97
+ document.addEventListener('DOMContentLoaded', initEditorBridge);
98
+ }
99
+ else {
100
+ initEditorBridge();
101
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Element Annotator — adds `data-nk-source` attributes to rendered Lit elements.
3
+ *
4
+ * In editor mode, this module observes the DOM for custom elements (those with a tag
5
+ * containing a hyphen) and annotates them with source file and line info so the
6
+ * editor bridge can map clicks back to source code.
7
+ *
8
+ * Source mapping relies on the `data-nk-source` attribute format: "file:line"
9
+ * This attribute is injected by a Vite transform or manually by the framework.
10
+ */
11
+ interface SourceInfo {
12
+ file: string;
13
+ line: number;
14
+ tag: string;
15
+ }
16
+ /**
17
+ * Parse a data-nk-source attribute value. Format: "file:line"
18
+ */
19
+ export declare function parseSourceAttr(value: string): SourceInfo | null;
20
+ /**
21
+ * Find the closest element with a data-nk-source attribute from an event.
22
+ * Traverses composed path for Shadow DOM support.
23
+ */
24
+ export declare function findAnnotatedElement(event: Event): {
25
+ element: HTMLElement;
26
+ source: SourceInfo;
27
+ } | null;
28
+ /**
29
+ * Start observing the DOM and annotate custom elements with sequential IDs.
30
+ * This helps the editor uniquely identify elements for AST modifications.
31
+ */
32
+ export declare function startAnnotator(): void;
33
+ export {};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Element Annotator — adds `data-nk-source` attributes to rendered Lit elements.
3
+ *
4
+ * In editor mode, this module observes the DOM for custom elements (those with a tag
5
+ * containing a hyphen) and annotates them with source file and line info so the
6
+ * editor bridge can map clicks back to source code.
7
+ *
8
+ * Source mapping relies on the `data-nk-source` attribute format: "file:line"
9
+ * This attribute is injected by a Vite transform or manually by the framework.
10
+ */
11
+ let annotatorActive = false;
12
+ /**
13
+ * Parse a data-nk-source attribute value. Format: "file:line"
14
+ */
15
+ export function parseSourceAttr(value) {
16
+ const lastColon = value.lastIndexOf(':');
17
+ if (lastColon === -1)
18
+ return null;
19
+ const file = value.substring(0, lastColon);
20
+ const line = parseInt(value.substring(lastColon + 1), 10);
21
+ if (isNaN(line))
22
+ return null;
23
+ return { file, line, tag: '' };
24
+ }
25
+ /**
26
+ * Find the closest element with a data-nk-source attribute from an event.
27
+ * Traverses composed path for Shadow DOM support.
28
+ */
29
+ export function findAnnotatedElement(event) {
30
+ const composedPath = event.composedPath();
31
+ for (const node of composedPath) {
32
+ if (!(node instanceof HTMLElement))
33
+ continue;
34
+ const sourceAttr = node.getAttribute('data-nk-source');
35
+ if (sourceAttr) {
36
+ const source = parseSourceAttr(sourceAttr);
37
+ if (source) {
38
+ source.tag = node.tagName.toLowerCase();
39
+ return { element: node, source };
40
+ }
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ /**
46
+ * Start observing the DOM and annotate custom elements with sequential IDs.
47
+ * This helps the editor uniquely identify elements for AST modifications.
48
+ */
49
+ export function startAnnotator() {
50
+ if (annotatorActive)
51
+ return;
52
+ annotatorActive = true;
53
+ let idCounter = 0;
54
+ const observer = new MutationObserver((mutations) => {
55
+ for (const mutation of mutations) {
56
+ for (const node of mutation.addedNodes) {
57
+ if (node instanceof HTMLElement) {
58
+ annotateTree(node);
59
+ }
60
+ }
61
+ }
62
+ });
63
+ function annotateTree(root) {
64
+ // Annotate the root if it's a custom element without an ID
65
+ if (root.tagName.includes('-') && !root.hasAttribute('data-nk-id')) {
66
+ root.setAttribute('data-nk-id', `nk-${idCounter++}`);
67
+ }
68
+ // Walk children
69
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
70
+ let current = walker.nextNode();
71
+ while (current) {
72
+ const el = current;
73
+ if (el.tagName.includes('-') && !el.hasAttribute('data-nk-id')) {
74
+ el.setAttribute('data-nk-id', `nk-${idCounter++}`);
75
+ }
76
+ current = walker.nextNode();
77
+ }
78
+ }
79
+ // Annotate existing elements
80
+ annotateTree(document.body);
81
+ // Watch for new elements
82
+ observer.observe(document.body, { childList: true, subtree: true });
83
+ }
@@ -0,0 +1 @@
1
+ export declare function setupHoverDetection(): void;