sinho 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 (117) hide show
  1. package/.github/workflows/ci.yml +24 -0
  2. package/.github/workflows/deploy-docs.yml +47 -0
  3. package/.prettierrc +3 -0
  4. package/LICENSE.md +21 -0
  5. package/README.md +33 -0
  6. package/ci/check-size.js +8 -0
  7. package/dist/array_mutation.d.ts +16 -0
  8. package/dist/array_mutation.js +75 -0
  9. package/dist/array_mutation.js.map +1 -0
  10. package/dist/bundle.d.ts +1126 -0
  11. package/dist/bundle.js +1074 -0
  12. package/dist/bundle.min.js +1 -0
  13. package/dist/component.d.ts +253 -0
  14. package/dist/component.js +256 -0
  15. package/dist/component.js.map +1 -0
  16. package/dist/context.d.ts +21 -0
  17. package/dist/context.js +34 -0
  18. package/dist/context.js.map +1 -0
  19. package/dist/create_element.d.ts +43 -0
  20. package/dist/create_element.js +43 -0
  21. package/dist/create_element.js.map +1 -0
  22. package/dist/dom.d.ts +602 -0
  23. package/dist/dom.js +97 -0
  24. package/dist/dom.js.map +1 -0
  25. package/dist/intrinsic/ClassComponent.d.ts +2 -0
  26. package/dist/intrinsic/ClassComponent.js +10 -0
  27. package/dist/intrinsic/ClassComponent.js.map +1 -0
  28. package/dist/intrinsic/Dynamic.d.ts +33 -0
  29. package/dist/intrinsic/Dynamic.js +53 -0
  30. package/dist/intrinsic/Dynamic.js.map +1 -0
  31. package/dist/intrinsic/ErrorBoundary.d.ts +14 -0
  32. package/dist/intrinsic/ErrorBoundary.js +36 -0
  33. package/dist/intrinsic/ErrorBoundary.js.map +1 -0
  34. package/dist/intrinsic/For.d.ts +10 -0
  35. package/dist/intrinsic/For.js +81 -0
  36. package/dist/intrinsic/For.js.map +1 -0
  37. package/dist/intrinsic/Fragment.d.ts +23 -0
  38. package/dist/intrinsic/Fragment.js +28 -0
  39. package/dist/intrinsic/Fragment.js.map +1 -0
  40. package/dist/intrinsic/If.d.ts +24 -0
  41. package/dist/intrinsic/If.js +47 -0
  42. package/dist/intrinsic/If.js.map +1 -0
  43. package/dist/intrinsic/Portal.d.ts +6 -0
  44. package/dist/intrinsic/Portal.js +15 -0
  45. package/dist/intrinsic/Portal.js.map +1 -0
  46. package/dist/intrinsic/Style.d.ts +7 -0
  47. package/dist/intrinsic/Style.js +70 -0
  48. package/dist/intrinsic/Style.js.map +1 -0
  49. package/dist/intrinsic/TagComponent.d.ts +4 -0
  50. package/dist/intrinsic/TagComponent.js +67 -0
  51. package/dist/intrinsic/TagComponent.js.map +1 -0
  52. package/dist/intrinsic/Text.d.ts +6 -0
  53. package/dist/intrinsic/Text.js +16 -0
  54. package/dist/intrinsic/Text.js.map +1 -0
  55. package/dist/intrinsic/mod.d.ts +5 -0
  56. package/dist/intrinsic/mod.js +6 -0
  57. package/dist/intrinsic/mod.js.map +1 -0
  58. package/dist/jsx-runtime/mod.d.ts +23 -0
  59. package/dist/jsx-runtime/mod.js +11 -0
  60. package/dist/jsx-runtime/mod.js.map +1 -0
  61. package/dist/mod.d.ts +8 -0
  62. package/dist/mod.js +7 -0
  63. package/dist/mod.js.map +1 -0
  64. package/dist/renderer.d.ts +13 -0
  65. package/dist/renderer.js +25 -0
  66. package/dist/renderer.js.map +1 -0
  67. package/dist/scope.d.ts +138 -0
  68. package/dist/scope.js +228 -0
  69. package/dist/scope.js.map +1 -0
  70. package/dist/template.d.ts +10 -0
  71. package/dist/template.js +7 -0
  72. package/dist/template.js.map +1 -0
  73. package/dist/utils.d.ts +6 -0
  74. package/dist/utils.js +13 -0
  75. package/dist/utils.js.map +1 -0
  76. package/package.json +71 -0
  77. package/src/array_mutation.ts +118 -0
  78. package/src/component.ts +624 -0
  79. package/src/context.ts +70 -0
  80. package/src/create_element.ts +89 -0
  81. package/src/dom.ts +819 -0
  82. package/src/intrinsic/ClassComponent.ts +17 -0
  83. package/src/intrinsic/For.ts +122 -0
  84. package/src/intrinsic/Fragment.ts +38 -0
  85. package/src/intrinsic/If.ts +73 -0
  86. package/src/intrinsic/Portal.ts +25 -0
  87. package/src/intrinsic/Style.ts +120 -0
  88. package/src/intrinsic/TagComponent.ts +102 -0
  89. package/src/intrinsic/Text.ts +24 -0
  90. package/src/intrinsic/mod.ts +5 -0
  91. package/src/jsx-runtime/mod.ts +41 -0
  92. package/src/mod.ts +37 -0
  93. package/src/renderer.ts +45 -0
  94. package/src/scope.ts +404 -0
  95. package/src/template.ts +16 -0
  96. package/src/utils.ts +29 -0
  97. package/terser.config.json +16 -0
  98. package/tsconfig.json +18 -0
  99. package/web/README.md +41 -0
  100. package/web/babel.config.js +3 -0
  101. package/web/dist/shingo.min.d.ts +1131 -0
  102. package/web/dist/shingo.min.js +1 -0
  103. package/web/docusaurus.config.ts +151 -0
  104. package/web/package-lock.json +14850 -0
  105. package/web/package.json +54 -0
  106. package/web/sidebars.ts +31 -0
  107. package/web/src/components/monacoEditor.tsx +72 -0
  108. package/web/src/components/playground.tsx +89 -0
  109. package/web/src/components/playgroundComponent.tsx +168 -0
  110. package/web/src/css/custom.css +37 -0
  111. package/web/src/pages/index.module.css +31 -0
  112. package/web/src/pages/index.tsx +73 -0
  113. package/web/src/pages/playground.tsx +64 -0
  114. package/web/static/.nojekyll +0 -0
  115. package/web/static/dist/bundle.d.ts +1126 -0
  116. package/web/static/dist/bundle.min.js +1 -0
  117. package/web/tsconfig.json +8 -0
@@ -0,0 +1,624 @@
1
+ import {
2
+ Cleanup,
3
+ MaybeSignal,
4
+ Signal,
5
+ useEffect,
6
+ useSubscope,
7
+ useSignal,
8
+ } from "./scope.js";
9
+ import type { DomEventProps, DomProps } from "./dom.js";
10
+ import { runWithRenderer } from "./renderer.js";
11
+ import {
12
+ camelCaseToKebabCase,
13
+ JsxPropNameToEventName,
14
+ jsxPropNameToEventName,
15
+ } from "./utils.js";
16
+ import { useScope } from "./scope.js";
17
+ import { Context, isContext, provideContext } from "./context.js";
18
+ import { Template } from "./template.js";
19
+
20
+ interface Tagged<in out T> {
21
+ _tag: T;
22
+ }
23
+
24
+ type OmitNever<T> = Omit<
25
+ T,
26
+ { [K in keyof T]: T[K] extends never ? K : never }[keyof T]
27
+ >;
28
+
29
+ type PartialRequire<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
30
+
31
+ /** @ignore */
32
+ export interface PropMeta<T> extends PropOptions<T>, Tagged<"p"> {
33
+ _type?: [T];
34
+ _defaultOrContext: T;
35
+ }
36
+
37
+ export interface AttributeOptions<T> {
38
+ /**
39
+ * The name of the attribute to observe.
40
+ *
41
+ * Defaults to the kebab-case version of the prop.
42
+ */
43
+ name?: string;
44
+ /**
45
+ * A function to transform the attribute value to the prop value.
46
+ */
47
+ transform: (value: string) => T;
48
+ /**
49
+ * Set to `true` to not observe the attribute for changes.
50
+ *
51
+ * @default false
52
+ */
53
+ static?: boolean;
54
+ }
55
+
56
+ type PartialPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
57
+
58
+ export interface PropOptions<T> {
59
+ attribute?:
60
+ | ((value: string) => T)
61
+ | (string extends T
62
+ ? PartialPartial<AttributeOptions<T>, "transform">
63
+ : AttributeOptions<T>);
64
+ }
65
+
66
+ type Props<M> = OmitNever<{
67
+ readonly [K in keyof M]: M[K] extends PropMeta<infer T> ? Signal<T> : never;
68
+ }>;
69
+
70
+ export type EventConstructor<T = any, E = Event> = new (
71
+ name: string,
72
+ arg: T,
73
+ ) => E;
74
+
75
+ /** @ignore */
76
+ export interface EventMeta<out E extends EventConstructor> extends Tagged<"e"> {
77
+ _event: E;
78
+ }
79
+
80
+ type Events<M> = OmitNever<
81
+ Omit<
82
+ {
83
+ readonly [K in keyof M]: K extends `on${string}`
84
+ ? M[K] extends EventMeta<infer E>
85
+ ? E
86
+ : never
87
+ : never;
88
+ },
89
+ `on${Lowercase<keyof HTMLElementEventMap>}`
90
+ >
91
+ >;
92
+
93
+ type GeneralJsxProps<T> = Partial<
94
+ OmitNever<{
95
+ [K in keyof T]: K extends
96
+ | typeof jsxPropsSym
97
+ | keyof DomProps<any>
98
+ | `on${string}`
99
+ // Readonly HTMLElement properties
100
+ | `${Uppercase<infer _>}${string}`
101
+ | "accessKeyLabel"
102
+ | "offsetHeight"
103
+ | "offsetLeft"
104
+ | "offsetParent"
105
+ | "offsetTop"
106
+ | "offsetWidth"
107
+ | "attributes"
108
+ | "classList"
109
+ | "clientHeight"
110
+ | "clientLeft"
111
+ | "clientTop"
112
+ | "clientWidth"
113
+ | "localName"
114
+ | "namespaceURI"
115
+ | "ownerDocument"
116
+ | "part"
117
+ | "prefix"
118
+ | "scrollHeight"
119
+ | "scrollWidth"
120
+ | "shadowRoot"
121
+ | "tagName"
122
+ | "baseURI"
123
+ | "childNodes"
124
+ | "firstChild"
125
+ | "isConnected"
126
+ | "lastChild"
127
+ | "nextSibling"
128
+ | "nodeName"
129
+ | "nodeType"
130
+ | "parentElement"
131
+ | "parentNode"
132
+ | "previousSibling"
133
+ | "nextElementSibling"
134
+ | "previousElementSibling"
135
+ | "childElementCount"
136
+ | "firstElementChild"
137
+ | "lastElementChild"
138
+ | "assignedSlot"
139
+ | "attributeStyleMap"
140
+ | "isContentEditable"
141
+ | "dataset"
142
+ ? never
143
+ : T[K] extends Function
144
+ ? never
145
+ : MaybeSignal<T[K]>;
146
+ }>
147
+ > &
148
+ DomProps<any> &
149
+ DomEventProps<never> &
150
+ // Allow other HTMLElement attributes
151
+ Record<string, any>;
152
+
153
+ export type JsxProps<T extends HTMLElement> = typeof jsxPropsSym extends keyof T
154
+ ? NonNullable<T[typeof jsxPropsSym]>
155
+ : any;
156
+
157
+ type ComponentJsxProps<M> = Partial<
158
+ OmitNever<{
159
+ [K in keyof Props<M>]: Props<M>[K] extends Signal<infer T>
160
+ ? MaybeSignal<T>
161
+ : never;
162
+ }> & {
163
+ [K in keyof Events<M>]: (evt: InstanceType<Events<M>[K]>) => void;
164
+ }
165
+ > &
166
+ GeneralJsxProps<HTMLElement>;
167
+
168
+ type EventEmitters<M> = OmitNever<
169
+ Omit<
170
+ {
171
+ [K in keyof Events<M>]: Events<M>[K] extends EventConstructor<infer E>
172
+ ? undefined extends E
173
+ ? (arg?: E) => boolean
174
+ : (arg: E) => boolean
175
+ : never;
176
+ },
177
+ `on${Lowercase<keyof HTMLElementEventMap>}`
178
+ >
179
+ >;
180
+
181
+ /**
182
+ * Defines a property in your component metadata that can be set from outside
183
+ * of the component.
184
+ *
185
+ * Make sure to avoid conflicts with native `HTMLElement` properties.
186
+ *
187
+ * You can get properties by accessing the {@link Signal} in `this.props`.
188
+ * It's also possible to set the properties directly on the component instance.
189
+ *
190
+ * It's also possible to define an attribute for the property by setting the
191
+ * `attribute` option. By default, the attribute name is the kebab-case version
192
+ * of the property name. The attribute will be observed and the signal updated
193
+ * on changes. You can also provide a custom name and a transform function to
194
+ * convert the attribute value to the property value.
195
+ *
196
+ * @example
197
+ * ```tsx
198
+ * class App extends Component("x-app", {
199
+ * greetingMessage: prop<string>("Hello, world!", {
200
+ * attribute: {
201
+ * name: "greeting",
202
+ * }
203
+ * }),
204
+ * }) {
205
+ * render() {
206
+ * return <h1>{this.props.greetingMessage}</h1>;
207
+ * }
208
+ * }
209
+ *
210
+ * defineComponents(App);
211
+ *
212
+ * const app = new App();
213
+ * app.greetingMessage = "Hello, universe!";
214
+ * ```
215
+ */
216
+ export const prop: (<T>(
217
+ context: Context<T>,
218
+ opts?: PropOptions<T>,
219
+ ) => PropMeta<T | undefined>) &
220
+ (<T>(defaultValue: T, opts?: PropOptions<T>) => PropMeta<T>) &
221
+ (<T>(
222
+ defaultValue?: T,
223
+ opts?: PropOptions<T | undefined>,
224
+ ) => PropMeta<T | undefined>) = <T>(
225
+ defaultOrContext?: Context<T> | T,
226
+ opts?: PropOptions<T>,
227
+ ): PropMeta<any> => ({
228
+ _tag: "p",
229
+ _defaultOrContext: defaultOrContext,
230
+ ...opts,
231
+ });
232
+
233
+ // CustomEvent<T> has a flaw in its constructor signature since it allows
234
+ // `detail` to be optional. This is a workaround to make it required unless
235
+ // `undefined` can be assigned to `T`.
236
+
237
+ type _CustomEventContructor<T> = undefined extends T
238
+ ? typeof CustomEvent<T>
239
+ : EventConstructor<
240
+ PartialRequire<CustomEventInit<T>, "detail">,
241
+ CustomEvent<T>
242
+ >;
243
+
244
+ /**
245
+ * Defines an event in your component metadata that can be dispatched by
246
+ * the component.
247
+ *
248
+ * Make sure your event name starts with `on` and to avoid conflicts with
249
+ * native `HTMLElement` events. The event name will be converted to kebab-case.
250
+ *
251
+ * You can dispatch events either using `HTMLElement.dispatchEvent` or by
252
+ * calling the event emitter function in `this.events` inside the `render`
253
+ * function of a component.
254
+ *
255
+ * @example
256
+ * ```tsx
257
+ * class App extends Component("x-app", {
258
+ * onSomethingHappen: event<string>(),
259
+ * // Event name will be `something-happen`
260
+ * }) {
261
+ * render() {
262
+ * // …
263
+ * this.events.onSomethingHappen({ detail: "Something happened! "});
264
+ * }
265
+ * }
266
+ *
267
+ * const app = new App();
268
+ * app.addEventListener("something-happen", (evt) => {
269
+ * // `evt` is `CustomEvent<string>`
270
+ * console.log(evt.detail);
271
+ * });
272
+ * ```
273
+ *
274
+ * You can also provide a custom event constructor:
275
+ *
276
+ * @example
277
+ * ```tsx
278
+ * class App extends Component("x-app", {
279
+ * onSomethingClick: event(() => MouseEvent),
280
+ * }) {
281
+ * render() {
282
+ * return (
283
+ * <button onclick={evt => this.events.onSomethingClick(evt)}>
284
+ * Click me!
285
+ * </button>
286
+ * );
287
+ * }
288
+ * }
289
+ * ```
290
+ */
291
+ export const event: (() => EventMeta<_CustomEventContructor<undefined>>) &
292
+ (<T>() => EventMeta<_CustomEventContructor<T>>) &
293
+ (<E extends EventConstructor>(eventConstructor: E) => EventMeta<E>) = ((
294
+ eventConstructor: EventConstructor = CustomEvent,
295
+ ): EventMeta<EventConstructor> => ({
296
+ _tag: "e",
297
+ _event: eventConstructor,
298
+ })) as any;
299
+
300
+ export type Metadata = {
301
+ // Forbid all library properties
302
+ [K in keyof ComponentInner<any> | "props" | "events"]?: never;
303
+ } & {
304
+ // Forbid all dom props
305
+ [K in keyof DomProps<any>]?: never;
306
+ } & {
307
+ // Forbid all HTMLElement props
308
+ [K in keyof HTMLElement]?: never;
309
+ } & {
310
+ [name: string]: PropMeta<any> | EventMeta<any>;
311
+ };
312
+
313
+ export const componentSym = Symbol("Component");
314
+ export declare const jsxPropsSym: unique symbol;
315
+
316
+ declare abstract class ComponentInner<M extends Metadata> {
317
+ protected props: Props<M>;
318
+ protected events: EventEmitters<M>;
319
+
320
+ readonly [jsxPropsSym]?: ComponentJsxProps<M>;
321
+ readonly [componentSym]: {
322
+ _scope?: ReturnType<typeof useScope>;
323
+ _destroy?: (() => void) | void;
324
+ };
325
+
326
+ connectedCallback(): void;
327
+ disconnectedCallback(): void;
328
+ attributeChangedCallback(
329
+ name: string,
330
+ oldValue: string | null,
331
+ value: string | null,
332
+ ): void;
333
+
334
+ addEventListener<K extends keyof Events<M> & string>(
335
+ type: JsxPropNameToEventName<K>,
336
+ listener: (event: InstanceType<Events<M>[K]>) => void,
337
+ options?: boolean | AddEventListenerOptions,
338
+ ): void;
339
+ removeEventListener<K extends keyof Events<M> & string>(
340
+ type: JsxPropNameToEventName<K>,
341
+ listener: (event: InstanceType<Events<M>[K]>) => void,
342
+ options?: boolean | EventListenerOptions,
343
+ ): void;
344
+
345
+ abstract render(): Template;
346
+ }
347
+
348
+ export type Component<M extends Metadata = {}> = {
349
+ -readonly [K in keyof Props<M>]: Props<M>[K] extends Signal<infer T>
350
+ ? T
351
+ : never;
352
+ } & ComponentInner<M> &
353
+ HTMLElement;
354
+
355
+ export interface ComponentConstructor<M extends Metadata = {}> {
356
+ /** @ignore */
357
+ readonly [componentSym]: {
358
+ readonly _tagName: string;
359
+ };
360
+ readonly observedAttributes: readonly string[];
361
+
362
+ new (): Component<M>;
363
+ }
364
+
365
+ export interface ComponentOptions {
366
+ /**
367
+ * Shadow DOM options. Set to `false` to disable shadow DOM.
368
+ *
369
+ * @default { mode: "open" }
370
+ */
371
+ shadow?: ShadowRootInit | false;
372
+ }
373
+
374
+ let mountEffects: [fn: () => Cleanup, deps?: Signal<unknown>[]][] | undefined;
375
+
376
+ /**
377
+ * Creates an effect which will rerun when any accessed signal changes.
378
+ *
379
+ * If used inside of a component and the component is not yet mounted, the
380
+ * effect will run only after the component is mounted. Otherwise, the effect
381
+ * will run immediately.
382
+ *
383
+ * @param fn The function to run; it may return a cleanup function.
384
+ */
385
+ export const useMountEffect = (
386
+ fn: () => Cleanup,
387
+ deps?: Signal<unknown>[],
388
+ ): void => {
389
+ if (mountEffects) {
390
+ mountEffects.push([fn, deps]);
391
+ } else {
392
+ useEffect(fn, deps);
393
+ }
394
+ };
395
+
396
+ /**
397
+ * Creates a new web component class.
398
+ *
399
+ * Specify props and events using the `metadata` parameter.
400
+ *
401
+ * @example
402
+ * ```tsx
403
+ * class MyComponent extends Component("my-component", {
404
+ * myProp: prop<string>("Hello, world!"),
405
+ * onMyEvent: event(),
406
+ * }) {
407
+ * render() {
408
+ * return (
409
+ * <>
410
+ * <h1>{this.props.myProp}</h1>
411
+ * <button onclick={() => this.events.onMyEvent()}>Click me</button>
412
+ * </>
413
+ * );
414
+ * },
415
+ * }
416
+ *
417
+ * customElements.define("my-component", MyComponent);
418
+ * ```
419
+ */
420
+ export const Component: ((tagName: string) => ComponentConstructor<{}>) &
421
+ (<const M extends Metadata>(
422
+ tagName: string,
423
+ metadata: M,
424
+ opts?: ComponentOptions,
425
+ ) => ComponentConstructor<M>) = ((
426
+ tagName: string,
427
+ metadata: Metadata = {},
428
+ opts: ComponentOptions = {},
429
+ ): ComponentConstructor => {
430
+ // Extract attribute information
431
+
432
+ const observedAttributes: string[] = [];
433
+ const attributePropMap = new Map<
434
+ string,
435
+ {
436
+ name: string;
437
+ meta: PropMeta<any> & {
438
+ attribute: Required<
439
+ NonNullable<Exclude<PropMeta<any>["attribute"], boolean | Function>>
440
+ >;
441
+ };
442
+ }
443
+ >();
444
+
445
+ for (const name in metadata) {
446
+ const meta = metadata[name] as PropMeta<any> | EventMeta<any>;
447
+
448
+ if (meta._tag == "p" && meta.attribute) {
449
+ if (typeof meta.attribute == "function") {
450
+ meta.attribute = { transform: meta.attribute };
451
+ }
452
+
453
+ const attribute: AttributeOptions<any> = (meta.attribute = {
454
+ name: camelCaseToKebabCase(name),
455
+ static: false,
456
+ transform: (x) => x,
457
+ ...meta.attribute,
458
+ });
459
+
460
+ attributePropMap.set(attribute.name!, {
461
+ name,
462
+ meta: meta as any,
463
+ });
464
+
465
+ if (!attribute.static) {
466
+ observedAttributes.push(attribute.name!);
467
+ }
468
+ }
469
+ }
470
+
471
+ // Create base class
472
+
473
+ opts.shadow ??= { mode: "open" };
474
+
475
+ const getRenderParent = (component: _Component) =>
476
+ opts.shadow
477
+ ? component.shadowRoot ?? component.attachShadow(opts.shadow)
478
+ : component;
479
+ abstract class _Component extends HTMLElement {
480
+ static readonly [componentSym]: ComponentConstructor[typeof componentSym] =
481
+ {
482
+ _tagName: tagName,
483
+ };
484
+ static readonly observedAttributes: readonly string[] = observedAttributes;
485
+
486
+ protected props: Record<string, Signal<any>> = {};
487
+ protected events: Record<string, (arg: unknown) => any> = {};
488
+
489
+ readonly [componentSym]: ComponentInner<any>[typeof componentSym] = {};
490
+
491
+ constructor() {
492
+ super();
493
+
494
+ for (const name in metadata) {
495
+ const meta = metadata[name];
496
+
497
+ if (meta._tag == "p") {
498
+ const context = isContext(meta._defaultOrContext)
499
+ ? meta._defaultOrContext
500
+ : null;
501
+ const [getter, setter] = useSignal<unknown>(
502
+ context ? undefined : meta._defaultOrContext,
503
+ );
504
+
505
+ this.props[name] = getter;
506
+
507
+ if (context) {
508
+ provideContext(context, this, getter);
509
+ }
510
+
511
+ Object.defineProperty(this, name, {
512
+ get: getter.peek,
513
+ set: (value) => setter(() => value, { force: true }),
514
+ });
515
+ } else if (meta._tag == "e" && name.startsWith("on")) {
516
+ const eventName = jsxPropNameToEventName(name as `on${string}`);
517
+
518
+ this.events[name] = (arg: unknown) =>
519
+ this.dispatchEvent(new meta._event(eventName, arg));
520
+ }
521
+ }
522
+ }
523
+
524
+ connectedCallback(): void {
525
+ const renderParent = getRenderParent(this);
526
+
527
+ this[componentSym]._destroy = useSubscope(() =>
528
+ runWithRenderer(
529
+ {
530
+ _svg: false,
531
+ _component: this as any,
532
+ _nodes: renderParent.childNodes.values(),
533
+ },
534
+ () => {
535
+ this[componentSym]._scope = useScope();
536
+
537
+ // Render
538
+
539
+ const prevMountEffects = mountEffects;
540
+ mountEffects = [];
541
+
542
+ try {
543
+ renderParent?.append(...this.render().build());
544
+
545
+ // Run mount effects
546
+
547
+ mountEffects.forEach(([fn, opts]) => useEffect(fn, opts));
548
+ } finally {
549
+ mountEffects = prevMountEffects;
550
+ }
551
+ },
552
+ ),
553
+ )[1];
554
+ }
555
+
556
+ disconnectedCallback(): void {
557
+ this[componentSym]._destroy?.();
558
+ }
559
+
560
+ attributeChangedCallback(
561
+ name: string,
562
+ _: string | null,
563
+ value: string | null,
564
+ ): void {
565
+ const prop = attributePropMap.get(name);
566
+
567
+ if (prop) {
568
+ this[prop.name as keyof this] =
569
+ value != null
570
+ ? prop.meta.attribute.transform.call(this, value)
571
+ : isContext(prop.meta._defaultOrContext)
572
+ ? undefined
573
+ : prop.meta._defaultOrContext;
574
+ }
575
+ }
576
+
577
+ abstract render(): Template;
578
+ }
579
+
580
+ return _Component as any;
581
+ }) as any;
582
+
583
+ /**
584
+ * Determines whether the given value is a component created by
585
+ * extending {@link ComponentConstructor}.
586
+ */
587
+ export const isComponent = (
588
+ value: any,
589
+ ): value is ComponentConstructor | Component => !!value?.[componentSym];
590
+
591
+ /**
592
+ * Represents a functional component.
593
+ *
594
+ * @example
595
+ * ```tsx
596
+ * const MyComponent: FunctionalComponent<{
597
+ * name: MaybeSignal<string>;
598
+ * }> = ({ name }) => {
599
+ * return <h1>Hello, {name}!</h1>;
600
+ * };
601
+ * ```
602
+ */
603
+ export interface FunctionalComponent<in P extends object = {}> {
604
+ (props: P): Template;
605
+ }
606
+
607
+ /**
608
+ * Defines a set of components with the given prefix.
609
+ */
610
+ export const defineComponents: ((
611
+ ...components: ComponentConstructor[]
612
+ ) => void) &
613
+ ((prefix: string, ...components: ComponentConstructor[]) => void) = (
614
+ ...args: [string | ComponentConstructor, ...ComponentConstructor[]]
615
+ ) => {
616
+ const [prefix, components] =
617
+ typeof args[0] == "string"
618
+ ? [args[0], args.slice(1) as ComponentConstructor[]]
619
+ : ["", args as ComponentConstructor[]];
620
+
621
+ for (const component of components) {
622
+ customElements.define(prefix + component[componentSym]._tagName, component);
623
+ }
624
+ };
package/src/context.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { useRenderer } from "./renderer.js";
2
+ import { useMemo, Signal, SetSignalOptions, MaybeSignal } from "./scope.js";
3
+
4
+ const contextSym = Symbol("Context");
5
+
6
+ type ContextEvent<T> = CustomEvent<(value: T) => void>;
7
+
8
+ /**
9
+ * A value that can be passed through the component tree without having to be
10
+ * explicitly passed as a prop.
11
+ */
12
+ export interface Context<in out T> {
13
+ readonly [contextSym]: string;
14
+ /** @ignore */
15
+ readonly _init: T;
16
+ /** @ignore */
17
+ readonly _opts?: SetSignalOptions;
18
+ }
19
+
20
+ /**
21
+ * Creates a new context with the given value.
22
+ */
23
+ export const createContext: (<T>(
24
+ value: T,
25
+ opts?: SetSignalOptions,
26
+ ) => Context<T>) &
27
+ (<T>(value?: T, opts?: SetSignalOptions) => Context<T | undefined>) = (<T>(
28
+ value?: T,
29
+ opts?: SetSignalOptions,
30
+ ): Context<T | undefined> => ({
31
+ [contextSym]: Math.random().toString(36).slice(2),
32
+ _init: value,
33
+ _opts: opts,
34
+ })) as any;
35
+
36
+ export const isContext = (value: any): value is Context<unknown> =>
37
+ !!value?.[contextSym];
38
+
39
+ export const provideContext = <T>(
40
+ context: Context<T>,
41
+ element: Element,
42
+ value: MaybeSignal<T | undefined>,
43
+ ) => {
44
+ element.addEventListener(context[contextSym], (evt) => {
45
+ const innerValue = MaybeSignal.get(value);
46
+
47
+ if (innerValue !== undefined) {
48
+ evt.stopPropagation();
49
+ (evt as ContextEvent<T>).detail(innerValue);
50
+ }
51
+ });
52
+ };
53
+
54
+ export const useContext = <T>(context: Context<T>): Signal<T> => {
55
+ const renderer = useRenderer();
56
+
57
+ return useMemo(() => {
58
+ let result = context._init;
59
+
60
+ renderer._component?.dispatchEvent(
61
+ new CustomEvent(context[contextSym], {
62
+ detail: (value) => (result = value),
63
+ bubbles: true,
64
+ composed: true,
65
+ }) as ContextEvent<T>,
66
+ );
67
+
68
+ return result;
69
+ });
70
+ };