@namostack/cli 0.0.1 → 0.1.0-rc.2
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/LICENSE +21 -0
- package/README.md +29 -1
- package/dist/index.mjs +5355 -0
- package/package.json +50 -4
- package/templates/react/_gitignore +2 -0
- package/templates/react/index.html +12 -0
- package/templates/react/package.json +25 -0
- package/templates/react/public/icon.svg +4 -0
- package/templates/react/src/App.tsx +84 -0
- package/templates/react/src/NamostackProvider.tsx +41 -0
- package/templates/react/src/components/Counter.tsx +101 -0
- package/templates/react/src/components/ExampleCard.tsx +55 -0
- package/templates/react/src/components/Notify.tsx +41 -0
- package/templates/react/src/components/styles.ts +24 -0
- package/templates/react/src/errors.ts +10 -0
- package/templates/react/src/main.tsx +20 -0
- package/templates/react/src/useNamostack.ts +16 -0
- package/templates/react/tsconfig.json +15 -0
- package/templates/react/vite.config.ts +7 -0
- package/templates/vanilla/_gitignore +2 -0
- package/templates/vanilla/index.html +61 -0
- package/templates/vanilla/package.json +18 -0
- package/templates/vanilla/public/icon.svg +4 -0
- package/templates/vanilla/src/main.ts +158 -0
- package/templates/vanilla/src/style.css +330 -0
- package/templates/vanilla/tsconfig.json +14 -0
- package/templates/vanilla/vite.config.ts +6 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BridgeError,
|
|
3
|
+
connect,
|
|
4
|
+
type PluginTheme,
|
|
5
|
+
type StoredDocument,
|
|
6
|
+
} from '@namostack/plugin-sdk';
|
|
7
|
+
import './style.css';
|
|
8
|
+
|
|
9
|
+
// seedui's fonts, as in the namostack app.
|
|
10
|
+
import '@fontsource-variable/inter';
|
|
11
|
+
import '@fontsource-variable/source-serif-4';
|
|
12
|
+
|
|
13
|
+
interface Clicks {
|
|
14
|
+
count: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Plugins only run inside the namostack app: anywhere else, this throws.
|
|
18
|
+
const namostack = await connect();
|
|
19
|
+
const { context, data, ui, user } = namostack;
|
|
20
|
+
|
|
21
|
+
function element<T extends HTMLElement = HTMLElement>(id: string): T {
|
|
22
|
+
return document.getElementById(id) as T;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Shows a failed SDK call; a missing permission says how to add it. */
|
|
26
|
+
function showError(target: HTMLElement, error: unknown, permission: string) {
|
|
27
|
+
target.hidden = false;
|
|
28
|
+
target.textContent =
|
|
29
|
+
error instanceof BridgeError && error.code === 'permission_denied'
|
|
30
|
+
? `Add "${permission}" to the permissions in namostack.plugin.json.`
|
|
31
|
+
: error instanceof BridgeError
|
|
32
|
+
? error.message
|
|
33
|
+
: String(error);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Follows the app's light or dark mode; style against `[data-mode="dark"]`. */
|
|
37
|
+
function applyTheme({ mode }: PluginTheme) {
|
|
38
|
+
document.documentElement.dataset.mode = mode;
|
|
39
|
+
document.documentElement.style.colorScheme = mode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
applyTheme(context.theme);
|
|
43
|
+
namostack.onThemeChange(applyTheme);
|
|
44
|
+
|
|
45
|
+
// textContent, not innerHTML: these are the developer's and user's own text.
|
|
46
|
+
element('name').textContent = context.name;
|
|
47
|
+
document.querySelector('main')!.hidden = false;
|
|
48
|
+
|
|
49
|
+
user
|
|
50
|
+
.getProfile()
|
|
51
|
+
.then(({ displayName }) => {
|
|
52
|
+
element('greeting').textContent =
|
|
53
|
+
`Hi ${displayName}, this is your new plugin.`;
|
|
54
|
+
})
|
|
55
|
+
.catch((error: unknown) =>
|
|
56
|
+
showError(element('greeting-error'), error, 'user:profile:read'),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// A counter saved for the current user: it survives reloads and devices.
|
|
60
|
+
const count = element('count');
|
|
61
|
+
const increment = element<HTMLButtonElement>('increment');
|
|
62
|
+
const reset = element<HTMLButtonElement>('reset');
|
|
63
|
+
const countError = element('count-error');
|
|
64
|
+
let clicks: StoredDocument<Clicks> | null = null;
|
|
65
|
+
|
|
66
|
+
function render() {
|
|
67
|
+
count.textContent = String(clicks?.count ?? 0);
|
|
68
|
+
reset.disabled = !clicks;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Like seedui's `isLoading`: keeps the button's size and swaps its label for a spinner. */
|
|
72
|
+
function setLoading(button: HTMLButtonElement, loading: boolean) {
|
|
73
|
+
if (loading) {
|
|
74
|
+
const { width, height } = button.getBoundingClientRect();
|
|
75
|
+
|
|
76
|
+
button.dataset.label = button.textContent ?? '';
|
|
77
|
+
button.style.width = `${width}px`;
|
|
78
|
+
button.style.height = `${height}px`;
|
|
79
|
+
button.replaceChildren(
|
|
80
|
+
Object.assign(document.createElement('div'), { className: 'spinner' }),
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
button.textContent = button.dataset.label ?? '';
|
|
84
|
+
button.style.width = '';
|
|
85
|
+
button.style.height = '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
button.ariaBusy = String(loading);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function run(
|
|
92
|
+
button: HTMLButtonElement,
|
|
93
|
+
change: () => Promise<StoredDocument<Clicks> | null>,
|
|
94
|
+
) {
|
|
95
|
+
const other = button === increment ? reset : increment;
|
|
96
|
+
|
|
97
|
+
setLoading(button, true);
|
|
98
|
+
other.disabled = true;
|
|
99
|
+
countError.hidden = true;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
clicks = await change();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
showError(countError, error, 'storage');
|
|
105
|
+
} finally {
|
|
106
|
+
setLoading(button, false);
|
|
107
|
+
increment.disabled = false;
|
|
108
|
+
render();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
increment.addEventListener('click', () => {
|
|
113
|
+
if (increment.ariaBusy === 'true') {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
void run(increment, () =>
|
|
118
|
+
clicks
|
|
119
|
+
? data.update<Clicks>('clicks', clicks.id, { count: clicks.count + 1 })
|
|
120
|
+
: data.insert<Clicks>('clicks', { count: 1 }),
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
reset.addEventListener('click', () => {
|
|
125
|
+
if (reset.ariaBusy === 'true') {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
void run(reset, async () => {
|
|
130
|
+
if (clicks) {
|
|
131
|
+
await data.delete('clicks', clicks.id);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
data
|
|
139
|
+
.find<Clicks>('clicks', { limit: 1 })
|
|
140
|
+
.then(({ items }) => {
|
|
141
|
+
clicks = items[0] ?? null;
|
|
142
|
+
})
|
|
143
|
+
.catch((error: unknown) => showError(countError, error, 'storage'))
|
|
144
|
+
.finally(() => {
|
|
145
|
+
increment.disabled = false;
|
|
146
|
+
render();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Shows a toast in the app, outside the plugin's frame.
|
|
150
|
+
element('notify').addEventListener('click', async () => {
|
|
151
|
+
element('notify-error').hidden = true;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
await ui.notify({ message: 'Hello from your plugin!', tone: 'success' });
|
|
155
|
+
} catch (error) {
|
|
156
|
+
showError(element('notify-error'), error, 'ui:notify');
|
|
157
|
+
}
|
|
158
|
+
});
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/* seedui's tokens (seedui.dev), so the plugin looks like the app and the react template. */
|
|
2
|
+
:root {
|
|
3
|
+
--text: #000;
|
|
4
|
+
--text-muted: #5a6480;
|
|
5
|
+
--surface: #fff;
|
|
6
|
+
--border: #eceef3;
|
|
7
|
+
--code: #eceef3;
|
|
8
|
+
--primary: #6344e5;
|
|
9
|
+
--primary-hover: #7b61ff;
|
|
10
|
+
--primary-active: #3f2c99;
|
|
11
|
+
--neutral: #d8dce5;
|
|
12
|
+
--neutral-hover: #eceef3;
|
|
13
|
+
--neutral-active: #8b93a5;
|
|
14
|
+
--transparent-hover: #eceef3;
|
|
15
|
+
--transparent-active: #b5bbc9;
|
|
16
|
+
--disabled: #eceef3;
|
|
17
|
+
--disabled-text: #d8dce5;
|
|
18
|
+
--disabled-transparent-text: #d8dce5;
|
|
19
|
+
--tag: #eceef3;
|
|
20
|
+
--tag-text: #5a6480;
|
|
21
|
+
--error: #d42b2b;
|
|
22
|
+
--focus-gap: #fff;
|
|
23
|
+
--shadow: 0 2px 8px rgb(0 0 0 / 10%), 0 1px 3px rgb(0 0 0 / 6%);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
:root[data-mode='dark'] {
|
|
27
|
+
--text: #fff;
|
|
28
|
+
--text-muted: #959fbc;
|
|
29
|
+
--surface: #1b1f25;
|
|
30
|
+
--border: #2e333c;
|
|
31
|
+
--code: #2e333c;
|
|
32
|
+
--primary: #7b61ff;
|
|
33
|
+
--primary-hover: #42357a;
|
|
34
|
+
--primary-active: #332b60;
|
|
35
|
+
--neutral: #3c414d;
|
|
36
|
+
--neutral-hover: #4f5565;
|
|
37
|
+
--neutral-active: #252930;
|
|
38
|
+
--transparent-hover: #252930;
|
|
39
|
+
--transparent-active: #252930;
|
|
40
|
+
--disabled: #2e333c;
|
|
41
|
+
--disabled-text: #3c414d;
|
|
42
|
+
--disabled-transparent-text: #2e333c;
|
|
43
|
+
--tag: #2e333c;
|
|
44
|
+
--tag-text: #798199;
|
|
45
|
+
--error: #e86060;
|
|
46
|
+
--focus-gap: #1b1f25;
|
|
47
|
+
--shadow: 0 2px 8px rgb(0 0 0 / 30%), 0 1px 3px rgb(0 0 0 / 25%);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
body {
|
|
51
|
+
margin: 0;
|
|
52
|
+
color: var(--text);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
[hidden] {
|
|
56
|
+
display: none !important;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Text: seedui's h3, h5, h2, p and caption. */
|
|
60
|
+
h1,
|
|
61
|
+
h2,
|
|
62
|
+
p {
|
|
63
|
+
margin: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.h3,
|
|
67
|
+
.h2 {
|
|
68
|
+
font-family: 'Source Serif 4 Variable', serif;
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.h3 {
|
|
73
|
+
font-size: 36px;
|
|
74
|
+
line-height: 44px;
|
|
75
|
+
letter-spacing: -0.01em;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.h2 {
|
|
79
|
+
font-size: 44px;
|
|
80
|
+
line-height: 54px;
|
|
81
|
+
letter-spacing: -0.02em;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.h5,
|
|
85
|
+
.p,
|
|
86
|
+
.caption,
|
|
87
|
+
.button {
|
|
88
|
+
font-family: 'Inter Variable';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.h5 {
|
|
92
|
+
font-size: 18px;
|
|
93
|
+
line-height: 24px;
|
|
94
|
+
font-weight: 600;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.p {
|
|
98
|
+
font-size: 14px;
|
|
99
|
+
line-height: 23px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.caption {
|
|
103
|
+
font-size: 12px;
|
|
104
|
+
line-height: 18px;
|
|
105
|
+
letter-spacing: 0.01em;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.muted {
|
|
109
|
+
color: var(--text-muted);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.error {
|
|
113
|
+
color: var(--error);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
code {
|
|
117
|
+
padding: 1px 6px;
|
|
118
|
+
border-radius: 2px;
|
|
119
|
+
background: var(--code);
|
|
120
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
121
|
+
font-size: 0.875em;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
a {
|
|
125
|
+
color: var(--primary);
|
|
126
|
+
text-decoration: none;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
a:hover {
|
|
130
|
+
text-decoration: underline;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Layout */
|
|
134
|
+
.page {
|
|
135
|
+
max-width: 560px;
|
|
136
|
+
margin: 0 auto;
|
|
137
|
+
padding: 40px 24px;
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
gap: 24px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.header {
|
|
144
|
+
display: flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
gap: 16px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.icon {
|
|
150
|
+
width: 56px;
|
|
151
|
+
height: 56px;
|
|
152
|
+
flex-shrink: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* seedui's outlined Card, padding 20. */
|
|
156
|
+
.card {
|
|
157
|
+
padding: 20px;
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
gap: 16px;
|
|
161
|
+
border: 1px solid var(--border);
|
|
162
|
+
border-radius: 6px;
|
|
163
|
+
background: var(--surface);
|
|
164
|
+
box-shadow: var(--shadow);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.card-heading {
|
|
168
|
+
display: flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
justify-content: space-between;
|
|
171
|
+
gap: 8px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.actions {
|
|
175
|
+
display: flex;
|
|
176
|
+
flex-wrap: wrap;
|
|
177
|
+
gap: 8px;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* seedui's small neutral Tag. */
|
|
181
|
+
.tag {
|
|
182
|
+
display: flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
padding: 7px 8px;
|
|
185
|
+
border-radius: 9999px;
|
|
186
|
+
background: var(--tag);
|
|
187
|
+
color: var(--tag-text);
|
|
188
|
+
font-family: 'Inter Variable';
|
|
189
|
+
font-size: 12px;
|
|
190
|
+
line-height: 12px;
|
|
191
|
+
letter-spacing: 0.01em;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* seedui's md Button: filled primary, filled neutral, transparent neutral. */
|
|
195
|
+
.button {
|
|
196
|
+
position: relative;
|
|
197
|
+
display: inline-flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
justify-content: center;
|
|
200
|
+
padding: 6px 10px;
|
|
201
|
+
border: none;
|
|
202
|
+
border-radius: 8px;
|
|
203
|
+
background: var(--primary);
|
|
204
|
+
color: #fff;
|
|
205
|
+
font-size: 14px;
|
|
206
|
+
line-height: 23px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.button:hover {
|
|
210
|
+
background: var(--primary-hover);
|
|
211
|
+
cursor: pointer;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.button:active {
|
|
215
|
+
background: var(--primary-active);
|
|
216
|
+
transform: scale(0.95);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.button:focus {
|
|
220
|
+
outline: none;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.button:focus-visible {
|
|
224
|
+
box-shadow:
|
|
225
|
+
0 0 0 1px var(--focus-gap),
|
|
226
|
+
0 0 0 3px var(--primary);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.button-neutral {
|
|
230
|
+
background: var(--neutral);
|
|
231
|
+
color: var(--text);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.button-neutral:hover {
|
|
235
|
+
background: var(--neutral-hover);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.button-neutral:active {
|
|
239
|
+
background: var(--neutral-active);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.button-transparent {
|
|
243
|
+
background: transparent;
|
|
244
|
+
color: var(--text);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.button-transparent:hover {
|
|
248
|
+
background: var(--transparent-hover);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.button-transparent:active {
|
|
252
|
+
background: var(--transparent-active);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.button:disabled {
|
|
256
|
+
background: var(--disabled);
|
|
257
|
+
color: var(--disabled-text);
|
|
258
|
+
cursor: default;
|
|
259
|
+
transform: none;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.button-transparent:disabled {
|
|
263
|
+
background: transparent;
|
|
264
|
+
color: var(--disabled-transparent-text);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* While loading, a button keeps its size and shows seedui's spinner. */
|
|
268
|
+
.button[aria-busy='true'] {
|
|
269
|
+
cursor: default;
|
|
270
|
+
transform: none;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.spinner {
|
|
274
|
+
width: 16px;
|
|
275
|
+
padding: 3px;
|
|
276
|
+
aspect-ratio: 1;
|
|
277
|
+
border-radius: 50%;
|
|
278
|
+
background: var(--primary);
|
|
279
|
+
--mask:
|
|
280
|
+
conic-gradient(#0000 10%, #000), linear-gradient(#000 0 0) content-box;
|
|
281
|
+
mask: var(--mask);
|
|
282
|
+
mask-composite: subtract;
|
|
283
|
+
animation: spin 1s infinite linear;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.button:not(.button-neutral, .button-transparent) .spinner {
|
|
287
|
+
background: #fff;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@keyframes spin {
|
|
291
|
+
to {
|
|
292
|
+
transform: rotate(1turn);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* seedui's breakpoints: tablet below 960px, mobile below 600px. */
|
|
297
|
+
@media only screen and (max-width: 960px) {
|
|
298
|
+
.h3 {
|
|
299
|
+
font-size: 30px;
|
|
300
|
+
line-height: 38px;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.h2 {
|
|
304
|
+
font-size: 36px;
|
|
305
|
+
line-height: 44px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.h5 {
|
|
309
|
+
font-size: 16px;
|
|
310
|
+
line-height: 22px;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
@media only screen and (max-width: 600px) {
|
|
315
|
+
.h3 {
|
|
316
|
+
font-size: 24px;
|
|
317
|
+
line-height: 32px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.h2 {
|
|
321
|
+
font-size: 26px;
|
|
322
|
+
line-height: 34px;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@media (max-width: 599.98px) {
|
|
327
|
+
.page {
|
|
328
|
+
padding: 24px 16px;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2023",
|
|
4
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"strict": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"noEmit": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src", "vite.config.ts"]
|
|
14
|
+
}
|