@justai/cuts 0.133.0 → 0.138.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -3
- package/src/ActionList.astro +31 -27
- package/src/ActionRow.astro +68 -64
- package/src/BackToZone.astro +39 -35
- package/src/Base.astro +21 -3
- package/src/Catalogue.astro +95 -319
- package/src/CatalogueCard.astro +72 -68
- package/src/ChildPersona.astro +55 -51
- package/src/ConfirmSheet.astro +69 -65
- package/src/FilePicker.astro +21 -378
- package/src/LangPicker.astro +49 -104
- package/src/Posts.astro +177 -396
- package/src/Preroll.astro +204 -487
- package/src/Profile.astro +39 -35
- package/src/Segmented.astro +62 -111
- package/src/Sheet.astro +247 -507
- package/src/Toolbar.astro +288 -362
- package/src/behaviour/catalogue.ts +239 -0
- package/src/behaviour/filePicker.ts +372 -0
- package/src/behaviour/langPicker.ts +70 -0
- package/src/behaviour/posts.ts +234 -0
- package/src/behaviour/preroll.ts +298 -0
- package/src/behaviour/segmented.ts +64 -0
- package/src/behaviour/sheet.ts +300 -0
- package/src/behaviour/toolbar.ts +89 -0
- package/src/ground.css +124 -120
package/src/Profile.astro
CHANGED
|
@@ -99,40 +99,44 @@ const vars = [
|
|
|
99
99
|
</section>
|
|
100
100
|
|
|
101
101
|
<style>
|
|
102
|
-
/*
|
|
103
|
-
|
|
104
|
-
.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
/* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
|
|
103
|
+
out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
|
|
104
|
+
@layer justai.components {
|
|
105
|
+
/* ---- PROFILE cut ---- */
|
|
106
|
+
.profile { padding-block-end: 8px; }
|
|
107
|
+
.cover {
|
|
108
|
+
height: var(--cover-h, 132px);
|
|
109
|
+
/* full-bleed to the persona's page edge (see --edge on Toolbar) */
|
|
110
|
+
margin-inline: calc(-1 * max(var(--edge, 16px), env(safe-area-inset-left))) calc(-1 * max(var(--edge, 16px), env(safe-area-inset-right)));
|
|
111
|
+
background: var(--cover, linear-gradient(135deg, var(--accent), transparent));
|
|
112
|
+
position: relative;
|
|
113
|
+
}
|
|
114
|
+
@media (min-width: 640px) { .cover { margin-inline: -28px; } }
|
|
115
|
+
.avatar {
|
|
116
|
+
width: 76px; height: 76px; border-radius: var(--radius-lg); margin-block-start: -40px;
|
|
117
|
+
display: grid; place-items: center; padding: var(--avatar-pad, 0);
|
|
118
|
+
background: var(--avatar-bg, #fff);
|
|
119
|
+
border: 3px solid var(--bg); box-shadow: 0 8px 22px -16px rgba(0, 0, 0, 0.22);
|
|
120
|
+
position: relative;
|
|
121
|
+
/* it is an <a> now — undo the link ground, or a slotted SVG drawn in currentColor turns accent */
|
|
122
|
+
color: inherit; text-decoration: none;
|
|
123
|
+
transition: transform 0.15s var(--ease-out, ease-out);
|
|
124
|
+
}
|
|
125
|
+
/* Pressed, not hovered. The tile is the persona's FACE: growing it under the cursor would animate the
|
|
126
|
+
one object on the page that should hold still. Depressing it on press is the iOS answer — it says
|
|
127
|
+
"this responds" at the moment of contact, on touch as well, where there is no hover to say it. */
|
|
128
|
+
.avatar:active { transform: scale(0.97); }
|
|
129
|
+
.avatar:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
130
|
+
@media (prefers-reduced-motion: reduce) { .avatar { transition: none; } }
|
|
131
|
+
/* the avatar content is slotted (persona-owned) → style it globally */
|
|
132
|
+
.avatar :global(svg) { width: 100%; height: 100%; display: block; }
|
|
133
|
+
.avatar :global(img) { width: 100%; height: 100%; display: block; object-fit: cover; }
|
|
134
|
+
@media (prefers-color-scheme: light) {
|
|
135
|
+
:global(html:not([data-theme="dark"])) .avatar { border-color: #fff; background: var(--avatar-bg-light, var(--avatar-bg, #fff)); }
|
|
136
|
+
}
|
|
137
|
+
.pname { font-size: var(--fs-lg); font-weight: 700; letter-spacing: -0.02em; margin-block-start: 12px; }
|
|
138
|
+
.handle { color: var(--text-dim); font-size: var(--fs-xs); margin-block-start: 2px; }
|
|
139
|
+
.bio { font-size: var(--fs-sm); line-height: 1.5; margin-block-start: 12px; }
|
|
140
|
+
.meta { color: var(--text-dim); font-size: var(--fs-2xs); margin-block-start: 10px; }
|
|
110
141
|
}
|
|
111
|
-
@media (min-width: 640px) { .cover { margin-inline: -28px; } }
|
|
112
|
-
.avatar {
|
|
113
|
-
width: 76px; height: 76px; border-radius: var(--radius-lg); margin-block-start: -40px;
|
|
114
|
-
display: grid; place-items: center; padding: var(--avatar-pad, 0);
|
|
115
|
-
background: var(--avatar-bg, #fff);
|
|
116
|
-
border: 3px solid var(--bg); box-shadow: 0 8px 22px -16px rgba(0, 0, 0, 0.22);
|
|
117
|
-
position: relative;
|
|
118
|
-
/* it is an <a> now — undo the link ground, or a slotted SVG drawn in currentColor turns accent */
|
|
119
|
-
color: inherit; text-decoration: none;
|
|
120
|
-
transition: transform 0.15s var(--ease-out, ease-out);
|
|
121
|
-
}
|
|
122
|
-
/* Pressed, not hovered. The tile is the persona's FACE: growing it under the cursor would animate the
|
|
123
|
-
one object on the page that should hold still. Depressing it on press is the iOS answer — it says
|
|
124
|
-
"this responds" at the moment of contact, on touch as well, where there is no hover to say it. */
|
|
125
|
-
.avatar:active { transform: scale(0.97); }
|
|
126
|
-
.avatar:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
127
|
-
@media (prefers-reduced-motion: reduce) { .avatar { transition: none; } }
|
|
128
|
-
/* the avatar content is slotted (persona-owned) → style it globally */
|
|
129
|
-
.avatar :global(svg) { width: 100%; height: 100%; display: block; }
|
|
130
|
-
.avatar :global(img) { width: 100%; height: 100%; display: block; object-fit: cover; }
|
|
131
|
-
@media (prefers-color-scheme: light) {
|
|
132
|
-
:global(html:not([data-theme="dark"])) .avatar { border-color: #fff; background: var(--avatar-bg-light, var(--avatar-bg, #fff)); }
|
|
133
|
-
}
|
|
134
|
-
.pname { font-size: var(--fs-lg); font-weight: 700; letter-spacing: -0.02em; margin-block-start: 12px; }
|
|
135
|
-
.handle { color: var(--text-dim); font-size: var(--fs-xs); margin-block-start: 2px; }
|
|
136
|
-
.bio { font-size: var(--fs-sm); line-height: 1.5; margin-block-start: 12px; }
|
|
137
|
-
.meta { color: var(--text-dim); font-size: var(--fs-2xs); margin-block-start: 10px; }
|
|
138
142
|
</style>
|
package/src/Segmented.astro
CHANGED
|
@@ -105,120 +105,71 @@ const on = value && options.some((o) => o.value === value) ? value : options[0]?
|
|
|
105
105
|
</div>
|
|
106
106
|
|
|
107
107
|
<style>
|
|
108
|
-
/*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
108
|
+
/* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
|
|
109
|
+
out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
|
|
110
|
+
@layer justai.components {
|
|
111
|
+
/* queries: segmented control styles · the chosen segment · ios segmented look
|
|
112
|
+
ONE TRACK, THE CHOSEN ONE RAISED. The track is recessed and the selection sits on top of it —
|
|
113
|
+
which is what makes a segmented control read as "these are the options" rather than as a row of
|
|
114
|
+
buttons where one happens to be coloured. */
|
|
115
|
+
.jseg {
|
|
116
|
+
/* ITS OWN BOX MODEL, not the consumer's. `width: 100%` plus a padding and a border comes out
|
|
117
|
+
SIX pixels wider than its parent unless something has already said `border-box` — and on a
|
|
118
|
+
bare page nothing has. Inheriting a reset is homework (law 1), and this is the shape that
|
|
119
|
+
homework takes when nobody notices: a control that fits everywhere it was tried. */
|
|
120
|
+
box-sizing: border-box;
|
|
121
|
+
display: inline-flex;
|
|
122
|
+
align-items: stretch;
|
|
123
|
+
gap: 2px;
|
|
124
|
+
padding: 2px;
|
|
125
|
+
border-radius: var(--radius-md);
|
|
126
|
+
background: var(--surface-2);
|
|
127
|
+
border: 1px solid var(--border);
|
|
128
|
+
/* A control is one tab stop. The roving tabindex below is what makes that true for the keyboard;
|
|
129
|
+
this is what makes it true for the eye. */
|
|
130
|
+
isolation: isolate;
|
|
131
|
+
}
|
|
132
|
+
/* queries: segmented control height · 44px touch target · the segments are too small
|
|
133
|
+
TWO SIZES, AND THE DIFFERENCE IS WHAT THE CONTROL IS.
|
|
134
|
+
`full` is a VIEW'S OWN SWITCH — a thumb's first target on the screen, so it meets the fleet's
|
|
135
|
+
44px like every row and every action does. `inline` is a TOOLBAR CHIP, sitting among other
|
|
136
|
+
chrome at chrome's size; forcing 44 there would make a toolbar taller than its content, which
|
|
137
|
+
is why iOS ships its own segmented controls shorter in exactly that position.
|
|
138
|
+
Found by a consumer's spec measuring 33px where its page promised 44 — a number the component
|
|
139
|
+
should have owned rather than each page discovering separately. */
|
|
140
|
+
.jseg[data-width="full"] { display: flex; width: 100%; }
|
|
141
|
+
.jseg[data-width="full"] .jseg-item { flex: 1; min-height: 44px; }
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
143
|
+
.jseg-item {
|
|
144
|
+
box-sizing: border-box;
|
|
145
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
|
146
|
+
min-width: 0; padding: 7px 12px;
|
|
147
|
+
border: 0; border-radius: calc(var(--radius-md) - 3px);
|
|
148
|
+
background: transparent; color: var(--text-dim);
|
|
149
|
+
font: inherit; font-size: var(--fs-sm); font-weight: 500; line-height: 1.2;
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
transition: background 0.18s var(--ease-out), color 0.18s var(--ease-out);
|
|
152
|
+
}
|
|
153
|
+
.jseg-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
154
|
+
.jseg-item :global(svg) { width: 16px; height: 16px; flex: none; }
|
|
152
155
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
/* THE CHOSEN ONE. `--surface` against a `--surface-2` track is the raise; the accent names it. */
|
|
157
|
+
.jseg-item[aria-checked="true"] {
|
|
158
|
+
background: var(--surface);
|
|
159
|
+
color: var(--accent);
|
|
160
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 0.06);
|
|
161
|
+
}
|
|
162
|
+
.jseg-item[aria-disabled="true"] { opacity: 0.45; cursor: default; }
|
|
163
|
+
.jseg-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
161
164
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
/* Motion is never forced on somebody who asked it off. */
|
|
166
|
+
@media (prefers-reduced-motion: reduce) {
|
|
167
|
+
.jseg-item { transition: none; }
|
|
168
|
+
}
|
|
165
169
|
}
|
|
166
170
|
</style>
|
|
167
171
|
|
|
168
|
-
<script>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// chosen option and the arrows move within the group. Leaving every option at `tabIndex 0` is the
|
|
173
|
-
// common mistake and it is invisible to anyone using a mouse — a person on a keyboard suddenly has
|
|
174
|
-
// to press Tab three times to get past a control they were not using.
|
|
175
|
-
//
|
|
176
|
-
// Lifted from qr, which had it right and had it alone.
|
|
177
|
-
const wire = (root: HTMLElement) => {
|
|
178
|
-
const items = () => Array.from(root.querySelectorAll<HTMLElement>('[role="radio"]'));
|
|
179
|
-
|
|
180
|
-
const set = (el: HTMLElement, focus?: boolean, quiet?: boolean) => {
|
|
181
|
-
if (el.getAttribute("aria-disabled") === "true") return;
|
|
182
|
-
const next = el.dataset.value!;
|
|
183
|
-
if (root.dataset.value === next) { if (focus) el.focus(); return; }
|
|
184
|
-
for (const r of items()) {
|
|
185
|
-
const chosen = r === el;
|
|
186
|
-
r.setAttribute("aria-checked", String(chosen));
|
|
187
|
-
r.tabIndex = chosen ? 0 : -1;
|
|
188
|
-
}
|
|
189
|
-
root.dataset.value = next;
|
|
190
|
-
if (focus) el.focus();
|
|
191
|
-
// Setting `.value` from code does NOT re-announce — a caller that just said what it wants does
|
|
192
|
-
// not need to be told, and a listener that reacts by setting the value would loop.
|
|
193
|
-
if (!quiet) root.dispatchEvent(new CustomEvent("change", { detail: { value: next }, bubbles: true }));
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
for (const el of items()) el.addEventListener("click", () => set(el, true));
|
|
197
|
-
|
|
198
|
-
root.addEventListener("keydown", (e) => {
|
|
199
|
-
const key = (e as KeyboardEvent).key;
|
|
200
|
-
const all = items().filter((r) => r.getAttribute("aria-disabled") !== "true");
|
|
201
|
-
const at = all.findIndex((r) => r.getAttribute("aria-checked") === "true");
|
|
202
|
-
let to = -1;
|
|
203
|
-
if (key === "ArrowRight" || key === "ArrowDown") to = (at + 1) % all.length;
|
|
204
|
-
else if (key === "ArrowLeft" || key === "ArrowUp") to = (at - 1 + all.length) % all.length;
|
|
205
|
-
else if (key === "Home") to = 0;
|
|
206
|
-
else if (key === "End") to = all.length - 1;
|
|
207
|
-
if (to < 0 || !all[to]) return;
|
|
208
|
-
e.preventDefault();
|
|
209
|
-
set(all[to], true);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// The one thing a caller needs beyond listening: saying what it wants.
|
|
213
|
-
Object.defineProperty(root, "value", {
|
|
214
|
-
get: () => root.dataset.value,
|
|
215
|
-
set: (v: string) => {
|
|
216
|
-
const el = items().find((r) => r.dataset.value === v);
|
|
217
|
-
if (el) set(el, false, true);
|
|
218
|
-
},
|
|
219
|
-
configurable: true,
|
|
220
|
-
});
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
for (const root of document.querySelectorAll<HTMLElement>(".jseg")) wire(root);
|
|
224
|
-
</script>
|
|
172
|
+
<script>
|
|
173
|
+
// The behaviour lives in ./behaviour/segmented.ts — see that file for why it is not in here.
|
|
174
|
+
import "./behaviour/segmented";
|
|
175
|
+
</script>
|