@ape-egg/vibe 4.0.1 → 4.1.3

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/spa.js CHANGED
@@ -1,31 +1,5 @@
1
- // @ape-egg/vibe/spa
2
- //
3
- // Standalone SPA router — the tier between a handrolled router and compiler
4
- // SPA mode. Maintains one contract: $.page = { path, route, params, src,
5
- // name }, which a reactive component src (<component src="@[page.src]">)
6
- // turns into a route outlet.
7
- //
8
- // SOFT DEPENDENCY: nothing in Vibe's runtime imports this, and deleting it
9
- // leaves a working framework. Its one import is the standalone template cache
10
- // (itself dependency-free), so navigation prefetches ride the same in-flight
11
- // promise the outlet's mount consumes — one request per fragment. The default
12
- // onNavigate writes to the runtime global window.$ at call time; pass a
13
- // custom onNavigate and the module stays a pure router (parse/claim/history)
14
- // over that cache.
15
- //
16
- // Route grammar (shared with the compiler's route table and route scanners):
17
- // /static/path literal segments
18
- // /brawlers/:index :param captures one segment
19
- // /docs/:rest* trailing :name* captures zero or more segments
20
- // * declared no-match fallback — deep links and popstate
21
- // only, never claims a click
22
- // Tables are pre-sorted most-specific-first; first match wins.
23
-
24
1
  import { fetchComponentTemplate } from './runtime/component-cache.js';
25
2
 
26
- // Captured params arrive percent-encoded from the URL bar; hand authors the
27
- // decoded value. A malformed escape (user-typed %zz) keeps the raw segment —
28
- // URLs are external input, not authored code.
29
3
  const decode = (segment) => {
30
4
  try {
31
5
  return decodeURIComponent(segment);
@@ -34,9 +8,6 @@ const decode = (segment) => {
34
8
  }
35
9
  };
36
10
 
37
- // Match one pathname against one route template. Returns the params object
38
- // (possibly empty) on match, null otherwise. Empty segments are dropped, so
39
- // trailing slashes resolve to the same route.
40
11
  const matchRoute = (path, route) => {
41
12
  const routeSegments = route.split('/').filter(Boolean);
42
13
  const pathSegments = path.split('/').filter(Boolean);
@@ -54,21 +25,12 @@ const matchRoute = (path, route) => {
54
25
  return pathSegments.length === routeSegments.length ? params : null;
55
26
  };
56
27
 
57
- // Route slug: segments joined with dashes, params flattened to their bare
58
- // name — '/' → 'home', '/pve/:id' → 'pve-id', '/docs/:rest*' → 'docs-rest'.
59
- // Stable across param values, so markup hangs page-scoped attributes and
60
- // active checks on it: <page @[page.name]>, page.name.startsWith('pve').
61
28
  const routeName = (route) =>
62
29
  route
63
30
  .replace(/^\/+|\/+$/g, '')
64
31
  .replace(/:(\w+)\*?/g, '$1')
65
32
  .replace(/\//g, '-') || 'home';
66
33
 
67
- // Resolve a location (anything with a .pathname, or a bare path string)
68
- // against a route table → { path, route, params, src, name, title? } | null.
69
- // Pure: the compiled shell seeds initial $.page with it, unit tests drive it
70
- // directly. '*' is skipped during matching and applied only when nothing
71
- // real matched — it is a declared fallback, not a positional route.
72
34
  export const resolve = (location, routes) => {
73
35
  const path = location.pathname ?? location;
74
36
  for (const entry of routes) {
@@ -85,29 +47,16 @@ export const resolve = (location, routes) => {
85
47
  };
86
48
  }
87
49
  const fallback = routes.find((entry) => entry.route === '*');
88
- // 'not-found', not '*': the slug feeds name bindings (<page @[page.name]>)
89
- // and setAttribute('*') throws — the fallback must be attribute-safe.
90
50
  return fallback
91
51
  ? { path, route: '*', params: {}, src: fallback.src, name: 'not-found', title: fallback.title }
92
52
  : null;
93
53
  };
94
54
 
95
- // Programmatic navigation without holding the setupSpa return value: app code
96
- // imports this and calls it like a link click. With no router active (MPA
97
- // output, or before setupSpa runs) it falls back to a native load, so the same
98
- // call site works in both output modes.
99
55
  let activeRouter = null;
100
56
  export const navigate = (path) =>
101
57
  activeRouter ? activeRouter.navigate(path) : location.assign(new URL(path, location.origin));
102
58
 
103
- // Wire the router: one document-level click listener + popstate. Claiming
104
- // rule: same-origin, unmodified, untargeted clicks whose pathname matches a
105
- // REAL route — '*' never claims, so unrouted paths navigate natively (which
106
- // is what makes mixed MPA/SPA output work). Returns { navigate, dispose }.
107
59
  export const setupSpa = ({ routes, onNavigate }) => {
108
- // Default Vibe binding: one fresh-object assignment so bindings diff
109
- // cleanly, reading window.$ at call time (page scripts boot vibe before
110
- // setupSpa runs). A route-supplied title swaps document.title.
111
60
  const apply = onNavigate ?? (({ path, route, params, src, name, title }) => {
112
61
  window.$.page = { path, route, params, src, name };
113
62
  if (title) document.title = title;
@@ -118,14 +67,6 @@ export const setupSpa = ({ routes, onNavigate }) => {
118
67
  return resolved && resolved.route !== '*' ? resolved : null;
119
68
  };
120
69
 
121
- // Two-phase commit: the URL flips at interaction time, but route state —
122
- // and with it every binding the outgoing page still renders — flips only
123
- // once the incoming fragment's template is in hand. The old view never
124
- // re-renders against the new route, and the prefetch IS the mount's fetch
125
- // (same cache entry), so a navigation still costs one request. A failed
126
- // prefetch resolves anyway: the flip proceeds and the mount path owns the
127
- // error, exactly as before. The token makes rapid navigations last-wins —
128
- // a superseded prefetch never applies a stale flip.
129
70
  let navigationToken = 0;
130
71
  const commit = (resolved, { scroll, hash }) => {
131
72
  const token = ++navigationToken;
@@ -135,12 +76,6 @@ export const setupSpa = ({ routes, onNavigate }) => {
135
76
  prefetch.then(() => {
136
77
  if (token !== navigationToken) return;
137
78
  apply(resolved);
138
- // A cross-page hash link scrolls to its anchor once the fragment's
139
- // mount settles — late ready fires exactly then. Hashless navigations
140
- // start at the top. getElementById + decode, not querySelector: a URL
141
- // fragment is an element id, not a CSS selector — digit-leading ids
142
- // are valid HTML but invalid selectors, and fragments arrive
143
- // percent-encoded.
144
79
  if (hash)
145
80
  window.$?.on?.('ready', () =>
146
81
  document.getElementById(decodeURIComponent(hash.slice(1)))?.scrollIntoView(),
@@ -160,30 +95,19 @@ export const setupSpa = ({ routes, onNavigate }) => {
160
95
  if (anchor.target || anchor.hasAttribute('download')) return;
161
96
  if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button) return;
162
97
  if (event.defaultPrevented) return;
163
- // Same-page hash links keep the native anchor jump.
164
98
  if (anchor.hash && anchor.pathname === location.pathname) return;
165
99
  const resolved = claim(anchor);
166
100
  if (!resolved) return;
167
101
  event.preventDefault();
168
- // Same-URL click: claimed (no reload), but no duplicate history entry —
169
- // Back must keep meaning "the previous page". The WHOLE url: a link to
170
- // the same pathname with a different query string is a real navigation.
171
102
  if (anchor.href === location.href) return;
172
103
  push(anchor.href, resolved, anchor.hash || null);
173
104
  };
174
105
 
175
- // History traversal re-resolves WITH the '*' fallback — symmetric with
176
- // deep-link entry, whose URL may itself be a '*' page. No scroll: the
177
- // browser restores scroll position on popstate. Same two-phase commit:
178
- // going back re-renders the old view only when the target is ready.
179
106
  const onPopstate = () => {
180
107
  const resolved = resolve(location, routes);
181
108
  if (resolved) commit(resolved, { scroll: false });
182
109
  };
183
110
 
184
- // Programmatic navigation, claiming like a link click: real route match →
185
- // SPA navigation, anything else → native load (the server rewrite serves
186
- // the shell, whose deep-link resolution may then mount '*').
187
111
  const navigate = (path) => {
188
112
  const url = new URL(path, location.origin);
189
113
  const resolved = claim(url);
package/vibe.css CHANGED
@@ -1,46 +1,7 @@
1
- [vibe-fouc],
2
- .vibe-fouc {
3
- visibility: hidden;
4
- }
1
+ [vibe-fouc], .vibe-fouc { visibility: hidden }
2
+ [vibe-staged] { display: none }
3
+ [vibe-fouc], .vibe-fouc, [vibe-fouc] *, .vibe-fouc * { transition: none !important; animation: none !important }
5
4
 
6
- /* A route/key remount stages the incoming wrapper as a hidden sibling of the
7
- * outgoing page (which keeps its place, visible and styled) until the commit
8
- * swaps them in one paint. display: none keeps the staged content out of
9
- * layout so the two never occupy the document together. */
10
- [vibe-staged] {
11
- display: none;
12
- }
5
+ /* vibe:optional everything below is skipped by disableVibeCss */
13
6
 
14
- /* Animations too, not just transitions: visibility: hidden does not pause
15
- * CSS animations — a covered element still animates and still fires
16
- * animationend, reaching inline handlers before the module chain defines
17
- * them. Suppressed under the cover, entrance animations instead play on
18
- * reveal, matching what the user actually sees. */
19
- [vibe-fouc],
20
- .vibe-fouc,
21
- [vibe-fouc] *,
22
- .vibe-fouc * {
23
- transition: none !important;
24
- animation: none !important;
25
- }
26
-
27
- /* Vibe Dehydrate - Skip Reactive Processing
28
- * Elements with [vibe-dehydrate] attribute or .vibe-dehydrate class are skipped during
29
- * Vibe's parsing and hydration. Useful for displaying literal @[variable] syntax in
30
- * documentation, examples, or code snippets without triggering reactivity.
31
- */
32
- [vibe-dehydrate],
33
- .vibe-dehydrate {
34
- /* No styles applied - marker only */
35
- }
36
-
37
- /* Component Wrappers - Layout Transparent
38
- * Component wrappers (<component> or <div class="component">) use display: contents
39
- * to make the wrapper invisible in the layout. Children render as if the wrapper doesn't exist.
40
- */
41
- component,
42
- div.component,
43
- slot,
44
- div.slot {
45
- display: contents;
46
- }
7
+ component, div.component, slot, div.slot { display: contents }