@familyboat/mini-framework 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.
- package/README.md +73 -0
- package/lib/README.md +542 -0
- package/lib/animate/index.css +95 -0
- package/lib/animate/index.ts +198 -0
- package/lib/index.ts +25 -0
- package/lib/interaction/index.ts +16 -0
- package/lib/modal/index.css +5 -0
- package/lib/modal/index.ts +153 -0
- package/lib/router/index.css +9 -0
- package/lib/router/index.ts +130 -0
- package/lib/toast/index.css +57 -0
- package/lib/toast/index.ts +145 -0
- package/lib/util.ts +45 -0
- package/package.json +58 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { fadeIn, fadeOut } from "../animate";
|
|
2
|
+
import "./index.css";
|
|
3
|
+
|
|
4
|
+
export type ToastPosition = "top" | "bottom";
|
|
5
|
+
export type ToastKind = "info" | "success" | "error";
|
|
6
|
+
|
|
7
|
+
export type ToastProps = {
|
|
8
|
+
message: string;
|
|
9
|
+
duration?: number;
|
|
10
|
+
position?: ToastPosition;
|
|
11
|
+
kind?: ToastKind;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class Toast {
|
|
15
|
+
private static readonly defaultDuration = 3000;
|
|
16
|
+
private static containers = new Map<ToastPosition, HTMLElement>();
|
|
17
|
+
|
|
18
|
+
private _props: Required<Pick<ToastProps, "duration" | "position" | "kind">> & Pick<ToastProps, "message">;
|
|
19
|
+
private _root: HTMLElement;
|
|
20
|
+
private _container: HTMLElement;
|
|
21
|
+
private _timer?: ReturnType<typeof setTimeout>;
|
|
22
|
+
private _destroyed = false;
|
|
23
|
+
private _operationId = 0;
|
|
24
|
+
|
|
25
|
+
private constructor(props: ToastProps) {
|
|
26
|
+
this._props = {
|
|
27
|
+
message: props.message,
|
|
28
|
+
duration: props.duration ?? Toast.defaultDuration,
|
|
29
|
+
position: props.position ?? "top",
|
|
30
|
+
kind: props.kind ?? "info",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
this._container = Toast.getContainer(this._props.position);
|
|
34
|
+
this._root = document.createElement("div");
|
|
35
|
+
this._root.classList.add("toast", `toast--${this._props.kind}`);
|
|
36
|
+
this._root.setAttribute("role", "status");
|
|
37
|
+
this._root.setAttribute("aria-live", "polite");
|
|
38
|
+
|
|
39
|
+
const message = document.createElement("span");
|
|
40
|
+
message.classList.add("toast__message");
|
|
41
|
+
message.textContent = this._props.message;
|
|
42
|
+
|
|
43
|
+
const closeButton = document.createElement("button");
|
|
44
|
+
closeButton.type = "button";
|
|
45
|
+
closeButton.classList.add("toast__close");
|
|
46
|
+
closeButton.textContent = "×";
|
|
47
|
+
closeButton.setAttribute("aria-label", "Close toast");
|
|
48
|
+
closeButton.addEventListener("click", () => {
|
|
49
|
+
this.dismiss();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
this._root.appendChild(message);
|
|
53
|
+
this._root.appendChild(closeButton);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Creates and shows a toast instance. */
|
|
57
|
+
static show(props: ToastProps): Toast {
|
|
58
|
+
const toast = new Toast(props);
|
|
59
|
+
toast.mount();
|
|
60
|
+
toast.show();
|
|
61
|
+
return toast;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Removes all visible toasts. */
|
|
65
|
+
static clear(): void {
|
|
66
|
+
for (const container of Toast.containers.values()) {
|
|
67
|
+
container.replaceChildren();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Dismisses this toast immediately. */
|
|
72
|
+
dismiss(): void {
|
|
73
|
+
if (this._destroyed) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.clearTimer();
|
|
78
|
+
this.beginOperation();
|
|
79
|
+
void fadeOut(this._root, { duration: "180ms" }).then((completed) => {
|
|
80
|
+
if (!this.isCurrentOperation(this._operationId) || !completed) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.destroy();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Removes this toast from the document. */
|
|
89
|
+
destroy(): void {
|
|
90
|
+
if (this._destroyed) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this._destroyed = true;
|
|
95
|
+
this.clearTimer();
|
|
96
|
+
this._root.remove();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private mount(): void {
|
|
100
|
+
this._container.appendChild(this._root);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private show(): void {
|
|
104
|
+
const operationId = this.beginOperation();
|
|
105
|
+
void fadeIn(this._root, { duration: "180ms" }).then((completed) => {
|
|
106
|
+
if (!this.isCurrentOperation(operationId) || !completed) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this._timer = setTimeout(() => {
|
|
111
|
+
this.dismiss();
|
|
112
|
+
}, this._props.duration);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private clearTimer(): void {
|
|
117
|
+
if (this._timer !== undefined) {
|
|
118
|
+
clearTimeout(this._timer);
|
|
119
|
+
this._timer = undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private beginOperation(): number {
|
|
124
|
+
this._operationId += 1;
|
|
125
|
+
return this._operationId;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private isCurrentOperation(operationId: number): boolean {
|
|
129
|
+
return operationId === this._operationId;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private static getContainer(position: ToastPosition): HTMLElement {
|
|
133
|
+
const existing = Toast.containers.get(position);
|
|
134
|
+
if (existing) {
|
|
135
|
+
return existing;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const container = document.createElement("div");
|
|
139
|
+
container.classList.add("toast-container", `toast-container--${position}`);
|
|
140
|
+
document.body.appendChild(container);
|
|
141
|
+
|
|
142
|
+
Toast.containers.set(position, container);
|
|
143
|
+
return container;
|
|
144
|
+
}
|
|
145
|
+
}
|
package/lib/util.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
type PromiseResolvers<T> = {
|
|
2
|
+
promise: Promise<T>
|
|
3
|
+
resolve: (value: T | PromiseLike<T>) => void
|
|
4
|
+
reject: (reason?: any) => void
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function html(strings: TemplateStringsArray, ...values: unknown[]): string {
|
|
8
|
+
return String.raw({ raw: strings }, ...values).trim()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type TemplateValues = Record<string, string | number | boolean | null | undefined>
|
|
12
|
+
|
|
13
|
+
export function renderTemplate(
|
|
14
|
+
template: string,
|
|
15
|
+
values: TemplateValues = {},
|
|
16
|
+
): string {
|
|
17
|
+
return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, key: string) => {
|
|
18
|
+
const value = values[key]
|
|
19
|
+
|
|
20
|
+
return value == null ? "" : String(value)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function withResolvers<T>(): PromiseResolvers<T> {
|
|
25
|
+
let resolve!: ConstructorParameters<typeof Promise<T>>[0] extends (
|
|
26
|
+
resolve: infer R,
|
|
27
|
+
reject: infer _J
|
|
28
|
+
) => unknown
|
|
29
|
+
? R
|
|
30
|
+
: never
|
|
31
|
+
|
|
32
|
+
let reject!: ConstructorParameters<typeof Promise<T>>[0] extends (
|
|
33
|
+
resolve: infer _R,
|
|
34
|
+
reject: infer J
|
|
35
|
+
) => unknown
|
|
36
|
+
? J
|
|
37
|
+
: never
|
|
38
|
+
|
|
39
|
+
const promise = new Promise<T>((res, rej) => {
|
|
40
|
+
resolve = res
|
|
41
|
+
reject = rej
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return { resolve, reject, promise }
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@familyboat/mini-framework",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./lib/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"types": "./lib/index.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"lib/**/*.ts",
|
|
12
|
+
"lib/**/*.css",
|
|
13
|
+
"lib/**/*.md",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"sideEffects": [
|
|
17
|
+
"**/*.css"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"dev": "vite",
|
|
21
|
+
"build": "tsc && vite build",
|
|
22
|
+
"preview": "vite preview",
|
|
23
|
+
"check:publish": "npm pack --dry-run",
|
|
24
|
+
"publish:check": "npm publish --dry-run --access public"
|
|
25
|
+
},
|
|
26
|
+
"description": "A lightweight DOM framework with route, modal, animation, interaction, and toast utilities.",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/familyboat/mini-framework.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/familyboat/mini-framework#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/familyboat/mini-framework/issues"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"typescript",
|
|
37
|
+
"dom",
|
|
38
|
+
"router",
|
|
39
|
+
"modal",
|
|
40
|
+
"animation",
|
|
41
|
+
"jsr"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"vite": "^8.2.2"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^26.4.1",
|
|
52
|
+
"typescript": "~6.0.2",
|
|
53
|
+
"vite": "^8.2.2"
|
|
54
|
+
},
|
|
55
|
+
"allowScripts": {
|
|
56
|
+
"fsevents@2.3.3": true
|
|
57
|
+
}
|
|
58
|
+
}
|