@justai/cuts 0.132.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justai/cuts",
3
- "version": "0.132.0",
3
+ "version": "0.138.0",
4
4
  "description": "A persona's named parts \u2014 the page shell that holds them, and the cuts themselves.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -41,8 +41,11 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@justai/core": "^0.35.0",
45
- "@justai/ui": "^0.22.0",
44
+ "@justai/core": "^0.36.0",
45
+ "@justai/ui": "^0.25.0",
46
46
  "@justai/atrium": "^0.1.9"
47
+ },
48
+ "scripts": {
49
+ "check": "bunx tsc -p tsconfig.json"
47
50
  }
48
51
  }
@@ -45,32 +45,36 @@ const { title, note } = Astro.props;
45
45
  </div>
46
46
 
47
47
  <style>
48
- /* queries: two action lists touching · no gap between groups · the second group sits on the first
49
- THE BREAK IS THE GRAMMAR, so the component owns it.
50
- A group carried its INTERNAL 7px and nothing between itself and the next one, which left the
51
- break — the thing that separates "things you might do" from "the door with no way back" — to
52
- whichever consumer remembered a wrapper. files did in one sheet and not in another, and the
53
- second one read as one long list with a red row at the end.
54
- `:last-child` so a lone group adds no trailing space it did not ask for. */
55
- .jal-group { display: flex; flex-direction: column; gap: 7px; }
56
- .jal-group + .jal-group { margin-block-start: 22px; }
57
- /* Inset and grouped: the card is the group, and `overflow: hidden` is what lets a row's own
58
- background reach the rounded corner instead of squaring it off on press. */
59
- .jal {
60
- display: flex;
61
- flex-direction: column;
62
- background: var(--surface-2);
63
- border: 1px solid var(--border);
64
- border-radius: var(--radius-md);
65
- overflow: hidden;
48
+ /* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
49
+ out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
50
+ @layer justai.components {
51
+ /* queries: two action lists touching · no gap between groups · the second group sits on the first
52
+ THE BREAK IS THE GRAMMAR, so the component owns it.
53
+ A group carried its INTERNAL 7px and nothing between itself and the next one, which left the
54
+ break — the thing that separates "things you might do" from "the door with no way back" — to
55
+ whichever consumer remembered a wrapper. files did in one sheet and not in another, and the
56
+ second one read as one long list with a red row at the end.
57
+ `:last-child` so a lone group adds no trailing space it did not ask for. */
58
+ .jal-group { display: flex; flex-direction: column; gap: 7px; }
59
+ .jal-group + .jal-group { margin-block-start: 22px; }
60
+ /* Inset and grouped: the card is the group, and `overflow: hidden` is what lets a row's own
61
+ background reach the rounded corner instead of squaring it off on press. */
62
+ .jal {
63
+ display: flex;
64
+ flex-direction: column;
65
+ background: var(--surface-2);
66
+ border: 1px solid var(--border);
67
+ border-radius: var(--radius-md);
68
+ overflow: hidden;
69
+ }
70
+ .jal-title {
71
+ font-size: var(--fs-xs);
72
+ font-weight: 600;
73
+ color: var(--text-dim);
74
+ text-transform: uppercase;
75
+ letter-spacing: 0.04em;
76
+ padding-inline: 4px;
77
+ }
78
+ .jal-note { font-size: var(--fs-xs); color: var(--text-dim); padding-inline: 4px; line-height: 1.45; }
66
79
  }
67
- .jal-title {
68
- font-size: var(--fs-xs);
69
- font-weight: 600;
70
- color: var(--text-dim);
71
- text-transform: uppercase;
72
- letter-spacing: 0.04em;
73
- padding-inline: 4px;
74
- }
75
- .jal-note { font-size: var(--fs-xs); color: var(--text-dim); padding-inline: 4px; line-height: 1.45; }
76
80
  </style>
@@ -69,72 +69,76 @@ const { id, label, tone = "plain", detail, hidden, i18n, ...rest } = Astro.props
69
69
  </button>
70
70
 
71
71
  <style>
72
- /* A ROW, not a button in a line: it takes the full width it is given, so the label is never the
73
- thing that has to shrink. The 44px floor is the one number iOS never moves — it is a fingertip,
74
- and `min-height` rather than a fixed height so a reader who raised their text size gets a
75
- taller row instead of a clipped one. */
76
- .jal-row {
77
- display: flex;
78
- align-items: center;
79
- gap: 12px;
80
- width: 100%;
81
- min-height: 44px;
82
- padding: 11px 14px;
83
- border: 0;
84
- background: none;
85
- color: var(--text);
86
- font: inherit;
87
- font-size: var(--fs-sm);
88
- font-weight: 500;
89
- text-align: start;
90
- cursor: pointer;
91
- /* The separator lives on the row and is removed from the last one by the list — so a row can be
92
- hidden or revealed without leaving a hairline hanging under nothing. */
93
- border-block-end: 1px solid var(--border);
94
- transition: background 0.15s;
95
- }
96
- .jal-row:last-child,
97
- .jal-row[hidden] + .jal-row:last-child { border-block-end: 0; }
98
- .jal-row:active { background: color-mix(in srgb, var(--text) 8%, transparent); }
99
- @media (hover: hover) {
100
- .jal-row:hover { background: color-mix(in srgb, var(--text) 5%, transparent); }
101
- }
102
- .jal-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
72
+ /* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
73
+ out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
74
+ @layer justai.components {
75
+ /* A ROW, not a button in a line: it takes the full width it is given, so the label is never the
76
+ thing that has to shrink. The 44px floor is the one number iOS never moves — it is a fingertip,
77
+ and `min-height` rather than a fixed height so a reader who raised their text size gets a
78
+ taller row instead of a clipped one. */
79
+ .jal-row {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 12px;
83
+ width: 100%;
84
+ min-height: 44px;
85
+ padding: 11px 14px;
86
+ border: 0;
87
+ background: none;
88
+ color: var(--text);
89
+ font: inherit;
90
+ font-size: var(--fs-sm);
91
+ font-weight: 500;
92
+ text-align: start;
93
+ cursor: pointer;
94
+ /* The separator lives on the row and is removed from the last one by the list — so a row can be
95
+ hidden or revealed without leaving a hairline hanging under nothing. */
96
+ border-block-end: 1px solid var(--border);
97
+ transition: background 0.15s;
98
+ }
99
+ .jal-row:last-child,
100
+ .jal-row[hidden] + .jal-row:last-child { border-block-end: 0; }
101
+ .jal-row:active { background: color-mix(in srgb, var(--text) 8%, transparent); }
102
+ @media (hover: hover) {
103
+ .jal-row:hover { background: color-mix(in srgb, var(--text) 5%, transparent); }
104
+ }
105
+ .jal-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
103
106
 
104
- .jal-ico {
105
- display: inline-flex;
106
- flex: none;
107
- color: var(--text-dim);
108
- }
109
- /* The glyph is cut to a fixed box, so it is sized in px on purpose — it is a mark, not reading
110
- text, and the box it sits in is a hand size rather than a type size. */
111
- .jal-ico :global(svg) { width: 20px; height: 20px; display: block; }
107
+ .jal-ico {
108
+ display: inline-flex;
109
+ flex: none;
110
+ color: var(--text-dim);
111
+ }
112
+ /* The glyph is cut to a fixed box, so it is sized in px on purpose — it is a mark, not reading
113
+ text, and the box it sits in is a hand size rather than a type size. */
114
+ .jal-ico :global(svg) { width: 20px; height: 20px; display: block; }
112
115
 
113
- /* The label takes the room. This is the whole point of the component: nothing here clips. */
114
- .jal-label { flex: 1; min-width: 0; }
115
- .jal-detail { color: var(--text-dim); font-size: var(--fs-xs); flex: none; }
116
+ /* The label takes the room. This is the whole point of the component: nothing here clips. */
117
+ .jal-label { flex: 1; min-width: 0; }
118
+ .jal-detail { color: var(--text-dim); font-size: var(--fs-xs); flex: none; }
116
119
 
117
- /* queries: the accent row is unreadable in light mode · is-accent contrast · accent as text ·
118
- my persona's accent is too bright to read in a sheet
119
- THE ACCENT AS INK IS NOT THE ACCENT AS PAINT. A fill only has to be SEEN; a word has to be READ,
120
- and a persona whose accent is light fails contrast here while looking perfect as a button — tts's
121
- amber measures 1.67:1 on the light surface against the 4.5 a word needs.
122
- This is the same problem ConfirmSheet already solved with `--confirm-accent`, one component over,
123
- for the same stated reason ("the action reads as TEXT"). The hook is additive: a consumer that
124
- sets nothing gets exactly what it got before, and one that sets `--accent-text` gets a readable
125
- row. Prefer fixing it here over a persona globalising this class — that is what tts had to do
126
- while this shipped, and a persona reaching into a cut's internals is a fix that rots. */
127
- .jal-row.is-accent { color: var(--accent-text, var(--accent)); font-weight: 600; }
128
- .jal-row.is-accent .jal-ico { color: var(--accent-text, var(--accent)); }
129
- .jal-row.is-danger { color: var(--danger); }
130
- .jal-row.is-danger .jal-ico { color: var(--danger); }
131
- .jal-row.is-danger:active { background: color-mix(in srgb, var(--danger) 12%, transparent); }
132
- @media (hover: hover) {
133
- .jal-row.is-danger:hover { background: color-mix(in srgb, var(--danger) 10%, transparent); }
134
- }
120
+ /* queries: the accent row is unreadable in light mode · is-accent contrast · accent as text ·
121
+ my persona's accent is too bright to read in a sheet
122
+ THE ACCENT AS INK IS NOT THE ACCENT AS PAINT. A fill only has to be SEEN; a word has to be READ,
123
+ and a persona whose accent is light fails contrast here while looking perfect as a button — tts's
124
+ amber measures 1.67:1 on the light surface against the 4.5 a word needs.
125
+ This is the same problem ConfirmSheet already solved with `--confirm-accent`, one component over,
126
+ for the same stated reason ("the action reads as TEXT"). The hook is additive: a consumer that
127
+ sets nothing gets exactly what it got before, and one that sets `--accent-text` gets a readable
128
+ row. Prefer fixing it here over a persona globalising this class — that is what tts had to do
129
+ while this shipped, and a persona reaching into a cut's internals is a fix that rots. */
130
+ .jal-row.is-accent { color: var(--accent-text, var(--accent)); font-weight: 600; }
131
+ .jal-row.is-accent .jal-ico { color: var(--accent-text, var(--accent)); }
132
+ .jal-row.is-danger { color: var(--danger); }
133
+ .jal-row.is-danger .jal-ico { color: var(--danger); }
134
+ .jal-row.is-danger:active { background: color-mix(in srgb, var(--danger) 12%, transparent); }
135
+ @media (hover: hover) {
136
+ .jal-row.is-danger:hover { background: color-mix(in srgb, var(--danger) 10%, transparent); }
137
+ }
135
138
 
136
- .jal-row[disabled] { opacity: 0.4; cursor: default; }
137
- /* A class-set display beats the user agent's `[hidden] { display: none }` — that has bitten this
138
- ecosystem twice already, in this very sheet. Said again where the display is set. */
139
- .jal-row[hidden] { display: none !important; }
139
+ .jal-row[disabled] { opacity: 0.4; cursor: default; }
140
+ /* A class-set display beats the user agent's `[hidden] { display: none }` — that has bitten this
141
+ ecosystem twice already, in this very sheet. Said again where the display is set. */
142
+ .jal-row[hidden] { display: none !important; }
143
+ }
140
144
  </style>
@@ -92,44 +92,48 @@ const href = `https://${apex}`;
92
92
  </a>
93
93
 
94
94
  <style>
95
- /* icon-only, flat — flush to the left edge of the (full-bleed) toolbar */
96
- .home {
97
- display: inline-grid; place-items: center; width: 36px; height: 36px;
98
- color: var(--text-dim); text-decoration: none; margin-inline-start: -7px;
99
- transition: color 0.2s, transform 0.2s var(--ease-spring);
100
- }
101
- .home:hover { color: var(--text); }
102
- .home:active { transform: scale(0.9); }
103
- .home svg { width: 20px; height: 20px; display: block; }
95
+ /* Scoped by Astro's hash AND placed in the cascade: a persona overriding this never has to
96
+ out-specify it. See @justai/ui/tokens.css for the order, and for what deliberately stays out. */
97
+ @layer justai.components {
98
+ /* icon-only, flat — flush to the left edge of the (full-bleed) toolbar */
99
+ .home {
100
+ display: inline-grid; place-items: center; width: 36px; height: 36px;
101
+ color: var(--text-dim); text-decoration: none; margin-inline-start: -7px;
102
+ transition: color 0.2s, transform 0.2s var(--ease-spring);
103
+ }
104
+ .home:hover { color: var(--text); }
105
+ .home:active { transform: scale(0.9); }
106
+ .home svg { width: 20px; height: 20px; display: block; }
104
107
 
105
- /* At rest every part of the mark is the row's ink, so the silhouette reads as one shape rather than
106
- a logo parked in a toolbar. The centre is FILLED rather than knocked out: in the coloured drawing
107
- it is a counter-shape separating four saturated arms, and in one colour there is nothing to
108
- separate — a hole there would be a gap the mark does not have. */
109
- .azmark .arm,
110
- .azmark .ctr { fill: currentColor; }
108
+ /* At rest every part of the mark is the row's ink, so the silhouette reads as one shape rather than
109
+ a logo parked in a toolbar. The centre is FILLED rather than knocked out: in the coloured drawing
110
+ it is a counter-shape separating four saturated arms, and in one colour there is nothing to
111
+ separate — a hole there would be a gap the mark does not have. */
112
+ .azmark .arm,
113
+ .azmark .ctr { fill: currentColor; }
111
114
 
112
- /* queries: the home icon is smaller than the other glyphs · 20px of ink · pgmark size
113
- FILLED, AND THE viewBox IS THE INK'S OWN BOUNDS. This mark was three round-capped strokes, which
114
- is the same shape — but getBBox reports a stroke's centreline only, and the fleet's "every icon
115
- paints 20px of ink" spec measured it at 9.9px beside 20px glyphs. The spec was right: at that
116
- size it really was half its neighbours. Rotated rounded rects draw exactly what the strokes did
117
- and measure what they look like. */
118
- .pgmark .arm { fill: currentColor; }
119
- .home:hover .pgmark .a1, .home:focus-visible .pgmark .a1 { fill: #3D6FD9; }
120
- .home:hover .pgmark .a2, .home:focus-visible .pgmark .a2 { fill: #E8503A; }
121
- .home:hover .pgmark .a3, .home:focus-visible .pgmark .a3 { fill: #F5A623; }
115
+ /* queries: the home icon is smaller than the other glyphs · 20px of ink · pgmark size
116
+ FILLED, AND THE viewBox IS THE INK'S OWN BOUNDS. This mark was three round-capped strokes, which
117
+ is the same shape — but getBBox reports a stroke's centreline only, and the fleet's "every icon
118
+ paints 20px of ink" spec measured it at 9.9px beside 20px glyphs. The spec was right: at that
119
+ size it really was half its neighbours. Rotated rounded rects draw exactly what the strokes did
120
+ and measure what they look like. */
121
+ .pgmark .arm { fill: currentColor; }
122
+ .home:hover .pgmark .a1, .home:focus-visible .pgmark .a1 { fill: #3D6FD9; }
123
+ .home:hover .pgmark .a2, .home:focus-visible .pgmark .a2 { fill: #E8503A; }
124
+ .home:hover .pgmark .a3, .home:focus-visible .pgmark .a3 { fill: #F5A623; }
122
125
 
123
- .home:hover .azmark .a1, .home:focus-visible .azmark .a1 { fill: url(#azHome1); }
124
- .home:hover .azmark .a2, .home:focus-visible .azmark .a2 { fill: url(#azHome2); }
125
- .home:hover .azmark .a3, .home:focus-visible .azmark .a3 { fill: url(#azHome3); }
126
- .home:hover .azmark .a4, .home:focus-visible .azmark .a4 { fill: url(#azHome4); }
127
- /* The centre follows the ground, and both values are the project's own: #F5F1E8 is the centre of
128
- Logo Black, drawn for dark grounds, and #2B2724 the centre of Logo White, drawn for pale ones.
129
- Dark is the un-queried default because the fleet's tokens are dark-first. */
130
- .home:hover .azmark .ctr, .home:focus-visible .azmark .ctr { fill: #F5F1E8; }
131
- @media (prefers-color-scheme: light) {
132
- :global(html:not([data-theme="dark"])) .home:hover .azmark .ctr, :global(html:not([data-theme="dark"])) .home:focus-visible .azmark .ctr { fill: #2B2724; }
126
+ .home:hover .azmark .a1, .home:focus-visible .azmark .a1 { fill: url(#azHome1); }
127
+ .home:hover .azmark .a2, .home:focus-visible .azmark .a2 { fill: url(#azHome2); }
128
+ .home:hover .azmark .a3, .home:focus-visible .azmark .a3 { fill: url(#azHome3); }
129
+ .home:hover .azmark .a4, .home:focus-visible .azmark .a4 { fill: url(#azHome4); }
130
+ /* The centre follows the ground, and both values are the project's own: #F5F1E8 is the centre of
131
+ Logo Black, drawn for dark grounds, and #2B2724 the centre of Logo White, drawn for pale ones.
132
+ Dark is the un-queried default because the fleet's tokens are dark-first. */
133
+ .home:hover .azmark .ctr, .home:focus-visible .azmark .ctr { fill: #F5F1E8; }
134
+ @media (prefers-color-scheme: light) {
135
+ :global(html:not([data-theme="dark"])) .home:hover .azmark .ctr, :global(html:not([data-theme="dark"])) .home:focus-visible .azmark .ctr { fill: #2B2724; }
136
+ }
133
137
  }
134
138
  </style>
135
139
 
package/src/Base.astro CHANGED
@@ -33,7 +33,7 @@ import { dir as dirOf } from "@justai/ui/locale";
33
33
  import "./ground.css";
34
34
  // The load-bearing behaviour is framework-free and lives in @justai/core, so `feed` (Next) injects the
35
35
  // SAME strings instead of hand-copying them. This binding just decides where they go in an Astro head.
36
- import { AUTH_CAPTURE_JS, CONSENT_JS, JS_CLASS_JS, REVEAL_CSS, XRAY_JS, analyticsJs, consentCopy } from "@justai/core";
36
+ import { AUTH_CAPTURE_JS, CONSENT_JS, JS_CLASS_JS, REVEAL_CSS, analyticsJs, consentCopy } from "@justai/core";
37
37
  // The consent ask IS a confirmation — a moment of trust — so it rides the fleet's ONE dialog
38
38
  // rather than keeping a private dialect. One anatomy for "are you sure?" and "may I count you?".
39
39
  import ConfirmSheet from "./ConfirmSheet.astro";
@@ -180,8 +180,26 @@ const localePath = (l: string) => (l === defaultLocale ? "/" : `/${l}/`);
180
180
  repo is one nobody has when they need it.
181
181
  It is glasses, not a renovation: nothing is added to the page's own elements, the overlay is
182
182
  fixed and reads `#kernel`'s rect, and `pointer-events` is none throughout. And it costs
183
- nothing when off — the script returns on its second line unless the flag is there. */}
184
- <script is:inline set:html={XRAY_JS} />
183
+ nothing when off — MEASURED, and it did not. */}
184
+ {/* queries: xray page weight · why is the x-ray a dynamic import · debug tool on every page ·
185
+ 5381 bytes of xray · lazy load a debug overlay
186
+
187
+ IT COST 5,381 BYTES ON EVERY PAGE OF EVERY PERSONA, TO EVERY VISITOR — 10 to 16% of the
188
+ document, measured on four live hosts. "It costs nothing when off" was true of the RUNTIME
189
+ (one `if` and a return) and false of the TRANSFER, and only one of those is what a visitor
190
+ pays. The whole overlay was inlined into the head so that a branch could decline to run it.
191
+
192
+ So the flag check stays inline and the instrument is fetched only by whoever asked for it.
193
+ The person debugging pays one request; everybody else pays the bytes below. Astro splits the
194
+ dynamic import into its own chunk, so nothing about it reaches a page that does not ask.
195
+
196
+ It is a real module now, not a string — see @justai/core/xray. */}
197
+ <script>
198
+ if (new URLSearchParams(location.search).has("xray")) {
199
+ const { xray } = await import("@justai/core/xray");
200
+ xray();
201
+ }
202
+ </script>
185
203
  {/* Reveal only after a page marks itself ready — gated behind .js so the page is always visible
186
204
  if JS is off, and no scroll flash before we position at the Kernel. This style is a TRAP as
187
205
  much as a feature, and the warning must stay beside it: ANY page using this layout without a