@startup-api/cloudflare 0.3.0 → 0.3.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/README.md +42 -38
- package/package.json +1 -1
- package/public/users/accounts.html +4 -7
- package/public/users/admin/index.html +73 -19
- package/public/users/power-strip.js +279 -42
- package/public/users/profile.html +5 -8
- package/public/users/style.css +149 -59
- package/src/PowerStrip.ts +1 -1
package/README.md
CHANGED
|
@@ -12,36 +12,35 @@ This application uses the Cloudflare Developer Platform, including Workers and D
|
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Start a new project with the **`npm create startup-api`** scaffolder. It generates a tiny Cloudflare Worker that pulls this framework in as the [`@startup-api/cloudflare`](https://www.npmjs.com/package/@startup-api/cloudflare) npm dependency — so you stay up to date with `npm update` instead of maintaining a fork of this repository.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
```bash
|
|
18
|
+
npm create startup-api my-app -- --origin https://your-app-origin.com
|
|
19
|
+
cd my-app
|
|
20
|
+
npm run dev # local dev at http://localhost:8787
|
|
21
|
+
npm run deploy # deploy to Cloudflare
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Run `npm create startup-api` with no arguments to be prompted for the project name and origin URL interactively. Useful flags: `--no-install` (skip `npm install`) and `--yes` / `-y` (non-interactive — requires a `name` and `--origin`).
|
|
25
|
+
|
|
26
|
+
What you get:
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
4. Select your forked `startup-api-cloudflare` repository
|
|
23
|
-
5. Pick the name for your site's worker (e.g. you might have multiple)
|
|
24
|
-
6. Deploy the Worker
|
|
25
|
-
7. In the **Settings** tab of your Worker, go to **Variables** and add the required `ORIGIN_URL` (see [Configuration](#configuration-details) below)
|
|
28
|
+
- A minimal `src/index.ts` that re-exports the worker plus a `wrangler.jsonc` you control. The framework ships as the `@startup-api/cloudflare` dependency, so your project stays small.
|
|
29
|
+
- A `.dev.vars` file with a random `SESSION_SECRET` for local development. For production, set your own with `npx wrangler secret put SESSION_SECRET`.
|
|
30
|
+
- Framework updates are just `npm update @startup-api/cloudflare` — no fork to rebase.
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
Then set the required `ORIGIN_URL` and any OAuth credentials (see [Configuration](#configuration-details) below) and run `npm run deploy`. See [create-startup-api](https://github.com/StartupAPI/create-startup-api) for full details.
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
### Automated deployments
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
git clone https://github.com/StartupAPI/startup-api-cloudflare.git
|
|
34
|
-
cd startup-api-cloudflare
|
|
35
|
-
npm install
|
|
36
|
-
```
|
|
37
|
-
2. **Configure Environment Variables**
|
|
36
|
+
`npm run deploy` deploys from your machine. To deploy automatically instead, push your scaffolded project to a GitHub repository and use either:
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
- **Cloudflare Workers GitHub app** — connect the repo to Cloudflare's [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/) Git integration and Cloudflare builds and deploys on every push, no CI config to maintain.
|
|
39
|
+
- **A GitHub Actions workflow** — run [`cloudflare/wrangler-action`](https://github.com/cloudflare/wrangler-action) on push to deploy with Wrangler. Add a `CLOUDFLARE_API_TOKEN` (and `CLOUDFLARE_ACCOUNT_ID`) repository secret so the action can authenticate.
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
```
|
|
41
|
+
Either way, set your production secrets (`SESSION_SECRET`, OAuth credentials) on the Worker in the Cloudflare dashboard or with `npx wrangler secret put` rather than committing them.
|
|
42
|
+
|
|
43
|
+
> **Working on the framework itself?** See [CONTRIBUTING.md](./CONTRIBUTING.md) for cloning and running this repository locally.
|
|
45
44
|
|
|
46
45
|
## Configuration Details
|
|
47
46
|
|
|
@@ -58,18 +57,18 @@ Use this option if you want to deploy from your local machine.
|
|
|
58
57
|
- **Using `wrangler.jsonc`:**
|
|
59
58
|
Add the variables to the `"vars"` object in your configuration file. See [Cloudflare documentation](https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables) for more details.
|
|
60
59
|
|
|
61
|
-
| Variable
|
|
62
|
-
|
|
|
63
|
-
| `ORIGIN_URL`
|
|
64
|
-
| `USERS_PATH`
|
|
65
|
-
| `AUTH_ORIGIN`
|
|
66
|
-
| `GOOGLE_CLIENT_ID`
|
|
67
|
-
| `GOOGLE_CLIENT_SECRET`
|
|
68
|
-
| `TWITCH_CLIENT_ID`
|
|
69
|
-
| `TWITCH_CLIENT_SECRET`
|
|
70
|
-
| `PATREON_CLIENT_ID`
|
|
71
|
-
| `PATREON_CLIENT_SECRET
|
|
72
|
-
| `PATREON_WEBHOOK_SECRET
|
|
60
|
+
| Variable | Required | Default | Description |
|
|
61
|
+
| :----------------------- | :------- | :-------- | :---------------------------------------------------------------------------- |
|
|
62
|
+
| `ORIGIN_URL` | **Yes** | N/A | The base URL of your origin application (e.g., `https://your-app-origin.com`) |
|
|
63
|
+
| `USERS_PATH` | No | `/users/` | The path used to serve internal assets like `power-strip.js` |
|
|
64
|
+
| `AUTH_ORIGIN` | No | N/A | Optional base URL for OAuth redirects (overrides request origin) |
|
|
65
|
+
| `GOOGLE_CLIENT_ID` | No | N/A | Google OAuth2 Client ID |
|
|
66
|
+
| `GOOGLE_CLIENT_SECRET` | No | N/A | Google OAuth2 Client Secret |
|
|
67
|
+
| `TWITCH_CLIENT_ID` | No | N/A | Twitch OAuth2 Client ID |
|
|
68
|
+
| `TWITCH_CLIENT_SECRET` | No | N/A | Twitch OAuth2 Client Secret |
|
|
69
|
+
| `PATREON_CLIENT_ID` | No | N/A | Patreon OAuth2 Client ID |
|
|
70
|
+
| `PATREON_CLIENT_SECRET` | No | N/A | Patreon OAuth2 Client Secret |
|
|
71
|
+
| `PATREON_WEBHOOK_SECRET` | No | N/A | Secret for verifying Patreon webhook signatures |
|
|
73
72
|
|
|
74
73
|
> Environment variables hold only credentials/secrets (OAuth client IDs and all secrets) plus the per‑deployment values `ORIGIN_URL`, `AUTH_ORIGIN`, `USERS_PATH`, `ADMIN_IDS`, and `ENVIRONMENT`. **All other configuration — OAuth scopes, Patreon campaign id, the access policy, entitlement freshness — is passed to the `createStartupAPI` factory** (see [Access policy & provider entitlements](#access-policy--provider-entitlements)).
|
|
75
74
|
|
|
@@ -144,7 +143,7 @@ By default the worker injects its own `<power-strip>` pinned to the top-right co
|
|
|
144
143
|
```
|
|
145
144
|
|
|
146
145
|
- **`providers` is optional.** If you omit it, the worker fills in the active providers for you (e.g. `providers="google,twitch,patreon"`). Set it yourself to override which login buttons appear.
|
|
147
|
-
- **Prefer an explicit closing tag.** `<power-strip></power-strip>` and `<power-strip/>` are both detected, but per the HTML spec `<power-strip/>` is
|
|
146
|
+
- **Prefer an explicit closing tag.** `<power-strip></power-strip>` and `<power-strip/>` are both detected, but per the HTML spec `<power-strip/>` is _not_ truly self-closing — the browser treats it as an open tag and nests the following content inside it. Use a closing tag (or place the element last in its container) to avoid surprises.
|
|
148
147
|
- **Script-only opt-out.** Use `<power-strip hidden>` to load `power-strip.js` (and its JS API) without rendering a visible strip.
|
|
149
148
|
|
|
150
149
|
## Access policy & provider entitlements
|
|
@@ -243,7 +242,12 @@ const api = createStartupAPI({
|
|
|
243
242
|
freshness: { ttl: true, cron: { schedule: '0 */6 * * *' }, webhook: true },
|
|
244
243
|
},
|
|
245
244
|
},
|
|
246
|
-
accessPolicy: {
|
|
245
|
+
accessPolicy: {
|
|
246
|
+
rules: [
|
|
247
|
+
/* ... */
|
|
248
|
+
],
|
|
249
|
+
default: { mode: 'public' },
|
|
250
|
+
},
|
|
247
251
|
});
|
|
248
252
|
|
|
249
253
|
export default api.default; // includes scheduled() because cron is enabled
|
|
@@ -254,7 +258,7 @@ export const { UserDO, AccountDO, SystemDO, CredentialDO } = api;
|
|
|
254
258
|
|
|
255
259
|
## Contributing
|
|
256
260
|
|
|
257
|
-
Contributions are welcome!
|
|
261
|
+
Contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for how to clone, run, test, and submit changes to the framework.
|
|
258
262
|
|
|
259
263
|
## License
|
|
260
264
|
|
package/package.json
CHANGED
|
@@ -12,10 +12,7 @@
|
|
|
12
12
|
data-ssr-members="{{ssr:account_members_json}}"
|
|
13
13
|
data-ssr-plans="{{ssr:plans_json}}"
|
|
14
14
|
>
|
|
15
|
-
<power-strip
|
|
16
|
-
providers="{{ssr:providers}}"
|
|
17
|
-
style="position: absolute; top: 0; right: 0; z-index: 9999; padding: 0.1rem; border-radius: 0 0 0 0.3rem"
|
|
18
|
-
>
|
|
15
|
+
<power-strip providers="{{ssr:providers}}" style="position: absolute; top: 0; right: 0; z-index: 9999; border-radius: 0 0 0 0.3rem">
|
|
19
16
|
<svg viewBox="0 0 24 24" style="width: 1rem; height: 1rem"><path d="M7 2v11h3v9l7-12h-4l4-8z" fill="#ffcc00" /></svg>
|
|
20
17
|
</power-strip>
|
|
21
18
|
<script src="/users/power-strip.js" async></script>
|
|
@@ -61,7 +58,7 @@
|
|
|
61
58
|
<div
|
|
62
59
|
id="account-avatar-placeholder"
|
|
63
60
|
class="account-avatar-large"
|
|
64
|
-
style="background:
|
|
61
|
+
style="background: var(--surface-alt); {{ssr:account_placeholder_display}} align-items: center; justify-content: center; color: var(--muted-badge-text)"
|
|
65
62
|
>
|
|
66
63
|
<svg viewBox="0 0 24 24" style="width: 48px; height: 48px; fill: currentColor">
|
|
67
64
|
<path
|
|
@@ -97,8 +94,8 @@
|
|
|
97
94
|
</button>
|
|
98
95
|
</div>
|
|
99
96
|
<div>
|
|
100
|
-
<h2 id="display-account-name" style="margin: 0; color:
|
|
101
|
-
<p id="display-account-plan" style="margin: 0.25rem 0 0 0; color:
|
|
97
|
+
<h2 id="display-account-name" style="margin: 0; color: var(--text)">{{ssr:account_name}}</h2>
|
|
98
|
+
<p id="display-account-plan" style="margin: 0.25rem 0 0 0; color: var(--text-faint)">{{ssr:account_plan_name}}</p>
|
|
102
99
|
</div>
|
|
103
100
|
</div>
|
|
104
101
|
|
|
@@ -5,6 +5,56 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Admin Dashboard</title>
|
|
7
7
|
<style>
|
|
8
|
+
/* Color tokens, flipped by the user's OS color-scheme preference (with a
|
|
9
|
+
[data-theme] escape hatch) — same approach as the shared user pages
|
|
10
|
+
(/users/style.css) and the landing page. */
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #f9f9f9;
|
|
13
|
+
--surface: #fff;
|
|
14
|
+
--surface-muted: #eee;
|
|
15
|
+
--hover-bg: #f0f0f0;
|
|
16
|
+
--text: #333;
|
|
17
|
+
--border: #ddd;
|
|
18
|
+
--border-light: #eee;
|
|
19
|
+
--accent: #ffcc00;
|
|
20
|
+
--accent-hover: #e6b800;
|
|
21
|
+
--on-accent: #202124;
|
|
22
|
+
--secondary-btn-bg: #eee;
|
|
23
|
+
--secondary-btn-hover-bg: #e0e0e0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@media (prefers-color-scheme: dark) {
|
|
27
|
+
:root:not([data-theme='light']) {
|
|
28
|
+
--bg: #1a1a1a;
|
|
29
|
+
--surface: #2d2d2d;
|
|
30
|
+
--surface-muted: #333;
|
|
31
|
+
--hover-bg: #3c4043;
|
|
32
|
+
--text: #e0e0e0;
|
|
33
|
+
--border: #5f6368;
|
|
34
|
+
--border-light: #444;
|
|
35
|
+
--accent: #ffcc00;
|
|
36
|
+
--accent-hover: #e6b800;
|
|
37
|
+
--on-accent: #202124;
|
|
38
|
+
--secondary-btn-bg: #3c4043;
|
|
39
|
+
--secondary-btn-hover-bg: #4a4f54;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
[data-theme='dark'] {
|
|
44
|
+
--bg: #1a1a1a;
|
|
45
|
+
--surface: #2d2d2d;
|
|
46
|
+
--surface-muted: #333;
|
|
47
|
+
--hover-bg: #3c4043;
|
|
48
|
+
--text: #e0e0e0;
|
|
49
|
+
--border: #5f6368;
|
|
50
|
+
--border-light: #444;
|
|
51
|
+
--accent: #ffcc00;
|
|
52
|
+
--accent-hover: #e6b800;
|
|
53
|
+
--on-accent: #202124;
|
|
54
|
+
--secondary-btn-bg: #3c4043;
|
|
55
|
+
--secondary-btn-hover-bg: #4a4f54;
|
|
56
|
+
}
|
|
57
|
+
|
|
8
58
|
body {
|
|
9
59
|
font-family:
|
|
10
60
|
system-ui,
|
|
@@ -13,14 +63,15 @@
|
|
|
13
63
|
padding: 2rem;
|
|
14
64
|
max-width: 1200px;
|
|
15
65
|
margin: 0 auto;
|
|
16
|
-
background:
|
|
66
|
+
background: var(--bg);
|
|
67
|
+
color: var(--text);
|
|
17
68
|
}
|
|
18
69
|
h1,
|
|
19
70
|
h2 {
|
|
20
|
-
color:
|
|
71
|
+
color: var(--text);
|
|
21
72
|
}
|
|
22
73
|
section {
|
|
23
|
-
background:
|
|
74
|
+
background: var(--surface);
|
|
24
75
|
padding: 1.5rem;
|
|
25
76
|
border-radius: 8px;
|
|
26
77
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
@@ -33,20 +84,22 @@
|
|
|
33
84
|
}
|
|
34
85
|
input[type='text'] {
|
|
35
86
|
padding: 0.5rem;
|
|
36
|
-
border: 1px solid
|
|
87
|
+
border: 1px solid var(--border);
|
|
37
88
|
border-radius: 4px;
|
|
38
89
|
flex-grow: 1;
|
|
90
|
+
background: var(--surface);
|
|
91
|
+
color: var(--text);
|
|
39
92
|
}
|
|
40
93
|
button {
|
|
41
94
|
padding: 0.5rem 1rem;
|
|
42
|
-
background:
|
|
43
|
-
color:
|
|
95
|
+
background: var(--accent);
|
|
96
|
+
color: var(--on-accent);
|
|
44
97
|
border: none;
|
|
45
98
|
border-radius: 4px;
|
|
46
99
|
cursor: pointer;
|
|
47
100
|
}
|
|
48
101
|
button:hover {
|
|
49
|
-
background:
|
|
102
|
+
background: var(--accent-hover);
|
|
50
103
|
}
|
|
51
104
|
table {
|
|
52
105
|
width: 100%;
|
|
@@ -56,21 +109,21 @@
|
|
|
56
109
|
td {
|
|
57
110
|
text-align: left;
|
|
58
111
|
padding: 0.75rem;
|
|
59
|
-
border-bottom: 1px solid
|
|
112
|
+
border-bottom: 1px solid var(--border-light);
|
|
60
113
|
}
|
|
61
114
|
th {
|
|
62
|
-
background:
|
|
115
|
+
background: var(--hover-bg);
|
|
63
116
|
}
|
|
64
117
|
.actions {
|
|
65
118
|
display: flex;
|
|
66
119
|
gap: 0.5rem;
|
|
67
120
|
}
|
|
68
121
|
.secondary-btn {
|
|
69
|
-
background:
|
|
70
|
-
color:
|
|
122
|
+
background: var(--secondary-btn-bg);
|
|
123
|
+
color: var(--text);
|
|
71
124
|
}
|
|
72
125
|
.secondary-btn:hover {
|
|
73
|
-
background:
|
|
126
|
+
background: var(--secondary-btn-hover-bg);
|
|
74
127
|
}
|
|
75
128
|
#toast {
|
|
76
129
|
position: fixed;
|
|
@@ -89,6 +142,8 @@
|
|
|
89
142
|
border-radius: 8px;
|
|
90
143
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
91
144
|
width: 400px;
|
|
145
|
+
background: var(--surface);
|
|
146
|
+
color: var(--text);
|
|
92
147
|
}
|
|
93
148
|
dialog::backdrop {
|
|
94
149
|
background: rgba(0, 0, 0, 0.5);
|
|
@@ -108,9 +163,11 @@
|
|
|
108
163
|
.form-group select {
|
|
109
164
|
width: 100%;
|
|
110
165
|
padding: 0.5rem;
|
|
111
|
-
border: 1px solid
|
|
166
|
+
border: 1px solid var(--border);
|
|
112
167
|
border-radius: 4px;
|
|
113
168
|
box-sizing: border-box;
|
|
169
|
+
background: var(--surface);
|
|
170
|
+
color: var(--text);
|
|
114
171
|
}
|
|
115
172
|
.modal-actions {
|
|
116
173
|
display: flex;
|
|
@@ -121,10 +178,7 @@
|
|
|
121
178
|
</style>
|
|
122
179
|
</head>
|
|
123
180
|
<body data-ssr-plans="{{ssr:plans_json}}">
|
|
124
|
-
<power-strip
|
|
125
|
-
providers="{{ssr:providers}}"
|
|
126
|
-
style="position: absolute; top: 0; right: 0; z-index: 9999; padding: 0.1rem; border-radius: 0 0 0 0.3rem"
|
|
127
|
-
>
|
|
181
|
+
<power-strip providers="{{ssr:providers}}" style="position: absolute; top: 0; right: 0; z-index: 9999; border-radius: 0 0 0 0.3rem">
|
|
128
182
|
<svg viewBox="0 0 24 24" style="width: 1rem; height: 1rem"><path d="M7 2v11h3v9l7-12h-4l4-8z" fill="#ffcc00" /></svg>
|
|
129
183
|
</power-strip>
|
|
130
184
|
<script src="/users/power-strip.js" async></script>
|
|
@@ -417,7 +471,7 @@
|
|
|
417
471
|
<button class="secondary-btn" onclick="openEditUser('${u.id}')">Edit</button>
|
|
418
472
|
<button class="secondary-btn" onclick="openUserMemberships('${u.id}')">Memberships</button>
|
|
419
473
|
<button onclick="impersonate('${u.id}')">Impersonate</button>
|
|
420
|
-
<button style="background: #d93025;" onclick="deleteUser('${u.id}')">Delete</button>
|
|
474
|
+
<button style="background: #d93025; color: #fff;" onclick="deleteUser('${u.id}')">Delete</button>
|
|
421
475
|
</td>
|
|
422
476
|
</tr>
|
|
423
477
|
`,
|
|
@@ -443,7 +497,7 @@
|
|
|
443
497
|
<td class="actions">
|
|
444
498
|
<button class="secondary-btn" onclick="openEditAccount('${a.id}')">Edit</button>
|
|
445
499
|
<button class="secondary-btn" onclick="openMembers('${a.id}')">Members</button>
|
|
446
|
-
<button style="background: #d93025;" onclick="deleteAccount('${a.id}')">Delete</button>
|
|
500
|
+
<button style="background: #d93025; color: #fff;" onclick="deleteAccount('${a.id}')">Delete</button>
|
|
447
501
|
</td>
|
|
448
502
|
</tr>
|
|
449
503
|
`;
|
|
@@ -5,6 +5,10 @@ class PowerStrip extends HTMLElement {
|
|
|
5
5
|
this.basePath = this.detectBasePath();
|
|
6
6
|
this.user = null;
|
|
7
7
|
this.accounts = [];
|
|
8
|
+
// Theme watchers, wired up once on connect and torn down on disconnect.
|
|
9
|
+
this._mediaQuery = null;
|
|
10
|
+
this._onPreferenceChange = null;
|
|
11
|
+
this._pageObserver = null;
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
detectBasePath() {
|
|
@@ -27,17 +31,130 @@ class PowerStrip extends HTMLElement {
|
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
async connectedCallback() {
|
|
34
|
+
// Resolve the theme before the first paint so the strip never flashes the
|
|
35
|
+
// wrong colors, then keep it in sync with the page from here on.
|
|
36
|
+
this.applyTheme();
|
|
37
|
+
this.watchThemeChanges();
|
|
30
38
|
await this.fetchUser();
|
|
31
39
|
this.render();
|
|
32
40
|
this.addEventListeners();
|
|
33
41
|
}
|
|
34
42
|
|
|
43
|
+
disconnectedCallback() {
|
|
44
|
+
if (this._mediaQuery && this._onPreferenceChange) {
|
|
45
|
+
this._mediaQuery.removeEventListener('change', this._onPreferenceChange);
|
|
46
|
+
}
|
|
47
|
+
if (this._pageObserver) {
|
|
48
|
+
this._pageObserver.disconnect();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
async refresh() {
|
|
36
53
|
await this.fetchUser();
|
|
37
54
|
this.render();
|
|
38
55
|
this.addEventListeners();
|
|
39
56
|
}
|
|
40
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Decide whether the strip should render light or dark by measuring the
|
|
60
|
+
* actual background the strip sits on. This makes the strip match the page
|
|
61
|
+
* regardless of *how* the page chose its theme — a hardcoded dark page, a
|
|
62
|
+
* hardcoded light page, or a page that respects the user's OS preference all
|
|
63
|
+
* resolve to a concrete background color we can read here. When nothing
|
|
64
|
+
* conclusive is found (e.g. a transparent body over an image) we fall back to
|
|
65
|
+
* the user's OS-level color-scheme preference.
|
|
66
|
+
*/
|
|
67
|
+
detectPageTheme() {
|
|
68
|
+
const bg = this.getEffectiveBackgroundColor();
|
|
69
|
+
if (bg) {
|
|
70
|
+
return this.isDarkColor(bg) ? 'dark' : 'light';
|
|
71
|
+
}
|
|
72
|
+
return this.prefersDark() ? 'dark' : 'light';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
prefersDark() {
|
|
76
|
+
return typeof window.matchMedia === 'function' && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Walk up from the strip's placement looking for the first ancestor that
|
|
81
|
+
* paints an opaque (or partly opaque) background, mirroring how the strip is
|
|
82
|
+
* actually composited over the page. Falls back to the document element.
|
|
83
|
+
*/
|
|
84
|
+
getEffectiveBackgroundColor() {
|
|
85
|
+
let el = this.parentElement;
|
|
86
|
+
while (el) {
|
|
87
|
+
const color = getComputedStyle(el).backgroundColor;
|
|
88
|
+
if (color && !this.isTransparentColor(color)) {
|
|
89
|
+
return color;
|
|
90
|
+
}
|
|
91
|
+
el = el.parentElement;
|
|
92
|
+
}
|
|
93
|
+
const rootColor = getComputedStyle(document.documentElement).backgroundColor;
|
|
94
|
+
if (rootColor && !this.isTransparentColor(rootColor)) {
|
|
95
|
+
return rootColor;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
parseColor(color) {
|
|
101
|
+
const parts = (color.match(/[\d.]+/g) || []).map(Number);
|
|
102
|
+
if (parts.length < 3) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return { r: parts[0], g: parts[1], b: parts[2], a: parts.length >= 4 ? parts[3] : 1 };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
isTransparentColor(color) {
|
|
109
|
+
if (!color || color === 'transparent') {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const c = this.parseColor(color);
|
|
113
|
+
return !c || c.a === 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
isDarkColor(color) {
|
|
117
|
+
const c = this.parseColor(color);
|
|
118
|
+
if (!c) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
// Perceived luminance (ITU-R BT.601). Below the midpoint reads as "dark".
|
|
122
|
+
const luminance = (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) / 255;
|
|
123
|
+
return luminance < 0.5;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Tag the host with the resolved theme. CSS keys all of its colors off this
|
|
128
|
+
* attribute, so updating it is enough to re-theme the whole shadow tree
|
|
129
|
+
* (panel, dialogs and all) without re-rendering or losing dialog state.
|
|
130
|
+
*/
|
|
131
|
+
applyTheme() {
|
|
132
|
+
const theme = this.detectPageTheme();
|
|
133
|
+
if (this.getAttribute('data-resolved-theme') !== theme) {
|
|
134
|
+
this.setAttribute('data-resolved-theme', theme);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
watchThemeChanges() {
|
|
139
|
+
// React to the user flipping their OS-level color-scheme preference.
|
|
140
|
+
if (typeof window.matchMedia === 'function') {
|
|
141
|
+
this._mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
142
|
+
this._onPreferenceChange = () => this.applyTheme();
|
|
143
|
+
this._mediaQuery.addEventListener('change', this._onPreferenceChange);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// React to the page re-theming itself at runtime — e.g. a theme toggle
|
|
147
|
+
// flipping data-theme/class/style on <html> or <body>.
|
|
148
|
+
if (typeof MutationObserver === 'function') {
|
|
149
|
+
this._pageObserver = new MutationObserver(() => this.applyTheme());
|
|
150
|
+
const observeOptions = { attributes: true, attributeFilter: ['data-theme', 'class', 'style'] };
|
|
151
|
+
this._pageObserver.observe(document.documentElement, observeOptions);
|
|
152
|
+
if (document.body) {
|
|
153
|
+
this._pageObserver.observe(document.body, observeOptions);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
41
158
|
async fetchUser() {
|
|
42
159
|
try {
|
|
43
160
|
const res = await fetch(`${this.basePath}/api/me`);
|
|
@@ -214,8 +331,8 @@ class PowerStrip extends HTMLElement {
|
|
|
214
331
|
|
|
215
332
|
const avatarContent = this.user.profile.picture
|
|
216
333
|
? `<img src="${this.user.profile.picture}" alt="${this.user.profile.name}" title="${this.user.profile.name}" class="avatar" width="16" height="16" />`
|
|
217
|
-
: `<div class="avatar placeholder"
|
|
218
|
-
<svg viewBox="0 0 24 24" style="width: 12px; height: 12px;
|
|
334
|
+
: `<div class="avatar placeholder">
|
|
335
|
+
<svg viewBox="0 0 24 24" style="width: 12px; height: 12px;">
|
|
219
336
|
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
|
220
337
|
</svg>
|
|
221
338
|
</div>`;
|
|
@@ -251,6 +368,108 @@ class PowerStrip extends HTMLElement {
|
|
|
251
368
|
:host {
|
|
252
369
|
display: block;
|
|
253
370
|
font-family: system-ui, -apple-system, sans-serif;
|
|
371
|
+
|
|
372
|
+
/* Light theme (the default). Every color in the strip is keyed off
|
|
373
|
+
these custom properties so the whole shadow tree can be re-themed
|
|
374
|
+
by flipping a single set of variables. The dark overrides live in
|
|
375
|
+
the rules below — one driven by the user's OS preference (used as a
|
|
376
|
+
flash-free fallback) and one driven by the [data-resolved-theme]
|
|
377
|
+
attribute the component measures and sets at runtime. */
|
|
378
|
+
--ps-panel-bg: rgba(255, 255, 255, 0.85);
|
|
379
|
+
--ps-panel-border: rgba(0, 0, 0, 0.15);
|
|
380
|
+
--ps-panel-shadow: 0 0.0625rem 0.25rem rgba(0, 0, 0, 0.25);
|
|
381
|
+
--ps-text: #333;
|
|
382
|
+
--ps-text-muted: #444;
|
|
383
|
+
--ps-accent: #1a73e8;
|
|
384
|
+
--ps-danger: #d93025;
|
|
385
|
+
--ps-warning: #c77700;
|
|
386
|
+
--ps-hover-bg: rgba(0, 0, 0, 0.06);
|
|
387
|
+
--ps-avatar-bg: #eee;
|
|
388
|
+
--ps-avatar-fg: #999;
|
|
389
|
+
|
|
390
|
+
--ps-dialog-bg: #fff;
|
|
391
|
+
--ps-dialog-text: #333;
|
|
392
|
+
--ps-dialog-shadow: 0 0.625rem 1.5625rem rgba(0, 0, 0, 0.25);
|
|
393
|
+
--ps-dialog-muted: #999;
|
|
394
|
+
--ps-dialog-hover-bg: #f0f0f0;
|
|
395
|
+
--ps-surface-bg: #fff;
|
|
396
|
+
--ps-surface-border: #ddd;
|
|
397
|
+
--ps-surface-soft-border: #eee;
|
|
398
|
+
--ps-surface-hover-bg: #f5f5f5;
|
|
399
|
+
--ps-neutral-btn-bg: #fff;
|
|
400
|
+
--ps-neutral-btn-text: #3c4043;
|
|
401
|
+
--ps-neutral-btn-border: #dadce0;
|
|
402
|
+
--ps-neutral-btn-hover-bg: #f8f9fa;
|
|
403
|
+
--ps-active-bg: #e8f0fe;
|
|
404
|
+
|
|
405
|
+
color-scheme: light;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/* Shared dark palette. Applied either when the page has been measured
|
|
409
|
+
as dark, or — before/without measurement — when the user's OS asks
|
|
410
|
+
for dark and the page hasn't been explicitly resolved to light. */
|
|
411
|
+
:host([data-resolved-theme='dark']) {
|
|
412
|
+
--ps-panel-bg: rgba(32, 33, 36, 0.92);
|
|
413
|
+
--ps-panel-border: rgba(255, 255, 255, 0.22);
|
|
414
|
+
--ps-panel-shadow: 0 0.0625rem 0.3125rem rgba(0, 0, 0, 0.65);
|
|
415
|
+
--ps-text: #e8eaed;
|
|
416
|
+
--ps-text-muted: #dadce0;
|
|
417
|
+
--ps-accent: #8ab4f8;
|
|
418
|
+
--ps-danger: #f28b82;
|
|
419
|
+
--ps-warning: #fdd663;
|
|
420
|
+
--ps-hover-bg: rgba(255, 255, 255, 0.12);
|
|
421
|
+
--ps-avatar-bg: #5f6368;
|
|
422
|
+
--ps-avatar-fg: #dadce0;
|
|
423
|
+
|
|
424
|
+
--ps-dialog-bg: #2a2b2e;
|
|
425
|
+
--ps-dialog-text: #e8eaed;
|
|
426
|
+
--ps-dialog-shadow: 0 0.625rem 1.5625rem rgba(0, 0, 0, 0.7);
|
|
427
|
+
--ps-dialog-muted: #9aa0a6;
|
|
428
|
+
--ps-dialog-hover-bg: #3c4043;
|
|
429
|
+
--ps-surface-bg: #303134;
|
|
430
|
+
--ps-surface-border: #5f6368;
|
|
431
|
+
--ps-surface-soft-border: #3c4043;
|
|
432
|
+
--ps-surface-hover-bg: #3c4043;
|
|
433
|
+
--ps-neutral-btn-bg: #303134;
|
|
434
|
+
--ps-neutral-btn-text: #e8eaed;
|
|
435
|
+
--ps-neutral-btn-border: #5f6368;
|
|
436
|
+
--ps-neutral-btn-hover-bg: #3c4043;
|
|
437
|
+
--ps-active-bg: #283142;
|
|
438
|
+
|
|
439
|
+
color-scheme: dark;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
@media (prefers-color-scheme: dark) {
|
|
443
|
+
:host(:not([data-resolved-theme='light'])) {
|
|
444
|
+
--ps-panel-bg: rgba(32, 33, 36, 0.92);
|
|
445
|
+
--ps-panel-border: rgba(255, 255, 255, 0.22);
|
|
446
|
+
--ps-panel-shadow: 0 0.0625rem 0.3125rem rgba(0, 0, 0, 0.65);
|
|
447
|
+
--ps-text: #e8eaed;
|
|
448
|
+
--ps-text-muted: #dadce0;
|
|
449
|
+
--ps-accent: #8ab4f8;
|
|
450
|
+
--ps-danger: #f28b82;
|
|
451
|
+
--ps-warning: #fdd663;
|
|
452
|
+
--ps-hover-bg: rgba(255, 255, 255, 0.12);
|
|
453
|
+
--ps-avatar-bg: #5f6368;
|
|
454
|
+
--ps-avatar-fg: #dadce0;
|
|
455
|
+
|
|
456
|
+
--ps-dialog-bg: #2a2b2e;
|
|
457
|
+
--ps-dialog-text: #e8eaed;
|
|
458
|
+
--ps-dialog-shadow: 0 0.625rem 1.5625rem rgba(0, 0, 0, 0.7);
|
|
459
|
+
--ps-dialog-muted: #9aa0a6;
|
|
460
|
+
--ps-dialog-hover-bg: #3c4043;
|
|
461
|
+
--ps-surface-bg: #303134;
|
|
462
|
+
--ps-surface-border: #5f6368;
|
|
463
|
+
--ps-surface-soft-border: #3c4043;
|
|
464
|
+
--ps-surface-hover-bg: #3c4043;
|
|
465
|
+
--ps-neutral-btn-bg: #303134;
|
|
466
|
+
--ps-neutral-btn-text: #e8eaed;
|
|
467
|
+
--ps-neutral-btn-border: #5f6368;
|
|
468
|
+
--ps-neutral-btn-hover-bg: #3c4043;
|
|
469
|
+
--ps-active-bg: #283142;
|
|
470
|
+
|
|
471
|
+
color-scheme: dark;
|
|
472
|
+
}
|
|
254
473
|
}
|
|
255
474
|
|
|
256
475
|
/* Honor the native [hidden] attribute so authors can load the script
|
|
@@ -272,10 +491,16 @@ class PowerStrip extends HTMLElement {
|
|
|
272
491
|
height: 1.3rem;
|
|
273
492
|
padding: 0.0625rem;
|
|
274
493
|
animation: fadeIn 0.4s ease-out;
|
|
275
|
-
background-color:
|
|
494
|
+
background-color: var(--ps-panel-bg);
|
|
495
|
+
/* A contrasting border keeps the chip distinguishable even when its
|
|
496
|
+
panel color happens to be close to the page background. */
|
|
497
|
+
border: 0.0625rem solid var(--ps-panel-border);
|
|
498
|
+
border-top: none;
|
|
499
|
+
border-right: none;
|
|
276
500
|
border-radius: 0 0 0 0.3rem;
|
|
277
|
-
box-shadow:
|
|
501
|
+
box-shadow: var(--ps-panel-shadow);
|
|
278
502
|
font-size: 1rem;
|
|
503
|
+
backdrop-filter: blur(0.25rem);
|
|
279
504
|
}
|
|
280
505
|
|
|
281
506
|
.trigger {
|
|
@@ -285,7 +510,7 @@ class PowerStrip extends HTMLElement {
|
|
|
285
510
|
border-radius: 0.25rem;
|
|
286
511
|
font-size: 0.8rem;
|
|
287
512
|
font-weight: 500;
|
|
288
|
-
color:
|
|
513
|
+
color: var(--ps-text-muted);
|
|
289
514
|
text-decoration: none;
|
|
290
515
|
border: none;
|
|
291
516
|
background: transparent;
|
|
@@ -293,19 +518,19 @@ class PowerStrip extends HTMLElement {
|
|
|
293
518
|
}
|
|
294
519
|
|
|
295
520
|
.trigger:hover {
|
|
296
|
-
background-color:
|
|
521
|
+
background-color: var(--ps-hover-bg);
|
|
297
522
|
text-decoration: underline;
|
|
298
|
-
color:
|
|
523
|
+
color: var(--ps-accent);
|
|
299
524
|
}
|
|
300
|
-
|
|
525
|
+
|
|
301
526
|
.switch-btn {
|
|
302
|
-
color:
|
|
527
|
+
color: var(--ps-accent);
|
|
303
528
|
}
|
|
304
529
|
|
|
305
530
|
svg.bolt, ::slotted(svg) {
|
|
306
531
|
width: 1rem !important;
|
|
307
532
|
height: 1rem !important;
|
|
308
|
-
fill: #ffcc00 !important;
|
|
533
|
+
fill: #ffcc00 !important;
|
|
309
534
|
filter: drop-shadow(0.0625rem 0.0625rem 0.0625rem rgba(0, 0, 0, 0.5));
|
|
310
535
|
flex-shrink: 0;
|
|
311
536
|
}
|
|
@@ -330,6 +555,17 @@ class PowerStrip extends HTMLElement {
|
|
|
330
555
|
object-fit: cover;
|
|
331
556
|
}
|
|
332
557
|
|
|
558
|
+
.avatar.placeholder {
|
|
559
|
+
background: var(--ps-avatar-bg);
|
|
560
|
+
display: flex;
|
|
561
|
+
align-items: center;
|
|
562
|
+
justify-content: center;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.avatar.placeholder svg {
|
|
566
|
+
fill: var(--ps-avatar-fg);
|
|
567
|
+
}
|
|
568
|
+
|
|
333
569
|
.provider-badge {
|
|
334
570
|
position: absolute;
|
|
335
571
|
bottom: -0.0625rem;
|
|
@@ -340,7 +576,7 @@ class PowerStrip extends HTMLElement {
|
|
|
340
576
|
align-items: center;
|
|
341
577
|
justify-content: center;
|
|
342
578
|
}
|
|
343
|
-
|
|
579
|
+
|
|
344
580
|
.provider-badge svg {
|
|
345
581
|
width: 0.5rem;
|
|
346
582
|
height: 0.5rem;
|
|
@@ -365,7 +601,7 @@ class PowerStrip extends HTMLElement {
|
|
|
365
601
|
|
|
366
602
|
.user-name {
|
|
367
603
|
font-size: 0.8rem;
|
|
368
|
-
color:
|
|
604
|
+
color: var(--ps-text);
|
|
369
605
|
max-width: 10rem;
|
|
370
606
|
white-space: nowrap;
|
|
371
607
|
overflow: hidden;
|
|
@@ -377,12 +613,12 @@ class PowerStrip extends HTMLElement {
|
|
|
377
613
|
|
|
378
614
|
.user-name:hover {
|
|
379
615
|
text-decoration: underline;
|
|
380
|
-
color:
|
|
616
|
+
color: var(--ps-accent);
|
|
381
617
|
}
|
|
382
|
-
|
|
618
|
+
|
|
383
619
|
.account-label {
|
|
384
620
|
font-size: 0.8rem;
|
|
385
|
-
color:
|
|
621
|
+
color: var(--ps-accent);
|
|
386
622
|
max-width: 10rem;
|
|
387
623
|
white-space: nowrap;
|
|
388
624
|
overflow: hidden;
|
|
@@ -400,11 +636,11 @@ class PowerStrip extends HTMLElement {
|
|
|
400
636
|
}
|
|
401
637
|
|
|
402
638
|
.admin-btn {
|
|
403
|
-
color:
|
|
639
|
+
color: var(--ps-danger) !important;
|
|
404
640
|
}
|
|
405
641
|
|
|
406
642
|
.stop-impersonation-btn {
|
|
407
|
-
color:
|
|
643
|
+
color: var(--ps-warning) !important;
|
|
408
644
|
font-weight: bold;
|
|
409
645
|
}
|
|
410
646
|
|
|
@@ -419,9 +655,9 @@ class PowerStrip extends HTMLElement {
|
|
|
419
655
|
border: none;
|
|
420
656
|
border-radius: 0.75rem;
|
|
421
657
|
padding: 0;
|
|
422
|
-
box-shadow:
|
|
423
|
-
background:
|
|
424
|
-
color:
|
|
658
|
+
box-shadow: var(--ps-dialog-shadow);
|
|
659
|
+
background: var(--ps-dialog-bg);
|
|
660
|
+
color: var(--ps-dialog-text);
|
|
425
661
|
max-width: 20rem;
|
|
426
662
|
width: 90%;
|
|
427
663
|
overflow: hidden;
|
|
@@ -442,7 +678,7 @@ class PowerStrip extends HTMLElement {
|
|
|
442
678
|
align-items: center;
|
|
443
679
|
margin-bottom: 1.25rem;
|
|
444
680
|
}
|
|
445
|
-
|
|
681
|
+
|
|
446
682
|
.dialog-title {
|
|
447
683
|
font-weight: 700;
|
|
448
684
|
font-size: 1.25rem;
|
|
@@ -454,7 +690,7 @@ class PowerStrip extends HTMLElement {
|
|
|
454
690
|
border: none;
|
|
455
691
|
cursor: pointer;
|
|
456
692
|
font-size: 1.5rem;
|
|
457
|
-
color:
|
|
693
|
+
color: var(--ps-dialog-muted);
|
|
458
694
|
padding: 0;
|
|
459
695
|
line-height: 1;
|
|
460
696
|
display: flex;
|
|
@@ -467,8 +703,8 @@ class PowerStrip extends HTMLElement {
|
|
|
467
703
|
}
|
|
468
704
|
|
|
469
705
|
.close-btn:hover {
|
|
470
|
-
background-color:
|
|
471
|
-
color:
|
|
706
|
+
background-color: var(--ps-dialog-hover-bg);
|
|
707
|
+
color: var(--ps-dialog-text);
|
|
472
708
|
}
|
|
473
709
|
|
|
474
710
|
.auth-buttons {
|
|
@@ -479,7 +715,7 @@ class PowerStrip extends HTMLElement {
|
|
|
479
715
|
|
|
480
716
|
.auth-btn {
|
|
481
717
|
padding: 0.75rem 1rem;
|
|
482
|
-
border: 1px solid
|
|
718
|
+
border: 1px solid var(--ps-surface-border);
|
|
483
719
|
border-radius: 0.375rem;
|
|
484
720
|
cursor: pointer;
|
|
485
721
|
display: flex;
|
|
@@ -491,7 +727,7 @@ class PowerStrip extends HTMLElement {
|
|
|
491
727
|
transition: all 0.2s ease;
|
|
492
728
|
text-decoration: none;
|
|
493
729
|
color: inherit;
|
|
494
|
-
background-color:
|
|
730
|
+
background-color: var(--ps-neutral-btn-bg);
|
|
495
731
|
}
|
|
496
732
|
|
|
497
733
|
.auth-btn:hover {
|
|
@@ -509,12 +745,12 @@ class PowerStrip extends HTMLElement {
|
|
|
509
745
|
}
|
|
510
746
|
|
|
511
747
|
.auth-btn.google {
|
|
512
|
-
color:
|
|
513
|
-
|
|
748
|
+
color: var(--ps-neutral-btn-text);
|
|
749
|
+
background-color: var(--ps-neutral-btn-bg);
|
|
750
|
+
border-color: var(--ps-neutral-btn-border);
|
|
514
751
|
}
|
|
515
752
|
.auth-btn.google:hover {
|
|
516
|
-
background-color:
|
|
517
|
-
border-color: #d2e3fc;
|
|
753
|
+
background-color: var(--ps-neutral-btn-hover-bg);
|
|
518
754
|
}
|
|
519
755
|
|
|
520
756
|
.auth-btn.twitch {
|
|
@@ -543,12 +779,13 @@ class PowerStrip extends HTMLElement {
|
|
|
543
779
|
flex-direction: column;
|
|
544
780
|
gap: 0.5rem;
|
|
545
781
|
}
|
|
546
|
-
|
|
782
|
+
|
|
547
783
|
.account-item {
|
|
548
784
|
padding: 0.75rem;
|
|
549
|
-
border: 1px solid
|
|
785
|
+
border: 1px solid var(--ps-surface-soft-border);
|
|
550
786
|
border-radius: 0.375rem;
|
|
551
|
-
background:
|
|
787
|
+
background: var(--ps-surface-bg);
|
|
788
|
+
color: var(--ps-dialog-text);
|
|
552
789
|
text-align: left;
|
|
553
790
|
cursor: pointer;
|
|
554
791
|
display: flex;
|
|
@@ -558,25 +795,25 @@ class PowerStrip extends HTMLElement {
|
|
|
558
795
|
font-size: 1rem;
|
|
559
796
|
gap: 1rem;
|
|
560
797
|
}
|
|
561
|
-
|
|
798
|
+
|
|
562
799
|
.account-item:hover {
|
|
563
|
-
background-color:
|
|
800
|
+
background-color: var(--ps-surface-hover-bg);
|
|
564
801
|
}
|
|
565
|
-
|
|
802
|
+
|
|
566
803
|
.account-item.active {
|
|
567
|
-
border-color:
|
|
568
|
-
background-color:
|
|
804
|
+
border-color: var(--ps-accent);
|
|
805
|
+
background-color: var(--ps-active-bg);
|
|
569
806
|
}
|
|
570
|
-
|
|
807
|
+
|
|
571
808
|
.current-badge {
|
|
572
809
|
font-size: 0.75rem;
|
|
573
|
-
background:
|
|
810
|
+
background: var(--ps-accent);
|
|
574
811
|
color: white;
|
|
575
812
|
padding: 0.125rem 0.375rem;
|
|
576
813
|
border-radius: 0.75rem;
|
|
577
814
|
}
|
|
578
815
|
</style>
|
|
579
|
-
|
|
816
|
+
|
|
580
817
|
<div class="container">
|
|
581
818
|
${content}
|
|
582
819
|
<slot></slot>
|
|
@@ -593,7 +830,7 @@ class PowerStrip extends HTMLElement {
|
|
|
593
830
|
</div>
|
|
594
831
|
</div>
|
|
595
832
|
</dialog>
|
|
596
|
-
|
|
833
|
+
|
|
597
834
|
${accountSwitcher}
|
|
598
835
|
`;
|
|
599
836
|
}
|
|
@@ -7,10 +7,7 @@
|
|
|
7
7
|
<link rel="stylesheet" href="/users/style.css" />
|
|
8
8
|
</head>
|
|
9
9
|
<body data-ssr-profile="{{ssr:profile_json}}" data-ssr-credentials="{{ssr:credentials_json}}">
|
|
10
|
-
<power-strip
|
|
11
|
-
providers="{{ssr:providers}}"
|
|
12
|
-
style="position: absolute; top: 0; right: 0; z-index: 9999; padding: 0.1rem; border-radius: 0 0 0 0.3rem"
|
|
13
|
-
>
|
|
10
|
+
<power-strip providers="{{ssr:providers}}" style="position: absolute; top: 0; right: 0; z-index: 9999; border-radius: 0 0 0 0.3rem">
|
|
14
11
|
<svg viewBox="0 0 24 24" style="width: 1rem; height: 1rem"><path d="M7 2v11h3v9l7-12h-4l4-8z" fill="#ffcc00" /></svg>
|
|
15
12
|
</power-strip>
|
|
16
13
|
<script src="/users/power-strip.js" async></script>
|
|
@@ -56,7 +53,7 @@
|
|
|
56
53
|
<div
|
|
57
54
|
id="profile-avatar-placeholder"
|
|
58
55
|
class="avatar-large"
|
|
59
|
-
style="background:
|
|
56
|
+
style="background: var(--surface-alt); {{ssr:profile_placeholder_display}} align-items: center; justify-content: center; color: var(--muted-badge-text)"
|
|
60
57
|
>
|
|
61
58
|
<svg
|
|
62
59
|
viewBox="0 0 24 24"
|
|
@@ -99,7 +96,7 @@
|
|
|
99
96
|
</button>
|
|
100
97
|
</div>
|
|
101
98
|
<div>
|
|
102
|
-
<p id="display-email" style="margin: 0.25rem 0 0 0; color:
|
|
99
|
+
<p id="display-email" style="margin: 0.25rem 0 0 0; color: var(--text-faint)">{{ssr:profile_email}}</p>
|
|
103
100
|
</div>
|
|
104
101
|
</div>
|
|
105
102
|
|
|
@@ -125,7 +122,7 @@
|
|
|
125
122
|
|
|
126
123
|
<section>
|
|
127
124
|
<h2>Login Credentials</h2>
|
|
128
|
-
<p style="color:
|
|
125
|
+
<p style="color: var(--text-faint); font-size: 0.9rem; margin-bottom: 1.5rem">Manage the login methods linked to your account.</p>
|
|
129
126
|
|
|
130
127
|
<div id="credentials-list" style="margin-bottom: 2rem">{{ssr:credentials_list_html}}</div>
|
|
131
128
|
|
|
@@ -383,7 +380,7 @@
|
|
|
383
380
|
${c.provider.charAt(0).toUpperCase() + c.provider.slice(1)}
|
|
384
381
|
${isCurrent ? '<span class="current-badge">logged in</span>' : ''}
|
|
385
382
|
</div>
|
|
386
|
-
<div style="font-size: 0.8rem; color:
|
|
383
|
+
<div style="font-size: 0.8rem; color: var(--text-faint);">${c.email || c.subject_id}</div>
|
|
387
384
|
</div>
|
|
388
385
|
</div>
|
|
389
386
|
<button class="remove-btn" onclick="removeCredential('${c.provider}')" ${isCurrent || credentials.length === 1 ? 'disabled title="' + (isCurrent ? 'Cannot remove the method you are currently logged in with' : 'Cannot remove your last login method') + '"' : ''}>
|
package/public/users/style.css
CHANGED
|
@@ -1,3 +1,89 @@
|
|
|
1
|
+
/* Color tokens. Every color below is keyed off these variables so the whole
|
|
2
|
+
page can be re-themed by flipping a single set of values — matching how the
|
|
3
|
+
landing page (index.html) and the <power-strip> component theme themselves.
|
|
4
|
+
The light palette is the default; the dark overrides are driven by the
|
|
5
|
+
user's OS color-scheme preference, with a [data-theme] escape hatch so a
|
|
6
|
+
page (or the power strip) can force a theme at runtime. */
|
|
7
|
+
:root {
|
|
8
|
+
--bg: #f9f9f9;
|
|
9
|
+
--surface: #fff;
|
|
10
|
+
--surface-muted: #f8f9fa;
|
|
11
|
+
--surface-alt: #f1f3f4;
|
|
12
|
+
--hover-bg: #f0f0f0;
|
|
13
|
+
--text: #333;
|
|
14
|
+
--text-secondary: #555;
|
|
15
|
+
--text-faint: #666;
|
|
16
|
+
--text-muted: #717171;
|
|
17
|
+
--border: #ddd;
|
|
18
|
+
--border-light: #eee;
|
|
19
|
+
--accent: #ffcc00;
|
|
20
|
+
--accent-hover: #e6b800;
|
|
21
|
+
--accent-text: #826700;
|
|
22
|
+
--accent-soft-bg: #fff7d6;
|
|
23
|
+
--on-accent: #202124;
|
|
24
|
+
--muted-badge-text: #5f6368;
|
|
25
|
+
--danger: #d93025;
|
|
26
|
+
--danger-hover: #ea4335;
|
|
27
|
+
--danger-soft-bg: #fce8e6;
|
|
28
|
+
--disabled-bg: #ccc;
|
|
29
|
+
--avatar-remove-bg: #727579;
|
|
30
|
+
--avatar-remove-icon: #fff;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@media (prefers-color-scheme: dark) {
|
|
34
|
+
:root:not([data-theme='light']) {
|
|
35
|
+
--bg: #1a1a1a;
|
|
36
|
+
--surface: #2d2d2d;
|
|
37
|
+
--surface-muted: #333;
|
|
38
|
+
--surface-alt: #3c4043;
|
|
39
|
+
--hover-bg: #3c4043;
|
|
40
|
+
--text: #e0e0e0;
|
|
41
|
+
--text-secondary: #c0c0c0;
|
|
42
|
+
--text-faint: #bdc1c6;
|
|
43
|
+
--text-muted: #9aa0a6;
|
|
44
|
+
--border: #5f6368;
|
|
45
|
+
--border-light: #444;
|
|
46
|
+
--accent: #ffcc00;
|
|
47
|
+
--accent-hover: #e6b800;
|
|
48
|
+
--accent-text: #ffcc00;
|
|
49
|
+
--accent-soft-bg: #3a3320;
|
|
50
|
+
--on-accent: #202124;
|
|
51
|
+
--muted-badge-text: #a8adb2;
|
|
52
|
+
--danger: #f28b82;
|
|
53
|
+
--danger-hover: #ee675c;
|
|
54
|
+
--danger-soft-bg: #3c2a28;
|
|
55
|
+
--disabled-bg: #5f6368;
|
|
56
|
+
--avatar-remove-bg: #5f6368;
|
|
57
|
+
--avatar-remove-icon: #e0e0e0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
[data-theme='dark'] {
|
|
62
|
+
--bg: #1a1a1a;
|
|
63
|
+
--surface: #2d2d2d;
|
|
64
|
+
--surface-muted: #333;
|
|
65
|
+
--surface-alt: #3c4043;
|
|
66
|
+
--hover-bg: #3c4043;
|
|
67
|
+
--text: #e0e0e0;
|
|
68
|
+
--text-secondary: #c0c0c0;
|
|
69
|
+
--text-faint: #bdc1c6;
|
|
70
|
+
--text-muted: #9aa0a6;
|
|
71
|
+
--border: #5f6368;
|
|
72
|
+
--border-light: #444;
|
|
73
|
+
--accent: #ffcc00;
|
|
74
|
+
--accent-hover: #e6b800;
|
|
75
|
+
--accent-text: #ffcc00;
|
|
76
|
+
--accent-soft-bg: #3a3320;
|
|
77
|
+
--on-accent: #202124;
|
|
78
|
+
--muted-badge-text: #a8adb2;
|
|
79
|
+
--danger: #f28b82;
|
|
80
|
+
--danger-hover: #ee675c;
|
|
81
|
+
--danger-soft-bg: #3c2a28;
|
|
82
|
+
--disabled-bg: #5f6368;
|
|
83
|
+
--avatar-remove-bg: #5f6368;
|
|
84
|
+
--avatar-remove-icon: #e0e0e0;
|
|
85
|
+
}
|
|
86
|
+
|
|
1
87
|
* {
|
|
2
88
|
box-sizing: border-box;
|
|
3
89
|
}
|
|
@@ -9,7 +95,8 @@ body {
|
|
|
9
95
|
sans-serif;
|
|
10
96
|
padding: 2rem;
|
|
11
97
|
margin: 0 auto;
|
|
12
|
-
background:
|
|
98
|
+
background: var(--bg);
|
|
99
|
+
color: var(--text);
|
|
13
100
|
}
|
|
14
101
|
|
|
15
102
|
.main-layout,
|
|
@@ -68,7 +155,7 @@ body {
|
|
|
68
155
|
.nav-link {
|
|
69
156
|
display: block;
|
|
70
157
|
padding: 0.75rem 1rem;
|
|
71
|
-
color:
|
|
158
|
+
color: var(--text-secondary);
|
|
72
159
|
text-decoration: none;
|
|
73
160
|
border-radius: 6px;
|
|
74
161
|
transition: all 0.2s;
|
|
@@ -77,20 +164,20 @@ body {
|
|
|
77
164
|
}
|
|
78
165
|
|
|
79
166
|
.nav-link:hover {
|
|
80
|
-
background:
|
|
81
|
-
color:
|
|
167
|
+
background: var(--hover-bg);
|
|
168
|
+
color: var(--accent-text);
|
|
82
169
|
}
|
|
83
170
|
|
|
84
171
|
.nav-link.active {
|
|
85
|
-
color:
|
|
172
|
+
color: var(--accent-text);
|
|
86
173
|
font-weight: 600;
|
|
87
|
-
border-left: 3px solid
|
|
174
|
+
border-left: 3px solid var(--accent-text);
|
|
88
175
|
border-radius: 0;
|
|
89
176
|
padding-left: calc(1rem - 3px);
|
|
90
177
|
}
|
|
91
178
|
|
|
92
179
|
h1.page-subtitle {
|
|
93
|
-
color:
|
|
180
|
+
color: var(--text-faint);
|
|
94
181
|
margin-bottom: 0.25rem;
|
|
95
182
|
font-size: 1.1rem;
|
|
96
183
|
text-transform: uppercase;
|
|
@@ -101,7 +188,7 @@ h1.page-subtitle {
|
|
|
101
188
|
.page-title {
|
|
102
189
|
font-size: 2.5rem;
|
|
103
190
|
font-weight: bold;
|
|
104
|
-
color:
|
|
191
|
+
color: var(--text);
|
|
105
192
|
margin-bottom: 0.5rem;
|
|
106
193
|
white-space: nowrap;
|
|
107
194
|
overflow: hidden;
|
|
@@ -111,7 +198,7 @@ h1.page-subtitle {
|
|
|
111
198
|
|
|
112
199
|
.subtitle {
|
|
113
200
|
font-size: 0.75rem;
|
|
114
|
-
color:
|
|
201
|
+
color: var(--text-muted);
|
|
115
202
|
margin-bottom: 2rem;
|
|
116
203
|
font-family: monospace;
|
|
117
204
|
display: flex;
|
|
@@ -121,7 +208,7 @@ h1.page-subtitle {
|
|
|
121
208
|
}
|
|
122
209
|
|
|
123
210
|
section {
|
|
124
|
-
background:
|
|
211
|
+
background: var(--surface);
|
|
125
212
|
padding: 1.5rem;
|
|
126
213
|
border-radius: 8px;
|
|
127
214
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
@@ -136,28 +223,30 @@ section {
|
|
|
136
223
|
display: block;
|
|
137
224
|
margin-bottom: 0.5rem;
|
|
138
225
|
font-weight: 500;
|
|
139
|
-
color:
|
|
226
|
+
color: var(--text-secondary);
|
|
140
227
|
}
|
|
141
228
|
|
|
142
229
|
.form-group input,
|
|
143
230
|
.form-group select {
|
|
144
231
|
width: 100%;
|
|
145
232
|
padding: 0.75rem;
|
|
146
|
-
border: 1px solid
|
|
233
|
+
border: 1px solid var(--border);
|
|
147
234
|
border-radius: 4px;
|
|
148
235
|
box-sizing: border-box;
|
|
149
236
|
font-size: 1rem;
|
|
237
|
+
background: var(--surface);
|
|
238
|
+
color: var(--text);
|
|
150
239
|
}
|
|
151
240
|
|
|
152
241
|
.form-group input:disabled {
|
|
153
|
-
background:
|
|
154
|
-
color:
|
|
242
|
+
background: var(--surface-muted);
|
|
243
|
+
color: var(--text-muted);
|
|
155
244
|
}
|
|
156
245
|
|
|
157
246
|
button {
|
|
158
247
|
padding: 0.75rem 1.5rem;
|
|
159
|
-
background:
|
|
160
|
-
color:
|
|
248
|
+
background: var(--accent);
|
|
249
|
+
color: var(--on-accent);
|
|
161
250
|
border: none;
|
|
162
251
|
border-radius: 4px;
|
|
163
252
|
cursor: pointer;
|
|
@@ -166,21 +255,21 @@ button {
|
|
|
166
255
|
}
|
|
167
256
|
|
|
168
257
|
button:hover {
|
|
169
|
-
background:
|
|
258
|
+
background: var(--accent-hover);
|
|
170
259
|
}
|
|
171
260
|
|
|
172
261
|
button.secondary-btn {
|
|
173
|
-
background:
|
|
174
|
-
color:
|
|
175
|
-
border: 1px solid
|
|
262
|
+
background: var(--surface);
|
|
263
|
+
color: var(--accent-text);
|
|
264
|
+
border: 1px solid var(--accent-text);
|
|
176
265
|
}
|
|
177
266
|
|
|
178
267
|
button.secondary-btn:hover {
|
|
179
|
-
background:
|
|
268
|
+
background: var(--surface-muted);
|
|
180
269
|
}
|
|
181
270
|
|
|
182
271
|
button:disabled {
|
|
183
|
-
background:
|
|
272
|
+
background: var(--disabled-bg);
|
|
184
273
|
cursor: not-allowed;
|
|
185
274
|
}
|
|
186
275
|
|
|
@@ -200,7 +289,7 @@ button:disabled {
|
|
|
200
289
|
.back-link {
|
|
201
290
|
display: inline-block;
|
|
202
291
|
margin-bottom: 1rem;
|
|
203
|
-
color:
|
|
292
|
+
color: var(--accent-text);
|
|
204
293
|
text-decoration: none;
|
|
205
294
|
}
|
|
206
295
|
|
|
@@ -210,29 +299,29 @@ button:disabled {
|
|
|
210
299
|
|
|
211
300
|
.remove-btn {
|
|
212
301
|
background: transparent;
|
|
213
|
-
color:
|
|
214
|
-
border: 1px solid
|
|
302
|
+
color: var(--danger);
|
|
303
|
+
border: 1px solid var(--danger);
|
|
215
304
|
padding: 0.4rem 0.8rem;
|
|
216
305
|
font-size: 0.85rem;
|
|
217
306
|
}
|
|
218
307
|
|
|
219
308
|
.remove-btn:hover {
|
|
220
|
-
background:
|
|
309
|
+
background: var(--danger-soft-bg);
|
|
221
310
|
}
|
|
222
311
|
|
|
223
312
|
.remove-btn:disabled {
|
|
224
|
-
background:
|
|
225
|
-
border-color:
|
|
226
|
-
color:
|
|
313
|
+
background: var(--surface-muted);
|
|
314
|
+
border-color: var(--border-light);
|
|
315
|
+
color: var(--text-muted);
|
|
227
316
|
cursor: not-allowed;
|
|
228
317
|
}
|
|
229
318
|
|
|
230
319
|
.btn-link {
|
|
231
320
|
display: inline-block;
|
|
232
321
|
padding: 0.75rem 1rem;
|
|
233
|
-
background:
|
|
234
|
-
color:
|
|
235
|
-
border: 1px solid
|
|
322
|
+
background: var(--surface);
|
|
323
|
+
color: var(--accent-text);
|
|
324
|
+
border: 1px solid var(--accent-text);
|
|
236
325
|
border-radius: 4px;
|
|
237
326
|
text-decoration: none;
|
|
238
327
|
font-weight: 500;
|
|
@@ -241,7 +330,7 @@ button:disabled {
|
|
|
241
330
|
}
|
|
242
331
|
|
|
243
332
|
.btn-link:hover {
|
|
244
|
-
background:
|
|
333
|
+
background: var(--surface-muted);
|
|
245
334
|
}
|
|
246
335
|
|
|
247
336
|
.remove-image-btn {
|
|
@@ -251,9 +340,9 @@ button:disabled {
|
|
|
251
340
|
width: 20px;
|
|
252
341
|
height: 20px;
|
|
253
342
|
border-radius: 50%;
|
|
254
|
-
background:
|
|
255
|
-
color:
|
|
256
|
-
border: 2px solid
|
|
343
|
+
background: var(--avatar-remove-bg);
|
|
344
|
+
color: var(--avatar-remove-icon);
|
|
345
|
+
border: 2px solid var(--surface);
|
|
257
346
|
cursor: pointer;
|
|
258
347
|
display: flex;
|
|
259
348
|
align-items: center;
|
|
@@ -284,7 +373,7 @@ button:disabled {
|
|
|
284
373
|
height: 100px;
|
|
285
374
|
border-radius: 50%;
|
|
286
375
|
object-fit: cover;
|
|
287
|
-
border: 3px solid
|
|
376
|
+
border: 3px solid var(--surface);
|
|
288
377
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
289
378
|
}
|
|
290
379
|
|
|
@@ -293,7 +382,7 @@ button:disabled {
|
|
|
293
382
|
height: 100px;
|
|
294
383
|
border-radius: 8px;
|
|
295
384
|
object-fit: cover;
|
|
296
|
-
border: 3px solid
|
|
385
|
+
border: 3px solid var(--surface);
|
|
297
386
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
298
387
|
}
|
|
299
388
|
|
|
@@ -302,20 +391,20 @@ button:disabled {
|
|
|
302
391
|
justify-content: space-between;
|
|
303
392
|
align-items: center;
|
|
304
393
|
padding: 1rem;
|
|
305
|
-
border: 1px solid
|
|
394
|
+
border: 1px solid var(--border-light);
|
|
306
395
|
border-radius: 8px;
|
|
307
396
|
margin-bottom: 0.75rem;
|
|
308
397
|
}
|
|
309
398
|
|
|
310
399
|
.credential-item.active {
|
|
311
|
-
border-color:
|
|
312
|
-
background-color:
|
|
400
|
+
border-color: var(--accent-text);
|
|
401
|
+
background-color: var(--accent-soft-bg);
|
|
313
402
|
}
|
|
314
403
|
|
|
315
404
|
.current-badge {
|
|
316
405
|
font-size: 0.75rem;
|
|
317
|
-
background:
|
|
318
|
-
color:
|
|
406
|
+
background: var(--accent);
|
|
407
|
+
color: var(--on-accent);
|
|
319
408
|
padding: 0.125rem 0.375rem;
|
|
320
409
|
border-radius: 0.75rem;
|
|
321
410
|
margin-left: 0.5rem;
|
|
@@ -349,13 +438,13 @@ button:disabled {
|
|
|
349
438
|
text-decoration: none;
|
|
350
439
|
font-weight: 500;
|
|
351
440
|
font-size: 0.875rem;
|
|
352
|
-
border: 1px solid
|
|
353
|
-
color:
|
|
441
|
+
border: 1px solid var(--border);
|
|
442
|
+
color: var(--text);
|
|
354
443
|
transition: background 0.2s;
|
|
355
444
|
}
|
|
356
445
|
|
|
357
446
|
.link-account-btn.google:hover {
|
|
358
|
-
background:
|
|
447
|
+
background: var(--surface-muted);
|
|
359
448
|
}
|
|
360
449
|
|
|
361
450
|
.link-account-btn.twitch {
|
|
@@ -378,7 +467,7 @@ button:disabled {
|
|
|
378
467
|
justify-content: space-between;
|
|
379
468
|
align-items: center;
|
|
380
469
|
padding: 1rem;
|
|
381
|
-
border-bottom: 1px solid
|
|
470
|
+
border-bottom: 1px solid var(--border-light);
|
|
382
471
|
}
|
|
383
472
|
|
|
384
473
|
.member-item:last-child {
|
|
@@ -398,11 +487,11 @@ button:disabled {
|
|
|
398
487
|
border-radius: 50%;
|
|
399
488
|
object-fit: cover;
|
|
400
489
|
flex-shrink: 0;
|
|
401
|
-
background:
|
|
490
|
+
background: var(--surface-alt);
|
|
402
491
|
display: flex;
|
|
403
492
|
align-items: center;
|
|
404
493
|
justify-content: center;
|
|
405
|
-
color:
|
|
494
|
+
color: var(--muted-badge-text);
|
|
406
495
|
}
|
|
407
496
|
|
|
408
497
|
.member-avatar svg {
|
|
@@ -436,27 +525,28 @@ button:disabled {
|
|
|
436
525
|
font-size: 0.75rem;
|
|
437
526
|
padding: 0.25rem 0.5rem;
|
|
438
527
|
border-radius: 1rem;
|
|
439
|
-
background:
|
|
440
|
-
color:
|
|
528
|
+
background: var(--surface-alt);
|
|
529
|
+
color: var(--muted-badge-text);
|
|
441
530
|
font-weight: 500;
|
|
442
531
|
}
|
|
443
532
|
|
|
444
533
|
.role-badge.admin {
|
|
445
|
-
background:
|
|
446
|
-
color:
|
|
534
|
+
background: var(--accent-soft-bg);
|
|
535
|
+
color: var(--accent-text);
|
|
447
536
|
}
|
|
448
537
|
|
|
449
538
|
.role-select {
|
|
450
539
|
padding: 0.25rem 0.5rem;
|
|
451
540
|
border-radius: 4px;
|
|
452
|
-
border: 1px solid
|
|
541
|
+
border: 1px solid var(--border);
|
|
453
542
|
font-size: 0.85rem;
|
|
454
|
-
background:
|
|
543
|
+
background: var(--surface);
|
|
544
|
+
color: var(--text);
|
|
455
545
|
}
|
|
456
546
|
|
|
457
547
|
.role-select:disabled {
|
|
458
|
-
background:
|
|
459
|
-
color:
|
|
548
|
+
background: var(--surface-alt);
|
|
549
|
+
color: var(--muted-badge-text);
|
|
460
550
|
border-color: transparent;
|
|
461
551
|
appearance: none;
|
|
462
552
|
-webkit-appearance: none;
|
|
@@ -475,7 +565,7 @@ button:disabled {
|
|
|
475
565
|
border: none;
|
|
476
566
|
padding: 0.25rem;
|
|
477
567
|
cursor: pointer;
|
|
478
|
-
color:
|
|
568
|
+
color: var(--accent-text);
|
|
479
569
|
display: flex;
|
|
480
570
|
align-items: center;
|
|
481
571
|
justify-content: center;
|
|
@@ -484,7 +574,7 @@ button:disabled {
|
|
|
484
574
|
}
|
|
485
575
|
|
|
486
576
|
.copy-btn:hover {
|
|
487
|
-
background:
|
|
577
|
+
background: var(--hover-bg);
|
|
488
578
|
}
|
|
489
579
|
|
|
490
580
|
.copy-btn svg {
|
package/src/PowerStrip.ts
CHANGED
|
@@ -31,7 +31,7 @@ export async function injectPowerStrip(response: Response, usersPath: string, pr
|
|
|
31
31
|
element.onEndTag((end) => {
|
|
32
32
|
if (!hasUserPowerStrip) {
|
|
33
33
|
end.before(
|
|
34
|
-
`<power-strip providers="${providersAttr}" style="position: absolute; top: 0; right: 0; z-index: 9999;
|
|
34
|
+
`<power-strip providers="${providersAttr}" style="position: absolute; top: 0; right: 0; z-index: 9999; border-radius: 0 0 0 0.3rem;">` +
|
|
35
35
|
'<svg viewBox="0 0 24 24" style="width: 1rem; height: 1rem;"><path d="M7 2v11h3v9l7-12h-4l4-8z"/></svg>' +
|
|
36
36
|
'</power-strip>',
|
|
37
37
|
{ html: true },
|