@c9up/nebula 0.1.5 → 0.1.6
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/dist/adapters/tailwind.d.ts +7 -0
- package/dist/adapters/tailwind.js +11 -1
- package/dist/adapters/unocss.js +11 -2
- package/dist/atoms/NativeSelect.js +27 -2
- package/dist/lib/motion.d.ts +25 -16
- package/dist/lib/motion.js +30 -24
- package/dist/organisms/Dialog.d.ts +11 -4
- package/dist/organisms/Dialog.js +2 -2
- package/dist/organisms/Sidebar.d.ts +9 -0
- package/dist/organisms/Sidebar.js +5 -2
- package/dist/primitives/floatingSurface.js +17 -8
- package/dist/primitives/presence.js +401 -5
- package/nebula.css +1 -1
- package/package.json +7 -6
- package/src/adapters/tailwind.ts +11 -1
- package/src/adapters/unocss.ts +11 -2
- package/src/atoms/NativeSelect.ts +27 -2
- package/src/lib/motion.ts +30 -26
- package/src/organisms/Dialog.ts +13 -6
- package/src/organisms/Sidebar.ts +15 -2
- package/src/primitives/floatingSurface.ts +18 -9
- package/src/primitives/presence.ts +428 -5
- package/theme.css +0 -35
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* a v4 setup produces an unstyled page.
|
|
15
15
|
* - `@custom-variant dark` makes `dark:` respond to a `.dark` class rather
|
|
16
16
|
* than the system preference, so a theme toggle can override the OS.
|
|
17
|
+
* - `tw-animate-css` supplies `animate-in` / `animate-out` and the `fade-*`,
|
|
18
|
+
* `zoom-*` and `slide-*` modifiers — where shadcn gets them too. Imported
|
|
19
|
+
* rather than reimplemented, so the components use its class strings
|
|
20
|
+
* verbatim. nebula previously declared four keyframes of its own and reached
|
|
21
|
+
* them through arbitrary animation values, which Tailwind compiles whether or
|
|
22
|
+
* not the keyframes exist: an app that skipped the stylesheet got overlays
|
|
23
|
+
* that never finished CLOSING rather than overlays that never animated.
|
|
17
24
|
* - `@theme inline` maps nebula's plain custom properties onto Tailwind's
|
|
18
25
|
* colour namespace, which is what turns `--primary` into a `bg-primary`
|
|
19
26
|
* utility.
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* a v4 setup produces an unstyled page.
|
|
15
15
|
* - `@custom-variant dark` makes `dark:` respond to a `.dark` class rather
|
|
16
16
|
* than the system preference, so a theme toggle can override the OS.
|
|
17
|
+
* - `tw-animate-css` supplies `animate-in` / `animate-out` and the `fade-*`,
|
|
18
|
+
* `zoom-*` and `slide-*` modifiers — where shadcn gets them too. Imported
|
|
19
|
+
* rather than reimplemented, so the components use its class strings
|
|
20
|
+
* verbatim. nebula previously declared four keyframes of its own and reached
|
|
21
|
+
* them through arbitrary animation values, which Tailwind compiles whether or
|
|
22
|
+
* not the keyframes exist: an app that skipped the stylesheet got overlays
|
|
23
|
+
* that never finished CLOSING rather than overlays that never animated.
|
|
17
24
|
* - `@theme inline` maps nebula's plain custom properties onto Tailwind's
|
|
18
25
|
* colour namespace, which is what turns `--primary` into a `bg-primary`
|
|
19
26
|
* utility.
|
|
@@ -123,7 +130,10 @@ function relativeFromCss(config) {
|
|
|
123
130
|
export const tailwindAdapter = {
|
|
124
131
|
name: "tailwind",
|
|
125
132
|
summary: "Tailwind CSS v4, configured in CSS. What shadcn/ui itself targets.",
|
|
126
|
-
|
|
133
|
+
// `tw-animate-css` is where the overlay animations come from — the same
|
|
134
|
+
// package shadcn tells you to install, and the reason the components can use
|
|
135
|
+
// its class strings verbatim.
|
|
136
|
+
packages: ["tailwindcss", "@tailwindcss/cli", "tw-animate-css"],
|
|
127
137
|
files(config) {
|
|
128
138
|
return [
|
|
129
139
|
{
|
package/dist/adapters/unocss.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
const CONFIG_PATH = "uno.config.ts";
|
|
17
17
|
function unoConfig(config) {
|
|
18
18
|
return `import { defineConfig, presetWind4 } from 'unocss'
|
|
19
|
+
import presetAnimations from 'unocss-preset-animations'
|
|
19
20
|
import { readFileSync } from 'node:fs'
|
|
20
21
|
import { createRequire } from 'node:module'
|
|
21
22
|
|
|
@@ -40,7 +41,15 @@ const tokens = readFileSync(require.resolve('@c9up/nebula/theme.css'), 'utf8')
|
|
|
40
41
|
const color = (name: string) => \`var(--\${name})\`
|
|
41
42
|
|
|
42
43
|
export default defineConfig({
|
|
43
|
-
|
|
44
|
+
/*
|
|
45
|
+
* \`presetAnimations\` is the UnoCSS port of the same utilities shadcn uses
|
|
46
|
+
* through \`tw-animate-css\`: \`animate-in\` / \`animate-out\` and the
|
|
47
|
+
* \`fade-*\` / \`zoom-*\` / \`slide-*\` modifiers. nebula's components emit
|
|
48
|
+
* shadcn's class strings verbatim, so the utilities have to exist under every
|
|
49
|
+
* engine — and registering them is what makes a missing setup produce no
|
|
50
|
+
* animation rather than an overlay that never finishes closing.
|
|
51
|
+
*/
|
|
52
|
+
presets: [presetWind4(), presetAnimations()],
|
|
44
53
|
content: {
|
|
45
54
|
filesystem: ['${config.paths.components}/**/*.{ts,js}'],
|
|
46
55
|
},
|
|
@@ -110,7 +119,7 @@ body {
|
|
|
110
119
|
export const unocssAdapter = {
|
|
111
120
|
name: "unocss",
|
|
112
121
|
summary: "UnoCSS with presetWind4 — same class syntax, no PostCSS, faster builds.",
|
|
113
|
-
packages: ["unocss", "@unocss/cli"],
|
|
122
|
+
packages: ["unocss", "@unocss/cli", "unocss-preset-animations"],
|
|
114
123
|
files(config) {
|
|
115
124
|
return [
|
|
116
125
|
{ path: CONFIG_PATH, contents: unoConfig(config), skipIfExists: true },
|
|
@@ -44,7 +44,25 @@ export const NativeSelect = component((props) => {
|
|
|
44
44
|
}
|
|
45
45
|
return out;
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* `?selected` on each option, NOT `.value` on the select alone.
|
|
49
|
+
*
|
|
50
|
+
* A property binding sits on the opening tag, so it is assigned before the
|
|
51
|
+
* options exist. `select.value = 'x'` against a childless `<select>` is a
|
|
52
|
+
* no-op the DOM does not report, and nothing re-runs it afterwards: the
|
|
53
|
+
* control then shows its FIRST option while holding the value it was given.
|
|
54
|
+
* Nothing throws and nothing logs, so the value the user sees is not the
|
|
55
|
+
* value the form will submit — in a form that edits money or tax status
|
|
56
|
+
* that is a wrong record written on save.
|
|
57
|
+
*
|
|
58
|
+
* Marking the option is order-independent, and it is also what makes the
|
|
59
|
+
* choice correct in server-rendered HTML before any script runs.
|
|
60
|
+
*/
|
|
61
|
+
const renderOption = (option) => html `<option
|
|
62
|
+
value="${option.value}"
|
|
63
|
+
?disabled="${option.disabled === true}"
|
|
64
|
+
?selected="${() => read(props.value) === option.value}"
|
|
65
|
+
>
|
|
48
66
|
${option.label}
|
|
49
67
|
</option>`;
|
|
50
68
|
return html `<div data-slot="native-select" class="relative inline-flex w-full items-center">
|
|
@@ -61,7 +79,14 @@ export const NativeSelect = component((props) => {
|
|
|
61
79
|
>
|
|
62
80
|
${props.placeholder === undefined
|
|
63
81
|
? null
|
|
64
|
-
: html `<option
|
|
82
|
+
: html `<option
|
|
83
|
+
value=""
|
|
84
|
+
disabled
|
|
85
|
+
?selected="${() => {
|
|
86
|
+
const current = read(props.value);
|
|
87
|
+
return current === undefined || current === "";
|
|
88
|
+
}}"
|
|
89
|
+
>${props.placeholder}</option>`}
|
|
65
90
|
${runs().map((run) => run.group === undefined
|
|
66
91
|
? run.items.map(renderOption)
|
|
67
92
|
: html `<optgroup label="${run.group}">${run.items.map(renderOption)}</optgroup>`)}
|
package/dist/lib/motion.d.ts
CHANGED
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Enter and exit animations for overlays.
|
|
3
3
|
*
|
|
4
|
-
* shadcn
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* plugin for.
|
|
4
|
+
* These are shadcn's own class strings, verbatim, and they come from the same
|
|
5
|
+
* place shadcn's do: `tw-animate-css`, which registers `animate-in` /
|
|
6
|
+
* `animate-out` and the `fade-*` / `zoom-*` / `slide-*` modifiers through
|
|
7
|
+
* `@theme inline` and `@utility`.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* nebula used to declare four bespoke keyframes and reference them through
|
|
10
|
+
* ARBITRARY animation values (`animate-[nebula-zoom-out_120ms_ease-in]`), to
|
|
11
|
+
* avoid the plugin. That is what made a missing stylesheet fatal rather than
|
|
12
|
+
* cosmetic: Tailwind compiles an arbitrary value unconditionally, so the
|
|
13
|
+
* `animation` property was always set while the keyframes behind it might exist
|
|
14
|
+
* nowhere — the browser then never fires `animationend` and every closed
|
|
15
|
+
* overlay stays in the document. With a theme-registered utility the class
|
|
16
|
+
* simply is not emitted when the theme is absent, the element animates not at
|
|
17
|
+
* all, and closing is instant. That is the whole reason upstream registers
|
|
18
|
+
* rather than inlines, and it is why the deviation is gone.
|
|
13
19
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* wait, sees no animation, and removes the node immediately. Reduced motion
|
|
17
|
-
* therefore gets an instant close rather than a delayed one, with no branch in
|
|
18
|
-
* the component.
|
|
20
|
+
* The durations are shadcn's too, which means asymmetric by way of the sheet
|
|
21
|
+
* variants only; everything else takes `tw-animate-css`'s defaults.
|
|
19
22
|
*/
|
|
20
23
|
import type { Side } from "../primitives/floating.js";
|
|
21
|
-
/**
|
|
22
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Popovers, menus, selects — scale up from the anchor.
|
|
26
|
+
*
|
|
27
|
+
* shadcn's dialog and popover string. `motion-reduce:animate-none` is ours and
|
|
28
|
+
* stays: it also makes `onExitFinished` see no animation and remove the node at
|
|
29
|
+
* once, so reduced motion gets an instant close with no branch in the component.
|
|
30
|
+
*/
|
|
31
|
+
export declare const zoomInOut = "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 motion-reduce:animate-none";
|
|
23
32
|
/** Backdrops and tooltips — no movement, just opacity. */
|
|
24
|
-
export declare const fadeInOut = "data-[state=open]:animate-[
|
|
33
|
+
export declare const fadeInOut = "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 motion-reduce:animate-none";
|
|
25
34
|
/**
|
|
26
35
|
* Panels that slide in from an edge — Sheet, Drawer.
|
|
27
36
|
*
|
package/dist/lib/motion.js
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Enter and exit animations for overlays.
|
|
3
3
|
*
|
|
4
|
-
* shadcn
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* plugin for.
|
|
4
|
+
* These are shadcn's own class strings, verbatim, and they come from the same
|
|
5
|
+
* place shadcn's do: `tw-animate-css`, which registers `animate-in` /
|
|
6
|
+
* `animate-out` and the `fade-*` / `zoom-*` / `slide-*` modifiers through
|
|
7
|
+
* `@theme inline` and `@utility`.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* nebula used to declare four bespoke keyframes and reference them through
|
|
10
|
+
* ARBITRARY animation values (`animate-[nebula-zoom-out_120ms_ease-in]`), to
|
|
11
|
+
* avoid the plugin. That is what made a missing stylesheet fatal rather than
|
|
12
|
+
* cosmetic: Tailwind compiles an arbitrary value unconditionally, so the
|
|
13
|
+
* `animation` property was always set while the keyframes behind it might exist
|
|
14
|
+
* nowhere — the browser then never fires `animationend` and every closed
|
|
15
|
+
* overlay stays in the document. With a theme-registered utility the class
|
|
16
|
+
* simply is not emitted when the theme is absent, the element animates not at
|
|
17
|
+
* all, and closing is instant. That is the whole reason upstream registers
|
|
18
|
+
* rather than inlines, and it is why the deviation is gone.
|
|
13
19
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* wait, sees no animation, and removes the node immediately. Reduced motion
|
|
17
|
-
* therefore gets an instant close rather than a delayed one, with no branch in
|
|
18
|
-
* the component.
|
|
20
|
+
* The durations are shadcn's too, which means asymmetric by way of the sheet
|
|
21
|
+
* variants only; everything else takes `tw-animate-css`'s defaults.
|
|
19
22
|
*/
|
|
20
|
-
/**
|
|
21
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Popovers, menus, selects — scale up from the anchor.
|
|
25
|
+
*
|
|
26
|
+
* shadcn's dialog and popover string. `motion-reduce:animate-none` is ours and
|
|
27
|
+
* stays: it also makes `onExitFinished` see no animation and remove the node at
|
|
28
|
+
* once, so reduced motion gets an instant close with no branch in the component.
|
|
29
|
+
*/
|
|
30
|
+
export const zoomInOut = "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 motion-reduce:animate-none";
|
|
22
31
|
/** Backdrops and tooltips — no movement, just opacity. */
|
|
23
|
-
export const fadeInOut = "data-[state=open]:animate-[
|
|
32
|
+
export const fadeInOut = "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 motion-reduce:animate-none";
|
|
24
33
|
/**
|
|
25
34
|
* Panels that slide in from an edge — Sheet, Drawer.
|
|
26
35
|
*
|
|
@@ -28,12 +37,9 @@ export const fadeInOut = "data-[state=open]:animate-[nebula-fade-in_150ms_ease-o
|
|
|
28
37
|
* depends on the panel's own size and only `translate-x-full` knows that.
|
|
29
38
|
*/
|
|
30
39
|
export function slideFrom(side) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
? "data-[state=closed]:-translate-y-full"
|
|
37
|
-
: "data-[state=closed]:translate-y-full";
|
|
38
|
-
return `transition-transform duration-300 ease-in-out data-[state=open]:translate-x-0 data-[state=open]:translate-y-0 ${axis} motion-reduce:transition-none`;
|
|
40
|
+
// shadcn's sheet: a slide utility per side, and its asymmetric durations —
|
|
41
|
+
// entering is slower than leaving, because a surface appearing wants to be
|
|
42
|
+
// noticed while one dismissed wants to be out of the way.
|
|
43
|
+
const slide = `data-[state=open]:slide-in-from-${side} data-[state=closed]:slide-out-to-${side}`;
|
|
44
|
+
return `data-[state=open]:animate-in data-[state=closed]:animate-out ${slide} data-[state=open]:duration-500 data-[state=closed]:duration-300 motion-reduce:animate-none`;
|
|
39
45
|
}
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* required rather than optional — `srOnlyTitle` is there for designs that show
|
|
17
17
|
* no visible heading.
|
|
18
18
|
*/
|
|
19
|
-
import type { Child } from "../lib/children.js";
|
|
20
19
|
import { type Slot } from "../lib/children.js";
|
|
21
20
|
import { type Reactive } from "../lib/props.js";
|
|
22
21
|
export declare const dialogBackdropClasses = "fixed inset-0 z-50 bg-black/50";
|
|
@@ -24,9 +23,17 @@ export declare const dialogPanelClasses = "bg-background fixed top-1/2 left-1/2
|
|
|
24
23
|
export interface DialogProps {
|
|
25
24
|
/** Rendered inside the trigger button. Omit to drive `open` yourself. */
|
|
26
25
|
trigger?: Slot;
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Announced on open. Hide it visually with `srOnlyTitle`.
|
|
28
|
+
*
|
|
29
|
+
* A `Slot`, so it may be an accessor: one Dialog driven between "create" and
|
|
30
|
+
* "edit" needs a title that follows. It already behaved that way — the
|
|
31
|
+
* renderer binds whatever it is given — while the type said `Child` and
|
|
32
|
+
* refused the function, so the working call did not compile and the type
|
|
33
|
+
* disagreed with `children` beside it for no reason.
|
|
34
|
+
*/
|
|
35
|
+
title: Slot;
|
|
36
|
+
description?: Slot;
|
|
30
37
|
children?: Slot;
|
|
31
38
|
/** Actions, laid out bottom-right. */
|
|
32
39
|
footer?: Slot;
|
package/dist/organisms/Dialog.js
CHANGED
|
@@ -59,14 +59,14 @@ export const Dialog = component((props) => {
|
|
|
59
59
|
id="${titleId}"
|
|
60
60
|
data-slot="dialog-title"
|
|
61
61
|
class="${cn("text-lg leading-none font-semibold", props.srOnlyTitle === true ? "sr-only" : "")}"
|
|
62
|
-
>${props.title}</h2>
|
|
62
|
+
>${slot(props.title)}</h2>
|
|
63
63
|
${props.description === undefined
|
|
64
64
|
? null
|
|
65
65
|
: html `<p
|
|
66
66
|
id="${descriptionId}"
|
|
67
67
|
data-slot="dialog-description"
|
|
68
68
|
class="text-muted-foreground text-sm"
|
|
69
|
-
>${props.description}</p>`}
|
|
69
|
+
>${slot(props.description)}</p>`}
|
|
70
70
|
</div>
|
|
71
71
|
${slot(props.children)}
|
|
72
72
|
${props.footer === undefined
|
|
@@ -69,6 +69,15 @@ export interface SidebarMenuItemProps {
|
|
|
69
69
|
tooltip?: string;
|
|
70
70
|
/** A trailing control — the count of unread items, a status dot. */
|
|
71
71
|
badge?: Child;
|
|
72
|
+
/**
|
|
73
|
+
* Extra classes, merged over the defaults.
|
|
74
|
+
*
|
|
75
|
+
* Every other component in this library takes one; this one did not, and a
|
|
76
|
+
* caller who wanted something as ordinary as dimming a disabled entry had
|
|
77
|
+
* to work around it. `cn` is tailwind-merge, so an override wins over the
|
|
78
|
+
* default it collides with rather than fighting it on specificity.
|
|
79
|
+
*/
|
|
80
|
+
class?: Reactive<string>;
|
|
72
81
|
onClick?: () => void;
|
|
73
82
|
}
|
|
74
83
|
/**
|
|
@@ -171,18 +171,21 @@ export const SidebarMenuItem = component((props) => {
|
|
|
171
171
|
data-slot="sidebar-menu-badge"
|
|
172
172
|
class="text-sidebar-foreground/70 ml-auto flex h-5 min-w-5 shrink-0 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums"
|
|
173
173
|
>${props.badge}</span>`}`;
|
|
174
|
+
// Reactive, because `props.class` may be a signal — a caller dimming an
|
|
175
|
+
// entry as its state changes should not have to remount it.
|
|
176
|
+
const merged = () => cn(classes, read(props.class));
|
|
174
177
|
const entry = props.href !== undefined
|
|
175
178
|
? html `<a
|
|
176
179
|
data-slot="sidebar-menu-item"
|
|
177
180
|
href="${props.href}"
|
|
178
181
|
aria-current="${() => (read(props.active) === true ? "page" : undefined)}"
|
|
179
|
-
class="${
|
|
182
|
+
class="${merged}"
|
|
180
183
|
>${body}</a>`
|
|
181
184
|
: html `<button
|
|
182
185
|
type="button"
|
|
183
186
|
data-slot="sidebar-menu-item"
|
|
184
187
|
aria-current="${() => (read(props.active) === true ? "page" : undefined)}"
|
|
185
|
-
class="${
|
|
188
|
+
class="${merged}"
|
|
186
189
|
@click="${props.onClick}"
|
|
187
190
|
>${body}</button>`;
|
|
188
191
|
if (props.tooltip === undefined)
|
|
@@ -33,17 +33,26 @@ import { onExitFinished } from "./presence.js";
|
|
|
33
33
|
export function floatingSurface(options) {
|
|
34
34
|
let live = null;
|
|
35
35
|
function show() {
|
|
36
|
-
if (live !== null) {
|
|
37
|
-
// Already open and mid-exit: cancel the teardown and reuse the node
|
|
38
|
-
// rather than stacking a second copy on top of the one fading out.
|
|
39
|
-
live.cancelExit?.();
|
|
40
|
-
live.cancelExit = null;
|
|
41
|
-
live.element.setAttribute("data-state", "open");
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
36
|
const anchor = options.anchor();
|
|
45
37
|
if (anchor === null)
|
|
46
38
|
return;
|
|
39
|
+
if (live !== null) {
|
|
40
|
+
if (live.anchor === anchor) {
|
|
41
|
+
// The SAME anchor, already open and possibly mid-exit: cancel
|
|
42
|
+
// the teardown and reuse the node rather than stacking a second
|
|
43
|
+
// copy on top of the one fading out.
|
|
44
|
+
live.cancelExit?.();
|
|
45
|
+
live.cancelExit = null;
|
|
46
|
+
live.element.setAttribute("data-state", "open");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// A DIFFERENT anchor. Content is built once per open, so reusing
|
|
50
|
+
// this node would show the previous anchor's content — one surface
|
|
51
|
+
// shared by the rows of a table showed the first row's entries
|
|
52
|
+
// however many rows were clicked. Its position, dismissal listeners
|
|
53
|
+
// and focus trap all belong to the old anchor too.
|
|
54
|
+
teardown();
|
|
55
|
+
}
|
|
47
56
|
const mount = portal(options.content());
|
|
48
57
|
const element = mount.host.firstElementChild;
|
|
49
58
|
if (!(element instanceof HTMLElement)) {
|