creo 0.0.3-dev → 0.0.4-dev
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/.env.development +1 -0
- package/.github/workflows/main.yml +24 -0
- package/README.md +1 -1
- package/TODOS.md +2 -0
- package/index.ts +1 -0
- package/package.json +7 -2
- package/src/DOM/Context.ts +36 -0
- package/src/DOM/DomEngine.ts +106 -0
- package/src/DOM/IRenderCycle.ts +9 -0
- package/src/DOM/Key.ts +1 -0
- package/src/DOM/Node.ts +472 -0
- package/src/DOM/Registry.ts +53 -0
- package/src/creo.ts +134 -0
- package/src/data-structures/assert/assert.ts +12 -0
- package/src/data-structures/indexed-map/IndexedMap.ts +281 -0
- package/src/data-structures/linked-map/LinkedMap.spec.ts +67 -0
- package/src/data-structures/linked-map/LinkedMap.ts +198 -0
- package/src/data-structures/list/List.spec.ts +181 -0
- package/src/data-structures/list/List.ts +195 -0
- package/src/data-structures/maybe/Maybe.ts +25 -0
- package/src/data-structures/null/null.ts +3 -0
- package/src/{tools/isRecordLike.spec.ts → data-structures/record/IsRecordLike.spec.ts} +1 -1
- package/src/{tools/isRecordLike.ts → data-structures/record/IsRecordLike.ts} +1 -1
- package/src/{record/record.spec.ts → data-structures/record/Record.spec.ts} +96 -2
- package/src/data-structures/record/Record.ts +145 -0
- package/src/data-structures/shalllowEqual/shallowEqual.ts +26 -0
- package/src/data-structures/simpleKey/simpleKey.ts +8 -0
- package/src/data-structures/wildcard/wildcard.ts +1 -0
- package/src/examples/SimpleTodoList/SimpleTodoList.ts +53 -0
- package/src/globals.d.ts +1 -0
- package/src/main.ts +22 -11
- package/src/style.css +24 -79
- package/src/ui/html/Block.ts +10 -0
- package/src/ui/html/Button.ts +12 -0
- package/src/ui/html/HStack.ts +10 -0
- package/src/ui/html/Inline.ts +12 -0
- package/src/ui/html/List.ts +10 -0
- package/src/ui/html/Text.ts +9 -0
- package/src/ui/html/VStack.ts +11 -0
- package/tsconfig.json +2 -2
- package/vite.config.js +10 -0
- package/bun.lockb +0 -0
- package/src/record/record.ts +0 -101
- package/src/tools/optional.ts +0 -25
- package/src/ui/component.ts +0 -1
- package/src/ui/prop.ts +0 -13
- package/src/ui/state.ts +0 -0
- /package/src/{ui/index.ts → examples/simple.ts} +0 -0
|
@@ -1,14 +1,43 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
import { record, onDidUpdate } from "./
|
|
1
|
+
import { expect, test, mock } from "bun:test";
|
|
2
|
+
import { record, RecordOf, onDidUpdate, isRecord } from "./Record";
|
|
3
3
|
|
|
4
4
|
test("Can define objects", () => {
|
|
5
5
|
const obj = record({
|
|
6
6
|
hello: "world",
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
+
// @ts-ignore
|
|
9
10
|
expect(obj).toEqual({ hello: "world" });
|
|
10
11
|
});
|
|
11
12
|
|
|
13
|
+
test("Refers to the same objects", () => {
|
|
14
|
+
const test = record({
|
|
15
|
+
foo: {
|
|
16
|
+
bar: "baz",
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(test.foo).toBe(test.foo);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("Updates fields correctly", () => {
|
|
24
|
+
const test: RecordOf<any> = record({
|
|
25
|
+
foo: {
|
|
26
|
+
bar: "baz",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(test.foo.bar).toBe("baz");
|
|
31
|
+
|
|
32
|
+
test.foo.bar = "123";
|
|
33
|
+
|
|
34
|
+
expect(test.foo.bar).toBe("123");
|
|
35
|
+
|
|
36
|
+
test.foo = { hello: "world" };
|
|
37
|
+
expect(test.foo.bar).toBe(undefined);
|
|
38
|
+
expect(test.foo.hello).toBe("world");
|
|
39
|
+
});
|
|
40
|
+
|
|
12
41
|
test("Notifies on object updates", async () => {
|
|
13
42
|
const obj = record({
|
|
14
43
|
hello: "world",
|
|
@@ -48,6 +77,7 @@ test("Implies updates immediately", async () => {
|
|
|
48
77
|
},
|
|
49
78
|
});
|
|
50
79
|
|
|
80
|
+
// @ts-ignore
|
|
51
81
|
expect(obj).toEqual({
|
|
52
82
|
hello: {
|
|
53
83
|
world: "foo",
|
|
@@ -56,6 +86,7 @@ test("Implies updates immediately", async () => {
|
|
|
56
86
|
|
|
57
87
|
obj.hello.world = "new";
|
|
58
88
|
|
|
89
|
+
// @ts-ignore
|
|
59
90
|
expect(obj).toEqual({
|
|
60
91
|
hello: {
|
|
61
92
|
world: "new",
|
|
@@ -144,3 +175,66 @@ test("Supports iterable", async () => {
|
|
|
144
175
|
|
|
145
176
|
iterate(...obj);
|
|
146
177
|
});
|
|
178
|
+
|
|
179
|
+
test("`has` works with records", async () => {
|
|
180
|
+
const originalObject = {
|
|
181
|
+
foo: "bar",
|
|
182
|
+
baz: "test",
|
|
183
|
+
nested: {
|
|
184
|
+
support: "exist",
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
const wrapped = record(originalObject);
|
|
188
|
+
|
|
189
|
+
expect(isRecord(originalObject)).toBe(false);
|
|
190
|
+
expect(isRecord(wrapped)).toBe(true);
|
|
191
|
+
expect("foo" in wrapped).toBe(true);
|
|
192
|
+
expect("test" in wrapped).toBe(false);
|
|
193
|
+
expect("support" in wrapped.nested).toBe(true);
|
|
194
|
+
expect("foo" in wrapped.nested).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("Double tracked on nested object works correctly", async () => {
|
|
198
|
+
const originalObject = {
|
|
199
|
+
foo: "bar",
|
|
200
|
+
baz: "test",
|
|
201
|
+
nested: {
|
|
202
|
+
support: "exist",
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// This object is not tracked under the same parent, but might be considered in future to allow better objects and record composition.
|
|
207
|
+
// It would require objects to have multiple parents (so essentially many<=>many concept.)
|
|
208
|
+
const additionalObject = {
|
|
209
|
+
nested: originalObject.nested,
|
|
210
|
+
foo: "123",
|
|
211
|
+
hello: "234",
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const originalWrappped = record(originalObject);
|
|
215
|
+
|
|
216
|
+
const additionalWrapped = record(additionalObject);
|
|
217
|
+
|
|
218
|
+
const recordedNested = record(originalWrappped.nested);
|
|
219
|
+
|
|
220
|
+
const mockFn = mock();
|
|
221
|
+
|
|
222
|
+
onDidUpdate(originalWrappped, () => {
|
|
223
|
+
mockFn();
|
|
224
|
+
expect(originalObject.nested).toEqual(originalWrappped.nested);
|
|
225
|
+
});
|
|
226
|
+
onDidUpdate(additionalWrapped, () => {
|
|
227
|
+
// We should never hit that path
|
|
228
|
+
expect(1).toBe(0);
|
|
229
|
+
mockFn();
|
|
230
|
+
});
|
|
231
|
+
onDidUpdate(recordedNested, () => {
|
|
232
|
+
mockFn();
|
|
233
|
+
// @ts-ignore
|
|
234
|
+
expect(recordedNested).toEqual({ support: "updated" });
|
|
235
|
+
});
|
|
236
|
+
originalWrappped.nested.support = "updated";
|
|
237
|
+
// Updates are propagated in microtick queue, so we wait single tick
|
|
238
|
+
await Promise.resolve();
|
|
239
|
+
expect(mockFn).toHaveBeenCalledTimes(2);
|
|
240
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ideas:
|
|
3
|
+
* [x] didUpdate support
|
|
4
|
+
* [x] Proxy proxifies all children as well
|
|
5
|
+
* [x] Support nested updates + nested listeners (e.g. only part of the object)
|
|
6
|
+
* [x] Cache records
|
|
7
|
+
* [ ] Keep track on updates, until there are no users on the old state
|
|
8
|
+
* [ ] Support symbol iterator
|
|
9
|
+
* [ ] Add js dispose tracker to automatically close listeners
|
|
10
|
+
* [ ] Allow to "stop propagate" changes if needed (or allow catching changes only on top level)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Maybe } from "../maybe/Maybe";
|
|
14
|
+
import { Wildcard } from "../wildcard/wildcard";
|
|
15
|
+
import { isRecordLike } from "./IsRecordLike";
|
|
16
|
+
|
|
17
|
+
// #region Record Type
|
|
18
|
+
const ParentRecord = Symbol("parent-record");
|
|
19
|
+
// const example: RecordOf<{foo: 'bar'}> = {
|
|
20
|
+
// foo: 'bar',
|
|
21
|
+
// [ParentRecord]: null // Root record
|
|
22
|
+
// }
|
|
23
|
+
export type RecordOf<T extends object> = T & {
|
|
24
|
+
[ParentRecord]: Maybe<WeakRef<RecordOf<Wildcard>>>;
|
|
25
|
+
};
|
|
26
|
+
type RecordDidChangeListener<T extends object> = (record: RecordOf<T>) => void;
|
|
27
|
+
|
|
28
|
+
export function isRecord<T extends object>(value: T): value is RecordOf<T> {
|
|
29
|
+
return ParentRecord in value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// #region Weak map for listenets
|
|
33
|
+
const didUpdateMap: WeakMap<
|
|
34
|
+
RecordOf<Wildcard>,
|
|
35
|
+
Set<RecordDidChangeListener<Wildcard>>
|
|
36
|
+
> = new WeakMap();
|
|
37
|
+
|
|
38
|
+
// #region Update notifier
|
|
39
|
+
const scheduledUpdatesNotifiers: Set<RecordOf<Wildcard>> = new Set();
|
|
40
|
+
let shouldScheduleMicrotask = true;
|
|
41
|
+
function queuedNotifier() {
|
|
42
|
+
function iterate(record: RecordOf<Wildcard>) {
|
|
43
|
+
const listeners = didUpdateMap.get(record);
|
|
44
|
+
listeners?.forEach((listener) => {
|
|
45
|
+
listener(record);
|
|
46
|
+
});
|
|
47
|
+
const maybeParent: Maybe<RecordOf<Wildcard>> = record[ParentRecord];
|
|
48
|
+
if (maybeParent) {
|
|
49
|
+
iterate(maybeParent);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
shouldScheduleMicrotask = true;
|
|
53
|
+
scheduledUpdatesNotifiers.forEach(iterate);
|
|
54
|
+
}
|
|
55
|
+
function recordDidUpdate<T extends object>(record: RecordOf<T>) {
|
|
56
|
+
scheduledUpdatesNotifiers.add(record);
|
|
57
|
+
shouldScheduleMicrotask && queueMicrotask(queuedNotifier);
|
|
58
|
+
shouldScheduleMicrotask = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type InternalOnly = never;
|
|
62
|
+
|
|
63
|
+
// #region Record creation, fields wrapper
|
|
64
|
+
function creoRecord<TNode extends object, T extends object>(
|
|
65
|
+
parent: Maybe<RecordOf<TNode>>,
|
|
66
|
+
value: T,
|
|
67
|
+
): RecordOf<T> {
|
|
68
|
+
const parentWeakRef = parent != null ? new WeakRef(parent) : null;
|
|
69
|
+
|
|
70
|
+
type CacheField<K extends keyof T> = T[K] extends object
|
|
71
|
+
? Maybe<RecordOf<T[K]>>
|
|
72
|
+
: never;
|
|
73
|
+
type Cache = { [K in keyof T]: CacheField<K> };
|
|
74
|
+
const cache: Cache = {} as Cache;
|
|
75
|
+
const record: RecordOf<T> = new Proxy(value, {
|
|
76
|
+
// @ts-ignore we override `has` to improve typing
|
|
77
|
+
has<K extends keyof T>(target: T, property: K): boolean {
|
|
78
|
+
if (property === ParentRecord) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return property in target;
|
|
82
|
+
},
|
|
83
|
+
// @ts-ignore we override `get` to improve typing
|
|
84
|
+
get<K extends keyof T>(target: T, property: K): T[K] {
|
|
85
|
+
const val = target[property];
|
|
86
|
+
|
|
87
|
+
if (property === ParentRecord) {
|
|
88
|
+
// Only for internal use
|
|
89
|
+
return parentWeakRef?.deref() as InternalOnly;
|
|
90
|
+
}
|
|
91
|
+
// If the value is cached, return the cached record:
|
|
92
|
+
if (cache[property] != null) {
|
|
93
|
+
return cache[property] as T[K];
|
|
94
|
+
}
|
|
95
|
+
// No cached value:
|
|
96
|
+
if (isRecordLike(val)) {
|
|
97
|
+
// Object / Array, etc.
|
|
98
|
+
// we proxify all nested objects / arrays to ensure correct behaviour
|
|
99
|
+
const childRecord = creoRecord(record, val);
|
|
100
|
+
cache[property] = childRecord as CacheField<K>;
|
|
101
|
+
return childRecord;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Primitive value:
|
|
105
|
+
return val;
|
|
106
|
+
},
|
|
107
|
+
set<K, TNewValue>(target: T, property: K, newValue: TNewValue) {
|
|
108
|
+
// property is actually the keyof K, but TS defines Proxy differently:
|
|
109
|
+
const prop: keyof T = property as keyof T;
|
|
110
|
+
const value: T[typeof prop] = newValue as T[typeof prop];
|
|
111
|
+
|
|
112
|
+
target[prop] = value;
|
|
113
|
+
if (cache[prop] != null) {
|
|
114
|
+
cache[prop] = null as Cache[keyof Cache];
|
|
115
|
+
}
|
|
116
|
+
recordDidUpdate(record);
|
|
117
|
+
return true;
|
|
118
|
+
},
|
|
119
|
+
}) as RecordOf<T>;
|
|
120
|
+
didUpdateMap.set(record, new Set());
|
|
121
|
+
return record;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// #region Public API
|
|
125
|
+
export function record<TNode extends object>(value: TNode): RecordOf<TNode> {
|
|
126
|
+
if (isRecord(value)) {
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
return creoRecord(null, value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function onDidUpdate<T extends object>(
|
|
133
|
+
record: RecordOf<T>,
|
|
134
|
+
listener: (record: RecordOf<T>) => void,
|
|
135
|
+
): () => void {
|
|
136
|
+
const listeners = didUpdateMap.get(record);
|
|
137
|
+
if (!listeners) {
|
|
138
|
+
// Safe-guard: Essentialy this path cannot happen
|
|
139
|
+
throw new TypeError(`Record ${record} was created without listener`);
|
|
140
|
+
}
|
|
141
|
+
listeners.add(listener);
|
|
142
|
+
return function unsubscribe() {
|
|
143
|
+
listeners.delete(listener);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Wildcard } from "../wildcard/wildcard";
|
|
2
|
+
|
|
3
|
+
export function shallowEqual(a: Wildcard, b: Wildcard): boolean {
|
|
4
|
+
if (a === b) return true;
|
|
5
|
+
|
|
6
|
+
if (
|
|
7
|
+
typeof a !== "object" ||
|
|
8
|
+
a === null ||
|
|
9
|
+
typeof b !== "object" ||
|
|
10
|
+
b === null
|
|
11
|
+
) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const keysA = Object.keys(a);
|
|
16
|
+
if (keysA.length !== Object.keys(b).length) return false;
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < keysA.length; i++) {
|
|
19
|
+
const key = keysA[i];
|
|
20
|
+
if (!Object.is(a[key], b[key])) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Wildcard = any;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Block, Button, creo, Inline, Text } from "../../creo";
|
|
2
|
+
import { Maybe } from "../../data-structures/maybe/Maybe";
|
|
3
|
+
import { _ } from "../../data-structures/null/null";
|
|
4
|
+
|
|
5
|
+
type Todo = { text: string };
|
|
6
|
+
|
|
7
|
+
export const SimpleTodoList = creo<{ text: string; todos: Array<Todo> }>(
|
|
8
|
+
(c) => {
|
|
9
|
+
const todos: Array<Todo> = c.tracked(c.p.todos);
|
|
10
|
+
let button: () => Maybe<HTMLElement>;
|
|
11
|
+
return {
|
|
12
|
+
didMount() {
|
|
13
|
+
console.warn("did mount");
|
|
14
|
+
console.log(button());
|
|
15
|
+
button()?.addEventListener("click", () => {
|
|
16
|
+
todos.push({ text: `Task #${todos.length}` });
|
|
17
|
+
});
|
|
18
|
+
// button?.extension.getButton().addEventListener("click", () => {
|
|
19
|
+
// todos.push({ text: `New Todo: ${counter++}` });
|
|
20
|
+
// });
|
|
21
|
+
},
|
|
22
|
+
render() {
|
|
23
|
+
console.log("rendering todo list");
|
|
24
|
+
Inline(_, () => {
|
|
25
|
+
Text(c.p.text);
|
|
26
|
+
});
|
|
27
|
+
Block(_, () => {
|
|
28
|
+
Text("Hello inside container");
|
|
29
|
+
Block(_, () => {
|
|
30
|
+
TodoList({ todos });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
button = Button(_, () => Text("Add todo"));
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export const TodoList = creo<{ todos: Array<Todo> }>((c) => {
|
|
40
|
+
const todos: Array<Todo> = c.tracked(c.p.todos);
|
|
41
|
+
return {
|
|
42
|
+
render() {
|
|
43
|
+
console.log("rendering todos");
|
|
44
|
+
todos.map((todo) => {
|
|
45
|
+
console.log(todo);
|
|
46
|
+
Block({ class: "todo" }, () => {
|
|
47
|
+
console.log(`Entity: ${todo.text}`);
|
|
48
|
+
Text(`Entity: ${todo.text}`);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
});
|
package/src/globals.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare const __DEV__: boolean;
|
package/src/main.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
|
+
import { record } from "./data-structures/record/Record";
|
|
2
|
+
import { DomEngine } from "./DOM/DomEngine";
|
|
3
|
+
import { SimpleTodoList } from "./examples/SimpleTodoList/SimpleTodoList";
|
|
1
4
|
import "./style.css";
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
const todoList = record([
|
|
7
|
+
{ text: "First" },
|
|
8
|
+
{ text: "Second" },
|
|
9
|
+
{ text: "Third" },
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const engine = new DomEngine(document.querySelector("#app") as HTMLElement);
|
|
13
|
+
engine.render(() => {
|
|
14
|
+
SimpleTodoList({
|
|
15
|
+
text: "Hello world",
|
|
16
|
+
todos: todoList,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
todoList.push({ text: "New item" });
|
|
21
|
+
|
|
22
|
+
setTimeout(() => {
|
|
23
|
+
todoList[2].text = "123";
|
|
24
|
+
}, 1000);
|
package/src/style.css
CHANGED
|
@@ -1,96 +1,41 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
3
|
+
line-height: 1.5;
|
|
4
|
+
font-weight: 400;
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
color-scheme: light dark;
|
|
7
|
+
color: rgba(255, 255, 255, 0.87);
|
|
8
|
+
background-color: #242424;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
font-synthesis: none;
|
|
11
|
+
text-rendering: optimizeLegibility;
|
|
12
|
+
-webkit-font-smoothing: antialiased;
|
|
13
|
+
-moz-osx-font-smoothing: grayscale;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
a {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
font-weight: 500;
|
|
18
|
+
color: #646cff;
|
|
19
|
+
text-decoration: inherit;
|
|
20
20
|
}
|
|
21
21
|
a:hover {
|
|
22
|
-
|
|
22
|
+
color: #535bf2;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
body {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
margin: 0;
|
|
27
|
+
display: flex;
|
|
28
|
+
place-items: center;
|
|
29
|
+
min-width: 320px;
|
|
30
|
+
min-height: 100vh;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
h1 {
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
font-size: 3.2em;
|
|
35
|
+
line-height: 1.1;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
padding: 2rem;
|
|
42
|
-
text-align: center;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.logo {
|
|
46
|
-
height: 6em;
|
|
47
|
-
padding: 1.5em;
|
|
48
|
-
will-change: filter;
|
|
49
|
-
transition: filter 300ms;
|
|
50
|
-
}
|
|
51
|
-
.logo:hover {
|
|
52
|
-
filter: drop-shadow(0 0 2em #646cffaa);
|
|
53
|
-
}
|
|
54
|
-
.logo.vanilla:hover {
|
|
55
|
-
filter: drop-shadow(0 0 2em #3178c6aa);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.card {
|
|
59
|
-
padding: 2em;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.read-the-docs {
|
|
63
|
-
color: #888;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
button {
|
|
67
|
-
border-radius: 8px;
|
|
68
|
-
border: 1px solid transparent;
|
|
69
|
-
padding: 0.6em 1.2em;
|
|
70
|
-
font-size: 1em;
|
|
71
|
-
font-weight: 500;
|
|
72
|
-
font-family: inherit;
|
|
73
|
-
background-color: #1a1a1a;
|
|
74
|
-
cursor: pointer;
|
|
75
|
-
transition: border-color 0.25s;
|
|
76
|
-
}
|
|
77
|
-
button:hover {
|
|
78
|
-
border-color: #646cff;
|
|
79
|
-
}
|
|
80
|
-
button:focus,
|
|
81
|
-
button:focus-visible {
|
|
82
|
-
outline: 4px auto -webkit-focus-ring-color;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@media (prefers-color-scheme: light) {
|
|
86
|
-
:root {
|
|
87
|
-
color: #213547;
|
|
88
|
-
background-color: #ffffff;
|
|
89
|
-
}
|
|
90
|
-
a:hover {
|
|
91
|
-
color: #747bff;
|
|
92
|
-
}
|
|
93
|
-
button {
|
|
94
|
-
background-color: #f9f9f9;
|
|
95
|
-
}
|
|
38
|
+
.todo {
|
|
39
|
+
margin: 10px;
|
|
40
|
+
border: 1px solid blue;
|
|
96
41
|
}
|
package/tsconfig.json
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
"target": "ES2020",
|
|
4
4
|
"useDefineForClassFields": true,
|
|
5
5
|
"module": "ESNext",
|
|
6
|
-
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"lib": ["ESNext", "ES2020", "DOM", "DOM.Iterable"],
|
|
7
7
|
"skipLibCheck": true,
|
|
8
|
+
"experimentalDecorators": true,
|
|
8
9
|
|
|
9
10
|
/* Bundler mode */
|
|
10
11
|
"moduleResolution": "bundler",
|
|
@@ -13,7 +14,6 @@
|
|
|
13
14
|
"isolatedModules": true,
|
|
14
15
|
"noEmit": true,
|
|
15
16
|
|
|
16
|
-
/* Linting */
|
|
17
17
|
"strict": true,
|
|
18
18
|
"noUnusedLocals": true,
|
|
19
19
|
"noUnusedParameters": true,
|
package/vite.config.js
ADDED
package/bun.lockb
DELETED
|
Binary file
|