create-wirejs-app 2.0.62 → 2.0.64

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 CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "create-wirejs-app",
3
- "version": "2.0.62",
3
+ "version": "2.0.64",
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 Array.fromAsync(todos);
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.38",
14
- "wirejs-resources": "^0.1.57"
13
+ "wirejs-dom": "^1.0.41",
14
+ "wirejs-resources": "^0.1.59",
15
+ "wirejs-components": "^0.1.2"
15
16
  },
16
17
  "devDependencies": {
17
- "wirejs-scripts": "^3.0.55",
18
+ "wirejs-scripts": "^3.0.57",
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 { attribute, html, node, text, id } from 'wirejs-dom/v2';
2
- import { authenticator } from './authenticator.js';
3
- import type {
4
- AuthenticationApi,
5
- AuthenticationMachineState,
6
- } from 'wirejs-resources';
7
-
8
- type Callback = (state: AuthenticationMachineState) => any;
9
-
10
- export const accountMenu = ({ api, initialState }: {
11
- api: AuthenticationApi,
12
- initialState?: AuthenticationMachineState
13
- }) => {
14
- const uiState = {
15
- expanded: false
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
- const page = html`
5
- <!doctype html>
6
- <html>
7
- <head>
8
- <meta name="viewport" content="width=device-width, initial-scale=1" />
9
- <title>Welcome!</title>
10
- </head>
11
- <body>
12
- <h1>Welcome!</h1>
13
- <p>This is your wirejs app!</p>
14
- <p>It comes with some sample API methods and pages.</p>
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 { accountMenu } from '../components/account-menu.js';
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
- <div style='float: right;'>${accountMenuNode}</div>
59
- ${node('content', html`<div>Loading ...</div>`)}
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
- const page = html`
67
- <!doctype html>
68
- <html>
69
- <head>
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, text, hydrate, node, list, attribute, KindaPretty } from 'wirejs-dom/v2';
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
- accountMenuNode.data.onchange(state => {
31
+ AuthMonitor.subscribe(state => {
48
32
  self.data.actions = actionsFor(state);
49
33
  });
50
34
 
51
- function actionsFor(state: AuthenticationMachineState) {
52
- return state.state === 'authenticated' ? signedInAction : signedOutAction;
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
- const page = html`
104
- <!doctype html>
105
- <html>
106
- <head>
107
- <meta name="viewport" content="width=device-width, initial-scale=1" />
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
- };