gonia 0.1.2 → 0.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.
@@ -9,7 +9,8 @@ import { processNativeSlot } from '../directives/slot.js';
9
9
  import { getLocalState, registerProvider, resolveFromProviders, registerDIProviders, resolveFromDIProviders } from '../providers.js';
10
10
  import { FOR_PROCESSED_ATTR } from '../directives/for.js';
11
11
  import { findParentScope, createElementScope, getElementScope } from '../scope.js';
12
- import { getInjectables } from '../inject.js';
12
+ import { resolveDependencies as resolveInjectables } from '../inject.js';
13
+ import { resolveContext } from '../context-registry.js';
13
14
  // Built-in directives
14
15
  import { text } from '../directives/text.js';
15
16
  import { show } from '../directives/show.js';
@@ -73,7 +74,14 @@ function getDirectivesForElement(el, registry) {
73
74
  for (const [name, directive] of registry) {
74
75
  const attr = el.getAttribute(`g-${name}`);
75
76
  if (attr !== null) {
76
- directives.push({ name, directive, expr: attr });
77
+ // Look up options from the global directive registry
78
+ const registration = getDirective(`g-${name}`);
79
+ directives.push({
80
+ name,
81
+ directive,
82
+ expr: attr,
83
+ using: registration?.options.using
84
+ });
77
85
  }
78
86
  }
79
87
  // Sort by priority (higher first)
@@ -124,48 +132,28 @@ export function setElementContext(el, ctx) {
124
132
  contextCache.set(el, ctx);
125
133
  }
126
134
  /**
127
- * Resolve dependencies for a directive based on its $inject array.
135
+ * Create resolver config for client-side dependency resolution.
128
136
  *
129
137
  * @internal
130
138
  */
131
- function resolveDependencies(directive, expr, el, ctx) {
132
- const inject = getInjectables(directive);
133
- return inject.map(name => {
134
- switch (name) {
135
- case '$expr':
136
- return expr;
137
- case '$element':
138
- return el;
139
- case '$eval':
140
- return ctx.eval.bind(ctx);
141
- case '$state':
142
- // Find nearest ancestor scope (including self)
143
- return findParentScope(el, true) ?? getLocalState(el);
144
- case '$rootState':
145
- // Deprecated: same as $state now (scoped state)
146
- return findParentScope(el, true) ?? getLocalState(el);
147
- case '$mode':
148
- return Mode.CLIENT;
149
- default: {
150
- // Look up in ancestor DI providers first (provide option)
151
- const diProvided = resolveFromDIProviders(el, name);
152
- if (diProvided !== undefined) {
153
- return diProvided;
154
- }
155
- // Look up in global services registry
156
- const service = services.get(name);
157
- if (service !== undefined) {
158
- return service;
159
- }
160
- // Look up in ancestor context providers ($context)
161
- const contextProvided = resolveFromProviders(el, name);
162
- if (contextProvided !== undefined) {
163
- return contextProvided;
164
- }
165
- throw new Error(`Unknown injectable: ${name}`);
166
- }
167
- }
168
- });
139
+ function createClientResolverConfig(el, ctx) {
140
+ return {
141
+ resolveContext: (key) => resolveContext(el, key),
142
+ resolveState: () => findParentScope(el, true) ?? getLocalState(el),
143
+ resolveCustom: (name) => {
144
+ // Look up in ancestor DI providers first (provide option)
145
+ const diProvided = resolveFromDIProviders(el, name);
146
+ if (diProvided !== undefined)
147
+ return diProvided;
148
+ // Look up in global services registry
149
+ const service = services.get(name);
150
+ if (service !== undefined)
151
+ return service;
152
+ // Look up in ancestor context providers ($context)
153
+ return resolveFromProviders(el, name);
154
+ },
155
+ mode: 'client'
156
+ };
169
157
  }
170
158
  /**
171
159
  * Process directives on a single element.
@@ -190,9 +178,10 @@ function processElement(el, registry) {
190
178
  const ctx = getContextForElement(el);
191
179
  // Process directives sequentially, handling async ones properly
192
180
  let chain;
193
- for (const { directive, expr } of directives) {
181
+ for (const { directive, expr, using } of directives) {
194
182
  const processDirective = () => {
195
- const args = resolveDependencies(directive, expr, el, ctx);
183
+ const config = createClientResolverConfig(el, ctx);
184
+ const args = resolveInjectables(directive, expr, el, ctx.eval.bind(ctx), config, using);
196
185
  const result = directive(...args);
197
186
  // Register as provider if directive declares $context
198
187
  if (directive.$context?.length) {
@@ -330,8 +319,8 @@ async function processDirectiveElements() {
330
319
  }
331
320
  const { fn, options } = registration;
332
321
  // Only process directives with templates (web components),
333
- // scope: true, or provide (DI overrides)
334
- if (!options.template && !options.scope && !options.provide) {
322
+ // scope: true, provide (DI overrides), or using (context dependencies)
323
+ if (!options.template && !options.scope && !options.provide && !options.using) {
335
324
  continue;
336
325
  }
337
326
  // Find all elements matching this directive's tag name
@@ -357,19 +346,12 @@ async function processDirectiveElements() {
357
346
  // 3. Call directive function if present (initializes state)
358
347
  if (fn) {
359
348
  const ctx = createContext(Mode.CLIENT, scope);
360
- const inject = getInjectables(fn);
361
- const args = inject.map((dep) => {
362
- switch (dep) {
363
- case '$element':
364
- return el;
365
- case '$state':
366
- return scope;
367
- case '$eval':
368
- return ctx.eval.bind(ctx);
369
- default:
370
- return undefined;
371
- }
372
- });
349
+ const config = {
350
+ resolveContext: (key) => resolveContext(el, key),
351
+ resolveState: () => scope,
352
+ mode: 'client'
353
+ };
354
+ const args = resolveInjectables(fn, '', el, ctx.eval.bind(ctx), config, options.using);
373
355
  const result = fn(...args);
374
356
  if (result instanceof Promise) {
375
357
  await result;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Type-safe context registry for sharing data across DOM ancestors/descendants.
3
+ *
4
+ * @remarks
5
+ * Provides a unified system for registering and resolving typed context values
6
+ * on DOM elements. Similar to React's Context or Vue's provide/inject but with
7
+ * full type safety through branded context keys.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ /**
12
+ * A branded context key that provides type safety for context values.
13
+ *
14
+ * @typeParam T - The type of value this context holds
15
+ *
16
+ * @remarks
17
+ * The `__type` property is a phantom type - it doesn't exist at runtime
18
+ * but provides compile-time type checking when registering and resolving.
19
+ */
20
+ export interface ContextKey<T> {
21
+ /** Unique identifier for this context */
22
+ readonly id: symbol;
23
+ /** Debug name for error messages */
24
+ readonly name: string;
25
+ /** Phantom type for TypeScript inference */
26
+ readonly __type?: T;
27
+ }
28
+ /**
29
+ * Create a typed context key.
30
+ *
31
+ * @typeParam T - The type of value this context will hold
32
+ * @param name - Debug name for the context (used in error messages)
33
+ * @returns A unique context key
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * interface UserData {
38
+ * name: string;
39
+ * email: string;
40
+ * }
41
+ *
42
+ * const UserContext = createContextKey<UserData>('User');
43
+ *
44
+ * // Register on an element
45
+ * registerContext(el, UserContext, { name: 'Alice', email: 'alice@example.com' });
46
+ *
47
+ * // Resolve from a descendant - fully typed!
48
+ * const user = resolveContext(childEl, UserContext);
49
+ * // user is UserData | undefined
50
+ * ```
51
+ */
52
+ export declare function createContextKey<T>(name: string): ContextKey<T>;
53
+ /**
54
+ * Register a context value on an element.
55
+ *
56
+ * @typeParam T - The context value type (inferred from key)
57
+ * @param el - The element to register the context on
58
+ * @param key - The context key
59
+ * @param value - The value to store
60
+ *
61
+ * @remarks
62
+ * Descendants can resolve this context using `resolveContext`.
63
+ * If a context with the same key is already registered on this element,
64
+ * it will be overwritten.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * const ThemeContext = createContextKey<{ mode: 'light' | 'dark' }>('Theme');
69
+ *
70
+ * registerContext(rootEl, ThemeContext, { mode: 'dark' });
71
+ * ```
72
+ */
73
+ export declare function registerContext<T>(el: Element, key: ContextKey<T>, value: T): void;
74
+ /**
75
+ * Resolve a context value from an ancestor element.
76
+ *
77
+ * @typeParam T - The context value type (inferred from key)
78
+ * @param el - The element to start searching from
79
+ * @param key - The context key to look for
80
+ * @param includeSelf - Whether to check the element itself (default: false)
81
+ * @returns The context value, or undefined if not found
82
+ *
83
+ * @remarks
84
+ * Walks up the DOM tree from the element's parent (or the element itself
85
+ * if `includeSelf` is true), looking for an ancestor with the specified
86
+ * context registered.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const ThemeContext = createContextKey<{ mode: 'light' | 'dark' }>('Theme');
91
+ *
92
+ * // Somewhere up the tree, ThemeContext was registered
93
+ * const theme = resolveContext(el, ThemeContext);
94
+ * if (theme) {
95
+ * console.log(theme.mode); // 'light' or 'dark'
96
+ * }
97
+ * ```
98
+ */
99
+ export declare function resolveContext<T>(el: Element, key: ContextKey<T>, includeSelf?: boolean): T | undefined;
100
+ /**
101
+ * Check if a context is registered on an element.
102
+ *
103
+ * @param el - The element to check
104
+ * @param key - The context key
105
+ * @returns True if the context is registered on this specific element
106
+ *
107
+ * @remarks
108
+ * This only checks the element itself, not ancestors.
109
+ * Use `resolveContext` to search up the tree.
110
+ */
111
+ export declare function hasContext<T>(el: Element, key: ContextKey<T>): boolean;
112
+ /**
113
+ * Remove a context from an element.
114
+ *
115
+ * @param el - The element to remove the context from
116
+ * @param key - The context key to remove
117
+ *
118
+ * @remarks
119
+ * Does nothing if the context wasn't registered.
120
+ */
121
+ export declare function removeContext<T>(el: Element, key: ContextKey<T>): void;
122
+ /**
123
+ * Clear all contexts from an element.
124
+ *
125
+ * @param el - The element to clear
126
+ *
127
+ * @remarks
128
+ * Called during element cleanup/removal.
129
+ */
130
+ export declare function clearContexts(el: Element): void;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Type-safe context registry for sharing data across DOM ancestors/descendants.
3
+ *
4
+ * @remarks
5
+ * Provides a unified system for registering and resolving typed context values
6
+ * on DOM elements. Similar to React's Context or Vue's provide/inject but with
7
+ * full type safety through branded context keys.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ import { findAncestor } from './dom.js';
12
+ /**
13
+ * Storage for all contexts per element.
14
+ *
15
+ * @internal
16
+ */
17
+ const contexts = new WeakMap();
18
+ /**
19
+ * Create a typed context key.
20
+ *
21
+ * @typeParam T - The type of value this context will hold
22
+ * @param name - Debug name for the context (used in error messages)
23
+ * @returns A unique context key
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * interface UserData {
28
+ * name: string;
29
+ * email: string;
30
+ * }
31
+ *
32
+ * const UserContext = createContextKey<UserData>('User');
33
+ *
34
+ * // Register on an element
35
+ * registerContext(el, UserContext, { name: 'Alice', email: 'alice@example.com' });
36
+ *
37
+ * // Resolve from a descendant - fully typed!
38
+ * const user = resolveContext(childEl, UserContext);
39
+ * // user is UserData | undefined
40
+ * ```
41
+ */
42
+ export function createContextKey(name) {
43
+ return {
44
+ id: Symbol(name),
45
+ name,
46
+ };
47
+ }
48
+ /**
49
+ * Register a context value on an element.
50
+ *
51
+ * @typeParam T - The context value type (inferred from key)
52
+ * @param el - The element to register the context on
53
+ * @param key - The context key
54
+ * @param value - The value to store
55
+ *
56
+ * @remarks
57
+ * Descendants can resolve this context using `resolveContext`.
58
+ * If a context with the same key is already registered on this element,
59
+ * it will be overwritten.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const ThemeContext = createContextKey<{ mode: 'light' | 'dark' }>('Theme');
64
+ *
65
+ * registerContext(rootEl, ThemeContext, { mode: 'dark' });
66
+ * ```
67
+ */
68
+ export function registerContext(el, key, value) {
69
+ let map = contexts.get(el);
70
+ if (!map) {
71
+ map = new Map();
72
+ contexts.set(el, map);
73
+ }
74
+ map.set(key.id, value);
75
+ }
76
+ /**
77
+ * Resolve a context value from an ancestor element.
78
+ *
79
+ * @typeParam T - The context value type (inferred from key)
80
+ * @param el - The element to start searching from
81
+ * @param key - The context key to look for
82
+ * @param includeSelf - Whether to check the element itself (default: false)
83
+ * @returns The context value, or undefined if not found
84
+ *
85
+ * @remarks
86
+ * Walks up the DOM tree from the element's parent (or the element itself
87
+ * if `includeSelf` is true), looking for an ancestor with the specified
88
+ * context registered.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * const ThemeContext = createContextKey<{ mode: 'light' | 'dark' }>('Theme');
93
+ *
94
+ * // Somewhere up the tree, ThemeContext was registered
95
+ * const theme = resolveContext(el, ThemeContext);
96
+ * if (theme) {
97
+ * console.log(theme.mode); // 'light' or 'dark'
98
+ * }
99
+ * ```
100
+ */
101
+ export function resolveContext(el, key, includeSelf = false) {
102
+ return findAncestor(el, (e) => {
103
+ const map = contexts.get(e);
104
+ if (map?.has(key.id)) {
105
+ return map.get(key.id);
106
+ }
107
+ return undefined;
108
+ }, includeSelf);
109
+ }
110
+ /**
111
+ * Check if a context is registered on an element.
112
+ *
113
+ * @param el - The element to check
114
+ * @param key - The context key
115
+ * @returns True if the context is registered on this specific element
116
+ *
117
+ * @remarks
118
+ * This only checks the element itself, not ancestors.
119
+ * Use `resolveContext` to search up the tree.
120
+ */
121
+ export function hasContext(el, key) {
122
+ const map = contexts.get(el);
123
+ return map?.has(key.id) ?? false;
124
+ }
125
+ /**
126
+ * Remove a context from an element.
127
+ *
128
+ * @param el - The element to remove the context from
129
+ * @param key - The context key to remove
130
+ *
131
+ * @remarks
132
+ * Does nothing if the context wasn't registered.
133
+ */
134
+ export function removeContext(el, key) {
135
+ const map = contexts.get(el);
136
+ if (map) {
137
+ map.delete(key.id);
138
+ if (map.size === 0) {
139
+ contexts.delete(el);
140
+ }
141
+ }
142
+ }
143
+ /**
144
+ * Clear all contexts from an element.
145
+ *
146
+ * @param el - The element to clear
147
+ *
148
+ * @remarks
149
+ * Called during element cleanup/removal.
150
+ */
151
+ export function clearContexts(el) {
152
+ contexts.delete(el);
153
+ }
@@ -9,12 +9,14 @@
9
9
  * @packageDocumentation
10
10
  */
11
11
  import { Directive } from '../types.js';
12
+ import { SlotContentContext } from './template.js';
12
13
  /**
13
14
  * Slot directive for content transclusion.
14
15
  *
15
16
  * @remarks
16
17
  * Finds the nearest template ancestor and transcludes the
17
- * matching slot content into itself.
18
+ * matching slot content into itself. The SlotContentContext
19
+ * is automatically injected via DI.
18
20
  *
19
21
  * If the slot name is an expression, wraps in an effect
20
22
  * for reactivity.
@@ -35,7 +37,7 @@ import { Directive } from '../types.js';
35
37
  * <slot></slot>
36
38
  * ```
37
39
  */
38
- export declare const slot: Directive<['$expr', '$element', '$eval']>;
40
+ export declare const slot: Directive<['$expr', '$element', '$eval', typeof SlotContentContext]>;
39
41
  /**
40
42
  * Process native <slot> elements.
41
43
  *
@@ -10,13 +10,15 @@
10
10
  */
11
11
  import { directive } from '../types.js';
12
12
  import { effect } from '../reactivity.js';
13
- import { findTemplateAncestor, getSavedContent } from './template.js';
13
+ import { resolveContext } from '../context-registry.js';
14
+ import { SlotContentContext } from './template.js';
14
15
  /**
15
16
  * Slot directive for content transclusion.
16
17
  *
17
18
  * @remarks
18
19
  * Finds the nearest template ancestor and transcludes the
19
- * matching slot content into itself.
20
+ * matching slot content into itself. The SlotContentContext
21
+ * is automatically injected via DI.
20
22
  *
21
23
  * If the slot name is an expression, wraps in an effect
22
24
  * for reactivity.
@@ -37,7 +39,7 @@ import { findTemplateAncestor, getSavedContent } from './template.js';
37
39
  * <slot></slot>
38
40
  * ```
39
41
  */
40
- export const slot = function slot($expr, $element, $eval) {
42
+ export const slot = function slot($expr, $element, $eval, $slotContent) {
41
43
  // Determine slot name
42
44
  // If expr is empty, check for name attribute, otherwise use 'default'
43
45
  const getName = () => {
@@ -50,16 +52,11 @@ export const slot = function slot($expr, $element, $eval) {
50
52
  };
51
53
  const transclude = () => {
52
54
  const name = getName();
53
- const templateEl = findTemplateAncestor($element);
54
- if (!templateEl) {
55
- // No template ancestor - leave slot as-is or clear it
55
+ // SlotContentContext is injected via DI
56
+ if (!$slotContent) {
56
57
  return;
57
58
  }
58
- const content = getSavedContent(templateEl);
59
- if (!content) {
60
- return;
61
- }
62
- const slotContent = content.slots.get(name);
59
+ const slotContent = $slotContent.slots.get(name);
63
60
  if (slotContent) {
64
61
  $element.innerHTML = slotContent;
65
62
  }
@@ -73,6 +70,7 @@ export const slot = function slot($expr, $element, $eval) {
73
70
  transclude();
74
71
  }
75
72
  };
73
+ slot.$inject = ['$expr', '$element', '$eval', SlotContentContext];
76
74
  directive('g-slot', slot);
77
75
  /**
78
76
  * Process native <slot> elements.
@@ -85,11 +83,8 @@ directive('g-slot', slot);
85
83
  */
86
84
  export function processNativeSlot(el) {
87
85
  const name = el.getAttribute('name') ?? 'default';
88
- const templateEl = findTemplateAncestor(el);
89
- if (!templateEl) {
90
- return;
91
- }
92
- const content = getSavedContent(templateEl);
86
+ // Resolve slot content from nearest template ancestor
87
+ const content = resolveContext(el, SlotContentContext);
93
88
  if (!content) {
94
89
  return;
95
90
  }
@@ -10,15 +10,22 @@
10
10
  */
11
11
  import { Directive } from '../types.js';
12
12
  import { EffectScope } from '../reactivity.js';
13
+ import { ContextKey } from '../context-registry.js';
13
14
  /**
14
15
  * Saved slot content for an element.
15
- *
16
- * @internal
17
16
  */
18
17
  export interface SlotContent {
19
18
  /** Content by slot name. 'default' for unnamed content. */
20
19
  slots: Map<string, string>;
21
20
  }
21
+ /**
22
+ * Context key for slot content.
23
+ *
24
+ * @remarks
25
+ * Templates register their slot content using this context, and
26
+ * slot directives resolve it to find their content.
27
+ */
28
+ export declare const SlotContentContext: ContextKey<SlotContent>;
22
29
  /**
23
30
  * Get saved slot content for an element.
24
31
  *
@@ -10,8 +10,16 @@
10
10
  */
11
11
  import { directive, DirectivePriority } from '../types.js';
12
12
  import { createEffectScope } from '../reactivity.js';
13
- /** WeakMap storing saved children per element. */
14
- const savedContent = new WeakMap();
13
+ import { findAncestor } from '../dom.js';
14
+ import { createContextKey, registerContext, resolveContext, hasContext } from '../context-registry.js';
15
+ /**
16
+ * Context key for slot content.
17
+ *
18
+ * @remarks
19
+ * Templates register their slot content using this context, and
20
+ * slot directives resolve it to find their content.
21
+ */
22
+ export const SlotContentContext = createContextKey('SlotContent');
15
23
  /** WeakMap storing effect scopes per element for cleanup. */
16
24
  const elementScopes = new WeakMap();
17
25
  /** Set tracking which templates are currently rendering (cycle detection). */
@@ -22,7 +30,7 @@ const renderingChain = new WeakMap();
22
30
  * @internal
23
31
  */
24
32
  export function getSavedContent(el) {
25
- return savedContent.get(el);
33
+ return resolveContext(el, SlotContentContext, true);
26
34
  }
27
35
  /**
28
36
  * Find the nearest ancestor with saved content (the template element).
@@ -30,14 +38,7 @@ export function getSavedContent(el) {
30
38
  * @internal
31
39
  */
32
40
  export function findTemplateAncestor(el) {
33
- let current = el.parentElement;
34
- while (current) {
35
- if (savedContent.has(current)) {
36
- return current;
37
- }
38
- current = current.parentElement;
39
- }
40
- return null;
41
+ return findAncestor(el, (e) => hasContext(e, SlotContentContext) ? e : undefined) ?? null;
41
42
  }
42
43
  /**
43
44
  * Check if a node is an Element.
@@ -116,11 +117,11 @@ export const template = async function template($expr, $element, $templates) {
116
117
  console.error(`Cycle detected: template "${templateName}" is already being rendered`);
117
118
  return;
118
119
  }
119
- // Save children for slots
120
+ // Save children for slots using typed context
120
121
  const slotContent = {
121
122
  slots: extractSlotContent($element)
122
123
  };
123
- savedContent.set($element, slotContent);
124
+ registerContext($element, SlotContentContext, slotContent);
124
125
  // Track this template in the chain
125
126
  const newChain = new Set(chain);
126
127
  newChain.add(templateName);
package/dist/dom.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * DOM traversal utilities.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+ /**
7
+ * Find an ancestor element matching a predicate.
8
+ *
9
+ * @remarks
10
+ * Walks up the DOM tree starting from the element's parent (or the element
11
+ * itself if `includeSelf` is true), calling the predicate on each ancestor.
12
+ * Returns the first non-undefined result from the predicate.
13
+ *
14
+ * @typeParam T - The type returned by the predicate
15
+ * @param el - The element to start from
16
+ * @param predicate - Function called on each ancestor, returns a value or undefined
17
+ * @param includeSelf - Whether to check the element itself (default: false)
18
+ * @returns The first non-undefined result from the predicate, or undefined
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * // Find ancestor with a specific attribute
23
+ * const ancestor = findAncestor(el, (e) => e.hasAttribute('data-scope') ? e : undefined);
24
+ *
25
+ * // Find value from a WeakMap on an ancestor
26
+ * const value = findAncestor(el, (e) => myWeakMap.get(e));
27
+ * ```
28
+ */
29
+ export declare function findAncestor<T>(el: Element, predicate: (el: Element) => T | undefined, includeSelf?: boolean): T | undefined;
package/dist/dom.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * DOM traversal utilities.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+ /**
7
+ * Find an ancestor element matching a predicate.
8
+ *
9
+ * @remarks
10
+ * Walks up the DOM tree starting from the element's parent (or the element
11
+ * itself if `includeSelf` is true), calling the predicate on each ancestor.
12
+ * Returns the first non-undefined result from the predicate.
13
+ *
14
+ * @typeParam T - The type returned by the predicate
15
+ * @param el - The element to start from
16
+ * @param predicate - Function called on each ancestor, returns a value or undefined
17
+ * @param includeSelf - Whether to check the element itself (default: false)
18
+ * @returns The first non-undefined result from the predicate, or undefined
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * // Find ancestor with a specific attribute
23
+ * const ancestor = findAncestor(el, (e) => e.hasAttribute('data-scope') ? e : undefined);
24
+ *
25
+ * // Find value from a WeakMap on an ancestor
26
+ * const value = findAncestor(el, (e) => myWeakMap.get(e));
27
+ * ```
28
+ */
29
+ export function findAncestor(el, predicate, includeSelf = false) {
30
+ let current = includeSelf ? el : el.parentElement;
31
+ while (current) {
32
+ const result = predicate(current);
33
+ if (result !== undefined) {
34
+ return result;
35
+ }
36
+ current = current.parentElement;
37
+ }
38
+ return undefined;
39
+ }