wirejs-components 0.1.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/README.md +3 -0
- package/dist/components/account-menu.d.ts +148 -0
- package/dist/components/account-menu.js +105 -0
- package/dist/components/authenticated-content.d.ts +18 -0
- package/dist/components/authenticated-content.js +26 -0
- package/dist/components/authenticator.d.ts +48 -0
- package/dist/components/authenticator.js +127 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/layouts/default.d.ts +60 -0
- package/dist/layouts/default.js +100 -0
- package/dist/layouts/index.d.ts +1 -0
- package/dist/layouts/index.js +1 -0
- package/dist/themes/default.d.ts +2 -0
- package/dist/themes/default.js +106 -0
- package/dist/themes/index.d.ts +1 -0
- package/dist/themes/index.js +1 -0
- package/dist/util/auth-monitor.d.ts +3 -0
- package/dist/util/auth-monitor.js +2 -0
- package/dist/util/index.d.ts +2 -0
- package/dist/util/index.js +2 -0
- package/dist/util/monitor.d.ts +22 -0
- package/dist/util/monitor.js +43 -0
- package/dist/utils/auth-monitor.d.ts +3 -0
- package/dist/utils/auth-monitor.js +2 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/monitor.d.ts +22 -0
- package/dist/utils/monitor.js +43 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { AuthenticationApi, AuthenticationMachineState, AuthenticationState } from 'wirejs-resources';
|
|
2
|
+
export declare const AccountMenu: ({ api, initialState, topItems, bottomItems }: {
|
|
3
|
+
api: AuthenticationApi;
|
|
4
|
+
initialState?: AuthenticationMachineState;
|
|
5
|
+
topItems?: (HTMLElement | ((state: AuthenticationState | undefined) => HTMLElement))[];
|
|
6
|
+
bottomItems?: (HTMLElement | ((state: AuthenticationState | undefined) => HTMLElement))[];
|
|
7
|
+
}) => HTMLElement & {
|
|
8
|
+
data: {
|
|
9
|
+
user: string;
|
|
10
|
+
menu: HTMLDivElement;
|
|
11
|
+
topSubmenu: HTMLElement[];
|
|
12
|
+
authenticator: HTMLElement & {
|
|
13
|
+
data: {
|
|
14
|
+
state: HTMLElement;
|
|
15
|
+
};
|
|
16
|
+
} & {
|
|
17
|
+
renderState(state: AuthenticationMachineState | {
|
|
18
|
+
errors: any[];
|
|
19
|
+
}): void;
|
|
20
|
+
} & {
|
|
21
|
+
data: {
|
|
22
|
+
setState: (state: AuthenticationMachineState) => void;
|
|
23
|
+
focus: () => void;
|
|
24
|
+
};
|
|
25
|
+
} & import("wirejs-dom/v2").DomEvents<HTMLElement & {
|
|
26
|
+
data: {
|
|
27
|
+
state: HTMLElement;
|
|
28
|
+
};
|
|
29
|
+
} & {
|
|
30
|
+
renderState(state: AuthenticationMachineState | {
|
|
31
|
+
errors: any[];
|
|
32
|
+
}): void;
|
|
33
|
+
} & {
|
|
34
|
+
data: {
|
|
35
|
+
setState: (state: AuthenticationMachineState) => void;
|
|
36
|
+
focus: () => void;
|
|
37
|
+
};
|
|
38
|
+
}> & import("wirejs-dom/v2").Extend<HTMLElement & {
|
|
39
|
+
data: {
|
|
40
|
+
state: HTMLElement;
|
|
41
|
+
};
|
|
42
|
+
} & {
|
|
43
|
+
renderState(state: AuthenticationMachineState | {
|
|
44
|
+
errors: any[];
|
|
45
|
+
}): void;
|
|
46
|
+
} & {
|
|
47
|
+
data: {
|
|
48
|
+
setState: (state: AuthenticationMachineState) => void;
|
|
49
|
+
focus: () => void;
|
|
50
|
+
};
|
|
51
|
+
}>;
|
|
52
|
+
bottomSubmenu: HTMLElement[];
|
|
53
|
+
};
|
|
54
|
+
} & import("wirejs-dom/v2").DomEvents<HTMLElement & {
|
|
55
|
+
data: {
|
|
56
|
+
user: string;
|
|
57
|
+
menu: HTMLDivElement;
|
|
58
|
+
topSubmenu: HTMLElement[];
|
|
59
|
+
authenticator: HTMLElement & {
|
|
60
|
+
data: {
|
|
61
|
+
state: HTMLElement;
|
|
62
|
+
};
|
|
63
|
+
} & {
|
|
64
|
+
renderState(state: AuthenticationMachineState | {
|
|
65
|
+
errors: any[];
|
|
66
|
+
}): void;
|
|
67
|
+
} & {
|
|
68
|
+
data: {
|
|
69
|
+
setState: (state: AuthenticationMachineState) => void;
|
|
70
|
+
focus: () => void;
|
|
71
|
+
};
|
|
72
|
+
} & import("wirejs-dom/v2").DomEvents<HTMLElement & {
|
|
73
|
+
data: {
|
|
74
|
+
state: HTMLElement;
|
|
75
|
+
};
|
|
76
|
+
} & {
|
|
77
|
+
renderState(state: AuthenticationMachineState | {
|
|
78
|
+
errors: any[];
|
|
79
|
+
}): void;
|
|
80
|
+
} & {
|
|
81
|
+
data: {
|
|
82
|
+
setState: (state: AuthenticationMachineState) => void;
|
|
83
|
+
focus: () => void;
|
|
84
|
+
};
|
|
85
|
+
}> & import("wirejs-dom/v2").Extend<HTMLElement & {
|
|
86
|
+
data: {
|
|
87
|
+
state: HTMLElement;
|
|
88
|
+
};
|
|
89
|
+
} & {
|
|
90
|
+
renderState(state: AuthenticationMachineState | {
|
|
91
|
+
errors: any[];
|
|
92
|
+
}): void;
|
|
93
|
+
} & {
|
|
94
|
+
data: {
|
|
95
|
+
setState: (state: AuthenticationMachineState) => void;
|
|
96
|
+
focus: () => void;
|
|
97
|
+
};
|
|
98
|
+
}>;
|
|
99
|
+
bottomSubmenu: HTMLElement[];
|
|
100
|
+
};
|
|
101
|
+
}> & import("wirejs-dom/v2").Extend<HTMLElement & {
|
|
102
|
+
data: {
|
|
103
|
+
user: string;
|
|
104
|
+
menu: HTMLDivElement;
|
|
105
|
+
topSubmenu: HTMLElement[];
|
|
106
|
+
authenticator: HTMLElement & {
|
|
107
|
+
data: {
|
|
108
|
+
state: HTMLElement;
|
|
109
|
+
};
|
|
110
|
+
} & {
|
|
111
|
+
renderState(state: AuthenticationMachineState | {
|
|
112
|
+
errors: any[];
|
|
113
|
+
}): void;
|
|
114
|
+
} & {
|
|
115
|
+
data: {
|
|
116
|
+
setState: (state: AuthenticationMachineState) => void;
|
|
117
|
+
focus: () => void;
|
|
118
|
+
};
|
|
119
|
+
} & import("wirejs-dom/v2").DomEvents<HTMLElement & {
|
|
120
|
+
data: {
|
|
121
|
+
state: HTMLElement;
|
|
122
|
+
};
|
|
123
|
+
} & {
|
|
124
|
+
renderState(state: AuthenticationMachineState | {
|
|
125
|
+
errors: any[];
|
|
126
|
+
}): void;
|
|
127
|
+
} & {
|
|
128
|
+
data: {
|
|
129
|
+
setState: (state: AuthenticationMachineState) => void;
|
|
130
|
+
focus: () => void;
|
|
131
|
+
};
|
|
132
|
+
}> & import("wirejs-dom/v2").Extend<HTMLElement & {
|
|
133
|
+
data: {
|
|
134
|
+
state: HTMLElement;
|
|
135
|
+
};
|
|
136
|
+
} & {
|
|
137
|
+
renderState(state: AuthenticationMachineState | {
|
|
138
|
+
errors: any[];
|
|
139
|
+
}): void;
|
|
140
|
+
} & {
|
|
141
|
+
data: {
|
|
142
|
+
setState: (state: AuthenticationMachineState) => void;
|
|
143
|
+
focus: () => void;
|
|
144
|
+
};
|
|
145
|
+
}>;
|
|
146
|
+
bottomSubmenu: HTMLElement[];
|
|
147
|
+
};
|
|
148
|
+
}>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { html, node, id, list, css } from 'wirejs-dom/v2';
|
|
2
|
+
import { Authenticator } from './authenticator.js';
|
|
3
|
+
import { AuthMonitor } from '../utils/auth-monitor.js';
|
|
4
|
+
const style = css `
|
|
5
|
+
accountmenu {
|
|
6
|
+
display: inline-block;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
accountmenu > .label {
|
|
10
|
+
display: inline-block;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.handle {
|
|
14
|
+
display: inline-block;
|
|
15
|
+
border: 1px solid silver;
|
|
16
|
+
border-radius: 0.25rem;
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
padding: 0 0.25em;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.menu {
|
|
22
|
+
display: none;
|
|
23
|
+
position: absolute;
|
|
24
|
+
border: 1px solid gray;
|
|
25
|
+
border-radius: 0.25rem;
|
|
26
|
+
background-color: white;
|
|
27
|
+
padding: 0.5rem;
|
|
28
|
+
box-shadow: -0.125rem 0.125rem 0.25rem lightgray;
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
export const AccountMenu = ({ api, initialState, topItems, bottomItems }) => {
|
|
32
|
+
const uiState = {
|
|
33
|
+
expanded: false
|
|
34
|
+
};
|
|
35
|
+
let lastKnownState = initialState;
|
|
36
|
+
const listenForClose = (event) => {
|
|
37
|
+
if ((event.type === 'click' && !self.data.menu.contains(event.target))
|
|
38
|
+
|| (event.type === 'keyup' && event.key === 'Escape')) {
|
|
39
|
+
close();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const close = () => {
|
|
43
|
+
uiState.expanded = false;
|
|
44
|
+
updateStyleToMatchState();
|
|
45
|
+
document.removeEventListener('click', listenForClose);
|
|
46
|
+
document.removeEventListener('keyup', listenForClose);
|
|
47
|
+
};
|
|
48
|
+
const removeListenForClose = () => {
|
|
49
|
+
document.removeEventListener('click', listenForClose);
|
|
50
|
+
document.removeEventListener('keyup', listenForClose);
|
|
51
|
+
};
|
|
52
|
+
const updateStyleToMatchState = () => {
|
|
53
|
+
self.data.menu.style.display = uiState.expanded ? 'block' : 'none';
|
|
54
|
+
const position = self.getBoundingClientRect();
|
|
55
|
+
self.data.menu.style.top = `${position.bottom + 1}px`;
|
|
56
|
+
self.data.menu.style.right = `${document.body.clientWidth - position.right + 16}px`;
|
|
57
|
+
};
|
|
58
|
+
const authenticator = Authenticator(api, initialState);
|
|
59
|
+
AuthMonitor.subscribe(state => {
|
|
60
|
+
const isSameState = lastKnownState && lastKnownState.state === state?.state;
|
|
61
|
+
const hasMessage = state?.message;
|
|
62
|
+
lastKnownState = state;
|
|
63
|
+
self.data.user = state?.user?.username || '';
|
|
64
|
+
if (!isSameState && !hasMessage)
|
|
65
|
+
close();
|
|
66
|
+
if (!isSameState) {
|
|
67
|
+
self.data.topSubmenu = getTopMenu();
|
|
68
|
+
self.data.bottomSubmenu = getBottomMenu();
|
|
69
|
+
AuthMonitor.signal(state);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
const handleMenuClick = () => {
|
|
73
|
+
uiState.expanded = !uiState.expanded;
|
|
74
|
+
updateStyleToMatchState();
|
|
75
|
+
if (uiState.expanded) {
|
|
76
|
+
authenticator.data.focus();
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
document.addEventListener('click', listenForClose);
|
|
79
|
+
document.addEventListener('keyup', listenForClose);
|
|
80
|
+
}, 1);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
removeListenForClose();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const getTopMenu = () => (topItems || []).map(item => typeof item === 'function' ? item(lastKnownState) : item);
|
|
87
|
+
const getBottomMenu = () => (bottomItems || []).map(item => typeof item === 'function' ? item(lastKnownState) : item);
|
|
88
|
+
const self = html `<accountmenu>
|
|
89
|
+
${style}
|
|
90
|
+
<div class='label'>${node('user', initialState?.user?.username || '', name => name ? html `<b>${name}</b>` : html `<i>Anonymous</i>`)}</div>
|
|
91
|
+
<div class='handle' onclick=${handleMenuClick}>☰</div>
|
|
92
|
+
<div class='menu' ${id('menu', HTMLDivElement)}>
|
|
93
|
+
${list('topSubmenu', getTopMenu())}
|
|
94
|
+
${node('authenticator', authenticator)}
|
|
95
|
+
${list('bottomSubmenu', getBottomMenu())}
|
|
96
|
+
</div>
|
|
97
|
+
</accountmenu>`.onadd(async (self) => {
|
|
98
|
+
if (!initialState) {
|
|
99
|
+
const state = await api.getState(null);
|
|
100
|
+
authenticator.data.setState(state);
|
|
101
|
+
self.data.user = state.user?.username || '';
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return self;
|
|
105
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { User } from "wirejs-resources";
|
|
2
|
+
export declare function AuthenticatedContent(options: {
|
|
3
|
+
authenticated: (user: User) => HTMLElement | Promise<HTMLElement>;
|
|
4
|
+
unauthenticated: () => HTMLElement | Promise<HTMLElement>;
|
|
5
|
+
containerElement?: string;
|
|
6
|
+
}): Promise<HTMLElement & {
|
|
7
|
+
data: {
|
|
8
|
+
content: HTMLElement;
|
|
9
|
+
};
|
|
10
|
+
} & import("wirejs-dom/v2").DomEvents<HTMLElement & {
|
|
11
|
+
data: {
|
|
12
|
+
content: HTMLElement;
|
|
13
|
+
};
|
|
14
|
+
}> & import("wirejs-dom/v2").Extend<HTMLElement & {
|
|
15
|
+
data: {
|
|
16
|
+
content: HTMLElement;
|
|
17
|
+
};
|
|
18
|
+
}>>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { html, node } from 'wirejs-dom/v2';
|
|
2
|
+
import { AuthMonitor } from "../utils/auth-monitor.js";
|
|
3
|
+
export async function AuthenticatedContent(options) {
|
|
4
|
+
const container = options.containerElement || 'authenticatedcontent';
|
|
5
|
+
const fieldSelector = (state) => {
|
|
6
|
+
return state.state;
|
|
7
|
+
};
|
|
8
|
+
const getContent = async (state) => {
|
|
9
|
+
return state?.state === 'authenticated'
|
|
10
|
+
? await options.authenticated(state.user)
|
|
11
|
+
: await options.unauthenticated();
|
|
12
|
+
};
|
|
13
|
+
const initialContent = await getContent(AuthMonitor.lastKnownState);
|
|
14
|
+
const render = async (state) => {
|
|
15
|
+
self.data.content = await getContent(state);
|
|
16
|
+
};
|
|
17
|
+
const self = html `<${container}>
|
|
18
|
+
${node('content', initialContent)}
|
|
19
|
+
</${container}>`.onadd(() => {
|
|
20
|
+
AuthMonitor.subscribe(render, fieldSelector);
|
|
21
|
+
}).onremove(() => {
|
|
22
|
+
AuthMonitor.unsubscribe(render, fieldSelector);
|
|
23
|
+
});
|
|
24
|
+
return self;
|
|
25
|
+
}
|
|
26
|
+
;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AuthenticationApi, AuthenticationMachineState, AuthenticationMachineAction, AuthenticationMachineInput } from 'wirejs-resources';
|
|
2
|
+
export declare const authenticatoraction: (action: AuthenticationMachineAction, act: (input: AuthenticationMachineInput) => void) => HTMLElement & {
|
|
3
|
+
data: unknown;
|
|
4
|
+
} & import("wirejs-dom/v2").DomEvents<HTMLElement & {
|
|
5
|
+
data: unknown;
|
|
6
|
+
}> & import("wirejs-dom/v2").Extend<HTMLElement & {
|
|
7
|
+
data: unknown;
|
|
8
|
+
}>;
|
|
9
|
+
export declare const Authenticator: (stateManager: AuthenticationApi, initialState?: AuthenticationMachineState) => HTMLElement & {
|
|
10
|
+
data: {
|
|
11
|
+
state: HTMLElement;
|
|
12
|
+
};
|
|
13
|
+
} & {
|
|
14
|
+
renderState(state: AuthenticationMachineState | {
|
|
15
|
+
errors: any[];
|
|
16
|
+
}): void;
|
|
17
|
+
} & {
|
|
18
|
+
data: {
|
|
19
|
+
setState: (state: AuthenticationMachineState) => void;
|
|
20
|
+
focus: () => void;
|
|
21
|
+
};
|
|
22
|
+
} & import("wirejs-dom/v2").DomEvents<HTMLElement & {
|
|
23
|
+
data: {
|
|
24
|
+
state: HTMLElement;
|
|
25
|
+
};
|
|
26
|
+
} & {
|
|
27
|
+
renderState(state: AuthenticationMachineState | {
|
|
28
|
+
errors: any[];
|
|
29
|
+
}): void;
|
|
30
|
+
} & {
|
|
31
|
+
data: {
|
|
32
|
+
setState: (state: AuthenticationMachineState) => void;
|
|
33
|
+
focus: () => void;
|
|
34
|
+
};
|
|
35
|
+
}> & import("wirejs-dom/v2").Extend<HTMLElement & {
|
|
36
|
+
data: {
|
|
37
|
+
state: HTMLElement;
|
|
38
|
+
};
|
|
39
|
+
} & {
|
|
40
|
+
renderState(state: AuthenticationMachineState | {
|
|
41
|
+
errors: any[];
|
|
42
|
+
}): void;
|
|
43
|
+
} & {
|
|
44
|
+
data: {
|
|
45
|
+
setState: (state: AuthenticationMachineState) => void;
|
|
46
|
+
focus: () => void;
|
|
47
|
+
};
|
|
48
|
+
}>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { attribute, html, node, css } from 'wirejs-dom/v2';
|
|
2
|
+
import { AuthMonitor } from '../utils/auth-monitor.js';
|
|
3
|
+
const style = css `
|
|
4
|
+
authenticator input {
|
|
5
|
+
width: calc(100% - 1rem);
|
|
6
|
+
margin-bottom: 0.5rem;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
authenticator .error {
|
|
10
|
+
color: darkred;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
authenticator {
|
|
14
|
+
display: block;
|
|
15
|
+
min-width: 15em;
|
|
16
|
+
color: var(--default-color, black);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
authenticatoraction > h4 {
|
|
20
|
+
margin-top: 1rem;
|
|
21
|
+
margin-bottom: 0.5rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
authenticatoraction > hr {
|
|
25
|
+
width: 33%;
|
|
26
|
+
height: 1px;
|
|
27
|
+
border: none;
|
|
28
|
+
background: var(--color-muted);
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
export const authenticatoraction = (action, act) => {
|
|
32
|
+
const inputs = Object.entries(action.fields || []).map(([name, { label, type }]) => {
|
|
33
|
+
const id = `input_${Math.floor(Math.random() * 1_000_000)}`;
|
|
34
|
+
const input = html `<div>
|
|
35
|
+
<label for=${id}>${label}</label>
|
|
36
|
+
<br />
|
|
37
|
+
<input
|
|
38
|
+
id=${id}
|
|
39
|
+
name=${name}
|
|
40
|
+
type=${type}
|
|
41
|
+
value=${attribute('value', '')}
|
|
42
|
+
/>
|
|
43
|
+
</div>`.extend(() => ({ data: { name } }));
|
|
44
|
+
return input;
|
|
45
|
+
});
|
|
46
|
+
const buttons = action.buttons?.map(b => html `<p>
|
|
47
|
+
<button type='submit' value='${b}'>${b}</button>
|
|
48
|
+
</p>`);
|
|
49
|
+
const link = buttons ? undefined : [
|
|
50
|
+
html `<menu><li><a
|
|
51
|
+
onclick=${() => act({ key: action.key })}
|
|
52
|
+
>${action.name}</a></li></menu>`
|
|
53
|
+
];
|
|
54
|
+
const actors = link ?? buttons;
|
|
55
|
+
if (action.fields && Object.keys(action.fields).length > 0) {
|
|
56
|
+
return html `<authenticatoraction>
|
|
57
|
+
<div>
|
|
58
|
+
<h4>${action.name}</h4>
|
|
59
|
+
<form
|
|
60
|
+
class='standard'
|
|
61
|
+
onsubmit=${(evt) => {
|
|
62
|
+
evt.preventDefault();
|
|
63
|
+
act({
|
|
64
|
+
key: action.key,
|
|
65
|
+
verb: evt.submitter?.value,
|
|
66
|
+
inputs: Object.fromEntries(inputs.map(input => ([
|
|
67
|
+
input.data.name,
|
|
68
|
+
input.data.value
|
|
69
|
+
])))
|
|
70
|
+
});
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
${inputs}
|
|
74
|
+
${actors}
|
|
75
|
+
</form>
|
|
76
|
+
<hr />
|
|
77
|
+
</div>
|
|
78
|
+
</authenticatoraction>`;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
return html `<authenticatoraction>
|
|
82
|
+
${actors}
|
|
83
|
+
</authenticatoraction>`;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
export const Authenticator = (stateManager, initialState) => {
|
|
87
|
+
let lastKnownState = undefined;
|
|
88
|
+
const self = html `<authenticator>
|
|
89
|
+
${style}
|
|
90
|
+
${node('state', html `<span>Loading ...</span>`)}
|
|
91
|
+
</authenticator>`.extend(() => ({
|
|
92
|
+
renderState(state) {
|
|
93
|
+
let message;
|
|
94
|
+
if ('errors' in state && state.errors) {
|
|
95
|
+
message = `<span class='error'>` + state.errors
|
|
96
|
+
.map((e) => e.message)
|
|
97
|
+
.join("\n\n") + `</span>`;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
lastKnownState = state;
|
|
101
|
+
message = lastKnownState.message;
|
|
102
|
+
}
|
|
103
|
+
self.data.state = html `<div>
|
|
104
|
+
<div>${message || ''}</div>
|
|
105
|
+
<div>${Object.entries(lastKnownState?.actions || []).map(([_key, action]) => {
|
|
106
|
+
return authenticatoraction({ ...action }, async (act) => {
|
|
107
|
+
self.renderState(await stateManager.setState(null, act));
|
|
108
|
+
});
|
|
109
|
+
})}</div>
|
|
110
|
+
</div>`;
|
|
111
|
+
AuthMonitor.signal(lastKnownState);
|
|
112
|
+
}
|
|
113
|
+
})).onadd(async (self) => {
|
|
114
|
+
if (initialState) {
|
|
115
|
+
console.log('authenticator render state');
|
|
116
|
+
self.renderState(initialState);
|
|
117
|
+
}
|
|
118
|
+
}).extend(self => ({
|
|
119
|
+
data: {
|
|
120
|
+
setState: (state) => self.renderState(state),
|
|
121
|
+
focus: () => {
|
|
122
|
+
[...self.getElementsByTagName('input')].shift()?.focus();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}));
|
|
126
|
+
return self;
|
|
127
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './components/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './components/index.js';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AuthenticationState } from 'wirejs-resources';
|
|
2
|
+
export declare function Main(slots: {
|
|
3
|
+
/**
|
|
4
|
+
* Replaces the default prefix in the final page title.
|
|
5
|
+
*/
|
|
6
|
+
siteTitle?: string | ((state: AuthenticationState) => string);
|
|
7
|
+
/**
|
|
8
|
+
* Appears on the page under the site title.
|
|
9
|
+
*
|
|
10
|
+
* Set to empty-string explicitly to omit the default.
|
|
11
|
+
*/
|
|
12
|
+
siteSubTitle?: string | ((state: AuthenticationState) => string);
|
|
13
|
+
/**
|
|
14
|
+
* The page title. Appears below the site title and subtitle when given.
|
|
15
|
+
*/
|
|
16
|
+
pageTitle?: string | ((state: AuthenticationState) => string);
|
|
17
|
+
/**
|
|
18
|
+
* Author for this page. Appears next to the title in lighter font and
|
|
19
|
+
* in the address bar.
|
|
20
|
+
*/
|
|
21
|
+
pageAuthor?: string | ((state: AuthenticationState) => string);
|
|
22
|
+
/**
|
|
23
|
+
* The main content for the page.
|
|
24
|
+
*/
|
|
25
|
+
content: HTMLElement | ((state: AuthenticationState) => HTMLElement);
|
|
26
|
+
/**
|
|
27
|
+
* Appears in the top of the footer.
|
|
28
|
+
*/
|
|
29
|
+
disclaimer?: string | HTMLElement | ((state: AuthenticationState) => string | HTMLElement);
|
|
30
|
+
}): Promise<HTMLElement & {
|
|
31
|
+
data: {
|
|
32
|
+
account: HTMLElement & {
|
|
33
|
+
data: unknown;
|
|
34
|
+
} & import("wirejs-dom/v2").DomEvents<HTMLElement & {
|
|
35
|
+
data: unknown;
|
|
36
|
+
}> & import("wirejs-dom/v2").Extend<HTMLElement & {
|
|
37
|
+
data: unknown;
|
|
38
|
+
}>;
|
|
39
|
+
};
|
|
40
|
+
} & import("wirejs-dom/v2").DomEvents<HTMLElement & {
|
|
41
|
+
data: {
|
|
42
|
+
account: HTMLElement & {
|
|
43
|
+
data: unknown;
|
|
44
|
+
} & import("wirejs-dom/v2").DomEvents<HTMLElement & {
|
|
45
|
+
data: unknown;
|
|
46
|
+
}> & import("wirejs-dom/v2").Extend<HTMLElement & {
|
|
47
|
+
data: unknown;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
}> & import("wirejs-dom/v2").Extend<HTMLElement & {
|
|
51
|
+
data: {
|
|
52
|
+
account: HTMLElement & {
|
|
53
|
+
data: unknown;
|
|
54
|
+
} & import("wirejs-dom/v2").DomEvents<HTMLElement & {
|
|
55
|
+
data: unknown;
|
|
56
|
+
}> & import("wirejs-dom/v2").Extend<HTMLElement & {
|
|
57
|
+
data: unknown;
|
|
58
|
+
}>;
|
|
59
|
+
};
|
|
60
|
+
}>>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { html, node, hydrate } from 'wirejs-dom/v2';
|
|
2
|
+
import { auth } from 'my-api';
|
|
3
|
+
import { AccountMenu } from '../components/index.js';
|
|
4
|
+
const TITLE = 'My New Site';
|
|
5
|
+
const SUBTITLE = 'Made with wirejs';
|
|
6
|
+
const MENU_ID = 'account-menu';
|
|
7
|
+
const DISCLAIMER = html `<div>
|
|
8
|
+
<p>Everything here is awesome.</p>
|
|
9
|
+
</div>`;
|
|
10
|
+
async function Account() {
|
|
11
|
+
return html `<div id='${MENU_ID}'>
|
|
12
|
+
${AccountMenu({ api: auth })}
|
|
13
|
+
</div>`;
|
|
14
|
+
}
|
|
15
|
+
export async function Main(slots) {
|
|
16
|
+
const pageAuthorElement = slots.pageAuthor ? html `
|
|
17
|
+
<span style='color: var(--color-muted);'>
|
|
18
|
+
: according to <a href='/wiki/view/~${slots.pageAuthor}'>${slots.pageAuthor}</a>
|
|
19
|
+
</span>
|
|
20
|
+
` : '';
|
|
21
|
+
const pageTitle = slots.pageTitle ? html `
|
|
22
|
+
<h2 style='font-variant: small-caps;'>
|
|
23
|
+
${slots.pageTitle} ${pageAuthorElement}
|
|
24
|
+
</h2>
|
|
25
|
+
` : '';
|
|
26
|
+
const browserBarTitle = [
|
|
27
|
+
slots.pageTitle,
|
|
28
|
+
slots.pageAuthor,
|
|
29
|
+
slots.siteTitle || TITLE,
|
|
30
|
+
slots.siteSubTitle || SUBTITLE,
|
|
31
|
+
].filter(Boolean).slice(0, 3).join(' - ');
|
|
32
|
+
const page = html `
|
|
33
|
+
<!doctype html>
|
|
34
|
+
<html id='root'>
|
|
35
|
+
<head>
|
|
36
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
37
|
+
<title>${browserBarTitle}</title>
|
|
38
|
+
<link rel='icon' type='image/svg+xml' href='/images/logo.svg' />
|
|
39
|
+
<link rel='stylesheet' type='text/css' href='/default.css' />
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<div style='
|
|
43
|
+
border-width: 0 1px;
|
|
44
|
+
border-color: silver;
|
|
45
|
+
border-style: solid;
|
|
46
|
+
max-width: 1200px;
|
|
47
|
+
min-height: 100vh;
|
|
48
|
+
padding: 0 1rem;
|
|
49
|
+
margin: 0 auto;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
'>
|
|
52
|
+
<div style='
|
|
53
|
+
display: flex;
|
|
54
|
+
width: 100%;
|
|
55
|
+
padding-bottom: 0.5rem;
|
|
56
|
+
border-bottom: 1px solid var(--border-color-muted, #777);
|
|
57
|
+
margin-bottom: 1rem;
|
|
58
|
+
'>
|
|
59
|
+
<!-- source: https://www.svgrepo.com/svg/322460/greek-temple -->
|
|
60
|
+
<svg
|
|
61
|
+
style='
|
|
62
|
+
margin-top: 1.4rem;
|
|
63
|
+
margin-right: 0.5rem;
|
|
64
|
+
'
|
|
65
|
+
height='4rem'
|
|
66
|
+
viewBox="0 0 512 512"
|
|
67
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
68
|
+
><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>
|
|
69
|
+
|
|
70
|
+
<div style='flex-basis: 0; flex-grow: 5;'>
|
|
71
|
+
<h1 style='font-variant: small-caps; text-shadow: silver 2px 2px 2px;'>
|
|
72
|
+
<a href='/' style='color: var(--color-strong, #000);'>${slots.siteTitle || TITLE}</a>
|
|
73
|
+
</h1>
|
|
74
|
+
|
|
75
|
+
<div style='
|
|
76
|
+
margin-top: -1rem;
|
|
77
|
+
color: var(--color-muted, #888);
|
|
78
|
+
'>${slots.siteSubTitle ?? SUBTITLE}</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div style='margin-top: 3.5rem; flex-grow: 0;'>
|
|
82
|
+
${node('account', await Account())}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
${pageTitle}
|
|
87
|
+
|
|
88
|
+
<div id='content'>${slots.content}</div>
|
|
89
|
+
|
|
90
|
+
<footer>
|
|
91
|
+
${slots.disclaimer ?? DISCLAIMER}
|
|
92
|
+
</footer>
|
|
93
|
+
</div>
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
96
|
+
`;
|
|
97
|
+
return page;
|
|
98
|
+
}
|
|
99
|
+
// TODO: fix wirejs requiring ... whatever it is here that it requires that breaks the type!
|
|
100
|
+
hydrate(MENU_ID, Account);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as defaultLayout } from './default.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as defaultLayout } from './default.js';
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { css } from 'wirejs-dom/v2';
|
|
2
|
+
export default css `
|
|
3
|
+
:root {
|
|
4
|
+
--color-muted: #888;
|
|
5
|
+
--color-error: #c33;
|
|
6
|
+
--color-action: #533;
|
|
7
|
+
--border-color-muted: #777;
|
|
8
|
+
--border-color-subtle: #aaa;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
html {
|
|
12
|
+
padding: 0;
|
|
13
|
+
margin: 0
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
font-family: math;
|
|
18
|
+
padding: 0;
|
|
19
|
+
margin: 0;
|
|
20
|
+
color: #333;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
a {
|
|
24
|
+
font-weight: bold;
|
|
25
|
+
color: var(--color-action);
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
menu {
|
|
30
|
+
padding-inline-start: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
li {
|
|
34
|
+
margin: 1rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
menu li {
|
|
38
|
+
list-style: none;
|
|
39
|
+
margin: 1rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
menu li a {
|
|
43
|
+
text-decoration: underline;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
form.standard {
|
|
47
|
+
margin: 1rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
form.standard div {
|
|
51
|
+
margin-bottom: 1rem;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
form.standard label, form.standard input, form.standard button {
|
|
55
|
+
display: inline-block;
|
|
56
|
+
width: 9rem;
|
|
57
|
+
margin: 0.25rem;
|
|
58
|
+
border-radius: 0.125rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
form.standard label {
|
|
62
|
+
font-weight: bold;
|
|
63
|
+
color: var(--color-muted);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
form.standard input {
|
|
67
|
+
width: 20rem;
|
|
68
|
+
border: 1px solid var(--border-color-muted);
|
|
69
|
+
margin-bottom: 0.5rem;
|
|
70
|
+
padding: 0.5rem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
form.standard textarea {
|
|
74
|
+
width: calc(100% - 2rem);
|
|
75
|
+
height: 15rem;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
form.standard button {
|
|
79
|
+
font-family: math;
|
|
80
|
+
font-size: medium;
|
|
81
|
+
border: 1px solid var(--border-color-muted);
|
|
82
|
+
padding: 0.5rem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
form.standard span.status {
|
|
86
|
+
display: inline-block;
|
|
87
|
+
color: var(--color-muted);
|
|
88
|
+
margin-left: 0.5rem;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.error form.standard span.status {
|
|
92
|
+
color: var(--color-error);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.section {
|
|
96
|
+
border-bottom: 1px dashed var(--border-color-subtle);
|
|
97
|
+
margin-bottom: 1.5rem;
|
|
98
|
+
padding-bottom: 1.5rem;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
footer {
|
|
102
|
+
margin-top: 4rem;
|
|
103
|
+
border-top: 1px solid var(--border-color-muted);
|
|
104
|
+
color: var(--color-muted);
|
|
105
|
+
}
|
|
106
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as defaultTheme } from './default.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as defaultTheme } from './default.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type Callback<T> = (state: T | undefined) => void;
|
|
2
|
+
type FieldExtractor<T> = (state: T) => string;
|
|
3
|
+
type MonitorCallback<T> = {
|
|
4
|
+
field: FieldExtractor<T> | undefined;
|
|
5
|
+
callback: Callback<T>;
|
|
6
|
+
};
|
|
7
|
+
export declare class Monitor<State> {
|
|
8
|
+
subscribers: MonitorCallback<State>[];
|
|
9
|
+
lastKnownState: State | undefined;
|
|
10
|
+
constructor(initialState?: State);
|
|
11
|
+
signal(state: State | undefined): void;
|
|
12
|
+
subscribe(callback: Callback<State>, field?: FieldExtractor<State>): void;
|
|
13
|
+
/**
|
|
14
|
+
* Unsubscribes a specific listener from state changes if and only if
|
|
15
|
+
* the `field` matches the original `subscribe()` field or lack thereof.
|
|
16
|
+
*
|
|
17
|
+
* @param callback
|
|
18
|
+
* @param field
|
|
19
|
+
*/
|
|
20
|
+
unsubscribe(callback: Callback<State>, field?: FieldExtractor<State>): void;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export class Monitor {
|
|
2
|
+
subscribers = [];
|
|
3
|
+
lastKnownState;
|
|
4
|
+
constructor(initialState) {
|
|
5
|
+
this.lastKnownState = initialState;
|
|
6
|
+
}
|
|
7
|
+
signal(state) {
|
|
8
|
+
if (typeof state === 'undefined'
|
|
9
|
+
&& typeof this.lastKnownState === 'undefined')
|
|
10
|
+
return;
|
|
11
|
+
for (const sub of this.subscribers) {
|
|
12
|
+
try {
|
|
13
|
+
if (typeof sub.field !== 'function'
|
|
14
|
+
|| (typeof state === 'undefined'
|
|
15
|
+
&& typeof this.lastKnownState !== 'undefined')
|
|
16
|
+
|| (typeof state !== 'undefined'
|
|
17
|
+
&& typeof this.lastKnownState === 'undefined')
|
|
18
|
+
|| sub.field(state) !== sub.field(this.lastKnownState)) {
|
|
19
|
+
sub.callback(state);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
console.error('Error calling Monitor watcher.', sub, err);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
this.lastKnownState = state;
|
|
27
|
+
}
|
|
28
|
+
subscribe(callback, field) {
|
|
29
|
+
this.subscribers.push({ field, callback });
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Unsubscribes a specific listener from state changes if and only if
|
|
33
|
+
* the `field` matches the original `subscribe()` field or lack thereof.
|
|
34
|
+
*
|
|
35
|
+
* @param callback
|
|
36
|
+
* @param field
|
|
37
|
+
*/
|
|
38
|
+
unsubscribe(callback, field) {
|
|
39
|
+
this.subscribers = this.subscribers.filter(sub => {
|
|
40
|
+
return !(sub.field === field && sub.callback === callback);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type Callback<T> = (state: T | undefined) => void;
|
|
2
|
+
type FieldExtractor<T> = (state: T) => string;
|
|
3
|
+
type MonitorCallback<T> = {
|
|
4
|
+
field: FieldExtractor<T> | undefined;
|
|
5
|
+
callback: Callback<T>;
|
|
6
|
+
};
|
|
7
|
+
export declare class Monitor<State> {
|
|
8
|
+
subscribers: MonitorCallback<State>[];
|
|
9
|
+
lastKnownState: State | undefined;
|
|
10
|
+
constructor(initialState?: State);
|
|
11
|
+
signal(state: State | undefined): void;
|
|
12
|
+
subscribe(callback: Callback<State>, field?: FieldExtractor<State>): void;
|
|
13
|
+
/**
|
|
14
|
+
* Unsubscribes a specific listener from state changes if and only if
|
|
15
|
+
* the `field` matches the original `subscribe()` field or lack thereof.
|
|
16
|
+
*
|
|
17
|
+
* @param callback
|
|
18
|
+
* @param field
|
|
19
|
+
*/
|
|
20
|
+
unsubscribe(callback: Callback<State>, field?: FieldExtractor<State>): void;
|
|
21
|
+
}
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export class Monitor {
|
|
2
|
+
subscribers = [];
|
|
3
|
+
lastKnownState;
|
|
4
|
+
constructor(initialState) {
|
|
5
|
+
this.lastKnownState = initialState;
|
|
6
|
+
}
|
|
7
|
+
signal(state) {
|
|
8
|
+
if (typeof state === 'undefined'
|
|
9
|
+
&& typeof this.lastKnownState === 'undefined')
|
|
10
|
+
return;
|
|
11
|
+
for (const sub of this.subscribers) {
|
|
12
|
+
try {
|
|
13
|
+
if (typeof sub.field !== 'function'
|
|
14
|
+
|| (typeof state === 'undefined'
|
|
15
|
+
&& typeof this.lastKnownState !== 'undefined')
|
|
16
|
+
|| (typeof state !== 'undefined'
|
|
17
|
+
&& typeof this.lastKnownState === 'undefined')
|
|
18
|
+
|| sub.field(state) !== sub.field(this.lastKnownState)) {
|
|
19
|
+
sub.callback(state);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
console.error('Error calling Monitor watcher.', sub, err);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
this.lastKnownState = state;
|
|
27
|
+
}
|
|
28
|
+
subscribe(callback, field) {
|
|
29
|
+
this.subscribers.push({ field, callback });
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Unsubscribes a specific listener from state changes if and only if
|
|
33
|
+
* the `field` matches the original `subscribe()` field or lack thereof.
|
|
34
|
+
*
|
|
35
|
+
* @param callback
|
|
36
|
+
* @param field
|
|
37
|
+
*/
|
|
38
|
+
unsubscribe(callback, field) {
|
|
39
|
+
this.subscribers = this.subscribers.filter(sub => {
|
|
40
|
+
return !(sub.field === field && sub.callback === callback);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wirejs-components",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Client-side components for wirejs apps, including basic UI and resource-aware components.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./utils": {
|
|
14
|
+
"types": "./dist/utils/index.d.ts",
|
|
15
|
+
"default": "./dist/utils/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/svidgen/create-wirejs-app.git",
|
|
24
|
+
"directory": "packages/wirejs-ui"
|
|
25
|
+
},
|
|
26
|
+
"author": "Jon Wire",
|
|
27
|
+
"license": "AGPL-3.0-only",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/svidgen/create-wirejs-app/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/svidgen/create-wirejs-app#readme",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"wirejs-dom": "^1.0.41",
|
|
34
|
+
"wirejs-resources": "^0.1.57"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"typescript": "^5.7.3"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"package.json",
|
|
41
|
+
"README.md",
|
|
42
|
+
"dist/*"
|
|
43
|
+
]
|
|
44
|
+
}
|