@justai/cuts 0.78.0 → 0.80.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 +4 -3
- package/src/ChildPersona.astro +219 -0
- package/src/Toolbar.astro +75 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justai/cuts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.80.0",
|
|
4
4
|
"description": "A persona's named parts — the page shell that holds them, and the cuts themselves.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"./ActionList.astro": "./src/ActionList.astro",
|
|
28
28
|
"./ActionRow.astro": "./src/ActionRow.astro",
|
|
29
29
|
"./Files.astro": "./src/FilePicker.astro",
|
|
30
|
-
"./AuthMount.astro": "./src/AuthMount.astro"
|
|
30
|
+
"./AuthMount.astro": "./src/AuthMount.astro",
|
|
31
|
+
"./ChildPersona.astro": "./src/ChildPersona.astro"
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|
|
33
34
|
"astro": ">=5"
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
"access": "public"
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
|
-
"@justai/core": "^0.
|
|
40
|
+
"@justai/core": "^0.11.0",
|
|
40
41
|
"@justai/ui": "^0",
|
|
41
42
|
"@justai/atrium": "^0.1.9"
|
|
42
43
|
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
---
|
|
2
|
+
import "./ground.css";
|
|
3
|
+
import Profile from "./Profile.astro";
|
|
4
|
+
import Toolbar from "./Toolbar.astro";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* <ChildPersona> — the shell every child persona wears, so a builder writes only its kernel.
|
|
8
|
+
*
|
|
9
|
+
* queries: child persona shell · files me feed shared codebase · how do I write a new child persona ·
|
|
10
|
+
* why does every child persona repeat the same CSS · .pp · persona page inside a sheet ·
|
|
11
|
+
* standalone and embedded · child persona template
|
|
12
|
+
*
|
|
13
|
+
* ── WHAT A CHILD PERSONA IS ──────────────────────────────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* A persona that can only exist UNDER another (foundations 01): `/me`, `/files`, `/feed`. Say "qr's
|
|
16
|
+
* files" and it means something; say "files" and it does not. The edge mounts ONE implementation onto
|
|
17
|
+
* every persona's host, in that host's origin, so it sees only that persona's storage.
|
|
18
|
+
*
|
|
19
|
+
* ── A CHILD PERSONA HAS FACES ────────────────────────────────────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* THE PATTERN IS APPLE'S WIDGET MODEL AND THE WORD IS NOT, and that split is the decision rather
|
|
22
|
+
* than a compromise. A widget declares which FAMILIES it supports — systemSmall, systemMedium,
|
|
23
|
+
* accessoryInline — and the host asks for one: one identity, several surfaces, each a different
|
|
24
|
+
* amount of room and a different amount of attention. That model is right, thought through for
|
|
25
|
+
* years, and we take it whole.
|
|
26
|
+
*
|
|
27
|
+
* The word `widget` is the part that does not travel, and this codebase already knows why. Look at
|
|
28
|
+
* the cuts: `Sheet`, `ActionRow`, `Segmented` are Apple's words because they are Apple's THINGS —
|
|
29
|
+
* components. `Profile`, `Posts`, `Toolbar`, `BackToZone` are the ecosystem's, because they are
|
|
30
|
+
* about the persona. A child persona's surface is not a component; it is the persona SHOWING UP.
|
|
31
|
+
*
|
|
32
|
+
* And the ownership runs the other way. An iOS widget belongs to the HOST's world — a thing the Home
|
|
33
|
+
* Screen contains. This belongs to the PERSONA's world: how it appears when somebody asks for it.
|
|
34
|
+
* Borrowing the word would import the wrong ownership, and it would walk back the rename foundations
|
|
35
|
+
* 01 already paid for — "sub-persona" became "child persona" precisely because *nobody argues that a
|
|
36
|
+
* sub-thing deserves a face*.
|
|
37
|
+
*
|
|
38
|
+
* `face` is also already ours: `profile.avatar` is "the canonical face", `PersonaFace` is a type,
|
|
39
|
+
* `/api/face` answers who owns a host. That is not a collision — the endpoint returns the MATERIALS
|
|
40
|
+
* of a face (mark, colour, name) and this prop says WHICH face is being worn. One concept, two uses.
|
|
41
|
+
*
|
|
42
|
+
* (Pattern borrowed, vocabulary kept — which is the ecosystem's own thesis stated in miniature: the
|
|
43
|
+
* mechanism is a commodity, the soul is ours.)
|
|
44
|
+
*
|
|
45
|
+
* A child persona has two faces today, and the axis is open on purpose:
|
|
46
|
+
*
|
|
47
|
+
* `page` — a person types `qr.app.zone/files`. A full persona page: profile, toolbar, kernel.
|
|
48
|
+
* `sheet` — qr's kernel calls it, and it arrives inside qr's sheet as a guest. Chrome is the
|
|
49
|
+
* host's; this draws content only.
|
|
50
|
+
*
|
|
51
|
+
* and the ones that will come are already visible from here — an INLINE face (a strip a kernel
|
|
52
|
+
* embeds in its own column rather than presenting over it), a COMPACT face (a summary that answers
|
|
53
|
+
* without being opened), a PICKER face (the value-returning one, which today is `sheet` doing double
|
|
54
|
+
* duty). Each is a widget family in Apple's sense and a face in ours.
|
|
55
|
+
*
|
|
56
|
+
* IT IS A NAMED FACE, NOT A BOOLEAN, and that is the whole reason this prop exists in this shape.
|
|
57
|
+
* `presented: true/false` describes today and cannot describe a third surface without becoming two
|
|
58
|
+
* booleans that can both be true. A face is a name; adding one is adding a name.
|
|
59
|
+
*
|
|
60
|
+
* A face is a PRESENTATION CLASS and never a permission. Anything that must not be forged — who may
|
|
61
|
+
* read a file, whose persona owns a write — is decided by the verified origin on the atrium channel.
|
|
62
|
+
* A face decides only how much room there is and which words a person reads.
|
|
63
|
+
*
|
|
64
|
+
* ── THE APPLE ANALOGY, AND WHERE IT STOPS ────────────────────────────────────────────────────────
|
|
65
|
+
*
|
|
66
|
+
* The founder's reading, and it is the right one: this is the widget model. The HOST supplies the
|
|
67
|
+
* frame — the sheet, its bar, its dismiss, its detent — and the guest supplies only the content. The
|
|
68
|
+
* two run in separate processes with no shared build and no version coupling, and the guest does not
|
|
69
|
+
* know where it was placed (atrium identity is hop-local: a callee sees who HANDED it the message).
|
|
70
|
+
*
|
|
71
|
+
* Where it stops is the part that shapes this file: an Apple widget is read-mostly and
|
|
72
|
+
* timeline-driven, and a child persona is interactive and RETURNS A VALUE. The closer relative is
|
|
73
|
+
* `UIDocumentPickerViewController` — the system's picker, presented inside your app, drawing its own
|
|
74
|
+
* content, wearing your sheet, handing you back what the person chose.
|
|
75
|
+
*
|
|
76
|
+
* So: **chrome belongs to the host, content belongs to the guest.** A presented child draws no way
|
|
77
|
+
* out of its own, because the sheet it is in already has one, and two exits put "leave" at the rank
|
|
78
|
+
* of the thing the person came to do.
|
|
79
|
+
*
|
|
80
|
+
* ── WHY `.pp` ───────────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
*
|
|
82
|
+
* Everything is scoped under one wrapper because that is what lets one document be both situations.
|
|
83
|
+
* It also has a consequence worth knowing before fighting it: `.pp .app` is (0,2,0) and the
|
|
84
|
+
* package's global `.app` is (0,1,0), so a child's own rules always win. That is not an accident to
|
|
85
|
+
* work around — it is what makes the two-mode wrapper possible — but it does mean a child cannot
|
|
86
|
+
* inherit `@justai/ui`'s geometry the way an ordinary persona does. This cut carries it instead.
|
|
87
|
+
*
|
|
88
|
+
* ── MEASURED, NOT ASSUMED ───────────────────────────────────────────────────────────────────────
|
|
89
|
+
*
|
|
90
|
+
* files (174 rules), me (87) and feed (79) were compared rule by rule on 2026-08-02: 26 are
|
|
91
|
+
* character-identical in all three, and they are exactly this shell — the column both halves,
|
|
92
|
+
* `.below`, the landing offset, the toolbar row, the mark, the reduced-motion rule. `files ∩ me` is
|
|
93
|
+
* 50, because both are rooms you browse. Everything they do NOT share is their kernel.
|
|
94
|
+
*
|
|
95
|
+
* That is the whole argument: a child persona is its kernel plus this file.
|
|
96
|
+
*/
|
|
97
|
+
interface Props {
|
|
98
|
+
/** The child's own handle — `files`, `me`, `feed`. Its name in the row and in its own title. */
|
|
99
|
+
child: string;
|
|
100
|
+
/** What the child is called, in the reader's language. */
|
|
101
|
+
name: string;
|
|
102
|
+
/** One line, the child's own voice, for the profile. */
|
|
103
|
+
bio?: string;
|
|
104
|
+
/** The small print under the bio — what it costs, what it keeps. */
|
|
105
|
+
meta?: string;
|
|
106
|
+
/** The host persona's face, if the page already resolved it server-side. Otherwise `childFace()`
|
|
107
|
+
* fills it in at runtime from /api/face — see @justai/core. */
|
|
108
|
+
host?: { name?: string; avatar?: string; accent?: string };
|
|
109
|
+
/** Where the parent's kernel is. Defaults to the host root, which is where a visitor came from. */
|
|
110
|
+
parentHref?: string;
|
|
111
|
+
parentLabel?: string;
|
|
112
|
+
/** Section labels for the row. */
|
|
113
|
+
sectionsLabel?: string;
|
|
114
|
+
profileLabel?: string;
|
|
115
|
+
kernelLabel?: string;
|
|
116
|
+
/** Which face to render. `page` standalone, `sheet` as a guest inside a host's chrome. Open by
|
|
117
|
+
* design — see the header: a child persona has faces, the way an iOS widget has families. */
|
|
118
|
+
face?: "page" | "sheet";
|
|
119
|
+
}
|
|
120
|
+
const {
|
|
121
|
+
child, name, bio, meta, host,
|
|
122
|
+
parentHref = "/#kernel", parentLabel = "Home",
|
|
123
|
+
sectionsLabel = "Sections", profileLabel, kernelLabel, face = "page",
|
|
124
|
+
} = Astro.props;
|
|
125
|
+
// `page` is the default because it is the face a child shows when nobody asked for one — somebody
|
|
126
|
+
// typed the address. A guest is always ASKED for, which is why the caller names it.
|
|
127
|
+
const chrome = face === "page";
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
{/* `.pp` IS the child persona. One wrapper, two situations — and `data-presented` is how the kernel
|
|
131
|
+
inside asks which one it is in without re-deriving it from the URL. */}
|
|
132
|
+
<div class="pp" id="pp" data-child={child} data-face={face}>
|
|
133
|
+
{/* A PRESENTED CHILD SHOWS NO PROFILE AND NO ROW. It is content inside somebody's sheet, and a
|
|
134
|
+
biography there is the guest introducing itself while the host is mid-sentence. Standalone it
|
|
135
|
+
is a full persona page, because then it IS the page. */}
|
|
136
|
+
{chrome && (
|
|
137
|
+
<>
|
|
138
|
+
<Profile
|
|
139
|
+
name={name}
|
|
140
|
+
handle={`@${child}`}
|
|
141
|
+
bio={bio}
|
|
142
|
+
meta={meta}
|
|
143
|
+
kernelLabel={name}
|
|
144
|
+
avatarBg="#fff"
|
|
145
|
+
avatarPad="14px"
|
|
146
|
+
cover="linear-gradient(135deg, color-mix(in srgb, var(--accent) 88%, #fff), var(--accent) 55%, color-mix(in srgb, var(--accent) 70%, #000))"
|
|
147
|
+
>
|
|
148
|
+
<Fragment slot="avatar"><slot name="mark" /></Fragment>
|
|
149
|
+
</Profile>
|
|
150
|
+
|
|
151
|
+
<div class="below" id="below">
|
|
152
|
+
<Toolbar
|
|
153
|
+
sectionsLabel={sectionsLabel}
|
|
154
|
+
profileLabel={profileLabel ?? name}
|
|
155
|
+
kernelLabel={kernelLabel ?? name}
|
|
156
|
+
postsLabel="Posts"
|
|
157
|
+
parent={{ href: parentHref, label: parentLabel, icon: host?.avatar }}
|
|
158
|
+
files={false}
|
|
159
|
+
>
|
|
160
|
+
<Fragment slot="profileIcon"><slot name="mark" /></Fragment>
|
|
161
|
+
<slot name="langPicker" slot="langPicker" />
|
|
162
|
+
</Toolbar>
|
|
163
|
+
<section class="cut kernel" id="kernel"><slot /></section>
|
|
164
|
+
</div>
|
|
165
|
+
</>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{!chrome && <section class="cut kernel" id="kernel" data-guest><slot /></section>}
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<style>
|
|
172
|
+
/* THE SHELL, scoped under `.pp` — the 26 rules files, me and feed each carried verbatim.
|
|
173
|
+
*
|
|
174
|
+
* They are written here at the SAME depth the children wrote them, and that is deliberate: a
|
|
175
|
+
* child's own `.pp .x` rule ties with this one and wins on order, so a child that needs to differ
|
|
176
|
+
* still can, without !important and without knowing anything about this file. */
|
|
177
|
+
|
|
178
|
+
.pp { min-height: 100dvh; }
|
|
179
|
+
.pp a { color: inherit; text-decoration: none; }
|
|
180
|
+
|
|
181
|
+
/* the column — the fleet's one presentation at every width */
|
|
182
|
+
.pp .app {
|
|
183
|
+
/* Set here AND read with a fallback, which is not belt-and-braces: a consumer that overrides
|
|
184
|
+
`.pp .app` — same selector, later stylesheet — drops the declaration and keeps the reads, and
|
|
185
|
+
an unresolved var() invalidates the whole `padding-inline`. The cut brings its own ground
|
|
186
|
+
(law 1) means the ground has to survive being partly overwritten. */
|
|
187
|
+
--gutter: 16px;
|
|
188
|
+
display: flex; flex-direction: column;
|
|
189
|
+
padding-inline: max(var(--gutter, 16px), env(safe-area-inset-left)) max(var(--gutter, 16px), env(safe-area-inset-right));
|
|
190
|
+
}
|
|
191
|
+
@media (min-width: 768px) {
|
|
192
|
+
.pp .app {
|
|
193
|
+
--gutter: 28px;
|
|
194
|
+
max-width: 480px; margin-inline: auto; padding-inline: var(--gutter, 28px);
|
|
195
|
+
border-inline: 1px solid var(--border); background: var(--surface);
|
|
196
|
+
backdrop-filter: blur(40px) saturate(1.4); -webkit-backdrop-filter: blur(40px) saturate(1.4);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.pp .below { position: relative; }
|
|
201
|
+
/* the sticky row means a section reached by its id arrives UNDER it unless it says otherwise */
|
|
202
|
+
.pp #profile, .pp #kernel { scroll-margin-top: 82px; }
|
|
203
|
+
|
|
204
|
+
/* THE `sheet` FACE: no column, no ground, no chrome. The sheet around it IS the frame, and a
|
|
205
|
+
second frame inside one is a box in a box. Keyed on the face name rather than on a boolean, so a
|
|
206
|
+
third face styles itself by adding a selector instead of by renaming this one. */
|
|
207
|
+
.pp[data-face="sheet"] { min-height: 0; }
|
|
208
|
+
.pp[data-face="sheet"] .app { max-width: none; margin-inline: 0; border: 0; background: none;
|
|
209
|
+
backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
210
|
+
|
|
211
|
+
/* a mark fills its box; the picture inside it covers */
|
|
212
|
+
.pp .mk { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
213
|
+
|
|
214
|
+
/* Motion is never forced on somebody who asked it not to be — and the biggest effects, the ones a
|
|
215
|
+
surface is proudest of, are exactly what that setting exists to suppress. */
|
|
216
|
+
@media (prefers-reduced-motion: reduce) {
|
|
217
|
+
.pp *, .pp *::before, .pp *::after { animation: none !important; transition: none !important; }
|
|
218
|
+
}
|
|
219
|
+
</style>
|
package/src/Toolbar.astro
CHANGED
|
@@ -61,6 +61,10 @@ interface Props {
|
|
|
61
61
|
// destination, and justai.pro is above it (sending the parent "home" to a child platform is
|
|
62
62
|
// the wrong direction). Every other persona keeps it.
|
|
63
63
|
home?: boolean;
|
|
64
|
+
|
|
65
|
+
// Posts is required only where there IS a stream (doctrine 02 — an empty cut shipped to look
|
|
66
|
+
// complete is the buffet). A child persona has none, so `parent` turns this off by default.
|
|
67
|
+
posts?: boolean;
|
|
64
68
|
// What the title strip says when the Kernel or the Posts cut is the one at the toolbar line.
|
|
65
69
|
//
|
|
66
70
|
// OMIT THEM AND NOTHING MOVES: the strip keeps naming the persona and its handle, which is right
|
|
@@ -93,9 +97,47 @@ interface Props {
|
|
|
93
97
|
// The `profileIcon` slot still wins when a persona passes one — an inline SVG can take
|
|
94
98
|
// `currentColor` where an <img> cannot, which is why qr passes its mark rather than pointing at it.
|
|
95
99
|
avatar?: string;
|
|
100
|
+
|
|
101
|
+
// queries: child persona toolbar · files me feed toolbar · the toolbar inside /files ·
|
|
102
|
+
// how does a child persona get back to its parent · parent icon in the toolbar
|
|
103
|
+
//
|
|
104
|
+
// ── THE CHILD PERSONA TOOLBAR ────────────────────────────────────────────────────────────────
|
|
105
|
+
//
|
|
106
|
+
// `me`, `files` and `feed` are sub-personas mounted BY THE EDGE on every persona's host — you meet
|
|
107
|
+
// them at `qr.app.zone/files`, wearing qr's face, in qr's origin. They are somebody else's
|
|
108
|
+
// codebase living at your address, and their row has to say so.
|
|
109
|
+
//
|
|
110
|
+
// Pass `parent` and the row becomes the child's:
|
|
111
|
+
//
|
|
112
|
+
// the PARENT's face → this child's own mark → the bolt
|
|
113
|
+
//
|
|
114
|
+
// which is the ordinary row's grammar, unchanged. Position one is still "where you came from" —
|
|
115
|
+
// for a persona that is app.zone, and for a child it is the persona above it. Position two is
|
|
116
|
+
// still WHO. Position three is still the way IN. Nothing new was invented; the same four kinds of
|
|
117
|
+
// thing are being said by a shorter row, and there is no app.zone or justai icon in it because
|
|
118
|
+
// neither is where a visitor to `qr.app.zone/files` came from.
|
|
119
|
+
//
|
|
120
|
+
// THE PARENT'S FACE COSTS NOTHING TO FIND. A child runs in the parent's origin, so `/icon.svg` —
|
|
121
|
+
// the same path this cut already uses for a persona's own face — resolves to the PARENT's mark
|
|
122
|
+
// when the page is a child's. One path, two meanings, both correct, and neither needs a registry
|
|
123
|
+
// lookup or a build-time value the edge would have to inject.
|
|
124
|
+
//
|
|
125
|
+
// A child has no Posts and no BackToZone: `parent` turns both off unless asked otherwise.
|
|
126
|
+
//
|
|
127
|
+
// `icon` is optional and almost always should be: `/icon.svg` is already the parent's face when the
|
|
128
|
+
// child runs in the parent's origin. It exists for the one child that does not have to guess —
|
|
129
|
+
// `feed` already reads the host persona's avatar from the registry, and an absolute URL it was
|
|
130
|
+
// given beats a relative one it inferred.
|
|
131
|
+
parent?: { href?: string; label?: string; icon?: string };
|
|
96
132
|
}
|
|
97
133
|
import BackToZone from "./BackToZone.astro";
|
|
98
|
-
const {
|
|
134
|
+
const {
|
|
135
|
+
sectionsLabel, profileLabel, kernelLabel, postsLabel,
|
|
136
|
+
files = true, filesLabel = "Files", kernelTitle, postsTitle, avatar = "/icon.svg", parent,
|
|
137
|
+
} = Astro.props;
|
|
138
|
+
// A child's row answers UP to its parent, not out to the zone, and has no stream of its own — so
|
|
139
|
+
// both default off the moment `parent` is given, rather than each consumer remembering to say so.
|
|
140
|
+
const { home = !parent, posts = !parent } = Astro.props;
|
|
99
141
|
import AuthMount from "./AuthMount.astro";
|
|
100
142
|
---
|
|
101
143
|
|
|
@@ -107,6 +149,15 @@ import AuthMount from "./AuthMount.astro";
|
|
|
107
149
|
<div class="tb-left">
|
|
108
150
|
{home && <BackToZone />}
|
|
109
151
|
<nav class="tb-nav-group" aria-label={sectionsLabel}>
|
|
152
|
+
{/* POSITION ONE OF A CHILD'S ROW: the persona this one is mounted inside. `/icon.svg` is the
|
|
153
|
+
parent's face here, because a child runs in the parent's origin — the same path, read from
|
|
154
|
+
a different address. `#kernel` rather than `/` so the way back lands on the parent's work
|
|
155
|
+
and not on its biography, which is where a person who came from there was standing. */}
|
|
156
|
+
{parent && (
|
|
157
|
+
<a class="tb-nav tb-mark" id="navParent" href={parent.href ?? "/#kernel"} aria-label={parent.label ?? "Home"} title={parent.label ?? "Home"}>
|
|
158
|
+
<img src={parent.icon ?? "/icon.svg"} alt="" width="19" height="19" loading="lazy" decoding="async" />
|
|
159
|
+
</a>
|
|
160
|
+
)}
|
|
110
161
|
{/* THE PROFILE ICON IS THE PERSONA'S OWN FACE. Always — the founder's rule, and app.zone and
|
|
111
162
|
qr had already settled it while thirteen siblings still showed a generic bust.
|
|
112
163
|
|
|
@@ -119,8 +170,24 @@ import AuthMount from "./AuthMount.astro";
|
|
|
119
170
|
`tb-mark` is set whichever way the face arrives, because it is what the sizing rule looks
|
|
120
171
|
for: a mark runs to the edge of its viewBox and a glyph does not, so they need different
|
|
121
172
|
boxes to put the same ink on screen. */}
|
|
122
|
-
|
|
123
|
-
|
|
173
|
+
{/* `tb-mark` says "a solid MARK is in this box" — it is what the sizing rule looks for, and a
|
|
174
|
+
mark runs to the edge of its viewBox where a stroke glyph sits inside 75% of one. A
|
|
175
|
+
persona's face is always a mark, so a persona always carries the class.
|
|
176
|
+
|
|
177
|
+
A CHILD IS THE OTHER CASE, and it was measured rather than assumed: files passes a stroke
|
|
178
|
+
folder, and with `tb-mark` on it, it rendered at 19px beside a 25.33px bolt — one size
|
|
179
|
+
smaller, which is exactly the defect the 25.33 rule exists to remove. So in child mode the
|
|
180
|
+
slotted icon takes the GLYPH box and the row reads level. A child that draws a solid mark
|
|
181
|
+
instead should say so in its own CSS; every child in the fleet draws a glyph today. */}
|
|
182
|
+
<a class:list={["tb-nav", { "tb-mark": !parent }]} id="navProfile" href="#profile" aria-label={profileLabel} title={profileLabel}>
|
|
183
|
+
{/* A CHILD MUST PASS ITS OWN MARK. The default `/icon.svg` is the face of whoever owns this
|
|
184
|
+
origin — right for a persona, and its PARENT for a child, which would put the same
|
|
185
|
+
picture in two places and say nothing in either. So when `parent` is given there is no
|
|
186
|
+
fallback: a child that passes nothing renders an empty box, which is a defect somebody
|
|
187
|
+
sees, rather than a duplicate face, which is a defect nobody reports. */}
|
|
188
|
+
{Astro.slots.has("profileIcon")
|
|
189
|
+
? <slot name="profileIcon" />
|
|
190
|
+
: (!parent && <img src={avatar} alt="" width="19" height="19" loading="lazy" decoding="async" />)}
|
|
124
191
|
</a>
|
|
125
192
|
{/* THE KERNEL IS ALWAYS THE BOLT. The founder's rule, and it is the shell's to draw rather
|
|
126
193
|
than the persona's to remember.
|
|
@@ -137,9 +204,11 @@ import AuthMount from "./AuthMount.astro";
|
|
|
137
204
|
<a class="tb-nav" id="navKernel" href="#kernel" aria-label={kernelLabel} title={kernelLabel}>
|
|
138
205
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.9 3L5.7 13.35h5.4L10.2 21l8.1-10.8h-5.4l.9-7.2z"/></svg>
|
|
139
206
|
</a>
|
|
140
|
-
|
|
141
|
-
<
|
|
142
|
-
|
|
207
|
+
{posts && (
|
|
208
|
+
<a class="tb-nav" id="navPosts" href="#posts" aria-label={postsLabel} title={postsLabel}>
|
|
209
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.4 8.4 0 0 1-9 8.3 9.6 9.6 0 0 1-4-.9L3 21l1.1-5A8.4 8.4 0 1 1 21 11.5z"/></svg>
|
|
210
|
+
</a>
|
|
211
|
+
)}
|
|
143
212
|
<slot name="navExtra" />
|
|
144
213
|
</nav>
|
|
145
214
|
{files && (
|