cradova 3.11.10 → 3.12.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/dist/index.js
CHANGED
|
@@ -53,7 +53,7 @@ var makeElement = (element, ElementChildrenAndPropertyList) => {
|
|
|
53
53
|
continue;
|
|
54
54
|
}
|
|
55
55
|
if (Array.isArray(value)) {
|
|
56
|
-
if (prop === "ref" && value.length === 2 && value[0] instanceof
|
|
56
|
+
if (prop === "ref" && value.length === 2 && value[0] instanceof RefInstance && typeof value[1] === "string") {
|
|
57
57
|
const [refInstance, name] = value;
|
|
58
58
|
refInstance.current[name] = element;
|
|
59
59
|
continue;
|
|
@@ -102,18 +102,6 @@ function unroll_child_list(l) {
|
|
|
102
102
|
}
|
|
103
103
|
return fg;
|
|
104
104
|
}
|
|
105
|
-
function $if(condition, ...elements) {
|
|
106
|
-
if (condition) {
|
|
107
|
-
return elements;
|
|
108
|
-
}
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
function $ifelse(condition, ifTrue, ifFalse) {
|
|
112
|
-
if (condition) {
|
|
113
|
-
return ifTrue;
|
|
114
|
-
}
|
|
115
|
-
return ifFalse;
|
|
116
|
-
}
|
|
117
105
|
var $case = (value, element) => {
|
|
118
106
|
return (key) => {
|
|
119
107
|
if (key === value) {
|
|
@@ -243,7 +231,7 @@ function useRef() {
|
|
|
243
231
|
if (typeof self !== "function") {
|
|
244
232
|
throw new Error("Cradova Hook Error: useRef called outside of a Cradova component context.");
|
|
245
233
|
}
|
|
246
|
-
return new
|
|
234
|
+
return new RefInstance;
|
|
247
235
|
}
|
|
248
236
|
function useReducer(reducer, initialArg, initializer) {
|
|
249
237
|
const self = this;
|
|
@@ -326,7 +314,7 @@ var toCompNoRender = (comp) => {
|
|
|
326
314
|
};
|
|
327
315
|
var funcManager = {
|
|
328
316
|
render(component) {
|
|
329
|
-
const html = component
|
|
317
|
+
const html = component(component, ...component._args || []);
|
|
330
318
|
if (html instanceof HTMLElement) {
|
|
331
319
|
component.reference = html;
|
|
332
320
|
component.rendered = true;
|
|
@@ -356,7 +344,7 @@ var funcManager = {
|
|
|
356
344
|
const node = component.reference;
|
|
357
345
|
if (document.body.contains(node)) {
|
|
358
346
|
resetComponent(component);
|
|
359
|
-
const newHtml = component
|
|
347
|
+
const newHtml = component(component, ...component._args || []);
|
|
360
348
|
if (newHtml instanceof HTMLElement) {
|
|
361
349
|
node.replaceWith(newHtml);
|
|
362
350
|
node.insertAdjacentElement("beforebegin", newHtml);
|
|
@@ -487,11 +475,6 @@ var tbody = cra("table");
|
|
|
487
475
|
var table = cra("tbody");
|
|
488
476
|
var td = cra("td");
|
|
489
477
|
var tr = cra("tr");
|
|
490
|
-
var svg = (svg2, properties) => {
|
|
491
|
-
const span2 = document.createElement("span");
|
|
492
|
-
span2.innerHTML = svg2;
|
|
493
|
-
return makeElement(span2, properties || []);
|
|
494
|
-
};
|
|
495
478
|
var raw = (html) => {
|
|
496
479
|
const div2 = document.createElement("div");
|
|
497
480
|
if (Array.isArray(html)) {
|
|
@@ -548,6 +531,27 @@ class Store {
|
|
|
548
531
|
}
|
|
549
532
|
}
|
|
550
533
|
|
|
534
|
+
class SilentStore {
|
|
535
|
+
$store;
|
|
536
|
+
constructor(store) {
|
|
537
|
+
this.$store = store;
|
|
538
|
+
for (const key in this.$store.$_internal_data) {
|
|
539
|
+
if (this.$store.$_internal_data.hasOwnProperty(key)) {
|
|
540
|
+
Object.defineProperty(this, key, {
|
|
541
|
+
get() {
|
|
542
|
+
return this.$store.$_internal_data[key];
|
|
543
|
+
},
|
|
544
|
+
set(value) {
|
|
545
|
+
this.$store.$_internal_data[key] = value;
|
|
546
|
+
},
|
|
547
|
+
enumerable: true,
|
|
548
|
+
configurable: true
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
551
555
|
class List2 {
|
|
552
556
|
_data;
|
|
553
557
|
_dirtyIndices;
|
|
@@ -592,6 +596,9 @@ class List2 {
|
|
|
592
596
|
this._dirtyIndices.add("all");
|
|
593
597
|
this.notifier("dataChanged", { type: "add", index });
|
|
594
598
|
}
|
|
599
|
+
map(callback) {
|
|
600
|
+
return this._data.map(callback);
|
|
601
|
+
}
|
|
595
602
|
remove(index, count = 1) {
|
|
596
603
|
if (index >= 0 && index < this._data.length && count > 0) {
|
|
597
604
|
this._data.splice(index, count);
|
|
@@ -622,6 +629,7 @@ class Signal {
|
|
|
622
629
|
isList = false;
|
|
623
630
|
subscribers = {};
|
|
624
631
|
store;
|
|
632
|
+
silentStore = undefined;
|
|
625
633
|
passers;
|
|
626
634
|
constructor(initial, props) {
|
|
627
635
|
if (!initial || typeof initial !== "object") {
|
|
@@ -631,6 +639,7 @@ class Signal {
|
|
|
631
639
|
this.store = new Store(initial, (key) => {
|
|
632
640
|
this.publish(key);
|
|
633
641
|
});
|
|
642
|
+
this.silentStore = new SilentStore(this.store);
|
|
634
643
|
} else {
|
|
635
644
|
this.isList = true;
|
|
636
645
|
this.store = new List2(initial, (eventType) => {
|
|
@@ -646,6 +655,7 @@ class Signal {
|
|
|
646
655
|
this.store = new Store(Object.assign(initial, restored), (key2) => {
|
|
647
656
|
this.publish(key2);
|
|
648
657
|
});
|
|
658
|
+
this.silentStore = new SilentStore(this.store);
|
|
649
659
|
} else if (Array.isArray(restored)) {
|
|
650
660
|
this.isList = true;
|
|
651
661
|
this.store = new List2(restored, (eventType) => {
|
|
@@ -1017,6 +1027,9 @@ class Router {
|
|
|
1017
1027
|
history.go(-1);
|
|
1018
1028
|
}
|
|
1019
1029
|
static navigate(href, data) {
|
|
1030
|
+
if (RouterBox["paused"]) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1020
1033
|
if (typeof href !== "string") {
|
|
1021
1034
|
console.error(" ✘ Cradova err: href must be a defined path but got " + href + " instead");
|
|
1022
1035
|
}
|
|
@@ -1059,14 +1072,25 @@ class Router {
|
|
|
1059
1072
|
throw new Error(`✘ Cradova err: please add '<div data-wrapper="app"></div>' to the body of your index.html file `);
|
|
1060
1073
|
}
|
|
1061
1074
|
window.addEventListener("pageshow", () => RouterBox.router());
|
|
1075
|
+
window.addEventListener("hashchange", () => {
|
|
1076
|
+
if (RouterBox["paused"]) {
|
|
1077
|
+
history.forward();
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
RouterBox.router();
|
|
1081
|
+
});
|
|
1062
1082
|
window.addEventListener("popstate", (_e) => {
|
|
1063
1083
|
_e.preventDefault();
|
|
1084
|
+
if (RouterBox["paused"]) {
|
|
1085
|
+
history.forward();
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1064
1088
|
RouterBox.router();
|
|
1065
1089
|
});
|
|
1066
1090
|
}
|
|
1067
1091
|
}
|
|
1068
1092
|
|
|
1069
|
-
class
|
|
1093
|
+
class RefInstance {
|
|
1070
1094
|
current = {};
|
|
1071
1095
|
bind(name) {
|
|
1072
1096
|
return [this, name];
|
|
@@ -1152,7 +1176,6 @@ export {
|
|
|
1152
1176
|
td,
|
|
1153
1177
|
tbody,
|
|
1154
1178
|
table,
|
|
1155
|
-
svg,
|
|
1156
1179
|
span,
|
|
1157
1180
|
select,
|
|
1158
1181
|
section,
|
|
@@ -1190,14 +1213,12 @@ export {
|
|
|
1190
1213
|
button,
|
|
1191
1214
|
audio,
|
|
1192
1215
|
a,
|
|
1193
|
-
__raw_ref,
|
|
1194
1216
|
VirtualList,
|
|
1195
1217
|
Signal,
|
|
1196
1218
|
Router,
|
|
1219
|
+
RefInstance,
|
|
1197
1220
|
Page,
|
|
1198
1221
|
List,
|
|
1199
1222
|
$switch,
|
|
1200
|
-
$ifelse,
|
|
1201
|
-
$if,
|
|
1202
1223
|
$case
|
|
1203
1224
|
};
|
|
@@ -8,6 +8,7 @@ declare class List<Type extends any[]> {
|
|
|
8
8
|
indexOf(item: Type[number]): number;
|
|
9
9
|
update(index: number, newItemData: Type[number]): void;
|
|
10
10
|
push(itemData: Type[number], index?: number): void;
|
|
11
|
+
map<T>(callback: (item: Type[number], index: number) => T): T[];
|
|
11
12
|
remove(index: number, count?: number): void;
|
|
12
13
|
}
|
|
13
14
|
/**
|
|
@@ -22,6 +23,7 @@ declare class List<Type extends any[]> {
|
|
|
22
23
|
*/
|
|
23
24
|
export declare class Signal<Type = any> {
|
|
24
25
|
store: Type extends Array<any> ? List<Type> : Type extends Record<string, any> ? Type : never;
|
|
26
|
+
silentStore: Type extends Array<any> ? never : Type;
|
|
25
27
|
passers?: Record<keyof Type, [string, Signal<Type>]>;
|
|
26
28
|
constructor(initial: Type, props?: {
|
|
27
29
|
persistName?: string | undefined;
|
|
@@ -40,7 +42,7 @@ export declare class Signal<Type = any> {
|
|
|
40
42
|
* @param name of event.
|
|
41
43
|
* @param callback function to call.
|
|
42
44
|
*/
|
|
43
|
-
notify<T extends keyof Type>(eventName: (T | "dataChanged" | "itemUpdated" | T[]) | (() => HTMLElement | void) | Comp | ((
|
|
45
|
+
notify<T extends keyof Type>(eventName: (T | "dataChanged" | "itemUpdated" | T[]) | (() => HTMLElement | void) | Comp | ((ctx: Comp) => HTMLElement), listener?: (() => HTMLElement | void) | Comp | ((this: Comp) => HTMLElement)): void;
|
|
44
46
|
computed<T extends keyof Type>(eventName: (T | "dataChanged" | "itemUpdated") | (() => HTMLElement) | Comp | ((this: Comp) => HTMLElement), element?: (() => HTMLElement) | Comp | ((this: Comp) => HTMLElement)): HTMLElement | undefined;
|
|
45
47
|
/**
|
|
46
48
|
* Cradova Signal
|
|
@@ -139,13 +141,13 @@ export declare class Router {
|
|
|
139
141
|
* ---
|
|
140
142
|
* make reference to dom elements
|
|
141
143
|
*/
|
|
142
|
-
export declare class
|
|
144
|
+
export declare class RefInstance<T = unknown> {
|
|
143
145
|
current: Record<string, T>;
|
|
144
146
|
/**
|
|
145
147
|
* Bind a DOM element to a reference name.
|
|
146
148
|
* @param name - The name to reference the DOM element by.
|
|
147
149
|
*/
|
|
148
|
-
bind(name: string): [
|
|
150
|
+
bind(name: string): [RefInstance<T>, string];
|
|
149
151
|
}
|
|
150
152
|
export declare class VirtualList {
|
|
151
153
|
idxs: number[];
|
|
@@ -1,38 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const
|
|
3
|
-
export declare const
|
|
4
|
-
export declare const
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const
|
|
8
|
-
export declare const
|
|
9
|
-
export declare const
|
|
10
|
-
export declare const
|
|
11
|
-
export declare const
|
|
12
|
-
export declare const
|
|
13
|
-
export declare const
|
|
14
|
-
export declare const
|
|
15
|
-
export declare const
|
|
16
|
-
export declare const
|
|
17
|
-
export declare const
|
|
18
|
-
export declare const
|
|
19
|
-
export declare const
|
|
20
|
-
export declare const
|
|
21
|
-
export declare const
|
|
22
|
-
export declare const
|
|
23
|
-
export declare const
|
|
24
|
-
export declare const
|
|
25
|
-
export declare const
|
|
26
|
-
export declare const
|
|
27
|
-
export declare const
|
|
28
|
-
export declare const
|
|
29
|
-
export declare const
|
|
30
|
-
export declare const
|
|
31
|
-
export declare const
|
|
32
|
-
export declare const
|
|
33
|
-
export declare const
|
|
34
|
-
export declare const
|
|
35
|
-
export declare const
|
|
36
|
-
export declare const tr: (...Children_and_Properties: VJS_params_TYPE<HTMLTableColElement>) => HTMLTableColElement;
|
|
37
|
-
export declare const svg: (svg: string, properties?: VJS_params_TYPE<HTMLSpanElement>) => HTMLSpanElement;
|
|
1
|
+
export declare const a: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLAnchorElement>) => HTMLAnchorElement;
|
|
2
|
+
export declare const audio: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLAudioElement>) => HTMLAudioElement;
|
|
3
|
+
export declare const button: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLButtonElement>) => HTMLButtonElement;
|
|
4
|
+
export declare const canvas: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLCanvasElement>) => HTMLCanvasElement;
|
|
5
|
+
export declare const div: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLDivElement>) => HTMLDivElement;
|
|
6
|
+
export declare const footer: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLElement>) => HTMLElement;
|
|
7
|
+
export declare const form: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLFormElement>) => HTMLFormElement;
|
|
8
|
+
export declare const h1: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLHeadingElement>) => HTMLHeadingElement;
|
|
9
|
+
export declare const h2: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLHeadingElement>) => HTMLHeadingElement;
|
|
10
|
+
export declare const h3: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLHeadingElement>) => HTMLHeadingElement;
|
|
11
|
+
export declare const h4: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLHeadingElement>) => HTMLHeadingElement;
|
|
12
|
+
export declare const h5: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLHeadingElement>) => HTMLHeadingElement;
|
|
13
|
+
export declare const h6: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLHeadingElement>) => HTMLHeadingElement;
|
|
14
|
+
export declare const header: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLHeadElement>) => HTMLHeadElement;
|
|
15
|
+
export declare const i: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLLIElement>) => HTMLLIElement;
|
|
16
|
+
export declare const iframe: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLIFrameElement>) => HTMLIFrameElement;
|
|
17
|
+
export declare const img: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLImageElement>) => HTMLImageElement;
|
|
18
|
+
export declare const input: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLInputElement>) => HTMLInputElement;
|
|
19
|
+
export declare const label: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLLabelElement>) => HTMLLabelElement;
|
|
20
|
+
export declare const li: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLLIElement>) => HTMLLIElement;
|
|
21
|
+
export declare const main: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLElement>) => HTMLElement;
|
|
22
|
+
export declare const nav: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLElement>) => HTMLElement;
|
|
23
|
+
export declare const ol: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLOListElement>) => HTMLOListElement;
|
|
24
|
+
export declare const option: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLOptionElement>) => HTMLOptionElement;
|
|
25
|
+
export declare const p: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLParagraphElement>) => HTMLParagraphElement;
|
|
26
|
+
export declare const section: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLElement>) => HTMLElement;
|
|
27
|
+
export declare const select: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLSelectElement>) => HTMLSelectElement;
|
|
28
|
+
export declare const span: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLSpanElement>) => HTMLSpanElement;
|
|
29
|
+
export declare const textarea: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLTextAreaElement>) => HTMLTextAreaElement;
|
|
30
|
+
export declare const ul: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLUListElement>) => HTMLUListElement;
|
|
31
|
+
export declare const video: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLVideoElement>) => HTMLVideoElement;
|
|
32
|
+
export declare const tbody: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLTableElement>) => HTMLTableElement;
|
|
33
|
+
export declare const table: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLTableSectionElement>) => HTMLTableSectionElement;
|
|
34
|
+
export declare const td: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLTableCellElement>) => HTMLTableCellElement;
|
|
35
|
+
export declare const tr: (...Children_and_Properties: import("./types.js").VJS_params_TYPE<HTMLTableColElement>) => HTMLTableColElement;
|
|
38
36
|
export declare const raw: (html: string | TemplateStringsArray) => DocumentFragment;
|
|
@@ -2,12 +2,6 @@ import * as CSS from "csstype";
|
|
|
2
2
|
import type { Comp, VJS_params_TYPE } from "./types.js";
|
|
3
3
|
import { Signal } from "./classes.js";
|
|
4
4
|
export declare const cra: <E extends HTMLElement>(tag: string) => (...Children_and_Properties: VJS_params_TYPE<E>) => E;
|
|
5
|
-
/**
|
|
6
|
-
* @param {expression} condition
|
|
7
|
-
* @param {function} elements[]
|
|
8
|
-
*/
|
|
9
|
-
export declare function $if(condition: any, ...elements: (() => HTMLElement | undefined)[]): (() => HTMLElement | undefined)[] | undefined;
|
|
10
|
-
export declare function $ifelse(condition: any, ifTrue: () => HTMLElement, ifFalse?: () => HTMLElement): HTMLElement | undefined;
|
|
11
5
|
type Case<K> = (key: K) => HTMLElement | undefined;
|
|
12
6
|
export declare const $case: (value: any, element: any) => Case<any>;
|
|
13
7
|
export declare function $switch<K = unknown>(key: K, ...cases: Case<K>[]): HTMLElement | undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as CSS from "csstype";
|
|
2
|
-
import {
|
|
2
|
+
import { Page, RefInstance, Signal } from "./classes.js";
|
|
3
3
|
interface Attributes<E extends HTMLElement> {
|
|
4
|
-
ref?: [
|
|
4
|
+
ref?: [RefInstance<any>, string];
|
|
5
5
|
value?: any;
|
|
6
6
|
style?: Partial<CSS.Properties>;
|
|
7
7
|
[key: `data-${string}`]: string | undefined;
|
|
@@ -11,7 +11,7 @@ interface Attributes<E extends HTMLElement> {
|
|
|
11
11
|
type StandardEvents<E extends HTMLElement> = {
|
|
12
12
|
[key in keyof E]: E[key] extends (this: E, event: Event) => void ? key : never;
|
|
13
13
|
}[keyof E];
|
|
14
|
-
export type VJS_params_TYPE<E extends HTMLElement> = (undefined | undefined[] | string | string[] | HTMLElement | HTMLElement[] | DocumentFragment | DocumentFragment[] | (() => HTMLElement) | (() => HTMLElement)[] |
|
|
14
|
+
export type VJS_params_TYPE<E extends HTMLElement> = (undefined | undefined[] | string | string[] | HTMLElement | HTMLElement[] | DocumentFragment | DocumentFragment[] | (() => HTMLElement) | (() => HTMLElement)[] | ((ctx: Comp) => HTMLElement) | ((ctx: Comp) => HTMLElement)[] | [string, Signal<any>] | (Attributes<E> & Omit<Partial<E>, keyof Attributes<E> | StandardEvents<E>>))[];
|
|
15
15
|
export type CradovaPageType = {
|
|
16
16
|
/**
|
|
17
17
|
* Cradova page
|
|
@@ -42,7 +42,7 @@ export type CradovaPageType = {
|
|
|
42
42
|
};
|
|
43
43
|
export type browserPageType<importType = Page> = importType | Promise<importType> | (() => Promise<importType>);
|
|
44
44
|
export interface Comp extends Function {
|
|
45
|
-
(
|
|
45
|
+
(ctx: Comp, ...args: any[]): HTMLElement;
|
|
46
46
|
useReducer: <S, A>(reducer: (state: S, action: A) => S, initialArg: S, initializer?: (arg: S) => S) => [S, (action: A) => void];
|
|
47
47
|
useState: <S>(initialValue: S) => [S, (newState: S | ((prevState: S) => S)) => void];
|
|
48
48
|
useEffect: (effect: () => (() => void) | void, deps?: unknown[]) => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cradova",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.1",
|
|
4
4
|
"description": "Build Powerful ⚡ Web Apps with Ease",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"compile": "bun bundle.ts",
|
|
18
|
-
"lint": "eslint . --fix"
|
|
18
|
+
"lint": "eslint . --fix",
|
|
19
|
+
"tsc": "tsc --target esnext tests/index.ts --watch --moduleResolution NodeNext --module NodeNext"
|
|
19
20
|
},
|
|
20
21
|
"keywords": [
|
|
21
22
|
"frontend library",
|