@justai/cuts 0.79.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justai/cuts",
3
- "version": "0.79.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.10.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>