kempo 3.2.1 → 4.2.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 (39) hide show
  1. package/dist/components/Component.js +1 -1
  2. package/dist/components/Dialog.js +1 -1
  3. package/dist/components/LazyComponent.js +1 -1
  4. package/dist/components/Split.js +1 -1
  5. package/dist/components/Toast.js +1 -0
  6. package/dist/utils/Server.js +1 -0
  7. package/dist/utils/drag.js +1 -1
  8. package/dist/utils/fs-utils.js +1 -0
  9. package/dist/utils/minify.js +1 -0
  10. package/dist/utils/watchWindowSize.js +1 -0
  11. package/docs/components/split.html +19 -0
  12. package/docs/components/toast.html +330 -0
  13. package/docs/icons/error.svg +1 -0
  14. package/docs/icons/warning.svg +1 -0
  15. package/docs/kempo/components/Component.js +1 -1
  16. package/docs/kempo/components/Dialog.js +1 -1
  17. package/docs/kempo/components/LazyComponent.js +1 -1
  18. package/docs/kempo/components/Split.js +1 -1
  19. package/docs/kempo/components/Toast.js +1 -0
  20. package/docs/kempo/utils/Server.js +1 -0
  21. package/docs/kempo/utils/drag.js +1 -1
  22. package/docs/kempo/utils/fs-utils.js +1 -0
  23. package/docs/kempo/utils/minify.js +1 -0
  24. package/docs/kempo/utils/watchWindowSize.js +1 -0
  25. package/docs/nav.inc.html +2 -1
  26. package/docs/utils/Server.html +119 -0
  27. package/icons/error.svg +1 -0
  28. package/icons/warning.svg +1 -0
  29. package/package.json +2 -4
  30. package/scripts/build.js +8 -8
  31. package/scripts/docs.js +20 -20
  32. package/src/components/Dialog.js +11 -1
  33. package/src/components/Split.js +38 -13
  34. package/src/components/Toast.js +424 -0
  35. package/src/utils/Server.js +189 -0
  36. package/src/utils/fs-utils.js +41 -0
  37. package/src/utils/watchWindowSize.js +15 -0
  38. package/test/index.js +27 -16
  39. package/scripts/test.js +0 -31
@@ -0,0 +1,424 @@
1
+ import Component from './Component.js';
2
+ import {
3
+ onEvent,
4
+ offEvent,
5
+ dispatchEvent
6
+ } from '../utils/element.js';
7
+
8
+ const _position = Symbol();
9
+ export class ToastContainer extends Component {
10
+ constructor(position = 'bottom center'){
11
+ super();
12
+
13
+ /* Private Members */
14
+ this[_position] = position.toLowerCase();
15
+
16
+ /* Init */
17
+ this.setAttribute('position', this[_position]);
18
+ }
19
+
20
+ /* Protected Members */
21
+ get position(){
22
+ return this[_position];
23
+ }
24
+ set position(val){
25
+ if(val !== this[_position]){
26
+ this[_position] = val.toLowerCase();
27
+ this.setAttribute('position', this[_position]);
28
+ }
29
+ }
30
+
31
+ /* Shadow DOM */
32
+ get shadowStyles(){
33
+ return /*css*/`
34
+ ${super.shadowStyles}
35
+ :host {
36
+ position: fixed;
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 8px;
40
+ z-index: 1000;
41
+ pointer-events: none;
42
+ padding: 16px;
43
+ box-sizing: border-box;
44
+ max-width: 100%;
45
+ max-height: 100%;
46
+ overflow: hidden;
47
+ }
48
+
49
+ :host([position*="top"]) {
50
+ top: 0;
51
+ }
52
+
53
+ :host([position*="bottom"]) {
54
+ bottom: 0;
55
+ }
56
+
57
+ :host([position*="left"]) {
58
+ left: 0;
59
+ align-items: flex-start;
60
+ }
61
+
62
+ :host([position*="right"]) {
63
+ right: 0;
64
+ align-items: flex-end;
65
+ }
66
+
67
+ :host([position*="center"]:not([position*="top"]):not([position*="bottom"])) {
68
+ top: 50%;
69
+ transform: translateY(-50%);
70
+ }
71
+
72
+ :host([position="center"]),
73
+ :host([position*="center"][position*="top"]),
74
+ :host([position*="center"][position*="bottom"]) {
75
+ left: 50%;
76
+ transform: translateX(-50%);
77
+ }
78
+
79
+ :host([position*="center"]:not([position="center"])) {
80
+ align-items: center;
81
+ }
82
+
83
+ /* Combined transforms for center vertically and horizontally */
84
+ :host([position*="center"]:not([position*="top"]):not([position*="bottom"]):not([position="center"])) {
85
+ transform: translate(-50%, -50%);
86
+ }
87
+
88
+ ::slotted(*) {
89
+ pointer-events: auto;
90
+ }
91
+ `;
92
+ }
93
+ }
94
+ window.customElements.define('k-toast-container', ToastContainer);
95
+
96
+ const closeHandler = Symbol(),
97
+ actionHandler = Symbol(),
98
+ timeoutId = Symbol(),
99
+ animationEndHandler = Symbol(),
100
+ _toastPosition = Symbol(),
101
+ _closing = Symbol();
102
+
103
+ export default class Toast extends Component {
104
+ constructor({
105
+ actionCallback = () => {},
106
+ closeCallback = () => {},
107
+ timeout = 0,
108
+ position = 'bottom center'
109
+ } = {}){
110
+ super();
111
+
112
+ /* Private Members */
113
+ this[timeoutId] = -1;
114
+ this[_toastPosition] = position.toLowerCase();
115
+ this[_closing] = false;
116
+
117
+ /* Private Methods */
118
+ this[closeHandler] = () => {
119
+ this.close();
120
+ }
121
+ this[actionHandler] = () => {
122
+ if(this.actionCallback() !== false){
123
+ this.close();
124
+ }
125
+ }
126
+ this[animationEndHandler] = (e) => {
127
+ if (e.animationName.includes('toast-hide')) {
128
+ this.removeAttribute('animating');
129
+ this.opened = false;
130
+ dispatchEvent(this, 'close openchange');
131
+ this.closeCallback();
132
+ this[_closing] = false;
133
+ } else if (e.animationName.includes('toast-show')) {
134
+ this.removeAttribute('animating');
135
+ }
136
+ }
137
+
138
+ /* Init */
139
+ this.registerProps({
140
+ actionCallback,
141
+ closeCallback
142
+ });
143
+ this.registerAttributes({
144
+ hasAction: false,
145
+ hasClose: false,
146
+ hasIcon: false,
147
+ timeout,
148
+ opened: false,
149
+ position: this[_toastPosition]
150
+ });
151
+ }
152
+
153
+ /* Lifecycle Callback */
154
+ async render(force){
155
+ if(await super.render(force)){
156
+ onEvent(this.shadowRoot.getElementById('action'), 'click', this[actionHandler]);
157
+ onEvent(this.shadowRoot.getElementById('close'), 'click', this[closeHandler]);
158
+ onEvent(this, 'animationend', this[animationEndHandler]);
159
+ return true;
160
+ }
161
+ return false;
162
+ }
163
+ disconnectedCallback(){
164
+ super.disconnectedCallback();
165
+ clearTimeout(this[timeoutId]);
166
+ offEvent(this.shadowRoot.getElementById('action'), 'click', this[actionHandler]);
167
+ offEvent(this.shadowRoot.getElementById('close'), 'click', this[closeHandler]);
168
+ offEvent(this, 'animationend', this[animationEndHandler]);
169
+ }
170
+ attributeChangedCallback(n, oV, nV){
171
+ super.attributeChangedCallback(n, oV, nV);
172
+ if(n === 'opened' && oV !== nV && nV){
173
+ this.hasAction = !!this.querySelector('[slot="action"]');
174
+ this.hasClose = !!this.querySelector('[slot="close"]');
175
+ this.hasIcon = !!this.querySelector('[slot="icon"]');
176
+ }
177
+ }
178
+
179
+ /* Public Methods */
180
+ open(){
181
+ this[_closing] = false;
182
+ this.setAttribute('animating', 'in');
183
+ this.opened = true;
184
+ if(this.timeout){
185
+ clearTimeout(this[timeoutId]);
186
+ this[timeoutId] = setTimeout(() => {
187
+ this.close();
188
+ }, this.timeout);
189
+ }
190
+ dispatchEvent(this, 'open openchange');
191
+ }
192
+
193
+ close(){
194
+ clearTimeout(this[timeoutId]);
195
+ if (this.opened && !this[_closing]) {
196
+ this[_closing] = true;
197
+ this.setAttribute('animating', 'out');
198
+ // We'll set opened to false in the animation end handler
199
+ }
200
+ }
201
+
202
+ /* Shadow DOM */
203
+ get shadowTemplate(){
204
+ return /*html*/`
205
+ <div id="icon">
206
+ <slot name="icon"></slot>
207
+ </div>
208
+ <div id="message">
209
+ ${super.shadowTemplate}
210
+ </div>
211
+ <button id="action" class="no-style">
212
+ <slot name="action"></slot>
213
+ </button>
214
+ <button id="close" class="no-style">
215
+ <slot name="close"></slot>
216
+ </button>
217
+ `;
218
+ }
219
+ get shadowStyles(){
220
+ return /*css*/`
221
+ ${super.shadowStyles}
222
+ @keyframes toast-show-bottom {
223
+ from { opacity: 0; transform: translateY(20px); }
224
+ to { opacity: 1; transform: translateY(0); }
225
+ }
226
+ @keyframes toast-hide-bottom {
227
+ from { opacity: 1; transform: translateY(0); }
228
+ to { opacity: 0; transform: translateY(20px); }
229
+ }
230
+ @keyframes toast-show-top {
231
+ from { opacity: 0; transform: translateY(-20px); }
232
+ to { opacity: 1; transform: translateY(0); }
233
+ }
234
+ @keyframes toast-hide-top {
235
+ from { opacity: 1; transform: translateY(0); }
236
+ to { opacity: 0; transform: translateY(-20px); }
237
+ }
238
+ @keyframes toast-show-left {
239
+ from { opacity: 0; transform: translateX(-20px); }
240
+ to { opacity: 1; transform: translateX(0); }
241
+ }
242
+ @keyframes toast-hide-left {
243
+ from { opacity: 1; transform: translateX(0); }
244
+ to { opacity: 0; transform: translateX(-20px); }
245
+ }
246
+ @keyframes toast-show-right {
247
+ from { opacity: 0; transform: translateX(20px); }
248
+ to { opacity: 1; transform: translateX(0); }
249
+ }
250
+ @keyframes toast-hide-right {
251
+ from { opacity: 1; transform: translateX(0); }
252
+ to { opacity: 0; transform: translateX(20px); }
253
+ }
254
+ :host {
255
+ display: none;
256
+ min-width: 16rem;
257
+ max-width: calc(100% - ( 2 * var(--spacer) ));
258
+ background-color: var(--c_bg);
259
+ border-radius: var(--radius);
260
+ box-shadow: var(--drop_shadow);
261
+ }
262
+ :host([opened="true"]) {
263
+ display: flex;
264
+ }
265
+ :host([animating="in"][position*="bottom"]) {
266
+ animation: toast-show-bottom var(--animation_ms, 300ms) ease forwards;
267
+ }
268
+ :host([animating="out"][position*="bottom"]) {
269
+ animation: toast-hide-bottom var(--animation_ms, 300ms) ease forwards;
270
+ }
271
+ :host([animating="in"][position*="top"]) {
272
+ animation: toast-show-top var(--animation_ms, 300ms) ease forwards;
273
+ }
274
+ :host([animating="out"][position*="top"]) {
275
+ animation: toast-hide-top var(--animation_ms, 300ms) ease forwards;
276
+ }
277
+ :host([animating="in"][position*="left"]:not([position*="top"]):not([position*="bottom"])) {
278
+ animation: toast-show-left var(--animation_ms, 300ms) ease forwards;
279
+ }
280
+ :host([animating="out"][position*="left"]:not([position*="top"]):not([position*="bottom"])) {
281
+ animation: toast-hide-left var(--animation_ms, 300ms) ease forwards;
282
+ }
283
+ :host([animating="in"][position*="right"]:not([position*="top"]):not([position*="bottom"])) {
284
+ animation: toast-show-right var(--animation_ms, 300ms) ease forwards;
285
+ }
286
+ :host([animating="out"][position*="right"]:not([position*="top"]):not([position*="bottom"])) {
287
+ animation: toast-hide-right var(--animation_ms, 300ms) ease forwards;
288
+ }
289
+ #icon {
290
+ padding: var(--spacer);
291
+ padding-right: 0;
292
+ }
293
+ #message {
294
+ padding: var(--spacer);
295
+ flex: 1 1 auto;
296
+ }
297
+ :host(:not([has-close])) #close,
298
+ :host(:not([has-action])) #action,
299
+ :host(:not([has-icon])) #icon {
300
+ display: none;
301
+ }
302
+ #action {
303
+ background: transparent;
304
+ border: none;
305
+ color: var(--tc_primary, blue);
306
+ cursor: pointer;
307
+ padding: var(--spacer);
308
+ }
309
+ #action:hover {
310
+ color: var(--tc_primary__hover, lightblue);
311
+ }
312
+ #close {
313
+ background: transparent;
314
+ border: none;
315
+ cursor: pointer;
316
+ padding: var(--spacer);
317
+ opacity: 0.7;
318
+ transition: opacity var(--animation_ms);
319
+ }
320
+ #close:hover {
321
+ opacity: 1;
322
+ }
323
+ `;
324
+ }
325
+
326
+ /* Static Members */
327
+ static observedAttributes = [...super.observedAttributes, 'opened'];
328
+
329
+ /* Static Methods */
330
+ static create(message, options = {}){
331
+ let {
332
+ position = 'auto',
333
+ removeOnClose = true,
334
+ closeCallback = () => {},
335
+ action = false,
336
+ close = false,
337
+ icon = false
338
+ } = options;
339
+ if(position === 'auto'){
340
+ position = window.innerWidth <= 768 ? 'bottom center' : 'top right';
341
+ }
342
+ let $container = document.querySelector(`k-toast-container[position="${position}"]`);
343
+ if(!$container){
344
+ $container = new ToastContainer(position);
345
+ document.body.appendChild($container);
346
+ }
347
+ const $toast = new Toast({
348
+ position,
349
+ timeout: 5000,
350
+ ...options,
351
+ closeCallback: (...args) => {
352
+ if(removeOnClose){
353
+ $toast.remove();
354
+ if($container.children.length === 0){
355
+ $container.remove();
356
+ }
357
+ }
358
+ closeCallback(...args);
359
+ }
360
+ });
361
+ $toast.innerHTML = message;
362
+ if(icon){
363
+ if(icon instanceof HTMLElement){
364
+ icon.slot = 'icon';
365
+ $toast.appencChild(icon);
366
+ } else if(typeof(icon) === 'string'){
367
+ const $span = document.createElement('span');
368
+ $span.slot = 'icon';
369
+ $span.innerHTML = icon;
370
+ $toast.appendChild($span);
371
+ }
372
+ }
373
+ if(action){
374
+ if(action instanceof HTMLElement){
375
+ action.slot = 'action';
376
+ $toast.appencChild(action);
377
+ } else if(typeof(action) === 'string'){
378
+ const $span = document.createElement('span');
379
+ $span.slot = 'action';
380
+ $span.innerHTML = action;
381
+ $toast.appendChild($span);
382
+ }
383
+ }
384
+ if(close){
385
+ if(close instanceof HTMLElement){
386
+ close.slot = 'close';
387
+ $toast.appencChild(close);
388
+ } else if(typeof(close) === 'string'){
389
+ const $span = document.createElement('span');
390
+ $span.slot = 'close';
391
+ $span.innerHTML = close;
392
+ $toast.appendChild($span);
393
+ }
394
+ }
395
+ $toast.open();
396
+ $container.appendChild($toast);
397
+ return $toast;
398
+ }
399
+ static success(message, options = {}){
400
+ const $toast = Toast.create(message, {
401
+ icon: '<k-icon name="check"></k-icon>',
402
+ ...options
403
+ });
404
+ $toast.classList.add('bg-success');
405
+ return $toast;
406
+ }
407
+ static warning(message, options = {}){
408
+ const $toast = Toast.create(message, {
409
+ icon: '<k-icon name="warning"></k-icon>',
410
+ ...options
411
+ });
412
+ $toast.classList.add('bg-warning');
413
+ return $toast;
414
+ }
415
+ static error(message, options = {}){
416
+ const $toast = Toast.create(message, {
417
+ icon: '<k-icon name="error"></k-icon>',
418
+ ...options
419
+ });
420
+ $toast.classList.add('bg-danger');
421
+ return $toast;
422
+ }
423
+ }
424
+ window.customElements.define('k-toast', Toast);
@@ -0,0 +1,189 @@
1
+ import http from 'http';
2
+ import { readFile } from 'fs/promises';
3
+ import { join, extname, normalize } from 'path';
4
+
5
+ export class Server {
6
+ constructor(options = {}) {
7
+ this.routes = [];
8
+ }
9
+
10
+ route(pattern, handler, methods) {
11
+ const routeMethods = methods ? methods.map(m => m.toUpperCase()) : ['ANY'];
12
+ this.routes.push({
13
+ pattern,
14
+ handler,
15
+ methods: routeMethods
16
+ });
17
+ return this;
18
+ }
19
+
20
+ get(path, handler) {
21
+ return this.route(path, handler, ['GET']);
22
+ }
23
+
24
+ post(path, handler) {
25
+ return this.route(path, handler, ['POST']);
26
+ }
27
+
28
+ put(path, handler) {
29
+ return this.route(path, handler, ['PUT']);
30
+ }
31
+
32
+ delete(path, handler) {
33
+ return this.route(path, handler, ['DELETE']);
34
+ }
35
+
36
+ patch(path, handler) {
37
+ return this.route(path, handler, ['PATCH']);
38
+ }
39
+
40
+ async handleRequest(req, res) {
41
+ try {
42
+ const [ url, paramStr = '' ] = req.url.split('?');
43
+ const params = paramStr.split('&').filter(p=>p!='').map(p => {
44
+ const [key, value] = p.split('=');
45
+ return { [key]: decodeURIComponent(value) };
46
+ });
47
+
48
+ const matchedRoute = this.routes.find(({ methods, pattern }) => {
49
+ if(
50
+ !methods.includes(req.method || 'GET') &&
51
+ !methods.includes('ANY')
52
+ ) return false;
53
+ const reqParts = url.split('/').filter(p=>p!='');
54
+ const patternParts = pattern.split('/').filter(p=>p!='');
55
+ if(reqParts.length == 0 && patternParts.length == 0) return true; // '/' == '/'
56
+ const hasWildcard = patternParts.length > 0 &&
57
+ patternParts[patternParts.length - 1] === '*';
58
+ if (!hasWildcard && reqParts.length !== patternParts.length) return false;
59
+ if (hasWildcard && reqParts.length < patternParts.length - 1) return false;
60
+ const partsToCompare = hasWildcard ? patternParts.length - 1 : patternParts.length;
61
+ for (let i = 0; i < partsToCompare; i++) {
62
+ if (patternParts[i].startsWith(':')) continue;
63
+ if (patternParts[i] !== reqParts[i]) return false;
64
+ }
65
+ return true;
66
+ });
67
+
68
+ if(!matchedRoute) {
69
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
70
+ res.end('Not Found');
71
+ return;
72
+ }
73
+
74
+ const { pattern, handler } = matchedRoute;
75
+ if(typeof(handler) === 'string') {
76
+ // Extract the remaining path after the matched pattern
77
+ const patternParts = pattern.split('/').filter(p=>p!='');
78
+ const reqParts = url.split('/').filter(p=>p!='');
79
+ const hasWildcard = patternParts[patternParts.length - 1] === '*';
80
+ const remainingPath = hasWildcard
81
+ ? reqParts.slice(patternParts.length - 1).join('/')
82
+ : '';
83
+
84
+ // Remove wildcard from handler if present
85
+ const baseHandler = handler.endsWith('*')
86
+ ? handler.slice(0, -1)
87
+ : handler;
88
+
89
+ const filePath = baseHandler.startsWith('/') || /^[A-Z]:/i.test(baseHandler)
90
+ ? normalize(join(baseHandler, remainingPath))
91
+ : join(process.cwd(), normalize(join(baseHandler, remainingPath)));
92
+
93
+ if (filePath.endsWith('/') || filePath.endsWith('\\')) {
94
+ await Server.serveFile(join(filePath, 'index.html'), res);
95
+ } else {
96
+ await Server.serveFile(filePath, res);
97
+ }
98
+ } else if(typeof(handler) === 'function'){
99
+ const reqVars = {};
100
+ if(pattern.includes(':')){ // Fix typo in includes
101
+ pattern.split('/').filter(p=>p!='').forEach((part, i) => {
102
+ if(part.startsWith(':')){
103
+ reqVars[part.slice(1)] = url.split('/').filter(p=>p!='')[i];
104
+ }
105
+ });
106
+ }
107
+ await handler(req, res, reqVars, params);
108
+ } else {
109
+ console.error('Invalid Route Handler');
110
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
111
+ res.end('Internal server error');
112
+ }
113
+
114
+ } catch (err) {
115
+ console.error('Request error:', err);
116
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
117
+ res.end('Internal server error');
118
+ }
119
+ }
120
+
121
+ listen(port = 8080) {
122
+ return new Promise((resolve, reject) => {
123
+ try {
124
+ const server = http.createServer((req, res) => this.handleRequest(req, res));
125
+
126
+ server.on('error', (err) => {
127
+ if (err.code === 'EADDRINUSE') {
128
+ reject(new Error(`Port ${port} is already in use`));
129
+ } else {
130
+ reject(err);
131
+ }
132
+ });
133
+
134
+ server.listen(port, () => {
135
+ resolve(server);
136
+ });
137
+ } catch (err) {
138
+ reject(err);
139
+ }
140
+ });
141
+ }
142
+
143
+ /*
144
+ Static Members
145
+ */
146
+ static mimeTypes = {
147
+ '.html': 'text/html; charset=utf-8',
148
+ '.js': 'text/javascript; charset=utf-8',
149
+ '.css': 'text/css; charset=utf-8',
150
+ '.json': 'application/json; charset=utf-8',
151
+ '.png': 'image/png',
152
+ '.jpg': 'image/jpeg',
153
+ '.svg': 'image/svg+xml',
154
+ '.ico': 'image/x-icon',
155
+ '.txt': 'text/plain; charset=utf-8',
156
+ '.md': 'text/plain; charset=utf-8',
157
+ '.map': 'application/json; charset=utf-8'
158
+ };
159
+
160
+ static textExtensions = ['.txt', '.md', '.js', '.css', '.html', '.json', '.map'];
161
+
162
+ /*
163
+ Static Methods
164
+ */
165
+ static getMimeType(filePath) {
166
+ const ext = extname(filePath).toLowerCase();
167
+ if (this.mimeTypes[ext]) return this.mimeTypes[ext];
168
+ if (this.textExtensions.includes(ext)) {
169
+ return 'text/plain; charset=utf-8';
170
+ }
171
+ return 'application/octet-stream';
172
+ }
173
+
174
+ static async serveFile(filePath, res, headers = {}) {
175
+ try {
176
+ const data = await readFile(filePath);
177
+ res.writeHead(200, {
178
+ 'Content-Type': Server.getMimeType(filePath),
179
+ ...headers
180
+ });
181
+ res.end(data);
182
+ } catch (err) {
183
+ const status = err.code === 'ENOENT' ? 404 : 500;
184
+ const message = status === 404 ? 'Not found' : 'Internal server error';
185
+ res.writeHead(status, { 'Content-Type': 'text/plain' });
186
+ res.end(message);
187
+ }
188
+ }
189
+ }
@@ -0,0 +1,41 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ export async function ensureDir(dirPath) {
5
+ try {
6
+ await fs.mkdir(dirPath, { recursive: true });
7
+ } catch (error) {
8
+ if (error.code !== 'EEXIST') {
9
+ throw error;
10
+ }
11
+ }
12
+ }
13
+
14
+ export async function copyDir(src, dest) {
15
+ await ensureDir(dest);
16
+ const entries = await fs.readdir(src, { withFileTypes: true });
17
+
18
+ for (const entry of entries) {
19
+ const srcPath = path.join(src, entry.name);
20
+ const destPath = path.join(dest, entry.name);
21
+
22
+ if (entry.isDirectory()) {
23
+ await copyDir(srcPath, destPath);
24
+ } else {
25
+ await fs.copyFile(srcPath, destPath);
26
+ }
27
+ }
28
+ }
29
+
30
+ export async function emptyDir(dirPath) {
31
+ try {
32
+ const entries = await fs.readdir(dirPath);
33
+ await Promise.all(entries.map(entry =>
34
+ fs.rm(path.join(dirPath, entry), { recursive: true, force: true })
35
+ ));
36
+ } catch (error) {
37
+ if (error.code !== 'ENOENT') {
38
+ throw error;
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,15 @@
1
+ import debounce from './debounce.js';
2
+
3
+ const handlers = new Set();
4
+ window.addEventListener('resize', debounce(()=>{
5
+ const width = window.innerWidth;
6
+ handlers.forEach( (handler) => handler(width));
7
+ }));
8
+
9
+ export const watchWindowSize = (handler) => {
10
+ handlers.add(handler);
11
+ return window.innerWidth;
12
+ }
13
+ export const unwatchWindowSize = (handler) => {
14
+ handlers.remove(handler);
15
+ }