@glimstone/core 4.1.1 → 4.2.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 +9 -3
- package/dist/appearance.d.ts +37 -9
- package/dist/appearance.js +86 -35
- package/dist/browserAccent.d.ts +12 -0
- package/dist/browserAccent.js +66 -0
- package/dist/contrast.d.ts +26 -0
- package/dist/contrast.js +107 -0
- package/dist/disco.js +2 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/signIn.d.ts +136 -0
- package/dist/signIn.js +225 -0
- package/dist/theme.d.ts +14 -0
- package/dist/theme.js +71 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/glyph-map.md +1 -0
- package/glyphs.json +13 -0
- package/glyphs.md +4 -3
- package/package.json +1 -1
- package/tokens.css +168 -10
package/dist/signIn.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// The sign-in card's flow without a framework (docs/design-language.md, "The
|
|
2
|
+
// sign-in card"). The app answers each step through a callback, the flow keeps
|
|
3
|
+
// what was typed and which form shows, and the React and Vue cards draw it.
|
|
4
|
+
import { shake } from "./selectModel.js";
|
|
5
|
+
/** English words, for an app in English and for the demos. */
|
|
6
|
+
export const signInTextEn = {
|
|
7
|
+
password: "Password",
|
|
8
|
+
signIn: "Sign in",
|
|
9
|
+
signingIn: "Signing in…",
|
|
10
|
+
passkey: "Sign in with a passkey",
|
|
11
|
+
forgot: "Forgot password?",
|
|
12
|
+
code: "Code",
|
|
13
|
+
codeHint: "Six digits from your authenticator app, or one of your recovery codes.",
|
|
14
|
+
codeInvalid: "That code is not valid.",
|
|
15
|
+
back: "Back",
|
|
16
|
+
resetCode: "Code from the container log",
|
|
17
|
+
resetCodeTip: "The app has written a code to its container log. On Unraid: Docker tab, the app's icon, Logs. A code is valid for 10 minutes.",
|
|
18
|
+
newPassword: "New password",
|
|
19
|
+
confirmPassword: "Confirm password",
|
|
20
|
+
setPassword: "Set password",
|
|
21
|
+
saving: "Saving…",
|
|
22
|
+
mismatch: "The two passwords are not the same.",
|
|
23
|
+
tooShort: "The new password needs at least 12 characters.",
|
|
24
|
+
resetRejected: "That code is wrong or has expired. If it expired, the log now shows a new one.",
|
|
25
|
+
failed: "That did not work. Please try again.",
|
|
26
|
+
show: "Show",
|
|
27
|
+
hide: "Hide",
|
|
28
|
+
};
|
|
29
|
+
const START = {
|
|
30
|
+
view: "signIn",
|
|
31
|
+
password: "",
|
|
32
|
+
code: "",
|
|
33
|
+
needCode: false,
|
|
34
|
+
viaPasskey: false,
|
|
35
|
+
resetCode: "",
|
|
36
|
+
newPassword: "",
|
|
37
|
+
confirm: "",
|
|
38
|
+
error: null,
|
|
39
|
+
refusal: null,
|
|
40
|
+
busy: null,
|
|
41
|
+
};
|
|
42
|
+
const BUTTON_OF = {
|
|
43
|
+
signIn: "submit",
|
|
44
|
+
setPassword: "set-password",
|
|
45
|
+
passkey: "passkey",
|
|
46
|
+
askCode: "forgot",
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The flow behind one card. `options` is read at every step, so a framework
|
|
50
|
+
* hands in its current props rather than the ones the card was made with.
|
|
51
|
+
*/
|
|
52
|
+
export function createSignInFlow(options) {
|
|
53
|
+
let state = START;
|
|
54
|
+
const listeners = new Set();
|
|
55
|
+
const update = (change) => {
|
|
56
|
+
state = { ...state, ...change };
|
|
57
|
+
listeners.forEach((fn) => fn());
|
|
58
|
+
};
|
|
59
|
+
// A refusal during a step waits for the step to end: the busy button wears a
|
|
60
|
+
// bubble that wraps it, and the shake has to land on the button that stays.
|
|
61
|
+
let pending = {};
|
|
62
|
+
const refuse = (error, button, field = null) => {
|
|
63
|
+
const change = { error, refusal: { button, field, count: (state.refusal?.count ?? 0) + 1 } };
|
|
64
|
+
if (state.busy)
|
|
65
|
+
pending = change;
|
|
66
|
+
else
|
|
67
|
+
update(change);
|
|
68
|
+
};
|
|
69
|
+
async function run(busy, work) {
|
|
70
|
+
if (state.busy)
|
|
71
|
+
return;
|
|
72
|
+
update({ busy, error: null });
|
|
73
|
+
pending = {};
|
|
74
|
+
try {
|
|
75
|
+
await work();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
refuse(options().text.failed, BUTTON_OF[busy]);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
update({ busy: null, ...pending });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function signIn() {
|
|
85
|
+
if (signInBlocked(state))
|
|
86
|
+
return;
|
|
87
|
+
const { text, onSignIn } = options();
|
|
88
|
+
return run("signIn", async () => {
|
|
89
|
+
const tried = state.needCode;
|
|
90
|
+
const res = await onSignIn({
|
|
91
|
+
password: state.viaPasskey ? "" : state.password,
|
|
92
|
+
code: tried ? state.code.trim() : undefined,
|
|
93
|
+
passkey: state.viaPasskey,
|
|
94
|
+
});
|
|
95
|
+
if (res.ok)
|
|
96
|
+
return;
|
|
97
|
+
if (res.needCode) {
|
|
98
|
+
// The first request for a code only opens the field.
|
|
99
|
+
update({ needCode: true, code: "" });
|
|
100
|
+
if (tried)
|
|
101
|
+
refuse(res.error ?? text.codeInvalid, "submit", "code");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// A passkey's sign-in that ended starts over with the password.
|
|
105
|
+
if (state.viaPasskey)
|
|
106
|
+
update({ viaPasskey: false, needCode: false, code: "" });
|
|
107
|
+
refuse(res.error ?? text.failed, "submit", state.needCode ? "code" : "password");
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async function setPassword() {
|
|
111
|
+
const { text, minPassword, onSetPassword, onAskCode } = options();
|
|
112
|
+
if (resetBlocked(state) || !onSetPassword)
|
|
113
|
+
return;
|
|
114
|
+
if (state.newPassword !== state.confirm)
|
|
115
|
+
return refuse(text.mismatch, "set-password", "confirm");
|
|
116
|
+
if ([...state.newPassword].length < minPassword)
|
|
117
|
+
return refuse(text.tooShort, "set-password", "newPassword");
|
|
118
|
+
return run("setPassword", async () => {
|
|
119
|
+
const res = await onSetPassword({ code: state.resetCode.trim(), password: state.newPassword });
|
|
120
|
+
if (res.ok)
|
|
121
|
+
return;
|
|
122
|
+
if (res.rejected) {
|
|
123
|
+
// The answer does not say whether the code expired, and the sentence
|
|
124
|
+
// promises a new one in that case. Asking writes one only when none is live.
|
|
125
|
+
await onAskCode?.().catch(() => undefined);
|
|
126
|
+
refuse(text.resetRejected, "set-password", "resetCode");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
refuse(res.error ?? text.failed, "set-password", "resetCode");
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
state: () => state,
|
|
134
|
+
subscribe(listener) {
|
|
135
|
+
listeners.add(listener);
|
|
136
|
+
return () => listeners.delete(listener);
|
|
137
|
+
},
|
|
138
|
+
set(field, value) {
|
|
139
|
+
update({ [field]: value });
|
|
140
|
+
},
|
|
141
|
+
submit: () => (state.view === "reset" ? setPassword() : signIn()),
|
|
142
|
+
async passkey() {
|
|
143
|
+
const { onPasskey } = options();
|
|
144
|
+
if (!onPasskey)
|
|
145
|
+
return;
|
|
146
|
+
return run("passkey", async () => {
|
|
147
|
+
const res = await onPasskey();
|
|
148
|
+
if (res.ok)
|
|
149
|
+
return;
|
|
150
|
+
if (res.needCode)
|
|
151
|
+
update({ viaPasskey: true, needCode: true, code: "" });
|
|
152
|
+
else if (res.error !== undefined)
|
|
153
|
+
refuse(res.error, "passkey");
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
async forgot() {
|
|
157
|
+
const { text, onAskCode } = options();
|
|
158
|
+
if (!onAskCode)
|
|
159
|
+
return;
|
|
160
|
+
return run("askCode", async () => {
|
|
161
|
+
const res = await onAskCode();
|
|
162
|
+
if (res.ok)
|
|
163
|
+
update({ view: "reset", resetCode: "", newPassword: "", confirm: "" });
|
|
164
|
+
else
|
|
165
|
+
refuse(res.error ?? text.failed, "forgot");
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
back() {
|
|
169
|
+
if (state.busy)
|
|
170
|
+
return;
|
|
171
|
+
update({ view: "signIn", needCode: false, viaPasskey: false, code: "", error: null });
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** Sign in waits for the password, and in the second step for the code. */
|
|
176
|
+
export function signInBlocked(s) {
|
|
177
|
+
if (s.busy)
|
|
178
|
+
return true;
|
|
179
|
+
if (s.needCode)
|
|
180
|
+
return s.code.trim() === "";
|
|
181
|
+
return s.password === "";
|
|
182
|
+
}
|
|
183
|
+
/** Set password waits for the code from the log and a new password. */
|
|
184
|
+
export function resetBlocked(s) {
|
|
185
|
+
return !!s.busy || s.resetCode.trim() === "" || s.newPassword === "";
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Which form shows, as a key that changes whenever the focus should move to
|
|
189
|
+
* the first empty field of the new form.
|
|
190
|
+
*/
|
|
191
|
+
export function signInStage(s) {
|
|
192
|
+
return `${s.view}:${s.needCode}:${s.viaPasskey}`;
|
|
193
|
+
}
|
|
194
|
+
// Walks up rather than reading the layout, which a page without one, such as a test's, does not have.
|
|
195
|
+
function shown(el) {
|
|
196
|
+
for (let e = el; e; e = e.parentElement)
|
|
197
|
+
if (getComputedStyle(e).display === "none")
|
|
198
|
+
return false;
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
const FIELDS = 'input:not([type="hidden"], [type="checkbox"], [type="radio"], :disabled), textarea:not(:disabled)';
|
|
202
|
+
/**
|
|
203
|
+
* Puts the focus in the first empty field inside `root`, or in its first field
|
|
204
|
+
* when all hold something, such as a name the app filled in.
|
|
205
|
+
*/
|
|
206
|
+
export function focusFirstField(root) {
|
|
207
|
+
if (!root)
|
|
208
|
+
return;
|
|
209
|
+
const fields = [...root.querySelectorAll(FIELDS)].filter(shown);
|
|
210
|
+
(fields.find((f) => f.value === "") ?? fields[0])?.focus({ preventScroll: true });
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Answers a refusal on screen: the button shakes once (`glim-shake`, which the
|
|
214
|
+
* motion engine stills) and the field takes the focus with its content
|
|
215
|
+
* selected, so the next try starts by typing. Returns the shake's cleanup.
|
|
216
|
+
*/
|
|
217
|
+
export function showRefusal(root, refusal) {
|
|
218
|
+
const field = refusal.field && root?.querySelector(`[data-signin-field="${refusal.field}"]`);
|
|
219
|
+
if (field) {
|
|
220
|
+
field.focus({ preventScroll: true });
|
|
221
|
+
field.select();
|
|
222
|
+
}
|
|
223
|
+
const button = root?.querySelector(`[data-signin="${refusal.button}"]`);
|
|
224
|
+
return button ? shake(button) : () => { };
|
|
225
|
+
}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Whether the dark ramp shows for a `data-theme` value and the system's preference. */
|
|
2
|
+
export declare function resolvesDark(theme: string | null | undefined, systemDark: boolean): boolean;
|
|
3
|
+
/** Whether the dark ramp is showing now, by choice or because the system is dark. */
|
|
4
|
+
export declare function darkShowing(): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Calls `fn` whenever the dark ramp starts or stops showing, through a theme
|
|
7
|
+
* choice or the system, so a settings page can say whether OLED is in effect.
|
|
8
|
+
* Returns the function that stops watching.
|
|
9
|
+
*/
|
|
10
|
+
export declare function watchDarkShowing(fn: (dark: boolean) => void): () => void;
|
|
11
|
+
/** Turns OLED on or off and remembers it on this device. */
|
|
12
|
+
export declare function applyOled(on: boolean): void;
|
|
13
|
+
/** Whether OLED is on for this device. */
|
|
14
|
+
export declare function storedOled(): boolean;
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Which ramp is showing, and the OLED variant of the dark one. The theme
|
|
2
|
+
// itself is `data-theme` on <html>, `light`, `dark` or unset for the system.
|
|
3
|
+
// OLED is `data-oled` on <html>, present or absent; tokens.css applies it only
|
|
4
|
+
// while the dark ramp shows, so it can stay on through a switch to light.
|
|
5
|
+
//
|
|
6
|
+
// OLED belongs to the screen, not the person, so it is kept on the device and
|
|
7
|
+
// never with settings a server syncs: the same account may sign in on an LCD
|
|
8
|
+
// laptop.
|
|
9
|
+
import { storageKey } from './storage.js';
|
|
10
|
+
const SYSTEM_DARK = '(prefers-color-scheme: dark)';
|
|
11
|
+
/** Whether the dark ramp shows for a `data-theme` value and the system's preference. */
|
|
12
|
+
export function resolvesDark(theme, systemDark) {
|
|
13
|
+
if (theme === 'dark')
|
|
14
|
+
return true;
|
|
15
|
+
if (theme === 'light')
|
|
16
|
+
return false;
|
|
17
|
+
return systemDark;
|
|
18
|
+
}
|
|
19
|
+
/** Whether the dark ramp is showing now, by choice or because the system is dark. */
|
|
20
|
+
export function darkShowing() {
|
|
21
|
+
return resolvesDark(document.documentElement.getAttribute('data-theme'), matchMedia(SYSTEM_DARK).matches);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Calls `fn` whenever the dark ramp starts or stops showing, through a theme
|
|
25
|
+
* choice or the system, so a settings page can say whether OLED is in effect.
|
|
26
|
+
* Returns the function that stops watching.
|
|
27
|
+
*/
|
|
28
|
+
export function watchDarkShowing(fn) {
|
|
29
|
+
let last = darkShowing();
|
|
30
|
+
const check = () => {
|
|
31
|
+
const next = darkShowing();
|
|
32
|
+
if (next === last)
|
|
33
|
+
return;
|
|
34
|
+
last = next;
|
|
35
|
+
fn(next);
|
|
36
|
+
};
|
|
37
|
+
const media = matchMedia(SYSTEM_DARK);
|
|
38
|
+
media.addEventListener('change', check);
|
|
39
|
+
const observer = new MutationObserver(check);
|
|
40
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
41
|
+
return () => {
|
|
42
|
+
media.removeEventListener('change', check);
|
|
43
|
+
observer.disconnect();
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Turns OLED on or off and remembers it on this device. */
|
|
47
|
+
export function applyOled(on) {
|
|
48
|
+
const root = document.documentElement;
|
|
49
|
+
if (on)
|
|
50
|
+
root.setAttribute('data-oled', '');
|
|
51
|
+
else
|
|
52
|
+
root.removeAttribute('data-oled');
|
|
53
|
+
try {
|
|
54
|
+
if (on)
|
|
55
|
+
localStorage.setItem(storageKey('oled'), '1');
|
|
56
|
+
else
|
|
57
|
+
localStorage.removeItem(storageKey('oled'));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// The switch still holds until the page closes.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Whether OLED is on for this device. */
|
|
64
|
+
export function storedOled() {
|
|
65
|
+
try {
|
|
66
|
+
return localStorage.getItem(storageKey('oled')) === '1';
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/glyph-map.md
CHANGED
|
@@ -228,6 +228,7 @@ ArrowLoop drew the glyphs for the segments of its Design, Corners and Motion sel
|
|
|
228
228
|
| Row height selector, Compact | `IconRowCompact` | none | none | `IconRowCompact`, standard |
|
|
229
229
|
| Row height selector, Medium | `IconRowMedium` | none | none | `IconRowMedium`, standard |
|
|
230
230
|
| Row height selector, Comfortable | `IconRowComfortable` | none | none | `IconRowComfortable`, standard |
|
|
231
|
+
| Accent row, the swatch that takes the system's colour | `IconAccentSystem` | none | none | none |
|
|
231
232
|
|
|
232
233
|
<br>
|
|
233
234
|
|
package/glyphs.json
CHANGED
|
@@ -1234,6 +1234,19 @@
|
|
|
1234
1234
|
],
|
|
1235
1235
|
"svg": "<path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M0 3A1.2 1.2 0 0 1 1.2 1.8H12.8A1.2 1.2 0 0 1 12.8 4.2H1.2A1.2 1.2 0 0 1 0 3ZM0 11A1.2 1.2 0 0 1 1.2 9.8H12.8A1.2 1.2 0 0 1 12.8 12.2H1.2A1.2 1.2 0 0 1 0 11Z\" />"
|
|
1236
1236
|
},
|
|
1237
|
+
{
|
|
1238
|
+
"name": "IconAccentSystem",
|
|
1239
|
+
"means": "The accent takes the system's colour",
|
|
1240
|
+
"source": "Drawn for GlimStone",
|
|
1241
|
+
"viewBox": "0.5 0.5 13 13",
|
|
1242
|
+
"ink": [
|
|
1243
|
+
0.5,
|
|
1244
|
+
0.5,
|
|
1245
|
+
13,
|
|
1246
|
+
13
|
|
1247
|
+
],
|
|
1248
|
+
"svg": "<path fill=\"currentColor\" d=\"M0.793 11.793L6.893 5.693A1 1 0 0 1 8.307 7.107L2.207 13.207A1 1 0 0 1 0.793 11.793ZM10.5 0.5Q11 3 13.5 3.5Q11 4 10.5 6.5Q10 4 7.5 3.5Q10 3 10.5 0.5ZM3.75 2.25Q4.1 3.65 5.5 4Q4.1 4.35 3.75 5.75Q3.4 4.35 2 4Q3.4 3.65 3.75 2.25ZM11 9Q11.35 10.4 12.75 10.75Q11.35 11.1 11 12.5Q10.65 11.1 9.25 10.75Q10.65 10.4 11 9Z\"></path>"
|
|
1249
|
+
},
|
|
1237
1250
|
{
|
|
1238
1251
|
"name": "IconGear",
|
|
1239
1252
|
"means": "Settings, open a setting",
|
package/glyphs.md
CHANGED
|
@@ -259,7 +259,7 @@ A file manager tells a file's kind by its glyph before its name is read, so thes
|
|
|
259
259
|
| `IconDiagnostics` | Diagnostics | Streamline `programming/bug.svg` |
|
|
260
260
|
| `IconKeyboard` | Keyboard shortcuts | Streamline `computer-devices/keyboard.svg` |
|
|
261
261
|
|
|
262
|
-
### Look settings, the segments of the Design, Corners, Motion, Labels and Row height selectors
|
|
262
|
+
### Look settings, the segments of the Design, Corners, Motion, Labels and Row height selectors, and the system accent swatch
|
|
263
263
|
|
|
264
264
|
| Name | Means | Source |
|
|
265
265
|
| --- | --- | --- |
|
|
@@ -279,8 +279,9 @@ A file manager tells a file's kind by its glyph before its name is read, so thes
|
|
|
279
279
|
| `IconRowCompact` | Compact rows | Drawn for GlimStone |
|
|
280
280
|
| `IconRowMedium` | Medium rows | Drawn for GlimStone |
|
|
281
281
|
| `IconRowComfortable` | Comfortable rows | Drawn for GlimStone |
|
|
282
|
+
| `IconAccentSystem` | The accent takes the system's colour | Drawn for GlimStone |
|
|
282
283
|
|
|
283
|
-
The Design selector's Dark and Light segments wear `IconMoon` and `IconSun` from the actions above, and System wears `IconThemeSystem`, a ring with one half filled. Each corner glyph is a frame drawn with the corners it names, so the four read as one family whatever corners the selector around them has. The motion glyphs are one line that swings further at each level, just as a level is the same animation with bigger numbers. `IconCornerLeaf` and `IconMotionStorm` belong to the hidden shape and the hidden level and stand only where the selector offers those. The four label glyphs draw a choice with a disc for the glyph and bars for the words: three bars for Text, a small disc before two bars for Text and glyph, a large disc alone for Glyph, and for Reactive a disc with a pointer arrow at its corner, since that mode opens the words under the pointer. The row glyphs are bars across the full width at three spacings, four close ones for Compact, three for Medium and two far apart for Comfortable. Only the gap between the bars sets the three apart.
|
|
284
|
+
The Design selector's Dark and Light segments wear `IconMoon` and `IconSun` from the actions above, and System wears `IconThemeSystem`, a ring with one half filled. Each corner glyph is a frame drawn with the corners it names, so the four read as one family whatever corners the selector around them has. The motion glyphs are one line that swings further at each level, just as a level is the same animation with bigger numbers. `IconCornerLeaf` and `IconMotionStorm` belong to the hidden shape and the hidden level and stand only where the selector offers those. The four label glyphs draw a choice with a disc for the glyph and bars for the words: three bars for Text, a small disc before two bars for Text and glyph, a large disc alone for Glyph, and for Reactive a disc with a pointer arrow at its corner, since that mode opens the words under the pointer. The row glyphs are bars across the full width at three spacings, four close ones for Compact, three for Medium and two far apart for Comfortable. Only the gap between the bars sets the three apart. `IconAccentSystem` stands on the swatch before the accent presets that takes the colour from the device: a wand with a large spark at its tip and two small ones beside it.
|
|
284
285
|
|
|
285
286
|
**`IconGrip` is two columns of three dots**, the handle TowerTorch and KnightLoader each drew for a row that drags.
|
|
286
287
|
|
|
@@ -288,7 +289,7 @@ The Design selector's Dark and Light segments wear `IconMoon` and `IconSun` from
|
|
|
288
289
|
|
|
289
290
|
**Taking something away draws a slash through it.** `IconEyeOff`, `IconKeyRevoke`, `IconShieldOff` and `IconUnlink` are an eye, a key, a shield and a chain link with one bar from the top left to the bottom right, so the undoing reads the same on every one of them.
|
|
290
291
|
|
|
291
|
-
**Close meanings wear different drawings.** `IconRetry` tries again, reloads or refreshes, and `IconRefresh` goes back to the default whatever its name says. `IconResetArrow` does the same for one row inside a colour card, where a full-size reset beside the swatches would read as resetting the card. `IconQueued` waits its turn, and `IconHourglass` marks a file that is a backup copy or not finished yet. `IconUpload` and `IconDownload` move a file to or from somewhere else, while `IconImport` and `IconExport` read a list or settings in and write them out. `IconCheck` marks a test and the success it reports, and `IconConfirm` is a person agreeing: Done, Confirm, Got it. `IconApply` takes something over in place of what was there, which is not saving. `IconCompressAuto` marks anything the app chooses by itself, the compression as well as the order of the providers, and keeps its name for the apps that generate from this list. `IconSync` checks for an update and fetches it, and `IconUpdate` marks an updates tab or section, where its cloud has room to read. The check answers with `IconUpToDate` when there was none to fetch, the cloud with a tick, and with `IconUpdateAvailable` when there is. `IconMoon` is the dark look and nothing else; a disk spinning down, a machine going to rest and a VM put into hibernation wear `IconSleep`.
|
|
292
|
+
**Close meanings wear different drawings.** `IconRetry` tries again, reloads or refreshes, and `IconRefresh` goes back to the default whatever its name says. `IconResetArrow` does the same for one row inside a colour card, where a full-size reset beside the swatches would read as resetting the card. `IconQueued` waits its turn, and `IconHourglass` marks a file that is a backup copy or not finished yet. `IconUpload` and `IconDownload` move a file to or from somewhere else, while `IconImport` and `IconExport` read a list or settings in and write them out. `IconCheck` marks a test and the success it reports, and `IconConfirm` is a person agreeing: Done, Confirm, Got it. `IconApply` takes something over in place of what was there, which is not saving. `IconCompressAuto` marks anything the app chooses by itself, the compression as well as the order of the providers, and keeps its name for the apps that generate from this list. Its wand and spark are Streamline's, and `IconAccentSystem` draws its own with three sparks, since taking the device's colour is a different meaning. `IconSync` checks for an update and fetches it, and `IconUpdate` marks an updates tab or section, where its cloud has room to read. The check answers with `IconUpToDate` when there was none to fetch, the cloud with a tick, and with `IconUpdateAvailable` when there is. `IconMoon` is the dark look and nothing else; a disk spinning down, a machine going to rest and a VM put into hibernation wear `IconSleep`.
|
|
292
293
|
|
|
293
294
|
**A brand mark is the brand's own and goes only where it is meant.** `IconCoffee` is Buy Me a Coffee's mark and stands only on the button that leads there, and `IconCaptcha` is reCAPTCHA's mark and stands only where a captcha is the subject. `IconPayPal` and `IconBitcoin` stand only on the buttons that open the PayPal and the crypto window. `IconContainers` and `IconGithub` follow the same rule (section 1).
|
|
294
295
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glimstone/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.2",
|
|
4
4
|
"description": "GlimStone's tokens, glyphs and engines, without a framework",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": { "type": "git", "url": "git+https://github.com/junkerderprovinz/glimstone.git", "directory": "packages/core" },
|
package/tokens.css
CHANGED
|
@@ -154,11 +154,13 @@
|
|
|
154
154
|
the same colour and the light theme is where they part, so use this rather
|
|
155
155
|
than --accent whenever the colour is read.
|
|
156
156
|
|
|
157
|
-
The theme sets
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
The theme sets --ink-mix, which <html> passes down, so the one-line mix
|
|
158
|
+
is right in both themes and the rainbow blocks need no light twin. 100% of
|
|
159
|
+
a colour mixed with black is that colour. An accent the mix leaves under
|
|
160
|
+
4.5:1 on a ramp's card gets that ramp's ink from applyAccent instead, as
|
|
161
|
+
--accent-ink-dark or --accent-ink-light; no preset needs one. */
|
|
160
162
|
--ink-mix: 100%;
|
|
161
|
-
--accent-ink: color-mix(in srgb, var(--accent) var(--ink-mix), black);
|
|
163
|
+
--accent-ink: var(--accent-ink-dark, color-mix(in srgb, var(--accent) var(--ink-mix), black));
|
|
162
164
|
/* The same ink for a line-drawn glyph. Rainbow never colours text, so a hued
|
|
163
165
|
subtree redefines this one and leaves --accent-ink on the single accent. */
|
|
164
166
|
--accent-glyph: var(--accent-ink);
|
|
@@ -240,6 +242,7 @@
|
|
|
240
242
|
--accent-contrast: #161616;
|
|
241
243
|
--accent-soft: rgba(252, 196, 25, 0.14);
|
|
242
244
|
--ink-mix: 55%;
|
|
245
|
+
--accent-ink: var(--accent-ink-light, color-mix(in srgb, var(--accent) var(--ink-mix), black));
|
|
243
246
|
|
|
244
247
|
--status-info-text: #8E6A00;
|
|
245
248
|
--status-info-bg: rgba(142, 106, 0, 0.12);
|
|
@@ -313,6 +316,7 @@
|
|
|
313
316
|
--accent-contrast: #161616;
|
|
314
317
|
--accent-soft: rgba(252, 196, 25, 0.14);
|
|
315
318
|
--ink-mix: 55%;
|
|
319
|
+
--accent-ink: var(--accent-ink-light, color-mix(in srgb, var(--accent) var(--ink-mix), black));
|
|
316
320
|
|
|
317
321
|
--status-info-text: #8E6A00;
|
|
318
322
|
--status-info-bg: rgba(142, 106, 0, 0.12);
|
|
@@ -342,6 +346,51 @@
|
|
|
342
346
|
--focus-ring: rgba(142, 106, 0, .5);
|
|
343
347
|
}
|
|
344
348
|
|
|
349
|
+
/* OLED, the dark ramp on true black for OLED and AMOLED screens (rule 43).
|
|
350
|
+
`data-oled` on <html> asks for it and counts only while the dark ramp shows,
|
|
351
|
+
chosen or from the system, so the two selectors below follow the two ways a
|
|
352
|
+
page goes dark. The surfaces move one Carbon step down and keep their order,
|
|
353
|
+
so every rule that names a step still lands on the step it meant. The hover
|
|
354
|
+
keeps its place between the card and surface2, and hover-raised is the step
|
|
355
|
+
above surface3, as on the dark ramp. The text ramp, the accent and the state
|
|
356
|
+
hues stay: they were measured on darker grounds than they get here.
|
|
357
|
+
|
|
358
|
+
A shadow on black is invisible, so the elevation becomes a ring in the
|
|
359
|
+
border colour. The switch track stays at #525252 rather than following
|
|
360
|
+
surface3 down, where it would fall to 1.6:1 on the card. The opaque warning
|
|
361
|
+
chip stands further from a #161616 card than from a #262626 one, and the
|
|
362
|
+
hairline and the scrim lose nothing on black, so they stay as well. A lit
|
|
363
|
+
brand tile in a near-black colour, such as GitHub's, would vanish into the
|
|
364
|
+
card, so --tile-edge rings every lit tile. */
|
|
365
|
+
[data-theme="dark"][data-oled] {
|
|
366
|
+
--carbon-bg: #000000;
|
|
367
|
+
--carbon-sidebar: #161616;
|
|
368
|
+
--carbon-surface: #161616;
|
|
369
|
+
--carbon-surface2: #262626;
|
|
370
|
+
--carbon-surface3: #393939;
|
|
371
|
+
--carbon-hover: #232323;
|
|
372
|
+
--carbon-hover-raised: #525252;
|
|
373
|
+
--carbon-border: #262626;
|
|
374
|
+
--elevation: 0 0 0 1px #262626;
|
|
375
|
+
--tile-edge: rgba(255, 255, 255, .2);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* The same values for the system's dark, with no light theme chosen. */
|
|
379
|
+
@media (prefers-color-scheme: dark) {
|
|
380
|
+
:root[data-oled]:not([data-theme="light"]) {
|
|
381
|
+
--carbon-bg: #000000;
|
|
382
|
+
--carbon-sidebar: #161616;
|
|
383
|
+
--carbon-surface: #161616;
|
|
384
|
+
--carbon-surface2: #262626;
|
|
385
|
+
--carbon-surface3: #393939;
|
|
386
|
+
--carbon-hover: #232323;
|
|
387
|
+
--carbon-hover-raised: #525252;
|
|
388
|
+
--carbon-border: #262626;
|
|
389
|
+
--elevation: 0 0 0 1px #262626;
|
|
390
|
+
--tile-edge: rgba(255, 255, 255, .2);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
345
394
|
/* The hover step of a filled tone that has no surface ramp of its own (rule
|
|
346
395
|
21): the fill mixed toward the theme's text colour, which is lighter on dark
|
|
347
396
|
and darker on light, the direction the ramp moves. 15% is about one step of
|
|
@@ -913,10 +962,13 @@ body {
|
|
|
913
962
|
glim-page-in var(--motion-page-dur) var(--motion-page-ease) both,
|
|
914
963
|
glim-fade-in var(--motion-page-dur) ease-out both;
|
|
915
964
|
}
|
|
965
|
+
/* Backwards, not both: a held end is an identity matrix rather than none,
|
|
966
|
+
and any transform makes the card the containing block of a fixed element
|
|
967
|
+
inside it. */
|
|
916
968
|
.glim-toast {
|
|
917
969
|
animation:
|
|
918
|
-
glim-toast-in var(--motion-toast-dur) var(--motion-toast-ease, var(--motion-page-ease))
|
|
919
|
-
glim-fade-in var(--motion-toast-dur) ease-out
|
|
970
|
+
glim-toast-in var(--motion-toast-dur) var(--motion-toast-ease, var(--motion-page-ease)) backwards,
|
|
971
|
+
glim-fade-in var(--motion-toast-dur) ease-out backwards;
|
|
920
972
|
}
|
|
921
973
|
.glim-live { animation: glim-pulse var(--motion-pulse-dur) ease-in-out infinite; }
|
|
922
974
|
/* Short, because the pointer is already waiting for the bubble. */
|
|
@@ -1188,7 +1240,7 @@ body {
|
|
|
1188
1240
|
state is hidden. */
|
|
1189
1241
|
@media (prefers-reduced-motion: reduce) {
|
|
1190
1242
|
:root:not([data-motion='storm']) .glim-page-enter { animation: glim-fade-in 180ms ease both; }
|
|
1191
|
-
:root:not([data-motion='storm']) .glim-toast { animation: glim-fade-in 160ms ease
|
|
1243
|
+
:root:not([data-motion='storm']) .glim-toast { animation: glim-fade-in 160ms ease backwards; }
|
|
1192
1244
|
/* An infinite animation stops outright, fully opaque, so "this is live"
|
|
1193
1245
|
reads from colour and position. Storm is not exempt here: wanting more
|
|
1194
1246
|
movement is not wanting something that never stops. */
|
|
@@ -1203,8 +1255,8 @@ body {
|
|
|
1203
1255
|
}
|
|
1204
1256
|
:root[data-motion='storm'] .glim-toast {
|
|
1205
1257
|
animation:
|
|
1206
|
-
glim-toast-in var(--motion-toast-dur) var(--motion-toast-ease, var(--motion-page-ease))
|
|
1207
|
-
glim-fade-in var(--motion-toast-dur) ease-out
|
|
1258
|
+
glim-toast-in var(--motion-toast-dur) var(--motion-toast-ease, var(--motion-page-ease)) backwards,
|
|
1259
|
+
glim-fade-in var(--motion-toast-dur) ease-out backwards;
|
|
1208
1260
|
}
|
|
1209
1261
|
:root[data-motion='storm'] .glim-shake { animation: glim-shake var(--motion-shake-dur) ease-in-out; }
|
|
1210
1262
|
}
|
|
@@ -1950,6 +2002,19 @@ body {
|
|
|
1950
2002
|
smallest floor. An explicit stage is a width of its own and stays. */
|
|
1951
2003
|
.glim-modal-foot > :is(.glim-btn-sm, .glim-btn-md, .glim-btn-lg),
|
|
1952
2004
|
.glim-modal-foot > .glim-tip-wrap > :is(.glim-btn-sm, .glim-btn-md, .glim-btn-lg) { min-width: var(--btn-w-xs); }
|
|
2005
|
+
/* A button wider than the whole foot, as on a phone, runs its words onto a
|
|
2006
|
+
second line and grows with them instead of ending in an ellipsis. */
|
|
2007
|
+
:is(.glim-modal-foot, .glim-modal-foot > .glim-tip-wrap) > .glim-btn {
|
|
2008
|
+
height: auto;
|
|
2009
|
+
min-height: var(--btn-h);
|
|
2010
|
+
padding-block: 0.375rem;
|
|
2011
|
+
}
|
|
2012
|
+
:is(.glim-modal-foot, .glim-modal-foot > .glim-tip-wrap) > .glim-btn-key { min-height: var(--btn-h-key); }
|
|
2013
|
+
:is(.glim-modal-foot, .glim-modal-foot > .glim-tip-wrap) > .glim-btn > .glim-btn-label {
|
|
2014
|
+
white-space: normal;
|
|
2015
|
+
text-wrap: balance;
|
|
2016
|
+
text-align: center;
|
|
2017
|
+
}
|
|
1953
2018
|
.glim-path > .glim-btn:not(.glim-btn-icon) { min-width: 0; }
|
|
1954
2019
|
|
|
1955
2020
|
/* Rule 40. A surface-filled button's hovered ground takes 22% of the tint and
|
|
@@ -2003,6 +2068,7 @@ body {
|
|
|
2003
2068
|
.group:hover > .glim-brand-tile {
|
|
2004
2069
|
background-color: var(--tile);
|
|
2005
2070
|
color: var(--tile-ink);
|
|
2071
|
+
box-shadow: inset 0 0 0 1px var(--tile-edge, transparent);
|
|
2006
2072
|
--mark-ink: var(--tile-ink);
|
|
2007
2073
|
--mark-cut: var(--tile);
|
|
2008
2074
|
--brand-android: var(--tile-ink);
|
|
@@ -3167,6 +3233,96 @@ body {
|
|
|
3167
3233
|
font-weight: inherit;
|
|
3168
3234
|
}
|
|
3169
3235
|
|
|
3236
|
+
/* The sign-in page (SignInCard): one card in the middle of the screen, the
|
|
3237
|
+
same width in every app, and the full width less the gutter on a phone. */
|
|
3238
|
+
.glim-signin {
|
|
3239
|
+
display: flex;
|
|
3240
|
+
align-items: center;
|
|
3241
|
+
justify-content: center;
|
|
3242
|
+
min-height: 100dvh;
|
|
3243
|
+
padding: 1rem;
|
|
3244
|
+
background-color: var(--carbon-bg);
|
|
3245
|
+
color: var(--carbon-text);
|
|
3246
|
+
}
|
|
3247
|
+
.glim-signin-card {
|
|
3248
|
+
display: flex;
|
|
3249
|
+
flex-direction: column;
|
|
3250
|
+
gap: 1.5rem;
|
|
3251
|
+
width: min(24rem, 100%);
|
|
3252
|
+
padding: 2rem;
|
|
3253
|
+
}
|
|
3254
|
+
.glim-signin-head {
|
|
3255
|
+
display: flex;
|
|
3256
|
+
flex-direction: column;
|
|
3257
|
+
align-items: center;
|
|
3258
|
+
gap: 0.75rem;
|
|
3259
|
+
}
|
|
3260
|
+
.glim-signin-logo {
|
|
3261
|
+
display: flex;
|
|
3262
|
+
align-items: center;
|
|
3263
|
+
justify-content: center;
|
|
3264
|
+
width: 4rem;
|
|
3265
|
+
height: 4rem;
|
|
3266
|
+
}
|
|
3267
|
+
.glim-signin-logo > :is(img, svg) {
|
|
3268
|
+
display: block;
|
|
3269
|
+
max-width: 100%;
|
|
3270
|
+
max-height: 100%;
|
|
3271
|
+
object-fit: contain;
|
|
3272
|
+
}
|
|
3273
|
+
.glim-signin-name {
|
|
3274
|
+
margin: 0;
|
|
3275
|
+
font-size: var(--text-status);
|
|
3276
|
+
font-weight: 600;
|
|
3277
|
+
line-height: 1.2;
|
|
3278
|
+
text-align: center;
|
|
3279
|
+
overflow-wrap: anywhere;
|
|
3280
|
+
}
|
|
3281
|
+
.glim-signin-form {
|
|
3282
|
+
display: flex;
|
|
3283
|
+
flex-direction: column;
|
|
3284
|
+
gap: 1rem;
|
|
3285
|
+
margin: 0;
|
|
3286
|
+
}
|
|
3287
|
+
/* Every button as wide as the fields, also one a bubble wraps while disabled. */
|
|
3288
|
+
.glim-signin-form > :is(.glim-btn, .glim-tip-wrap),
|
|
3289
|
+
.glim-signin-form > .glim-tip-wrap > .glim-btn { width: 100%; }
|
|
3290
|
+
.glim-signin-form p { margin: 0; }
|
|
3291
|
+
.glim-signin-field {
|
|
3292
|
+
display: flex;
|
|
3293
|
+
flex-direction: column;
|
|
3294
|
+
gap: 0.375rem;
|
|
3295
|
+
}
|
|
3296
|
+
.glim-signin-field[hidden] { display: none; }
|
|
3297
|
+
.glim-signin-label {
|
|
3298
|
+
display: flex;
|
|
3299
|
+
align-items: center;
|
|
3300
|
+
gap: 0.375rem;
|
|
3301
|
+
color: var(--carbon-text-sub);
|
|
3302
|
+
font-size: var(--text-meta);
|
|
3303
|
+
font-weight: 500;
|
|
3304
|
+
}
|
|
3305
|
+
.glim-signin-hint {
|
|
3306
|
+
color: var(--carbon-text-sub);
|
|
3307
|
+
font-size: var(--text-meta);
|
|
3308
|
+
}
|
|
3309
|
+
.glim-signin-error {
|
|
3310
|
+
color: var(--status-fail-text);
|
|
3311
|
+
font-size: var(--text-meta);
|
|
3312
|
+
}
|
|
3313
|
+
.glim-signin-note {
|
|
3314
|
+
padding: 0.6rem 0.8rem;
|
|
3315
|
+
border-radius: var(--radius-control);
|
|
3316
|
+
background-color: var(--carbon-surface2);
|
|
3317
|
+
font-size: var(--text-subline);
|
|
3318
|
+
}
|
|
3319
|
+
.glim-signin-code { letter-spacing: 0.35em; }
|
|
3320
|
+
.glim-signin-log-code,
|
|
3321
|
+
.glim-signin-log-code::placeholder {
|
|
3322
|
+
text-transform: uppercase;
|
|
3323
|
+
letter-spacing: 0.2em;
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3170
3326
|
/* What stands in place of a control the environment forbids
|
|
3171
3327
|
(UnavailableNotice). A paragraph, because there is no control to hang an
|
|
3172
3328
|
info bubble off. */
|
|
@@ -3423,7 +3579,9 @@ body {
|
|
|
3423
3579
|
margin: 0;
|
|
3424
3580
|
text-wrap: pretty;
|
|
3425
3581
|
font-size: 0.875rem;
|
|
3426
|
-
line
|
|
3582
|
+
/* Whole pixels per line, or every toast stacked beside this one lands on a
|
|
3583
|
+
fraction and Chromium rounds its edges differently from paint to paint. */
|
|
3584
|
+
line-height: 1.25rem;
|
|
3427
3585
|
overflow-wrap: break-word;
|
|
3428
3586
|
}
|
|
3429
3587
|
/* A phone keeps the window in view: the newest two across its width. */
|