@justai/cuts 0.50.0 → 0.52.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.50.0",
3
+ "version": "0.52.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": {
package/src/Base.astro CHANGED
@@ -66,7 +66,7 @@ interface Props {
66
66
  themeLight?: string;
67
67
  /** app.zone's property by default. justai.pro passes its own. Pass `null` for a persona that runs
68
68
  * NO analytics at all — the whole GA block and the consent banner are then not rendered. The
69
- * privacy/terms sub-personas use this: the surface that proves it collects nothing by running
69
+ * privacy/terms child personas use this: the surface that proves it collects nothing by running
70
70
  * nothing. */
71
71
  gaId?: string | null;
72
72
  }
@@ -200,7 +200,7 @@ const localePath = (l: string) => (l === defaultLocale ? "/" : `/${l}/`);
200
200
  request and no console error, which is the only kind of analytics bug nobody finds. */}
201
201
  {/* gaId can be null: a persona that measures NOTHING. Pass gaId={null} and the entire analytics
202
202
  block below and the consent banner in the body are simply not rendered — no gtag, no cookie,
203
- no request. The privacy/terms sub-personas run this way, and it is not an exception but their
203
+ no request. The privacy/terms child personas run this way, and it is not an exception but their
204
204
  character: the one surface that can prove it collects nothing by running nothing (doctrine 15).
205
205
  The default stays app.zone's property, so every existing persona is unchanged — this costs the
206
206
  ten that measure themselves exactly nothing. */}
package/src/Posts.astro CHANGED
@@ -106,7 +106,43 @@ const vars = [
106
106
  .trow.last .cc { padding-block-end: 0; }
107
107
 
108
108
  .pa { flex: none; width: 42px; height: 42px; border-radius: var(--radius-sm); display: grid; place-items: center; font-size: 22px; background: var(--feed-av-bg, #fff); border: 1px solid var(--border); overflow: hidden; }
109
- .uav { flex: none; width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center; font-size: 13px; font-weight: 600; color: #fff; background: var(--c, #888); }
109
+ /* A human's avatar: their initial on a disc in their own colour, which arrives from the API as --c.
110
+ It was the last flat-filled object in the family, so it takes the house FILL recipe
111
+ (app-zone/.justai/expert/how-an-app-zone-icon-is-lit.md): an off-centre radial at 37% 31% with three
112
+ stops at L+22 / L+4 / L−11, so the light falls from the upper left and the lower right carries the
113
+ shade.
114
+
115
+ THE SPECULAR IS DELIBERATELY OMITTED, which is the one place this departs from the recipe. The doc
116
+ defines it as "what makes it a sphere rather than a lighter disc" — exactly right for howlong's beads,
117
+ which are beads, and wrong here. With it the disc read as a ball; without it the same three stops read
118
+ as a flat disc with light across it, which is what an avatar should be. Measured on Dana's #0a84ff: the
119
+ highlight sits 63 luminance steps above the flat colour instead of 94, and the rim is untouched at L99
120
+ either way — only the middle came down.
121
+
122
+ The stops are derived in CSS rather than precomputed, because the colour only exists at runtime.
123
+ `hsl(from var(--c) h s calc(l ± n))` is the rule stated literally: hue and saturation are passed
124
+ through untouched and lightness does all the shading. Measured — lit end 210.2° S100, shaded end
125
+ 210.1° S100, so nothing drifts.
126
+
127
+ NO % ON THOSE NUMBERS. Inside relative colour syntax `l` resolves to a <number>, so `calc(l + 22%)`
128
+ mixes types, the declaration is dropped, and the disc silently renders flat — it looked finished and
129
+ measured as zero light-to-shadow difference until the units came off. `background-color` is kept as a
130
+ separate declaration for the same reason: where the syntax is unsupported the disc loses its light
131
+ rather than disappearing.
132
+
133
+ `max(0, 39 - l)` is the doc's L28 floor as a LIFT applied to all three stops, not a clamp on the dark
134
+ end: clamping compresses the spread and weakens the emboss unevenly. 39 is the shortfall threshold
135
+ (28 + 11), so a colour light enough to clear it adds nothing. */
136
+ .uav {
137
+ flex: none; width: 30px; height: 30px; border-radius: 50%;
138
+ display: grid; place-items: center; font-size: 13px; font-weight: 600; color: #fff;
139
+ background-color: var(--c, #888);
140
+ background-image:
141
+ radial-gradient(ellipse 80% 80% at 37% 31%,
142
+ hsl(from var(--c, #888) h s calc(l + 22 + max(0, 39 - l))) 0%,
143
+ hsl(from var(--c, #888) h s calc(l + 4 + max(0, 39 - l))) 40%,
144
+ hsl(from var(--c, #888) h s calc(l - 11 + max(0, 39 - l))) 100%);
145
+ }
110
146
  .rav { flex: none; width: 30px; height: 30px; border-radius: var(--radius-sm); display: grid; place-items: center; font-size: 15px; background: var(--feed-av-bg, #fff); border: 1px solid var(--border); overflow: hidden; }
111
147
  .pa :global(svg), .rav :global(svg) { display: block; }
112
148
  .pa .mk, .rav .mk { width: 100%; height: 100%; object-fit: cover; display: block; }
@@ -118,8 +154,22 @@ const vars = [
118
154
  go from the CONTAINER, which is why this reads upward from the image with :has(). Scoping it to the image
119
155
  is what keeps a sibling persona's tiled icon tiled: .rav wraps their reply as well as a self-reply.
120
156
  `contain` rather than `cover`, because a mark with transparent margins must not be cropped to fill. */
121
- .pa:has(.mk.bare), .rav:has(.mk.bare) { background: none; border-color: transparent; }
122
- .pa .mk.bare, .rav .mk.bare { object-fit: contain; }
157
+ .pa:has(.mk.bare), .rav:has(.mk.bare) {
158
+ background: none; border-color: transparent;
159
+ /* The slot rounds its corners and clips, which is right for a tile whose own corners are round and wrong
160
+ for a mark with no tile: the arms reach the corners, so the radius sliced their round outer tips flat.
161
+ Measured before the fix — 27.5px wide against 28.0 tall inside a square 30px slot, the asymmetry being
162
+ the clipping showing up as numbers. Nothing is left to clip once the mark is inset below, but both go
163
+ anyway, or a later change to the inset brings the slicing back silently. */
164
+ border-radius: 0; overflow: visible;
165
+ }
166
+ /* 72%, measured rather than judged. Filling the slot, the arms painted 92% of it; a sibling's tiled icon
167
+ paints its glyph across 61%, because the glyph sits inset within its own tile. So app.zone carried half
168
+ again the painted width of every persona it was replying to, in a thread where it is one speaker among
169
+ peers rather than the platform above them. At 72% the arms land near 67% — a shade above the siblings,
170
+ which suits a bracket form being lighter than a filled ring. The feed permalink page uses the same number
171
+ for the same reason; the two surfaces render one conversation and must not disagree about it. */
172
+ .pa .mk.bare, .rav .mk.bare { width: 72%; height: 72%; object-fit: contain; }
123
173
  /* The scheme pair: the page decides, because the SVG's own media query does not survive <img>.
124
174
  Written as `.pa .mk.av-*` rather than `.av-*` deliberately — `.pa .mk { display: block }` above is two
125
175
  classes deep, so a single-class `.av-light { display: none }` loses on specificity and the toggle silently