create-wirejs-app 2.0.62 → 2.0.63
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/package.json +6 -1
- package/templates/default/api/index.ts +13 -2
- package/templates/default/package.json +7 -3
- package/templates/default/src/components/account-menu.ts +17 -118
- package/templates/default/src/components/index.ts +1 -0
- package/templates/default/src/layouts/main.ts +157 -0
- package/templates/default/src/ssg/index.ts +12 -19
- package/templates/default/src/ssg/todo-app.ts +12 -29
- package/templates/default/src/ssr/simple-wiki/%.ts +11 -39
- package/templates/default/static/default.css +108 -0
- package/templates/default/src/components/authenticator.ts +0 -133
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-wirejs-app",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.63",
|
|
4
4
|
"description": "Initializes a wirejs package.",
|
|
5
5
|
"author": "Jon Wire",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/svidgen/create-wirejs-app.git",
|
|
10
|
+
"directory": "packages/create-wirejs-app"
|
|
11
|
+
},
|
|
7
12
|
"bin": {
|
|
8
13
|
"create-wirejs-app": "./bin.js"
|
|
9
14
|
},
|
|
@@ -44,7 +44,7 @@ export const todos = withContext(context => ({
|
|
|
44
44
|
list: { eq: list ?? 'default' }
|
|
45
45
|
},
|
|
46
46
|
});
|
|
47
|
-
const todosArray = await
|
|
47
|
+
const todosArray = await fromAsync(todos);
|
|
48
48
|
return todosArray
|
|
49
49
|
.sort((a, b) => a.order - b.order)
|
|
50
50
|
.map(todo => ({
|
|
@@ -111,4 +111,15 @@ export const wiki = withContext(context => ({
|
|
|
111
111
|
|
|
112
112
|
return true;
|
|
113
113
|
}
|
|
114
|
-
}));
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* For node 20, which doesn't have `Array.fromAsync()`.
|
|
118
|
+
*/
|
|
119
|
+
async function fromAsync<T>(gen: AsyncGenerator<T>): Promise<T[]> {
|
|
120
|
+
const items: T[] = [];
|
|
121
|
+
for await (const item of gen) {
|
|
122
|
+
items.push(item);
|
|
123
|
+
}
|
|
124
|
+
return items;
|
|
125
|
+
}
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"dompurify": "^3.2.3",
|
|
12
12
|
"marked": "^15.0.6",
|
|
13
|
-
"wirejs-dom": "^1.0.
|
|
14
|
-
"wirejs-resources": "^0.1.
|
|
13
|
+
"wirejs-dom": "^1.0.41",
|
|
14
|
+
"wirejs-resources": "^0.1.58",
|
|
15
|
+
"wirejs-components": "^0.1.1"
|
|
15
16
|
},
|
|
16
17
|
"devDependencies": {
|
|
17
|
-
"wirejs-scripts": "^3.0.
|
|
18
|
+
"wirejs-scripts": "^3.0.56",
|
|
18
19
|
"typescript": "^5.7.3"
|
|
19
20
|
},
|
|
20
21
|
"scripts": {
|
|
@@ -22,5 +23,8 @@
|
|
|
22
23
|
"prestart": "npm run prestart --workspaces --if-present",
|
|
23
24
|
"start": "wirejs-scripts start",
|
|
24
25
|
"build": "wirejs-scripts build"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20.0.0"
|
|
25
29
|
}
|
|
26
30
|
}
|
|
@@ -1,119 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const listeners = new Set<Callback>();
|
|
19
|
-
|
|
20
|
-
const listenForClose = (
|
|
21
|
-
event: (MouseEvent | KeyboardEvent)
|
|
22
|
-
) => {
|
|
23
|
-
if (
|
|
24
|
-
(event.type === 'click' && !self.data.menu.contains(event.target as any))
|
|
25
|
-
|| (event.type === 'keyup' && (event as any).key === 'Escape')
|
|
26
|
-
) {
|
|
27
|
-
close()
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const close = () => {
|
|
32
|
-
uiState.expanded = false;
|
|
33
|
-
updateStyleToMatchState();
|
|
34
|
-
document.removeEventListener('click', listenForClose);
|
|
35
|
-
document.removeEventListener('keyup', listenForClose);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const removeListenForClose = () => {
|
|
39
|
-
document.removeEventListener('click', listenForClose);
|
|
40
|
-
document.removeEventListener('keyup', listenForClose);
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const updateStyleToMatchState = () => {
|
|
44
|
-
self.data.menu.style.display = uiState.expanded ? '' : 'none';
|
|
45
|
-
const position = self.getBoundingClientRect();
|
|
46
|
-
self.data.menu.style.top = `${position.bottom + 1}px`;
|
|
47
|
-
self.data.menu.style.right = `${document.body.clientWidth - position.right + 16}px`;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const authenticatorNode = authenticator(api, initialState);
|
|
51
|
-
authenticatorNode.data.onchange(state => {
|
|
52
|
-
self.data.user = state.user?.username || '';
|
|
53
|
-
close();
|
|
54
|
-
for (const listener of listeners) {
|
|
55
|
-
try {
|
|
56
|
-
listener(state);
|
|
57
|
-
} catch (error) {
|
|
58
|
-
console.error("Error calling auth state listener.");
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
import { html } from "wirejs-dom/v2";
|
|
2
|
+
import { AccountMenu as BaseAccountMenu } from "wirejs-components";
|
|
3
|
+
|
|
4
|
+
type BaseOptions = Omit<Parameters<typeof BaseAccountMenu>[0], 'topItems' | 'bottomItems'>;
|
|
5
|
+
|
|
6
|
+
export function AccountMenu(options: BaseOptions) {
|
|
7
|
+
return BaseAccountMenu({
|
|
8
|
+
...options,
|
|
9
|
+
topItems: [
|
|
10
|
+
(state) => state?.state !== 'authenticated'
|
|
11
|
+
? html`<div><i>Sign in to get started!</i></div>`
|
|
12
|
+
: html`<div>Welcome!</div>`
|
|
13
|
+
],
|
|
14
|
+
bottomItems: [
|
|
15
|
+
html`<div>Made with wirejs</div>`
|
|
16
|
+
]
|
|
61
17
|
});
|
|
62
|
-
|
|
63
|
-
const self = html`<accountmenu style='display: inline-block;'>
|
|
64
|
-
<div
|
|
65
|
-
style='display: inline-block;'
|
|
66
|
-
>${node(
|
|
67
|
-
'user',
|
|
68
|
-
initialState?.user?.username || '',
|
|
69
|
-
name => name ? html`<b>${name}</b>` : html`<i>Anonymous</i>`)}</div>
|
|
70
|
-
<div style='
|
|
71
|
-
display: inline-block;
|
|
72
|
-
border: 1px solid silver;
|
|
73
|
-
border-radius: 0.25rem;
|
|
74
|
-
cursor: pointer;
|
|
75
|
-
padding: 0 0.25em;
|
|
76
|
-
'
|
|
77
|
-
onclick=${() => {
|
|
78
|
-
uiState.expanded = !uiState.expanded;
|
|
79
|
-
updateStyleToMatchState();
|
|
80
|
-
if (uiState.expanded) {
|
|
81
|
-
authenticatorNode.data.focus();
|
|
82
|
-
setTimeout(() => {
|
|
83
|
-
document.addEventListener('click', listenForClose);
|
|
84
|
-
document.addEventListener('keyup', listenForClose);
|
|
85
|
-
}, 1);
|
|
86
|
-
} else {
|
|
87
|
-
removeListenForClose()
|
|
88
|
-
}
|
|
89
|
-
}}
|
|
90
|
-
>☰</div>
|
|
91
|
-
<div ${id('menu', HTMLDivElement)} style='
|
|
92
|
-
display: none;
|
|
93
|
-
position: absolute;
|
|
94
|
-
border: 1px solid gray;
|
|
95
|
-
border-radius: 0.25rem;
|
|
96
|
-
background-color: white;
|
|
97
|
-
padding: 0.5rem;
|
|
98
|
-
box-shadow: -0.125rem 0.125rem 0.25rem lightgray;
|
|
99
|
-
'>${node('authenticator', authenticatorNode)}</div>
|
|
100
|
-
</accountmenu>`.onadd(async self => {
|
|
101
|
-
if (!initialState) {
|
|
102
|
-
const state = await api.getState(null);
|
|
103
|
-
authenticatorNode.data.setState(state);
|
|
104
|
-
self.data.user = state.user?.username || '';
|
|
105
|
-
}
|
|
106
|
-
}).extend(self => ({
|
|
107
|
-
data: {
|
|
108
|
-
onchange: (callback: Callback) => {
|
|
109
|
-
listeners.add(callback);
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
removeonchange: (callback: Callback) => {
|
|
113
|
-
listeners.delete(callback);
|
|
114
|
-
},
|
|
115
|
-
}
|
|
116
|
-
}));
|
|
117
|
-
|
|
118
|
-
return self;
|
|
119
|
-
};
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AccountMenu } from './account-menu.js';
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { html, node, hydrate } from 'wirejs-dom/v2';
|
|
2
|
+
import { auth } from 'my-api';
|
|
3
|
+
import { AccountMenu } from '../components/index.js';
|
|
4
|
+
import type { AuthenticationState } from 'wirejs-resources';
|
|
5
|
+
|
|
6
|
+
const TITLE = 'My New Site';
|
|
7
|
+
const SUBTITLE = 'Made with wirejs';
|
|
8
|
+
const MENU_ID = 'account-menu';
|
|
9
|
+
const DISCLAIMER = html`<div>
|
|
10
|
+
<p>For the purposes of awesomeness only.</p>
|
|
11
|
+
</div>`;
|
|
12
|
+
|
|
13
|
+
async function Account() {
|
|
14
|
+
return html`<div id='${MENU_ID}'>
|
|
15
|
+
${AccountMenu({ api: auth })}
|
|
16
|
+
</div>`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function Main(slots: {
|
|
20
|
+
/**
|
|
21
|
+
* Replaces the default prefix in the final page title.
|
|
22
|
+
*/
|
|
23
|
+
siteTitle?:
|
|
24
|
+
| string
|
|
25
|
+
| ((state: AuthenticationState) => string);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Appears on the page under the site title.
|
|
29
|
+
*
|
|
30
|
+
* Set to empty-string explicitly to omit the default.
|
|
31
|
+
*/
|
|
32
|
+
siteSubTitle?:
|
|
33
|
+
| string
|
|
34
|
+
| ((state: AuthenticationState) => string);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The page title. Appears below the site title and subtitle when given.
|
|
38
|
+
*/
|
|
39
|
+
pageTitle?:
|
|
40
|
+
| string
|
|
41
|
+
| ((state: AuthenticationState) => string);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Author for this page. Appears next to the title in lighter font and
|
|
45
|
+
* in the address bar.
|
|
46
|
+
*/
|
|
47
|
+
pageAuthor?:
|
|
48
|
+
| string
|
|
49
|
+
| ((state: AuthenticationState) => string);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The main content for the page.
|
|
53
|
+
*/
|
|
54
|
+
content:
|
|
55
|
+
| HTMLElement
|
|
56
|
+
| ((state: AuthenticationState) => HTMLElement);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Appears in the top of the footer.
|
|
60
|
+
*/
|
|
61
|
+
disclaimer?:
|
|
62
|
+
| string
|
|
63
|
+
| HTMLElement
|
|
64
|
+
| ((state: AuthenticationState) => string | HTMLElement);
|
|
65
|
+
}) {
|
|
66
|
+
|
|
67
|
+
const pageAuthorElement = slots.pageAuthor ? html`
|
|
68
|
+
<span style='color: var(--color-muted);'>
|
|
69
|
+
: according to <a href='/wiki/view/~${slots.pageAuthor}'>${slots.pageAuthor}</a>
|
|
70
|
+
</span>
|
|
71
|
+
` : '';
|
|
72
|
+
|
|
73
|
+
const pageTitle = slots.pageTitle ? html`
|
|
74
|
+
<h2 style='font-variant: small-caps;'>
|
|
75
|
+
${slots.pageTitle} ${pageAuthorElement}
|
|
76
|
+
</h2>
|
|
77
|
+
` : '';
|
|
78
|
+
|
|
79
|
+
const browserBarTitle = [
|
|
80
|
+
slots.pageTitle,
|
|
81
|
+
slots.pageAuthor,
|
|
82
|
+
slots.siteTitle || TITLE,
|
|
83
|
+
slots.siteSubTitle || SUBTITLE,
|
|
84
|
+
].filter(Boolean).slice(0, 3).join(' - ');
|
|
85
|
+
|
|
86
|
+
const page = html`
|
|
87
|
+
<!doctype html>
|
|
88
|
+
<html id='root'>
|
|
89
|
+
<head>
|
|
90
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
91
|
+
<title>${browserBarTitle}</title>
|
|
92
|
+
<link rel='icon' type='image/svg+xml' href='/images/logo.svg' />
|
|
93
|
+
<link rel='stylesheet' type='text/css' href='/default.css' />
|
|
94
|
+
</head>
|
|
95
|
+
<body>
|
|
96
|
+
<div style='
|
|
97
|
+
border-width: 0 1px;
|
|
98
|
+
border-color: silver;
|
|
99
|
+
border-style: solid;
|
|
100
|
+
max-width: 1200px;
|
|
101
|
+
min-height: 100vh;
|
|
102
|
+
padding: 0 1rem;
|
|
103
|
+
margin: 0 auto;
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
'>
|
|
106
|
+
<div style='
|
|
107
|
+
display: flex;
|
|
108
|
+
width: 100%;
|
|
109
|
+
padding-bottom: 0.5rem;
|
|
110
|
+
border-bottom: 1px solid var(--border-color-muted, #777);
|
|
111
|
+
margin-bottom: 1rem;
|
|
112
|
+
'>
|
|
113
|
+
<!-- source: https://www.svgrepo.com/svg/322460/greek-temple -->
|
|
114
|
+
<svg
|
|
115
|
+
style='
|
|
116
|
+
margin-top: 1.4rem;
|
|
117
|
+
margin-right: 0.5rem;
|
|
118
|
+
'
|
|
119
|
+
height='4rem'
|
|
120
|
+
viewBox="0 0 512 512"
|
|
121
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
122
|
+
><path fill="#000000" d="M256 26.2L52 135h408L256 26.2zM73 153v14h366v-14H73zm16 32v206h30V185H89zm101.334 0v206h30V185h-30zm101.332 0v206h30V185h-30zM393 185v206h30V185h-30zM73 409v30h366v-30H73zm-32 48v30h430v-30H41z"/></svg>
|
|
123
|
+
|
|
124
|
+
<div style='flex-basis: 0; flex-grow: 5;'>
|
|
125
|
+
<h1 style='font-variant: small-caps; text-shadow: silver 2px 2px 2px;'>
|
|
126
|
+
<a href='/' style='color: var(--color-strong, #000);'>${
|
|
127
|
+
slots.siteTitle || TITLE
|
|
128
|
+
}</a>
|
|
129
|
+
</h1>
|
|
130
|
+
|
|
131
|
+
<div style='
|
|
132
|
+
margin-top: -1rem;
|
|
133
|
+
color: var(--color-muted, #888);
|
|
134
|
+
'>${slots.siteSubTitle ?? SUBTITLE}</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div style='margin-top: 3.5rem; flex-grow: 0;'>
|
|
138
|
+
${node('account', await Account())}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
${pageTitle}
|
|
143
|
+
|
|
144
|
+
<div id='content'>${slots.content}</div>
|
|
145
|
+
|
|
146
|
+
<footer>
|
|
147
|
+
${slots.disclaimer ?? DISCLAIMER}
|
|
148
|
+
</footer>
|
|
149
|
+
</div>
|
|
150
|
+
</body>
|
|
151
|
+
</html>
|
|
152
|
+
`;
|
|
153
|
+
return page;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// TODO: fix wirejs `hydrate()` type
|
|
157
|
+
hydrate(MENU_ID, Account as any);
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
import { html } from 'wirejs-dom/v2';
|
|
2
|
+
import { Main } from '../layouts/main.js';
|
|
2
3
|
|
|
3
4
|
export async function generate() {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<ul>
|
|
16
|
-
<li><a href='/todo-app.html'>Todo App</a></li>
|
|
17
|
-
<li><a href='/simple-wiki/index.html'>Simple Wiki</a></li>
|
|
18
|
-
</ul>
|
|
19
|
-
</body>
|
|
20
|
-
</html>
|
|
21
|
-
`;
|
|
22
|
-
return page;
|
|
5
|
+
return Main({
|
|
6
|
+
pageTitle: 'Welcome!',
|
|
7
|
+
content: html`<div>
|
|
8
|
+
<p>This is your wirejs app!</p>
|
|
9
|
+
<p>It comes with some sample API methods and pages.</p>
|
|
10
|
+
<ul>
|
|
11
|
+
<li><a href='/todo-app.html'>Todo App</a></li>
|
|
12
|
+
<li><a href='/simple-wiki/index.html'>Simple Wiki</a></li>
|
|
13
|
+
</ul>
|
|
14
|
+
</div>`
|
|
15
|
+
})
|
|
23
16
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { html, node, list, attribute, hydrate } from 'wirejs-dom/v2';
|
|
2
|
-
import {
|
|
2
|
+
import { AuthenticatedContent } from 'wirejs-components';
|
|
3
3
|
import { auth, todos, Todo } from 'my-api';
|
|
4
|
+
import { Main } from '../layouts/main.js';
|
|
4
5
|
|
|
5
6
|
function Todos() {
|
|
6
7
|
const remove = (todo: Todo) => {
|
|
@@ -44,41 +45,23 @@ function Todos() {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
async function App() {
|
|
47
|
-
const accountMenuNode = accountMenu({ api: auth });
|
|
48
|
-
|
|
49
|
-
accountMenuNode.data.onchange(async state => {
|
|
50
|
-
if (state.state === 'authenticated') {
|
|
51
|
-
self.data.content = Todos();
|
|
52
|
-
} else {
|
|
53
|
-
self.data.content = html`<div>You need to sign in to add your todo list.</div>`;
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
|
|
57
48
|
const self = html`<div id='app'>
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
${await AuthenticatedContent({
|
|
50
|
+
authenticated: () => Todos(),
|
|
51
|
+
unauthenticated: () => html`<div>
|
|
52
|
+
You need to sign in to add your todo list.
|
|
53
|
+
</div>`
|
|
54
|
+
})}
|
|
60
55
|
</div>`;
|
|
61
56
|
|
|
62
57
|
return self;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
export async function generate() {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
71
|
-
<title>Todo App</title>
|
|
72
|
-
</head>
|
|
73
|
-
<body>
|
|
74
|
-
<p><a href='/'>Home</a></p>
|
|
75
|
-
<h1>Todo App</h1>
|
|
76
|
-
${await App()}
|
|
77
|
-
</body>
|
|
78
|
-
</html>
|
|
79
|
-
`;
|
|
80
|
-
|
|
81
|
-
return page;
|
|
61
|
+
return Main({
|
|
62
|
+
pageTitle: 'Todo App',
|
|
63
|
+
content: await App(),
|
|
64
|
+
});
|
|
82
65
|
}
|
|
83
66
|
|
|
84
67
|
hydrate('app', App as any);
|
|
@@ -1,24 +1,11 @@
|
|
|
1
1
|
import { marked } from 'marked';
|
|
2
2
|
import DOMPurify from 'dompurify';
|
|
3
|
-
import { html, id,
|
|
3
|
+
import { html, id, hydrate, node, } from 'wirejs-dom/v2';
|
|
4
4
|
import type { AuthenticationMachineState, Context } from 'wirejs-resources';
|
|
5
|
-
import { accountMenu } from '../../components/account-menu.js';
|
|
6
5
|
import { auth, wiki } from 'my-api';
|
|
6
|
+
import { AuthMonitor } from 'wirejs-components/utils';
|
|
7
|
+
import { Main } from '../../layouts/main.js';
|
|
7
8
|
|
|
8
|
-
type WithoutNodes<T> = undefined | (
|
|
9
|
-
T extends object ? {
|
|
10
|
-
[K in keyof T
|
|
11
|
-
as T[K] extends Node ? never : K]: WithoutNodes<T[K]>
|
|
12
|
-
} : T
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Shallow check for a `data` hydration property. If present, returns the
|
|
17
|
-
* argument typed according to the given `T`.
|
|
18
|
-
*/
|
|
19
|
-
function initData<T>(arg0: any): WithoutNodes<T> | undefined {
|
|
20
|
-
return arg0;
|
|
21
|
-
}
|
|
22
9
|
|
|
23
10
|
async function Wiki(init: { context?: Context, data?: any }) {
|
|
24
11
|
const { context, data } = init;
|
|
@@ -31,8 +18,6 @@ async function Wiki(init: { context?: Context, data?: any }) {
|
|
|
31
18
|
const initialState: AuthenticationMachineState =
|
|
32
19
|
data?.initialAuthState ?? await auth.getState(context);
|
|
33
20
|
|
|
34
|
-
const accountMenuNode = accountMenu({ api: auth, initialState });
|
|
35
|
-
|
|
36
21
|
let markdown: string = content ?? `This page doesn't exist yet`;
|
|
37
22
|
const signedOutAction = html`<i>(<b>Sign in</b> to edit.)</i>`;
|
|
38
23
|
const signedInAction = html`<button onclick=${enableEditing}>edit</button>`;
|
|
@@ -40,16 +25,15 @@ async function Wiki(init: { context?: Context, data?: any }) {
|
|
|
40
25
|
const editor = html`<div>
|
|
41
26
|
<textarea
|
|
42
27
|
${id('textarea', HTMLTextAreaElement)}
|
|
43
|
-
style='width: 20em; height: 10em;'
|
|
44
28
|
></textarea>
|
|
45
29
|
</div>`;
|
|
46
30
|
|
|
47
|
-
|
|
31
|
+
AuthMonitor.subscribe(state => {
|
|
48
32
|
self.data.actions = actionsFor(state);
|
|
49
33
|
});
|
|
50
34
|
|
|
51
|
-
function actionsFor(state: AuthenticationMachineState) {
|
|
52
|
-
return state
|
|
35
|
+
function actionsFor(state: AuthenticationMachineState | undefined) {
|
|
36
|
+
return state?.state === 'authenticated' ? signedInAction : signedOutAction;
|
|
53
37
|
}
|
|
54
38
|
|
|
55
39
|
function enableEditing() {
|
|
@@ -76,7 +60,6 @@ async function Wiki(init: { context?: Context, data?: any }) {
|
|
|
76
60
|
}
|
|
77
61
|
|
|
78
62
|
const self = html`<div id='wiki'>
|
|
79
|
-
<div style='float: right;'>${accountMenuNode}</div>
|
|
80
63
|
${node('content', markdown, md =>
|
|
81
64
|
html`<div>${DOMPurify.sanitize(marked.parse(md!) as string)}</div>`)
|
|
82
65
|
}
|
|
@@ -100,22 +83,11 @@ export async function generate(context: Context) {
|
|
|
100
83
|
.replace(/\s+/g, ' ')
|
|
101
84
|
;
|
|
102
85
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<title>Wiki ${visiblePath}</title>
|
|
109
|
-
</head>
|
|
110
|
-
<body>
|
|
111
|
-
<p><a href='/'>Home</a></p>
|
|
112
|
-
<h1>Wiki ${visiblePath}</h1>
|
|
113
|
-
${await Wiki({ context })}
|
|
114
|
-
</body>
|
|
115
|
-
</html>
|
|
116
|
-
`;
|
|
117
|
-
|
|
118
|
-
return page;
|
|
86
|
+
return Main({
|
|
87
|
+
siteSubTitle: 'A simple sample wiki',
|
|
88
|
+
pageTitle: visiblePath,
|
|
89
|
+
content: await Wiki({ context }),
|
|
90
|
+
})
|
|
119
91
|
}
|
|
120
92
|
|
|
121
93
|
hydrate('wiki', Wiki);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--color-muted: #888;
|
|
3
|
+
--color-error: #c33;
|
|
4
|
+
--color-action: #533;
|
|
5
|
+
--border-color-muted: #777;
|
|
6
|
+
--border-color-subtle: #aaa;
|
|
7
|
+
--background-color-muted: #eee;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
html {
|
|
11
|
+
padding: 0;
|
|
12
|
+
margin: 0
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
font-family: math;
|
|
17
|
+
padding: 0;
|
|
18
|
+
margin: 0;
|
|
19
|
+
color: #333;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
a {
|
|
23
|
+
font-weight: bold;
|
|
24
|
+
color: var(--color-action);
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
menu {
|
|
29
|
+
padding-inline-start: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
li {
|
|
33
|
+
margin: 1rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
menu li {
|
|
37
|
+
list-style: none;
|
|
38
|
+
margin: 1rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
menu li a {
|
|
42
|
+
text-decoration: underline;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
form {
|
|
46
|
+
margin: 1rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
form div {
|
|
50
|
+
margin-bottom: 1rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
label, input, button {
|
|
54
|
+
display: inline-block;
|
|
55
|
+
width: 9rem;
|
|
56
|
+
margin: 0.25rem;
|
|
57
|
+
border-radius: 0.125rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
label {
|
|
61
|
+
font-weight: bold;
|
|
62
|
+
color: var(--color-muted);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
input {
|
|
66
|
+
width: 20rem;
|
|
67
|
+
border: 1px solid var(--border-color-muted);
|
|
68
|
+
margin-bottom: 0.5rem;
|
|
69
|
+
padding: 0.5rem;
|
|
70
|
+
font-size: medium;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
textarea {
|
|
74
|
+
width: calc(100% - 2rem);
|
|
75
|
+
height: 15rem;
|
|
76
|
+
font-size: medium;
|
|
77
|
+
font-family: 'Courier New', Courier, monospace;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
button, input[type="submit"] {
|
|
81
|
+
font-family: math;
|
|
82
|
+
font-size: medium;
|
|
83
|
+
border: 1px solid var(--border-color-muted);
|
|
84
|
+
padding: 0.5rem;
|
|
85
|
+
background-color: var(--background-color-muted);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
form span.status {
|
|
89
|
+
display: inline-block;
|
|
90
|
+
color: var(--color-muted);
|
|
91
|
+
margin-left: 0.5rem;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.error form span.status {
|
|
95
|
+
color: var(--color-error);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.section {
|
|
99
|
+
border-bottom: 1px dashed var(--border-color-subtle);
|
|
100
|
+
margin-bottom: 1.5rem;
|
|
101
|
+
padding-bottom: 1.5rem;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
footer {
|
|
105
|
+
margin-top: 4rem;
|
|
106
|
+
border-top: 1px solid var(--border-color-muted);
|
|
107
|
+
color: var(--color-muted);
|
|
108
|
+
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { attribute, html, node } from 'wirejs-dom/v2';
|
|
2
|
-
import type {
|
|
3
|
-
AuthenticationApi,
|
|
4
|
-
AuthenticationState,
|
|
5
|
-
AuthenticationMachineState,
|
|
6
|
-
AuthenticationMachineAction,
|
|
7
|
-
AuthenticationMachineInput,
|
|
8
|
-
} from 'wirejs-resources';
|
|
9
|
-
|
|
10
|
-
type AuthCallback = (state: AuthenticationMachineState) => any;
|
|
11
|
-
|
|
12
|
-
export const authenticatoraction = (
|
|
13
|
-
action: AuthenticationMachineAction,
|
|
14
|
-
act: (input: AuthenticationMachineInput) => void
|
|
15
|
-
) => {
|
|
16
|
-
const inputs = Object.entries(action.fields || []).map(([name, { label, type }]) => {
|
|
17
|
-
const id = `input_${Math.floor(Math.random() * 1_000_000)}`;
|
|
18
|
-
const input = html`<div>
|
|
19
|
-
<label for=${id}>${label}</label>
|
|
20
|
-
<br />
|
|
21
|
-
<input
|
|
22
|
-
id=${id}
|
|
23
|
-
name=${name}
|
|
24
|
-
type=${type}
|
|
25
|
-
value=${attribute('value', '')}
|
|
26
|
-
style='width: calc(100% - 1rem); margin-bottom: 0.5rem;'
|
|
27
|
-
/>
|
|
28
|
-
</div>`.extend(self => ({
|
|
29
|
-
data: { name }
|
|
30
|
-
}));
|
|
31
|
-
return input;
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const buttons = action.buttons?.map(b => html`<p>
|
|
35
|
-
<button type='submit' value='${b}'>${b}</button>
|
|
36
|
-
</p>`);
|
|
37
|
-
|
|
38
|
-
const link = buttons ? undefined : [
|
|
39
|
-
html`<p><a
|
|
40
|
-
style='cursor: pointer; font-weight: bold;'
|
|
41
|
-
onclick=${() => act({ key: action.key })}
|
|
42
|
-
>${action.name}</a></p>`
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
const actors = link ?? buttons;
|
|
46
|
-
|
|
47
|
-
if (action.fields && Object.keys(action.fields).length > 0) {
|
|
48
|
-
return html`<authenticatoraction>
|
|
49
|
-
<div>
|
|
50
|
-
<h4 style='margin-top: 1rem; margin-bottom: 0.5rem;'>${action.name}</h4>
|
|
51
|
-
<form
|
|
52
|
-
onsubmit=${(evt: SubmitEvent) => {
|
|
53
|
-
evt.preventDefault();
|
|
54
|
-
act({
|
|
55
|
-
key: action.key,
|
|
56
|
-
verb: (evt.submitter as any)?.value,
|
|
57
|
-
inputs: Object.fromEntries(inputs.map(input => ([
|
|
58
|
-
input.data.name,
|
|
59
|
-
input.data.value
|
|
60
|
-
])))
|
|
61
|
-
});
|
|
62
|
-
}}
|
|
63
|
-
>
|
|
64
|
-
${inputs}
|
|
65
|
-
${actors}
|
|
66
|
-
</form>
|
|
67
|
-
<hr style='width: 33%; height: 1px; border: none; background: silver;' />
|
|
68
|
-
</div>
|
|
69
|
-
</authenticatoraction>`;
|
|
70
|
-
} else {
|
|
71
|
-
return html`<authenticatoraction>
|
|
72
|
-
${actors}
|
|
73
|
-
</authenticatoraction>`;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export const authenticator = (
|
|
78
|
-
stateManager: AuthenticationApi,
|
|
79
|
-
initialState?: AuthenticationMachineState
|
|
80
|
-
) => {
|
|
81
|
-
const listeners = new Set<AuthCallback>();
|
|
82
|
-
let lastKnownState: AuthenticationMachineState | undefined = undefined;
|
|
83
|
-
|
|
84
|
-
const self = html`<authenticator style='display: block; min-width: 15em;'>
|
|
85
|
-
${node('state', html`<span>Loading ...</span>` as HTMLElement)}
|
|
86
|
-
</authenticator>`.extend(() => ({
|
|
87
|
-
renderState(state: AuthenticationMachineState | { errors: any[] }) {
|
|
88
|
-
if ('errors' in state && state.errors) {
|
|
89
|
-
alert((state as any).errors.map((e: any) => e.message).join("\n\n"));
|
|
90
|
-
} else {
|
|
91
|
-
lastKnownState = state as AuthenticationMachineState;
|
|
92
|
-
self.data.state = html`<div>
|
|
93
|
-
<div>${lastKnownState.message || ''}</div>
|
|
94
|
-
<div>${Object.entries(lastKnownState.actions).map(([_key, action]) => {
|
|
95
|
-
return authenticatoraction({...action}, async act => {
|
|
96
|
-
self.renderState(await stateManager.setState(null, act));
|
|
97
|
-
});
|
|
98
|
-
})}</div>
|
|
99
|
-
</div>`;
|
|
100
|
-
}
|
|
101
|
-
for (const listener of listeners) {
|
|
102
|
-
try {
|
|
103
|
-
listener(state as AuthenticationMachineState);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
console.error("Error calling auth state listener.");
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
})).onadd(async (self) => {
|
|
110
|
-
if (initialState) {
|
|
111
|
-
console.log('authenticator render state');
|
|
112
|
-
self.renderState(initialState)
|
|
113
|
-
}
|
|
114
|
-
}).extend(self => ({
|
|
115
|
-
data: {
|
|
116
|
-
setState: (state: AuthenticationMachineState) => self.renderState(state),
|
|
117
|
-
onchange: (callback: AuthCallback) => {
|
|
118
|
-
listeners.add(callback);
|
|
119
|
-
},
|
|
120
|
-
removeonchange: (callback: AuthCallback) => {
|
|
121
|
-
listeners.delete(callback);
|
|
122
|
-
},
|
|
123
|
-
focus: () => {
|
|
124
|
-
[...self.getElementsByTagName('input')].shift()?.focus();
|
|
125
|
-
},
|
|
126
|
-
get lastKnownState() {
|
|
127
|
-
return lastKnownState;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}));
|
|
131
|
-
|
|
132
|
-
return self;
|
|
133
|
-
};
|