melina 1.3.0 → 1.3.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "melina",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "A lightweight, islands-architecture web framework for Bun with Next.js-style routing.",
5
5
  "module": "./src/web.ts",
6
6
  "main": "./src/web.ts",
package/src/jsx-dom.ts CHANGED
@@ -25,7 +25,45 @@ export function jsx(
25
25
  return tag(finalProps);
26
26
  }
27
27
 
28
- const el = document.createElement(tag);
28
+ // SVG elements must be created with the SVG namespace
29
+ const SVG_NS = 'http://www.w3.org/2000/svg';
30
+ const SVG_TAGS = new Set([
31
+ 'svg', 'path', 'circle', 'ellipse', 'line', 'polyline', 'polygon',
32
+ 'rect', 'g', 'defs', 'use', 'text', 'tspan', 'clipPath', 'mask',
33
+ 'image', 'pattern', 'linearGradient', 'radialGradient', 'stop',
34
+ 'filter', 'feGaussianBlur', 'feOffset', 'feMerge', 'feMergeNode',
35
+ 'foreignObject', 'marker', 'symbol', 'animate', 'animateTransform',
36
+ ]);
37
+ const isSVG = SVG_TAGS.has(tag);
38
+ const el = isSVG
39
+ ? document.createElementNS(SVG_NS, tag)
40
+ : document.createElement(tag);
41
+
42
+ // React camelCase → SVG dash-case attribute mapping
43
+ const SVG_ATTR_MAP: Record<string, string> = {
44
+ strokeWidth: 'stroke-width',
45
+ strokeLinecap: 'stroke-linecap',
46
+ strokeLinejoin: 'stroke-linejoin',
47
+ strokeDasharray: 'stroke-dasharray',
48
+ strokeDashoffset: 'stroke-dashoffset',
49
+ strokeMiterlimit: 'stroke-miterlimit',
50
+ strokeOpacity: 'stroke-opacity',
51
+ fillOpacity: 'fill-opacity',
52
+ fillRule: 'fill-rule',
53
+ clipRule: 'clip-rule',
54
+ clipPath: 'clip-path',
55
+ fontFamily: 'font-family',
56
+ fontSize: 'font-size',
57
+ fontWeight: 'font-weight',
58
+ textAnchor: 'text-anchor',
59
+ dominantBaseline: 'dominant-baseline',
60
+ colorInterpolation: 'color-interpolation',
61
+ colorInterpolationFilters: 'color-interpolation-filters',
62
+ floodColor: 'flood-color',
63
+ floodOpacity: 'flood-opacity',
64
+ lightingColor: 'lighting-color',
65
+ baselineShift: 'baseline-shift',
66
+ };
29
67
 
30
68
  // Set attributes/properties
31
69
  if (props) {
@@ -34,9 +72,13 @@ export function jsx(
34
72
  if (value === null || value === undefined || value === false) continue;
35
73
 
36
74
  if (key === 'style' && typeof value === 'object') {
37
- Object.assign(el.style, value);
75
+ Object.assign((el as HTMLElement).style, value);
38
76
  } else if (key === 'className' || key === 'class') {
39
- el.className = String(value);
77
+ if (isSVG) {
78
+ el.setAttribute('class', String(value));
79
+ } else {
80
+ (el as HTMLElement).className = String(value);
81
+ }
40
82
  } else if (key === 'htmlFor') {
41
83
  el.setAttribute('for', String(value));
42
84
  } else if (key === 'dangerouslySetInnerHTML') {
@@ -50,7 +92,9 @@ export function jsx(
50
92
  } else if (value === true) {
51
93
  el.setAttribute(key, '');
52
94
  } else {
53
- el.setAttribute(key, String(value));
95
+ // Map React camelCase SVG attributes to dash-case
96
+ const attr = isSVG ? (SVG_ATTR_MAP[key] || key) : key;
97
+ el.setAttribute(attr, String(value));
54
98
  }
55
99
  }
56
100
 
package/src/runtime.ts CHANGED
@@ -149,17 +149,12 @@ async function navigate(href: string) {
149
149
  window.scrollTo(0, 0);
150
150
  };
151
151
 
152
- if (document.startViewTransition) {
153
- const transition = document.startViewTransition(() => updateDOM());
154
- transition.finished.then(() => {
155
- mountPage(didPartialSwap);
156
- window.dispatchEvent(new CustomEvent('melina:navigated'));
157
- });
158
- } else {
159
- updateDOM();
160
- mountPage(didPartialSwap);
161
- window.dispatchEvent(new CustomEvent('melina:navigated'));
162
- }
152
+ // Run the DOM update directly — no View Transitions for partial swaps.
153
+ // View Transitions snapshot the entire viewport, causing untouched
154
+ // layout elements (like messenger) to visually flicker.
155
+ updateDOM();
156
+ mountPage(didPartialSwap);
157
+ window.dispatchEvent(new CustomEvent('melina:navigated'));
163
158
 
164
159
  } catch (error) {
165
160
  window.location.href = href;
@@ -226,16 +221,9 @@ function initializeLinkInterception() {
226
221
  }
227
222
  };
228
223
 
229
- if (document.startViewTransition) {
230
- document.startViewTransition(() => updateDOM()).finished.then(() => {
231
- mountPage(didPartialSwap);
232
- window.dispatchEvent(new CustomEvent('melina:navigated'));
233
- });
234
- } else {
235
- updateDOM();
236
- mountPage(didPartialSwap);
237
- window.dispatchEvent(new CustomEvent('melina:navigated'));
238
- }
224
+ updateDOM();
225
+ mountPage(didPartialSwap);
226
+ window.dispatchEvent(new CustomEvent('melina:navigated'));
239
227
  })
240
228
  .catch(err => {
241
229
  window.location.reload();
package/src/web.ts CHANGED
@@ -1268,8 +1268,8 @@ export function createAppRouter(options: AppRouterOptions = {}): Handler {
1268
1268
 
1269
1269
  if (isStringMode) {
1270
1270
  // ─── String mode: components return raw HTML strings ───
1271
- // Page content is already a string, wrap with layouts
1272
- let content = pageContent as string;
1271
+ // Page content is already a string, wrap in #melina-page-content then layouts
1272
+ let content = `<div id="melina-page-content" style="display:contents">${pageContent as string}</div>`;
1273
1273
  for (let i = match.route.layouts.length - 1; i >= 0; i--) {
1274
1274
  const layoutPath = match.route.layouts[i];
1275
1275
  const layoutModule = await importSSR(layoutPath);
@@ -1289,7 +1289,8 @@ export function createAppRouter(options: AppRouterOptions = {}): Handler {
1289
1289
  const React = await import('react');
1290
1290
  const ReactDOMServer = await import('react-dom/server');
1291
1291
 
1292
- let tree: any = pageContent;
1292
+ // Wrap page content in #melina-page-content for partial swap
1293
+ let tree: any = React.createElement('div', { id: 'melina-page-content', style: { display: 'contents' } }, pageContent);
1293
1294
 
1294
1295
  // Wrap with layouts (innermost to outermost)
1295
1296
  for (let i = match.route.layouts.length - 1; i >= 0; i--) {