enigmatic 0.33.0 → 0.34.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/{CLIENT_JS_DOCS.md → README.md} +178 -17
- package/__tests__/e2.test.js +42 -12
- package/beemap.js +47 -0
- package/bun-server.js +26 -34
- package/package.json +1 -1
- package/public/client.js +58 -20
- package/public/custom.js +23 -18
- package/public/index.html +3 -0
- package/public/index2.html +3 -2
- package/public/client.css +0 -286
- package/public/theme.css +0 -9
|
@@ -1,4 +1,118 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Enigmatic
|
|
2
|
+
|
|
3
|
+
  
|
|
4
|
+
|
|
5
|
+
A lightweight client-side JavaScript library for DOM manipulation, reactive state management, and API interactions, with an optional Bun server for backend functionality.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
### Using client.js via CDN
|
|
10
|
+
|
|
11
|
+
Include `client.js` in any HTML file using the unpkg CDN:
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<!DOCTYPE html>
|
|
15
|
+
<html>
|
|
16
|
+
<head>
|
|
17
|
+
<script src="https://unpkg.com/enigmatic@0.34.0/public/client.js"></script>
|
|
18
|
+
<script>
|
|
19
|
+
// Configure your API URL
|
|
20
|
+
window.api_url = 'https://your-server.com';
|
|
21
|
+
|
|
22
|
+
// Define custom elements
|
|
23
|
+
window.custom = {
|
|
24
|
+
"hello-world": (data) => `Hello ${data || 'World'}`
|
|
25
|
+
};
|
|
26
|
+
</script>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<hello-world data="message"></hello-world>
|
|
30
|
+
<script>
|
|
31
|
+
window.state.message = "Hello World";
|
|
32
|
+
</script>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Note:** Replace `0.34.0` with the latest version number from [npm](https://www.npmjs.com/package/enigmatic).
|
|
38
|
+
|
|
39
|
+
### Using the Bun Server
|
|
40
|
+
|
|
41
|
+
The Bun server provides a complete backend implementation with:
|
|
42
|
+
- Key-value storage (using BeeMap)
|
|
43
|
+
- File storage (using Cloudflare R2/S3)
|
|
44
|
+
- Authentication (using Auth0)
|
|
45
|
+
- Static file serving
|
|
46
|
+
|
|
47
|
+
#### Installation
|
|
48
|
+
|
|
49
|
+
1. Install Bun (if not already installed):
|
|
50
|
+
```bash
|
|
51
|
+
curl -fsSL https://bun.sh/install | bash
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
2. Install dependencies:
|
|
55
|
+
```bash
|
|
56
|
+
bun install
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
3. Generate HTTPS certificates (for local development):
|
|
60
|
+
```bash
|
|
61
|
+
cd server
|
|
62
|
+
./generate-certs.sh
|
|
63
|
+
cd ..
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Environment Variables
|
|
67
|
+
|
|
68
|
+
Create a `.env` file in the project root with the following variables:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Auth0 Configuration
|
|
72
|
+
AUTH0_DOMAIN=your-tenant.auth0.com
|
|
73
|
+
AUTH0_CLIENT_ID=your-client-id
|
|
74
|
+
AUTH0_CLIENT_SECRET=your-client-secret
|
|
75
|
+
|
|
76
|
+
# Cloudflare R2 Configuration (optional, for file storage)
|
|
77
|
+
CLOUDFLARE_ACCESS_KEY_ID=your-access-key-id
|
|
78
|
+
CLOUDFLARE_SECRET_ACCESS_KEY=your-secret-access-key
|
|
79
|
+
CLOUDFLARE_BUCKET_NAME=your-bucket-name
|
|
80
|
+
CLOUDFLARE_PUBLIC_URL=https://your-account-id.r2.cloudflarestorage.com
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### Running the Server
|
|
84
|
+
|
|
85
|
+
Start the server with hot reload:
|
|
86
|
+
```bash
|
|
87
|
+
npm start
|
|
88
|
+
# or
|
|
89
|
+
bun --hot ./bun-server.js
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The server will start on `https://localhost:3000` (HTTPS is required for Auth0 cookies).
|
|
93
|
+
|
|
94
|
+
#### Server Features
|
|
95
|
+
|
|
96
|
+
- **Static File Serving**: Automatically serves any files from the `public/` folder
|
|
97
|
+
- **Key-Value Storage**: Per-user KV storage using BeeMap (persisted to JSONL files)
|
|
98
|
+
- **File Storage**: Per-user file storage using Cloudflare R2 (or compatible S3)
|
|
99
|
+
- **Authentication**: OAuth2 flow with Auth0
|
|
100
|
+
- **CORS**: Enabled for all origins (configurable)
|
|
101
|
+
|
|
102
|
+
#### Server Endpoints
|
|
103
|
+
|
|
104
|
+
- `GET /` or `GET /index.html` - Serves `public/index.html`
|
|
105
|
+
- `GET /{path}` - Serves static files from `public/` folder
|
|
106
|
+
- `GET /login` - Initiates Auth0 login flow
|
|
107
|
+
- `GET /callback` - Auth0 callback handler
|
|
108
|
+
- `GET /logout` - Logs out user
|
|
109
|
+
- `GET /{key}` - Retrieves KV value (requires auth)
|
|
110
|
+
- `POST /{key}` - Stores KV value (requires auth)
|
|
111
|
+
- `DELETE /{key}` - Deletes KV value (requires auth)
|
|
112
|
+
- `PUT /{key}` - Uploads file to R2 (requires auth)
|
|
113
|
+
- `PURGE /{key}` - Deletes file from R2 (requires auth)
|
|
114
|
+
- `PROPFIND /` - Lists files in R2 (requires auth)
|
|
115
|
+
- `PATCH /{key}` - Downloads file from R2 (requires auth)
|
|
2
116
|
|
|
3
117
|
## Overview
|
|
4
118
|
|
|
@@ -36,13 +150,15 @@ Configures the base URL for all API requests. Modify this to point to your serve
|
|
|
36
150
|
- Set a property: `window.state.myKey = 'value'`
|
|
37
151
|
- Elements with `data="myKey"` attribute are automatically updated
|
|
38
152
|
- The system looks for custom element handlers in `window.custom[tagName]`
|
|
153
|
+
- Only elements with matching custom element handlers are updated
|
|
39
154
|
- Supports both function and object-based custom elements
|
|
40
155
|
|
|
41
156
|
**Example:**
|
|
42
157
|
```html
|
|
43
|
-
<
|
|
158
|
+
<my-element data="message">Initial</my-element>
|
|
44
159
|
<script>
|
|
45
|
-
window.
|
|
160
|
+
window.custom['my-element'] = (data) => `<div>${data}</div>`;
|
|
161
|
+
window.state.message = "Updated!"; // Automatically updates the element
|
|
46
162
|
</script>
|
|
47
163
|
```
|
|
48
164
|
|
|
@@ -169,7 +285,7 @@ window.logout();
|
|
|
169
285
|
|
|
170
286
|
## Custom Elements System
|
|
171
287
|
|
|
172
|
-
Custom elements are defined in `window.custom` object and automatically initialized when the DOM loads.
|
|
288
|
+
Custom elements are defined in `window.custom` object and automatically initialized when the DOM loads or when elements are added dynamically.
|
|
173
289
|
|
|
174
290
|
### Initialization
|
|
175
291
|
|
|
@@ -178,6 +294,17 @@ The library automatically:
|
|
|
178
294
|
2. Iterates through all keys in `window.custom`
|
|
179
295
|
3. Finds all matching HTML elements by tag name
|
|
180
296
|
4. Calls the custom element handler and sets `innerHTML`
|
|
297
|
+
5. Watches for new elements added to the DOM via MutationObserver and initializes them automatically
|
|
298
|
+
|
|
299
|
+
### Proxy Behavior
|
|
300
|
+
|
|
301
|
+
`window.custom` is a Proxy that automatically initializes matching elements when you add a new custom element definition:
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
// Adding a new custom element automatically initializes all matching elements in the DOM
|
|
305
|
+
window.custom['my-element'] = (data) => `<div>${data}</div>`;
|
|
306
|
+
// All <my-element> tags are immediately initialized
|
|
307
|
+
```
|
|
181
308
|
|
|
182
309
|
### Defining Custom Elements
|
|
183
310
|
|
|
@@ -196,12 +323,15 @@ window.custom = {
|
|
|
196
323
|
<my-element></my-element>
|
|
197
324
|
```
|
|
198
325
|
|
|
199
|
-
When
|
|
326
|
+
When used with reactive state, the function receives the state value:
|
|
200
327
|
```html
|
|
201
328
|
<my-element data="myKey"></my-element>
|
|
329
|
+
<script>
|
|
330
|
+
window.state.myKey = 'value'; // Function is called with 'value'
|
|
331
|
+
</script>
|
|
202
332
|
```
|
|
203
333
|
|
|
204
|
-
The function receives the state value as the first parameter.
|
|
334
|
+
The function receives the state value as the first parameter. If no state value is set, it receives `undefined`.
|
|
205
335
|
|
|
206
336
|
#### Object-based Custom Element
|
|
207
337
|
|
|
@@ -267,18 +397,23 @@ window.get('key').catch(err => console.error(err));
|
|
|
267
397
|
<!DOCTYPE html>
|
|
268
398
|
<html>
|
|
269
399
|
<head>
|
|
270
|
-
<script src="
|
|
271
|
-
<script
|
|
400
|
+
<script src="https://unpkg.com/enigmatic@0.34.0/public/client.js"></script>
|
|
401
|
+
<script>
|
|
402
|
+
// Configure API URL
|
|
403
|
+
window.api_url = 'https://localhost:3000';
|
|
404
|
+
|
|
405
|
+
// Define custom elements
|
|
406
|
+
window.custom = {
|
|
407
|
+
"hello-world": (data) => `Hello ${data || 'World'}`
|
|
408
|
+
};
|
|
409
|
+
</script>
|
|
272
410
|
</head>
|
|
273
411
|
<body>
|
|
274
|
-
<!-- Custom element -->
|
|
275
|
-
<
|
|
276
|
-
|
|
277
|
-
<!-- Reactive state element -->
|
|
278
|
-
<div data="message">Initial</div>
|
|
412
|
+
<!-- Custom element with reactive state -->
|
|
413
|
+
<hello-world data="message"></hello-world>
|
|
279
414
|
|
|
280
415
|
<script>
|
|
281
|
-
// Set reactive state
|
|
416
|
+
// Set reactive state (triggers updates to elements with data="message")
|
|
282
417
|
window.state.message = "Hello World";
|
|
283
418
|
|
|
284
419
|
// Use API functions
|
|
@@ -305,18 +440,44 @@ window.get('key').catch(err => console.error(err));
|
|
|
305
440
|
|
|
306
441
|
## Dependencies
|
|
307
442
|
|
|
308
|
-
- Requires
|
|
309
|
-
- Requires a backend server that implements the API endpoints
|
|
443
|
+
- Requires a backend server that implements the API endpoints (or use the included Bun server)
|
|
310
444
|
- Requires browser support for:
|
|
311
445
|
- `fetch` API
|
|
312
446
|
- `Proxy` API
|
|
313
447
|
- `Blob` API
|
|
314
448
|
- `URL.createObjectURL`
|
|
449
|
+
- `MutationObserver` API
|
|
450
|
+
|
|
451
|
+
**Note:** Custom element definitions can be loaded before or after `client.js` - the Proxy system will handle initialization either way.
|
|
315
452
|
|
|
316
453
|
## Notes
|
|
317
454
|
|
|
318
455
|
- All API functions automatically encode keys using `encodeURIComponent`
|
|
319
456
|
- The `window.download()` function uses PATCH method internally (browsers don't support custom HTTP methods)
|
|
320
|
-
- Custom elements are initialized
|
|
457
|
+
- Custom elements are automatically initialized:
|
|
458
|
+
- On page load (when DOM is ready)
|
|
459
|
+
- When new custom element definitions are added to `window.custom`
|
|
460
|
+
- When new matching elements are added to the DOM (via MutationObserver)
|
|
321
461
|
- The reactive state system only updates elements with matching `data` attributes
|
|
322
462
|
- Custom element handlers can be async functions
|
|
463
|
+
- When a custom element has a `data` attribute, it automatically reads from `window.state[dataValue]` if no explicit value is provided
|
|
464
|
+
|
|
465
|
+
## Development
|
|
466
|
+
|
|
467
|
+
### Running Tests
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
npm test
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Building
|
|
474
|
+
|
|
475
|
+
The library is ready to use as-is. For production, you can use the minified version:
|
|
476
|
+
|
|
477
|
+
```html
|
|
478
|
+
<script src="https://unpkg.com/enigmatic@0.34.0/public/client.min.js"></script>
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## License
|
|
482
|
+
|
|
483
|
+
MIT
|
package/__tests__/e2.test.js
CHANGED
|
@@ -11,13 +11,37 @@ describe('client.js', () => {
|
|
|
11
11
|
global.document.body.innerHTML = ''
|
|
12
12
|
global.document.head.innerHTML = ''
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
global.window.
|
|
14
|
+
// Set api_url
|
|
15
|
+
global.window.api_url = 'https://localhost:3000'
|
|
16
16
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
// Clear window properties that might have descriptors
|
|
18
|
+
try {
|
|
19
|
+
delete global.window.custom
|
|
20
|
+
delete global.window.state
|
|
21
|
+
delete global.window.$
|
|
22
|
+
delete global.window.$$
|
|
23
|
+
delete global.window.$c
|
|
24
|
+
delete global.window.get
|
|
25
|
+
delete global.window.set
|
|
26
|
+
delete global.window.put
|
|
27
|
+
delete global.window.delete
|
|
28
|
+
delete global.window.purge
|
|
29
|
+
delete global.window.list
|
|
30
|
+
delete global.window.login
|
|
31
|
+
delete global.window.logout
|
|
32
|
+
delete global.window.download
|
|
33
|
+
delete global.window.initCustomElements
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// Ignore errors
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Execute client.js code first (sets up Proxy)
|
|
20
39
|
eval(clientCode)
|
|
40
|
+
// Execute custom.js (defines window.custom components)
|
|
41
|
+
eval(customCode)
|
|
42
|
+
|
|
43
|
+
// Wait for initialization
|
|
44
|
+
return new Promise(resolve => setTimeout(resolve, 100))
|
|
21
45
|
})
|
|
22
46
|
|
|
23
47
|
describe('$ and $$ selectors', () => {
|
|
@@ -71,7 +95,8 @@ describe('client.js', () => {
|
|
|
71
95
|
const result = await window.get('test key')
|
|
72
96
|
|
|
73
97
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
74
|
-
`${window.api_url}/test%20key
|
|
98
|
+
`${window.api_url}/test%20key`,
|
|
99
|
+
expect.objectContaining({ method: 'GET' })
|
|
75
100
|
)
|
|
76
101
|
expect(result).toEqual({ value: 'test' })
|
|
77
102
|
})
|
|
@@ -207,7 +232,7 @@ describe('client.js', () => {
|
|
|
207
232
|
const result = await window.list()
|
|
208
233
|
|
|
209
234
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
210
|
-
window.api_url
|
|
235
|
+
`${window.api_url}/`,
|
|
211
236
|
expect.objectContaining({
|
|
212
237
|
method: 'PROPFIND'
|
|
213
238
|
})
|
|
@@ -255,29 +280,32 @@ describe('client.js', () => {
|
|
|
255
280
|
|
|
256
281
|
|
|
257
282
|
describe('window.state proxy', () => {
|
|
258
|
-
test('state.set updates DOM elements with data attribute', () => {
|
|
283
|
+
test('state.set updates DOM elements with data attribute', async () => {
|
|
259
284
|
global.document.body.innerHTML = '<hello-world data="name"></hello-world>'
|
|
260
285
|
|
|
261
286
|
window.state.name = 'John'
|
|
287
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
262
288
|
|
|
263
289
|
const el = global.document.querySelector('hello-world')
|
|
264
290
|
expect(el.innerHTML).toBe('Hello John')
|
|
265
291
|
})
|
|
266
292
|
|
|
267
|
-
test('state.set updates multiple elements with same data attribute', () => {
|
|
293
|
+
test('state.set updates multiple elements with same data attribute', async () => {
|
|
268
294
|
global.document.body.innerHTML = '<hello-world data="name"></hello-world><hello-world data="name"></hello-world>'
|
|
269
295
|
|
|
270
296
|
window.state.name = 'Jane'
|
|
297
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
271
298
|
|
|
272
299
|
const els = global.document.querySelectorAll('hello-world')
|
|
273
300
|
expect(els[0].innerHTML).toBe('Hello Jane')
|
|
274
301
|
expect(els[1].innerHTML).toBe('Hello Jane')
|
|
275
302
|
})
|
|
276
303
|
|
|
277
|
-
test('state.set works with object components', () => {
|
|
304
|
+
test('state.set works with object components', async () => {
|
|
278
305
|
global.document.body.innerHTML = '<hello-world-2 data="test"></hello-world-2>'
|
|
279
306
|
|
|
280
307
|
window.state.test = 'Hello'
|
|
308
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
281
309
|
|
|
282
310
|
const el = global.document.querySelector('hello-world-2')
|
|
283
311
|
expect(el.innerHTML).toBe('Hello World')
|
|
@@ -288,20 +316,22 @@ describe('client.js', () => {
|
|
|
288
316
|
expect(window.state.test).toBe('value')
|
|
289
317
|
})
|
|
290
318
|
|
|
291
|
-
test('state.set handles multiple properties', () => {
|
|
319
|
+
test('state.set handles multiple properties', async () => {
|
|
292
320
|
global.document.body.innerHTML = '<hello-world data="a"></hello-world><hello-world data="b"></hello-world>'
|
|
293
321
|
|
|
294
322
|
window.state.a = 'A'
|
|
295
323
|
window.state.b = 'B'
|
|
324
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
296
325
|
|
|
297
326
|
expect(global.document.querySelector('[data="a"]').innerHTML).toBe('Hello A')
|
|
298
327
|
expect(global.document.querySelector('[data="b"]').innerHTML).toBe('Hello B')
|
|
299
328
|
})
|
|
300
329
|
|
|
301
|
-
test('state.set does not update elements without matching data attribute', () => {
|
|
330
|
+
test('state.set does not update elements without matching data attribute', async () => {
|
|
302
331
|
global.document.body.innerHTML = '<hello-world data="name"></hello-world><div data="other">Original</div>'
|
|
303
332
|
|
|
304
333
|
window.state.name = 'John'
|
|
334
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
305
335
|
|
|
306
336
|
expect(global.document.querySelector('[data="name"]').innerHTML).toBe('Hello John')
|
|
307
337
|
expect(global.document.querySelector('[data="other"]').innerHTML).toBe('Original')
|
package/beemap.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class BeeMap extends Map {
|
|
2
|
+
constructor(jsonlFile, timems) {
|
|
3
|
+
super();
|
|
4
|
+
this.jsonlFile = jsonlFile;
|
|
5
|
+
this.timems = timems;
|
|
6
|
+
this.intervalId = null;
|
|
7
|
+
|
|
8
|
+
// Load existing data
|
|
9
|
+
this.load();
|
|
10
|
+
|
|
11
|
+
// Set up interval to save
|
|
12
|
+
if (!timems) return;
|
|
13
|
+
this.intervalId = setInterval(() => {
|
|
14
|
+
this.save();
|
|
15
|
+
}, timems);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async load() {
|
|
19
|
+
const file = Bun.file(this.jsonlFile);
|
|
20
|
+
if (!(await file.exists())) return;
|
|
21
|
+
|
|
22
|
+
const text = await file.text();
|
|
23
|
+
const lines = text.trim().split('\n').filter(line => line.length > 0);
|
|
24
|
+
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const [key, value] = JSON.parse(line);
|
|
27
|
+
super.set(key, value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async save() {
|
|
32
|
+
const lines = [];
|
|
33
|
+
for (const [key, value] of this) {
|
|
34
|
+
lines.push(JSON.stringify([key, value]));
|
|
35
|
+
}
|
|
36
|
+
await Bun.write(this.jsonlFile, lines.join('\n') + '\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
destroy() {
|
|
40
|
+
if (this.intervalId) {
|
|
41
|
+
clearInterval(this.intervalId);
|
|
42
|
+
this.intervalId = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { BeeMap };
|
package/bun-server.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { S3Client } from "bun";
|
|
2
|
+
import { BeeMap } from "./beemap.js";
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
const db = await Bun.file(dbPath).json().catch(() => ({}));
|
|
4
|
+
const sessions = new BeeMap("sessions.jsonl", 20000), beeMaps = {};
|
|
5
5
|
const s3 = new S3Client({
|
|
6
6
|
accessKeyId: Bun.env.CLOUDFLARE_ACCESS_KEY_ID,
|
|
7
7
|
secretAccessKey: Bun.env.CLOUDFLARE_SECRET_ACCESS_KEY,
|
|
8
8
|
bucket: Bun.env.CLOUDFLARE_BUCKET_NAME,
|
|
9
9
|
endpoint: Bun.env.CLOUDFLARE_PUBLIC_URL
|
|
10
10
|
});
|
|
11
|
+
|
|
11
12
|
const json = (data, status = 200, extraHeaders = {}) => new Response(JSON.stringify(data), {
|
|
12
|
-
status,
|
|
13
|
-
headers: {
|
|
13
|
+
status, headers: {
|
|
14
14
|
"Access-Control-Allow-Origin": "*",
|
|
15
15
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PURGE, PROPFIND, DOWNLOAD, OPTIONS",
|
|
16
16
|
"Access-Control-Allow-Headers": "Content-Type, Authorization, Cookie, X-HTTP-Method-Override",
|
|
@@ -19,7 +19,7 @@ const json = (data, status = 200, extraHeaders = {}) => new Response(JSON.string
|
|
|
19
19
|
...extraHeaders
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
const redirect = (url, cookie = null) => new Response(null, {
|
|
24
24
|
status: 302,
|
|
25
25
|
headers: { Location: url, ...(cookie && { "Set-Cookie": cookie }) }
|
|
@@ -27,21 +27,21 @@ const redirect = (url, cookie = null) => new Response(null, {
|
|
|
27
27
|
|
|
28
28
|
export default {
|
|
29
29
|
async fetch(req) {
|
|
30
|
-
|
|
31
|
-
const url = new URL(req.url);
|
|
32
|
-
const key = url.pathname.slice(1);
|
|
33
|
-
const cb = `${url.origin}/callback`;
|
|
30
|
+
const url = new URL(req.url), key = url.pathname.slice(1);
|
|
34
31
|
const token = req.headers.get("Cookie")?.match(/token=([^;]+)/)?.[1];
|
|
35
|
-
const user = token ?
|
|
32
|
+
const user = token ? sessions.get(token) : null;
|
|
36
33
|
|
|
37
34
|
if (req.method === "OPTIONS") return json(null, 204);
|
|
38
35
|
|
|
36
|
+
// Serve static files from public folder
|
|
37
|
+
if (req.method === 'GET') {
|
|
38
|
+
const file = Bun.file(`./public${url.pathname === '/' ? '/index.html' : url.pathname}`);
|
|
39
|
+
if (await file.exists()) return new Response(file);
|
|
40
|
+
}
|
|
41
|
+
|
|
39
42
|
if (url.pathname === '/login') {
|
|
40
43
|
return Response.redirect(`https://${Bun.env.AUTH0_DOMAIN}/authorize?${new URLSearchParams({
|
|
41
|
-
response_type: "code",
|
|
42
|
-
client_id: Bun.env.AUTH0_CLIENT_ID,
|
|
43
|
-
redirect_uri: cb,
|
|
44
|
-
scope: "openid email profile"
|
|
44
|
+
response_type: "code", client_id: Bun.env.AUTH0_CLIENT_ID, redirect_uri: cb, scope: "openid email profile"
|
|
45
45
|
})}`);
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -49,14 +49,10 @@ export default {
|
|
|
49
49
|
const code = url.searchParams.get("code");
|
|
50
50
|
if (!code) return json({ error: 'No code' }, 400);
|
|
51
51
|
const tRes = await fetch(`https://${Bun.env.AUTH0_DOMAIN}/oauth/token`, {
|
|
52
|
-
method: "POST",
|
|
53
|
-
headers: { "content-type": "application/json" },
|
|
52
|
+
method: "POST", headers: { "content-type": "application/json" },
|
|
54
53
|
body: JSON.stringify({
|
|
55
|
-
grant_type: "authorization_code",
|
|
56
|
-
|
|
57
|
-
client_secret: Bun.env.AUTH0_CLIENT_SECRET,
|
|
58
|
-
code,
|
|
59
|
-
redirect_uri: cb
|
|
54
|
+
grant_type: "authorization_code", client_id: Bun.env.AUTH0_CLIENT_ID,
|
|
55
|
+
client_secret: Bun.env.AUTH0_CLIENT_SECRET, code, redirect_uri: `${url.origin}/callback`
|
|
60
56
|
})
|
|
61
57
|
});
|
|
62
58
|
if (!tRes.ok) return json({ error: 'Auth error' }, 401);
|
|
@@ -65,37 +61,32 @@ export default {
|
|
|
65
61
|
headers: { Authorization: `Bearer ${tokens.access_token}` }
|
|
66
62
|
})).json();
|
|
67
63
|
const session = crypto.randomUUID();
|
|
68
|
-
|
|
64
|
+
sessions.set(session, {
|
|
69
65
|
...userInfo,
|
|
70
66
|
login_time: new Date().toISOString(),
|
|
71
67
|
access_token_expires_at: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000).toISOString() : null
|
|
72
|
-
};
|
|
73
|
-
await writeDb();
|
|
68
|
+
});
|
|
74
69
|
return redirect(url.origin, `token=${session}; HttpOnly; Path=/; Secure; SameSite=Lax; Max-Age=86400`);
|
|
75
70
|
}
|
|
76
71
|
|
|
77
72
|
if (!token || !user) return json({ error: 'Unauthorized' }, 401);
|
|
78
73
|
|
|
79
74
|
if (url.pathname === '/logout') {
|
|
80
|
-
delete
|
|
81
|
-
await writeDb();
|
|
75
|
+
sessions.delete(token);
|
|
82
76
|
return redirect(url.origin, "token=; Max-Age=0; Path=/");
|
|
83
77
|
}
|
|
84
78
|
|
|
85
|
-
|
|
86
|
-
if (
|
|
79
|
+
// Initialize user's beeMap if needed
|
|
80
|
+
if (!beeMaps[user.sub]) beeMaps[user.sub] = new BeeMap(`kv_${user.sub}.jsonl`, 20000);
|
|
87
81
|
|
|
88
|
-
console.log(req.method);
|
|
89
82
|
switch (req.method) {
|
|
90
|
-
case 'GET': return json(
|
|
83
|
+
case 'GET': return json(beeMaps[user.sub].get(key) || null);
|
|
91
84
|
case 'POST':
|
|
92
85
|
const value = await req.text();
|
|
93
|
-
|
|
94
|
-
await writeDb();
|
|
86
|
+
beeMaps[user.sub].set(key, (() => { try { return JSON.parse(value); } catch { return value; } })());
|
|
95
87
|
return json({ key, value });
|
|
96
88
|
case 'DELETE':
|
|
97
|
-
delete
|
|
98
|
-
await writeDb();
|
|
89
|
+
beeMaps[user.sub].delete(key);
|
|
99
90
|
return json({ status: "Deleted" });
|
|
100
91
|
case 'PUT':
|
|
101
92
|
await s3.write(`${user.sub}/${key}`, req.body);
|
|
@@ -125,6 +116,7 @@ export default {
|
|
|
125
116
|
default: return json({ error: 'Method not allowed' }, 405);
|
|
126
117
|
}
|
|
127
118
|
},
|
|
119
|
+
|
|
128
120
|
port: 3000,
|
|
129
121
|
tls: { cert: Bun.file("cert.pem"), key: Bun.file("key.pem") }
|
|
130
122
|
};
|
package/package.json
CHANGED
package/public/client.js
CHANGED
|
@@ -6,7 +6,13 @@ const ren = async (el, v) => {
|
|
|
6
6
|
if (f) {
|
|
7
7
|
const dataAttr = el.getAttribute('data');
|
|
8
8
|
const val = v !== undefined ? v : (dataAttr ? W.state[dataAttr] : undefined);
|
|
9
|
-
try {
|
|
9
|
+
try {
|
|
10
|
+
if (f.render) {
|
|
11
|
+
el.innerHTML = await f.render.call(f, val);
|
|
12
|
+
} else if (typeof f === 'function') {
|
|
13
|
+
el.innerHTML = await f(val);
|
|
14
|
+
}
|
|
15
|
+
} catch(e) { console.error(e) }
|
|
10
16
|
}
|
|
11
17
|
};
|
|
12
18
|
|
|
@@ -14,11 +20,25 @@ const ren = async (el, v) => {
|
|
|
14
20
|
const cProx = new Proxy({}, {
|
|
15
21
|
set(t, p, v) {
|
|
16
22
|
t[p] = v;
|
|
17
|
-
setTimeout(() =>
|
|
23
|
+
setTimeout(() => {
|
|
24
|
+
if (W.$$ && D.body) {
|
|
25
|
+
W.$$(p).forEach(el => ren(el));
|
|
26
|
+
}
|
|
27
|
+
}, 0);
|
|
18
28
|
return true;
|
|
19
29
|
}
|
|
20
30
|
});
|
|
21
|
-
Object.defineProperty(W, 'custom', {
|
|
31
|
+
Object.defineProperty(W, 'custom', {
|
|
32
|
+
get: () => cProx,
|
|
33
|
+
set: v => {
|
|
34
|
+
Object.keys(v || {}).forEach(k => cProx[k] = v[k]);
|
|
35
|
+
// Defer initialization to ensure DOM and functions are ready
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
if (W.initCustomElements && D.body) W.initCustomElements();
|
|
38
|
+
}, 50);
|
|
39
|
+
},
|
|
40
|
+
configurable: true
|
|
41
|
+
});
|
|
22
42
|
|
|
23
43
|
const sProx = new Proxy({}, {
|
|
24
44
|
set(o, p, v) {
|
|
@@ -55,26 +75,44 @@ Object.assign(W, {
|
|
|
55
75
|
a.click();
|
|
56
76
|
URL.revokeObjectURL(a.href);
|
|
57
77
|
},
|
|
58
|
-
initCustomElements: () =>
|
|
78
|
+
initCustomElements: () => {
|
|
79
|
+
if (!D.body) return;
|
|
80
|
+
Object.keys(W.custom || {}).forEach(t => {
|
|
81
|
+
const elements = W.$$(t);
|
|
82
|
+
if (elements.length > 0) {
|
|
83
|
+
elements.forEach(el => ren(el));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
59
87
|
});
|
|
60
88
|
|
|
61
89
|
// 4. Initialization & Observers
|
|
62
90
|
const boot = () => {
|
|
63
|
-
W.initCustomElements
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
if (W.initCustomElements) {
|
|
92
|
+
// Run immediately and also after a short delay to catch any elements added during script execution
|
|
93
|
+
W.initCustomElements();
|
|
94
|
+
setTimeout(() => W.initCustomElements(), 10);
|
|
95
|
+
}
|
|
96
|
+
if (D.body) {
|
|
97
|
+
new MutationObserver((mutations) => {
|
|
98
|
+
mutations.forEach(m => {
|
|
99
|
+
m.addedNodes.forEach(node => {
|
|
100
|
+
if (node.nodeType === 1) { // Element node
|
|
101
|
+
const tag = node.tagName?.toLowerCase();
|
|
102
|
+
if (tag && W.custom?.[tag]) ren(node);
|
|
103
|
+
// Also check children
|
|
104
|
+
node.querySelectorAll && Array.from(node.querySelectorAll('*')).forEach(child => {
|
|
105
|
+
const childTag = child.tagName?.toLowerCase();
|
|
106
|
+
if (childTag && W.custom?.[childTag]) ren(child);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
});
|
|
76
110
|
});
|
|
77
|
-
});
|
|
78
|
-
}
|
|
111
|
+
}).observe(D.body, { childList: true, subtree: true });
|
|
112
|
+
}
|
|
79
113
|
};
|
|
80
|
-
D.readyState === 'loading'
|
|
114
|
+
if (D.readyState === 'loading') {
|
|
115
|
+
D.addEventListener('DOMContentLoaded', boot);
|
|
116
|
+
} else {
|
|
117
|
+
setTimeout(boot, 0);
|
|
118
|
+
}
|
package/public/custom.js
CHANGED
|
@@ -6,24 +6,29 @@ window.custom = {
|
|
|
6
6
|
return this.prop(data);
|
|
7
7
|
}
|
|
8
8
|
},
|
|
9
|
-
"file-widget": async () => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
"file-widget": async () => {
|
|
10
|
+
try {
|
|
11
|
+
const list = await window.list();
|
|
12
|
+
const style = `<style>.w-c{font:13px sans-serif;border:1px solid #ddd;border-radius:6px;overflow:hidden;max-width:320px}.w-i{display:flex;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #f0f0f0;align-items:center}.w-i:hover{background:#f9f9f9}.w-d{border:none;background:none;cursor:pointer;opacity:.5;transition:.2s}.w-d:hover{opacity:1}.w-u{display:block;padding:10px;background:#f5f5f5;text-align:center;cursor:pointer;color:#555;font-weight:600;transition:.2s}.w-u:hover{background:#eee}.w-e{padding:20px;text-align:center;color:#999}</style>`;
|
|
13
|
+
|
|
14
|
+
const items = Array.isArray(list) ? list.map(item => `
|
|
15
|
+
<div class="w-i">
|
|
16
|
+
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:10px">${item.name}</span>
|
|
17
|
+
<button class="w-d" onclick="window.download('${item.name}')" title="Download">⬇️</button>
|
|
18
|
+
<button class="w-d" onclick="(async()=>{await window.purge('${item.name}');location.reload()})()" title="Delete">🗑️</button>
|
|
19
|
+
</div>`
|
|
20
|
+
).join('') : '';
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const upload = `
|
|
23
|
+
<label class="w-u">
|
|
24
|
+
📂 Upload
|
|
25
|
+
<input type="file" style="display:none" onchange="(async()=>{const f=this.files[0];if(f){await window.put(f.name,f);location.reload()})()">
|
|
26
|
+
</label>`;
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
return style + `<div class="w-c">${items || '<div class="w-e">No files</div>'}${upload}</div>`;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
const style = `<style>.w-c{font:13px sans-serif;border:1px solid #ddd;border-radius:6px;overflow:hidden;max-width:320px}.w-e{padding:20px;text-align:center;color:#999}</style>`;
|
|
31
|
+
return style + `<div class="w-c"><div class="w-e">Please <button onclick="window.login()" style="background:#007bff;color:white;border:none;padding:5px 10px;border-radius:3px;cursor:pointer">Login</button> to view files</div></div>`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
28
34
|
}
|
|
29
|
-
}
|
package/public/index.html
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>API Test</title>
|
|
7
|
+
<script>
|
|
8
|
+
window.api_url = 'https://localhost:3000';
|
|
9
|
+
</script>
|
|
7
10
|
<script src="custom.js"></script>
|
|
8
11
|
<script src="client.js"></script>
|
|
9
12
|
<style>
|
package/public/index2.html
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
<script src='
|
|
1
|
+
<script src='https://unpkg.com/enigmatic'></script>
|
|
2
|
+
<script src='custom.js'></script>
|
|
2
3
|
|
|
3
4
|
<script>
|
|
4
5
|
window.api_url = 'http://localhost:3000';
|
|
@@ -6,4 +7,4 @@
|
|
|
6
7
|
state.message = "World";
|
|
7
8
|
</script>
|
|
8
9
|
|
|
9
|
-
<
|
|
10
|
+
<file-widget></file-widget>
|
package/public/client.css
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
html {
|
|
2
|
-
max-width: 70ch;
|
|
3
|
-
/* larger spacing on larger screens, very small spacing on tiny screens */
|
|
4
|
-
padding: calc(1vmin + .5rem);
|
|
5
|
-
/* shorthand for margin-left/margin-right */
|
|
6
|
-
margin-inline: auto;
|
|
7
|
-
/* fluid sizing: https://frontaid.io/blog/fluid-typography-2d-css-locks-clamp/ */
|
|
8
|
-
font-size: clamp(1em, 0.909em + 0.45vmin, 1.25em);
|
|
9
|
-
/* use system font stack: https://developer.mozilla.org/en-US/docs/Web/CSS/font-family */
|
|
10
|
-
font-family: system-ui
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/* increase line-height for everything except headings */
|
|
14
|
-
body :not(:is(h1, h2, h3, h4, h5, h6)) {
|
|
15
|
-
line-height: 1.75;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
h1,
|
|
19
|
-
h2,
|
|
20
|
-
h3,
|
|
21
|
-
h4,
|
|
22
|
-
h5,
|
|
23
|
-
h6 {
|
|
24
|
-
margin: 3em 0 1em;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
p,
|
|
28
|
-
ul,
|
|
29
|
-
ol {
|
|
30
|
-
margin-bottom: 2em;
|
|
31
|
-
color: #1d1d1d;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
body,
|
|
35
|
-
section {
|
|
36
|
-
display: grid;
|
|
37
|
-
margin: 0;
|
|
38
|
-
grid-template-columns: var(--cols, 1fr 4fr 1fr);
|
|
39
|
-
grid-template-rows: var(--rows, 1fr 9fr 1fr);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
* {
|
|
43
|
-
grid-column: span var(--span, 1);
|
|
44
|
-
grid-row: span var(--span-rows, 1);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
.flex {
|
|
48
|
-
display: flex;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** positioning ***/
|
|
52
|
-
|
|
53
|
-
.center {
|
|
54
|
-
position: fixed;
|
|
55
|
-
top: 50%;
|
|
56
|
-
left: 50%;
|
|
57
|
-
margin-top: -50px;
|
|
58
|
-
margin-left: -100px;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.right {
|
|
62
|
-
float: right;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.left {
|
|
66
|
-
float: left;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.fixed {
|
|
70
|
-
position: fixed;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.top {
|
|
74
|
-
top: 0;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.bottom {
|
|
78
|
-
bottom: 0
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.fill {
|
|
82
|
-
height: 100vh;
|
|
83
|
-
width: 100wh
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.hide {
|
|
87
|
-
opacity: 0;
|
|
88
|
-
transition: opacity 0.25s linear;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.show {
|
|
92
|
-
opacity: 1;
|
|
93
|
-
transition: opacity 0.25s linear;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.slide-in {
|
|
97
|
-
animation: slide-in 0.1s forwards;
|
|
98
|
-
-webkit-animation: slide-in 0.1s forwards;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.slide-out {
|
|
102
|
-
animation: slide-out 0.1s forwards;
|
|
103
|
-
-webkit-animation: slide-out 0.1s forwards;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
@keyframes slide-in {
|
|
107
|
-
100% {
|
|
108
|
-
transform: translateX(0%);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
@-webkit-keyframes slide-in {
|
|
113
|
-
100% {
|
|
114
|
-
-webkit-transform: translateX(0%);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
@keyframes slide-out {
|
|
119
|
-
0% {
|
|
120
|
-
transform: translateX(0%);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
100% {
|
|
124
|
-
transform: translateX(-100%);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
@-webkit-keyframes slide-out {
|
|
129
|
-
0% {
|
|
130
|
-
-webkit-transform: translateX(0%);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
100% {
|
|
134
|
-
-webkit-transform: translateX(-100%);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
a {
|
|
139
|
-
text-decoration: none;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
.shadow {
|
|
143
|
-
box-shadow: 6px 6px 6px #dbdbdb;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
.cursor {
|
|
147
|
-
cursor: default;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
.margins {
|
|
151
|
-
margin: var(--margins, 15px);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.padding {
|
|
155
|
-
padding: var(--padding, 15px);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.rem {
|
|
159
|
-
font-size: var(--rem, 2rem);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
.faded {
|
|
163
|
-
opacity: 0.5;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
.fade {
|
|
167
|
-
opacity: 1;
|
|
168
|
-
transition: opacity 0.25s ease-in-out;
|
|
169
|
-
-moz-transition: opacity 0.25s ease-in-out;
|
|
170
|
-
-webkit-transition: opacity 0.25s ease-in-out;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.fade:hover {
|
|
174
|
-
opacity: 0.5;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
.unfade {
|
|
178
|
-
opacity: 0.5;
|
|
179
|
-
transition: opacity 0.25s ease-in-out;
|
|
180
|
-
-moz-transition: opacity 0.25s ease-in-out;
|
|
181
|
-
-webkit-transition: opacity 0.25s ease-in-out;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
.unfade:hover {
|
|
185
|
-
opacity: 1;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
.rounded {
|
|
189
|
-
-moz-border-radius: 10px;
|
|
190
|
-
-webkit-border-radius: 10px;
|
|
191
|
-
border-radius: 10px;
|
|
192
|
-
-khtml-border-radius: 10px;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
.round {
|
|
196
|
-
vertical-align: middle;
|
|
197
|
-
width: 50px;
|
|
198
|
-
height: 50px;
|
|
199
|
-
border-radius: 50%;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** html elements **/
|
|
203
|
-
|
|
204
|
-
canvas {
|
|
205
|
-
position: fixed;
|
|
206
|
-
top: 0;
|
|
207
|
-
left: 0;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
ul {
|
|
211
|
-
list-style-type: none;
|
|
212
|
-
border: 20px;
|
|
213
|
-
padding: 20px;
|
|
214
|
-
width: 50%;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
li {
|
|
218
|
-
list-style-type: none;
|
|
219
|
-
border: 10px;
|
|
220
|
-
padding: 10px;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
li:hover {
|
|
224
|
-
background-color: rgb(243, 241, 241);
|
|
225
|
-
cursor: pointer;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
.opacity1 {
|
|
229
|
-
opacity: .1
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
.opacity2 {
|
|
233
|
-
opacity: .2
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
.opacity3 {
|
|
237
|
-
opacity: .3
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
.opacity4 {
|
|
241
|
-
opacity: .4
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
.opacity5 {
|
|
245
|
-
opacity: .5
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
.opacity6 {
|
|
249
|
-
opacity: .6
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
.opacity7 {
|
|
253
|
-
opacity: .7
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
.opacity8 {
|
|
257
|
-
opacity: .8
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
.opacity9 {
|
|
261
|
-
opacity: .9
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
.bg-red {
|
|
265
|
-
background-color: red;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
.bg-blue {
|
|
269
|
-
background-color: blue;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
.bg-yellow {
|
|
273
|
-
background-color: yellow;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
.bg-green {
|
|
277
|
-
background-color: green;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
.bg-black {
|
|
281
|
-
background-color: black;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
.white {
|
|
285
|
-
color: white;
|
|
286
|
-
}
|