@multiplatform.one/components 7.81.1 → 7.82.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.
Files changed (187) hide show
  1. package/dist/cjs/charts/BarChart.cjs +1 -0
  2. package/dist/cjs/charts/BarChart.native.js +1 -0
  3. package/dist/cjs/charts/BarChart.native.js.map +1 -1
  4. package/dist/cjs/charts/ChartDataView.cjs +89 -9
  5. package/dist/cjs/charts/ChartDataView.native.js +94 -9
  6. package/dist/cjs/charts/ChartDataView.native.js.map +1 -1
  7. package/dist/cjs/charts/ChartSurface.cjs +26 -8
  8. package/dist/cjs/charts/ChartSurface.native.js +27 -9
  9. package/dist/cjs/charts/ChartSurface.native.js.map +1 -1
  10. package/dist/cjs/charts/chartHeight.cjs +1 -1
  11. package/dist/cjs/charts/chartHeight.native.js +1 -1
  12. package/dist/cjs/charts/chartHeight.native.js.map +1 -1
  13. package/dist/cjs/charts/composeChart.cjs +1 -1
  14. package/dist/cjs/charts/composeChart.native.js +1 -1
  15. package/dist/cjs/charts/composeChart.native.js.map +1 -1
  16. package/dist/cjs/code/CodeBlock/index.cjs +38 -31
  17. package/dist/cjs/code/CodeBlock/index.native.js +38 -31
  18. package/dist/cjs/code/CodeBlock/index.native.js.map +1 -1
  19. package/dist/cjs/code/LogView.cjs +142 -0
  20. package/dist/cjs/code/LogView.native.js +151 -0
  21. package/dist/cjs/code/LogView.native.js.map +1 -0
  22. package/dist/cjs/code/index.cjs +2 -1
  23. package/dist/cjs/code/index.native.js +1 -0
  24. package/dist/cjs/code/index.native.js.map +1 -1
  25. package/dist/cjs/layouts/EmptyState.cjs +9 -43
  26. package/dist/cjs/layouts/EmptyState.native.js +9 -45
  27. package/dist/cjs/layouts/EmptyState.native.js.map +1 -1
  28. package/dist/cjs/layouts/PageTemplates.cjs +61 -0
  29. package/dist/cjs/layouts/PageTemplates.native.js +62 -0
  30. package/dist/cjs/layouts/PageTemplates.native.js.map +1 -1
  31. package/dist/cjs/layouts/barTitleSpan.cjs +54 -0
  32. package/dist/cjs/layouts/barTitleSpan.native.js +78 -0
  33. package/dist/cjs/layouts/barTitleSpan.native.js.map +1 -0
  34. package/dist/cjs/layouts/page.cjs +130 -41
  35. package/dist/cjs/layouts/page.native.js +143 -45
  36. package/dist/cjs/layouts/page.native.js.map +1 -1
  37. package/dist/cjs/views/ImageGrid.cjs +9 -0
  38. package/dist/cjs/views/ImageGrid.native.js +12 -1
  39. package/dist/cjs/views/ImageGrid.native.js.map +1 -1
  40. package/dist/esm/charts/BarChart.mjs +1 -0
  41. package/dist/esm/charts/BarChart.mjs.map +1 -1
  42. package/dist/esm/charts/BarChart.native.js +1 -0
  43. package/dist/esm/charts/BarChart.native.js.map +1 -1
  44. package/dist/esm/charts/ChartDataView.mjs +83 -3
  45. package/dist/esm/charts/ChartDataView.mjs.map +1 -1
  46. package/dist/esm/charts/ChartDataView.native.js +88 -3
  47. package/dist/esm/charts/ChartDataView.native.js.map +1 -1
  48. package/dist/esm/charts/ChartSurface.mjs +26 -8
  49. package/dist/esm/charts/ChartSurface.mjs.map +1 -1
  50. package/dist/esm/charts/ChartSurface.native.js +27 -9
  51. package/dist/esm/charts/ChartSurface.native.js.map +1 -1
  52. package/dist/esm/charts/chartHeight.mjs +1 -1
  53. package/dist/esm/charts/chartHeight.mjs.map +1 -1
  54. package/dist/esm/charts/chartHeight.native.js +1 -1
  55. package/dist/esm/charts/chartHeight.native.js.map +1 -1
  56. package/dist/esm/charts/composeChart.mjs +1 -1
  57. package/dist/esm/charts/composeChart.mjs.map +1 -1
  58. package/dist/esm/charts/composeChart.native.js +1 -1
  59. package/dist/esm/charts/composeChart.native.js.map +1 -1
  60. package/dist/esm/code/CodeBlock/index.mjs +39 -32
  61. package/dist/esm/code/CodeBlock/index.mjs.map +1 -1
  62. package/dist/esm/code/CodeBlock/index.native.js +39 -32
  63. package/dist/esm/code/CodeBlock/index.native.js.map +1 -1
  64. package/dist/esm/code/LogView.mjs +117 -0
  65. package/dist/esm/code/LogView.mjs.map +1 -0
  66. package/dist/esm/code/LogView.native.js +123 -0
  67. package/dist/esm/code/LogView.native.js.map +1 -0
  68. package/dist/esm/code/index.mjs +1 -0
  69. package/dist/esm/code/index.mjs.map +1 -1
  70. package/dist/esm/code/index.native.js +1 -0
  71. package/dist/esm/code/index.native.js.map +1 -1
  72. package/dist/esm/layouts/EmptyState.mjs +9 -43
  73. package/dist/esm/layouts/EmptyState.mjs.map +1 -1
  74. package/dist/esm/layouts/EmptyState.native.js +9 -45
  75. package/dist/esm/layouts/EmptyState.native.js.map +1 -1
  76. package/dist/esm/layouts/PageTemplates.mjs +61 -1
  77. package/dist/esm/layouts/PageTemplates.mjs.map +1 -1
  78. package/dist/esm/layouts/PageTemplates.native.js +62 -1
  79. package/dist/esm/layouts/PageTemplates.native.js.map +1 -1
  80. package/dist/esm/layouts/barTitleSpan.mjs +27 -0
  81. package/dist/esm/layouts/barTitleSpan.mjs.map +1 -0
  82. package/dist/esm/layouts/barTitleSpan.native.js +48 -0
  83. package/dist/esm/layouts/barTitleSpan.native.js.map +1 -0
  84. package/dist/esm/layouts/page.mjs +132 -43
  85. package/dist/esm/layouts/page.mjs.map +1 -1
  86. package/dist/esm/layouts/page.native.js +145 -47
  87. package/dist/esm/layouts/page.native.js.map +1 -1
  88. package/dist/esm/views/ImageGrid.mjs +10 -1
  89. package/dist/esm/views/ImageGrid.mjs.map +1 -1
  90. package/dist/esm/views/ImageGrid.native.js +13 -2
  91. package/dist/esm/views/ImageGrid.native.js.map +1 -1
  92. package/dist/jsx/charts/BarChart.mjs +1 -0
  93. package/dist/jsx/charts/BarChart.mjs.map +1 -1
  94. package/dist/jsx/charts/BarChart.native.js +1 -0
  95. package/dist/jsx/charts/BarChart.native.js.map +1 -1
  96. package/dist/jsx/charts/ChartDataView.mjs +83 -3
  97. package/dist/jsx/charts/ChartDataView.mjs.map +1 -1
  98. package/dist/jsx/charts/ChartDataView.native.js +94 -9
  99. package/dist/jsx/charts/ChartDataView.native.js.map +1 -1
  100. package/dist/jsx/charts/ChartSurface.mjs +26 -8
  101. package/dist/jsx/charts/ChartSurface.mjs.map +1 -1
  102. package/dist/jsx/charts/ChartSurface.native.js +27 -9
  103. package/dist/jsx/charts/ChartSurface.native.js.map +1 -1
  104. package/dist/jsx/charts/chartHeight.mjs +1 -1
  105. package/dist/jsx/charts/chartHeight.mjs.map +1 -1
  106. package/dist/jsx/charts/chartHeight.native.js +1 -1
  107. package/dist/jsx/charts/chartHeight.native.js.map +1 -1
  108. package/dist/jsx/charts/composeChart.mjs +1 -1
  109. package/dist/jsx/charts/composeChart.mjs.map +1 -1
  110. package/dist/jsx/charts/composeChart.native.js +1 -1
  111. package/dist/jsx/charts/composeChart.native.js.map +1 -1
  112. package/dist/jsx/code/CodeBlock/index.mjs +39 -32
  113. package/dist/jsx/code/CodeBlock/index.mjs.map +1 -1
  114. package/dist/jsx/code/CodeBlock/index.native.js +38 -31
  115. package/dist/jsx/code/CodeBlock/index.native.js.map +1 -1
  116. package/dist/jsx/code/LogView.mjs +117 -0
  117. package/dist/jsx/code/LogView.mjs.map +1 -0
  118. package/dist/jsx/code/LogView.native.js +151 -0
  119. package/dist/jsx/code/LogView.native.js.map +1 -0
  120. package/dist/jsx/code/index.mjs +1 -0
  121. package/dist/jsx/code/index.mjs.map +1 -1
  122. package/dist/jsx/code/index.native.js +1 -0
  123. package/dist/jsx/code/index.native.js.map +1 -1
  124. package/dist/jsx/layouts/EmptyState.mjs +9 -43
  125. package/dist/jsx/layouts/EmptyState.mjs.map +1 -1
  126. package/dist/jsx/layouts/EmptyState.native.js +9 -45
  127. package/dist/jsx/layouts/EmptyState.native.js.map +1 -1
  128. package/dist/jsx/layouts/PageTemplates.mjs +61 -1
  129. package/dist/jsx/layouts/PageTemplates.mjs.map +1 -1
  130. package/dist/jsx/layouts/PageTemplates.native.js +62 -0
  131. package/dist/jsx/layouts/PageTemplates.native.js.map +1 -1
  132. package/dist/jsx/layouts/barTitleSpan.mjs +27 -0
  133. package/dist/jsx/layouts/barTitleSpan.mjs.map +1 -0
  134. package/dist/jsx/layouts/barTitleSpan.native.js +78 -0
  135. package/dist/jsx/layouts/barTitleSpan.native.js.map +1 -0
  136. package/dist/jsx/layouts/page.mjs +132 -43
  137. package/dist/jsx/layouts/page.mjs.map +1 -1
  138. package/dist/jsx/layouts/page.native.js +143 -45
  139. package/dist/jsx/layouts/page.native.js.map +1 -1
  140. package/dist/jsx/views/ImageGrid.mjs +10 -1
  141. package/dist/jsx/views/ImageGrid.mjs.map +1 -1
  142. package/dist/jsx/views/ImageGrid.native.js +12 -1
  143. package/dist/jsx/views/ImageGrid.native.js.map +1 -1
  144. package/package.json +9 -9
  145. package/src/charts/BarChart.tsx +8 -2
  146. package/src/charts/ChartDataView.spec.tsx +23 -2
  147. package/src/charts/ChartDataView.tsx +87 -6
  148. package/src/charts/ChartSurface.tsx +17 -8
  149. package/src/charts/chartHeight.ts +8 -2
  150. package/src/charts/composeChart.ts +3 -2
  151. package/src/code/CodeBlock/CodeBlock.copy.spec.tsx +13 -0
  152. package/src/code/CodeBlock/index.tsx +46 -30
  153. package/src/code/LogView.tsx +127 -0
  154. package/src/code/index.ts +1 -0
  155. package/src/layouts/DenseContent.stories.tsx +93 -0
  156. package/src/layouts/EmptyState.spec.tsx +2 -0
  157. package/src/layouts/EmptyState.tsx +6 -39
  158. package/src/layouts/PageHeader.stories.tsx +238 -38
  159. package/src/layouts/PageTemplates.tsx +90 -0
  160. package/src/layouts/barTitleSpan.spec.ts +35 -0
  161. package/src/layouts/barTitleSpan.ts +44 -0
  162. package/src/layouts/glyphCentre.spec.tsx +218 -0
  163. package/src/layouts/navBar.spec.tsx +58 -2
  164. package/src/layouts/page.tsx +126 -34
  165. package/src/views/ImageGrid.tsx +19 -2
  166. package/types/charts/BarChart.d.ts +1 -1
  167. package/types/charts/BarChart.d.ts.map +1 -1
  168. package/types/charts/ChartDataView.d.ts +3 -1
  169. package/types/charts/ChartDataView.d.ts.map +1 -1
  170. package/types/charts/ChartSurface.d.ts.map +1 -1
  171. package/types/charts/chartHeight.d.ts +2 -1
  172. package/types/charts/chartHeight.d.ts.map +1 -1
  173. package/types/charts/composeChart.d.ts +2 -1
  174. package/types/charts/composeChart.d.ts.map +1 -1
  175. package/types/code/CodeBlock/index.d.ts.map +1 -1
  176. package/types/code/LogView.d.ts +31 -0
  177. package/types/code/LogView.d.ts.map +1 -0
  178. package/types/code/index.d.ts +1 -0
  179. package/types/code/index.d.ts.map +1 -1
  180. package/types/layouts/EmptyState.d.ts.map +1 -1
  181. package/types/layouts/PageTemplates.d.ts +26 -0
  182. package/types/layouts/PageTemplates.d.ts.map +1 -1
  183. package/types/layouts/barTitleSpan.d.ts +29 -0
  184. package/types/layouts/barTitleSpan.d.ts.map +1 -0
  185. package/types/layouts/page.d.ts +6 -1
  186. package/types/layouts/page.d.ts.map +1 -1
  187. package/types/views/ImageGrid.d.ts.map +1 -1
@@ -10,8 +10,7 @@
10
10
  * not a second description. `intent="error"` is sober failure chrome
11
11
  * (DG-ST-01) — never for a successful empty dataset.
12
12
  */
13
- import { Button, Spinner } from "@multiplatform.one/forms";
14
- import { CopyIcon } from "@phosphor-icons/react";
13
+ import { Spinner } from "@multiplatform.one/forms";
15
14
  import {
16
15
  IconSlot,
17
16
  Intent,
@@ -26,7 +25,6 @@ import {
26
25
  H2,
27
26
  Paragraph,
28
27
  type SizeTokens,
29
- Text,
30
28
  View,
31
29
  XStack,
32
30
  YStack,
@@ -34,8 +32,7 @@ import {
34
32
  type YStackProps,
35
33
  } from "tamagui";
36
34
  import { componentColors, sectionHeading } from "../componentColors";
37
- import { NotifyRegion } from "../feedback/NotifyHost";
38
- import { useCopyFeedback } from "../feedback/copy";
35
+ import { CodeBlock } from "../code/CodeBlock";
39
36
 
40
37
  export interface EmptyStateProps extends Omit<YStackProps, "size"> {
41
38
  /** One short title: what is true now ("No Pokémon yet"), not an instruction. */
@@ -102,43 +99,13 @@ function StateSymbol({
102
99
  }
103
100
 
104
101
  /**
105
- * The refusal gate's one command: mono, one line, with copy. The confirm is
106
- * notify() and the glyph never flips (DG-FB-01 / LC-16).
102
+ * The refusal gate's one command, as a code block with its copy inside
103
+ * (LC-267).
107
104
  */
108
105
  function CommandLine({ command }: { command: string }) {
109
- const { knobProps } = useResolvedKnobs();
110
- const { region, copy } = useCopyFeedback();
111
106
  return (
112
- <YStack data-empty-command="" alignItems="center" {...readingWidthStyle()}>
113
- <XStack
114
- alignItems="center"
115
- maxWidth="100%"
116
- backgroundColor="$color3"
117
- paddingLeft="$3"
118
- paddingRight="$1"
119
- paddingVertical="$1"
120
- {...knobProps.containerRadius}
121
- {...knobProps.gapSm}
122
- >
123
- <Text
124
- {...knobProps.body}
125
- fontFamily="$mono"
126
- color={componentColors.text.primary}
127
- numberOfLines={1}
128
- flexShrink={1}
129
- >
130
- {command}
131
- </Text>
132
- <Button
133
- nested
134
- chromeless
135
- flexShrink={0}
136
- icon={<CopyIcon />}
137
- aria-label="Copy to clipboard"
138
- onPress={() => void copy(command)}
139
- />
140
- </XStack>
141
- <NotifyRegion scope="field" region={region} size="$3" />
107
+ <YStack data-empty-command="" alignSelf="center" {...readingWidthStyle()}>
108
+ <CodeBlock language="text">{command}</CodeBlock>
142
109
  </YStack>
143
110
  );
144
111
  }
@@ -1,27 +1,30 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
2
2
  // Catalog Button (G-15 story honesty / LC-18): raw tamagui buttons ignored
3
3
  // the knobs and pinned a non-house radius (UX-P06).
4
- import { Button, IconButton, Progress } from "@multiplatform.one/forms";
4
+ import { Button, IconButton } from "@multiplatform.one/forms";
5
5
  import {
6
- CopyIcon,
7
- DotsThreeIcon,
6
+ CpuIcon,
7
+ DatabaseIcon,
8
8
  MagnifyingGlassIcon,
9
+ MemoryIcon,
9
10
  PlusIcon,
10
11
  SignOutIcon,
11
- TrashIcon,
12
12
  UserIcon,
13
13
  } from "@phosphor-icons/react";
14
14
  import { Preset } from "@multiplatform.one/theme";
15
15
  import type { ReactNode } from "react";
16
- import { XStack, YStack } from "tamagui";
16
+ import { Circle, Theme, View, XStack, YStack } from "tamagui";
17
17
  import { Avatar } from "../Avatar";
18
18
  import { Card } from "../Card";
19
+ import { themed } from "../icons/themed";
20
+ import { Chip } from "../Chip";
21
+ import { CodeBlock } from "../code/CodeBlock";
19
22
  import { DropdownMenu } from "../DropdownMenu";
20
23
  import { Popover } from "../Popover";
21
24
  import { SchemeToggle } from "../SchemeToggle";
22
25
  import { PopoverContent } from "../surfaces";
23
26
  import { Tabs } from "../Tabs";
24
- import { PageBackButton, PageHeader, Screen, ScreenToolbar } from "./page";
27
+ import { PageBackButton, PageHeader, PageSection, Screen, ScreenToolbar } from "./page";
25
28
  import { Paragraph, SizableText } from "../Text";
26
29
 
27
30
  const meta: Meta<typeof PageHeader> = {
@@ -174,23 +177,237 @@ export const LongTitleNarrow: Story = {
174
177
  ),
175
178
  };
176
179
 
177
- // FZ-8020 LC-176: the SHC console Nodes screen with every icon-button
178
- // context, drawn by the platform with no use-site paint. Its bar (back,
179
- // search, add) takes the accent, on Liquid Glass on iOS 26; content actions
180
- // (the inline copy, the card's overflow) are bold glyphs in the text colour;
181
- // the delete keeps its intent ink; the tile's favourite rides the media plate.
182
- function Meter({ label, value, detail }: { label: string; value: number; detail: string }) {
180
+ // FZ-8442: the SHC console's Nodes screen as it is now (shc
181
+ // features/console/src/manage/app/NodesPane.tsx and ManageApp.tsx's
182
+ // NodesPlace): sites as sections, each node a front panel whose whole face
183
+ // links to its detail, where the verbs live. The "+" opens one panel over
184
+ // itself holding the join command as a code block (LC-112, LC-267). The bar
185
+ // keeps every LC-176 icon-button context.
186
+ const faceplate = {
187
+ ear: 18,
188
+ screw: 9,
189
+ slot: 1.5,
190
+ led: 14,
191
+ ledGap: 2,
192
+ digitWidth: 8,
193
+ digitHeight: 14,
194
+ segment: 2,
195
+ digitGap: 2,
196
+ window: 6,
197
+ } as const;
198
+ const LED_COUNT = 20;
199
+ const bayGlyph = { cpu: themed(CpuIcon), memory: themed(MemoryIcon), disk: themed(DatabaseIcon) };
200
+
201
+ function Screw({ angle }: { angle: number }) {
202
+ const inner = faceplate.screw - 2;
203
+ return (
204
+ <Circle
205
+ size={faceplate.screw}
206
+ borderWidth={1}
207
+ borderColor="$color9"
208
+ backgroundColor="$color6"
209
+ position="relative"
210
+ >
211
+ <View
212
+ position="absolute"
213
+ top={(inner - faceplate.slot) / 2}
214
+ left={0}
215
+ width={inner}
216
+ height={faceplate.slot}
217
+ backgroundColor="$color11"
218
+ rotate={`${angle}deg`}
219
+ />
220
+ </Circle>
221
+ );
222
+ }
223
+
224
+ function RackEar({ side, angles }: { side: "left" | "right"; angles: [number, number] }) {
225
+ return (
226
+ <YStack
227
+ width={faceplate.ear}
228
+ alignSelf="stretch"
229
+ alignItems="center"
230
+ justifyContent="space-between"
231
+ paddingVertical="$3"
232
+ backgroundColor="$color4"
233
+ borderColor="$color6"
234
+ borderLeftWidth={side === "right" ? 1 : 0}
235
+ borderRightWidth={side === "left" ? 1 : 0}
236
+ aria-hidden
237
+ >
238
+ <Screw angle={angles[0]} />
239
+ <Screw angle={angles[1]} />
240
+ </YStack>
241
+ );
242
+ }
243
+
244
+ const segmentsOf: Record<string, string> = {
245
+ "0": "abcdef",
246
+ "1": "bc",
247
+ "2": "abdeg",
248
+ "3": "abcdg",
249
+ "4": "bcfg",
250
+ "5": "acdfg",
251
+ "6": "acdefg",
252
+ "7": "abc",
253
+ "8": "abcdefg",
254
+ "9": "abcdfg",
255
+ };
256
+
257
+ function segmentBox(name: string, width: number) {
258
+ const { digitHeight: h, segment: t } = faceplate;
259
+ const middle = (h - t) / 2;
260
+ const stroke = (h + t) / 2;
261
+ const boxes: Record<string, { top: number; left: number; width: number; height: number }> = {
262
+ a: { top: 0, left: 0, width, height: t },
263
+ b: { top: 0, left: width - t, width: t, height: stroke },
264
+ c: { top: middle, left: width - t, width: t, height: stroke },
265
+ d: { top: h - t, left: 0, width, height: t },
266
+ e: { top: middle, left: 0, width: t, height: stroke },
267
+ f: { top: 0, left: 0, width: t, height: stroke },
268
+ g: { top: middle, left: 0, width, height: t },
269
+ };
270
+ return boxes[name]!;
271
+ }
272
+
273
+ function AddressDisplay({ address }: { address: string }) {
274
+ return (
275
+ <Theme name="dark">
276
+ <XStack
277
+ role="img"
278
+ aria-label={address}
279
+ alignItems="center"
280
+ gap={faceplate.digitGap}
281
+ backgroundColor="$color1"
282
+ borderColor="$color6"
283
+ borderWidth={1}
284
+ borderRadius="$2"
285
+ padding={faceplate.window}
286
+ >
287
+ {[...address].map((char, index) => {
288
+ if (char === "." || char === ":")
289
+ return (
290
+ <YStack
291
+ key={index}
292
+ width={faceplate.segment}
293
+ height={faceplate.digitHeight}
294
+ justifyContent={char === "." ? "flex-end" : "space-evenly"}
295
+ >
296
+ <View
297
+ width={faceplate.segment}
298
+ height={faceplate.segment}
299
+ backgroundColor="$blue10"
300
+ />
301
+ {char === ":" ? (
302
+ <View
303
+ width={faceplate.segment}
304
+ height={faceplate.segment}
305
+ backgroundColor="$blue10"
306
+ />
307
+ ) : null}
308
+ </YStack>
309
+ );
310
+ const width = char === "1" ? faceplate.segment : faceplate.digitWidth;
311
+ return (
312
+ <View key={index} width={width} height={faceplate.digitHeight} position="relative">
313
+ {[...(segmentsOf[char] ?? "")].map((name) => (
314
+ <View
315
+ key={name}
316
+ position="absolute"
317
+ {...segmentBox(name, width)}
318
+ backgroundColor="$blue10"
319
+ />
320
+ ))}
321
+ </View>
322
+ );
323
+ })}
324
+ </XStack>
325
+ </Theme>
326
+ );
327
+ }
328
+
329
+ const ledInk = { lit: "$color11", warning: "$orange9", critical: "$red9" } as const;
330
+ const lampAt = (index: number) =>
331
+ index >= LED_COUNT * 0.9 ? "critical" : index >= LED_COUNT * 0.7 ? "warning" : "lit";
332
+
333
+ function LedBar({
334
+ bay,
335
+ label,
336
+ percent,
337
+ amount,
338
+ }: {
339
+ bay: keyof typeof bayGlyph;
340
+ label: string;
341
+ percent: number;
342
+ amount: string;
343
+ }) {
344
+ const lit = Math.round((percent / 100) * LED_COUNT);
345
+ const Glyph = bayGlyph[bay];
183
346
  return (
184
- <YStack gap="$2">
185
- <XStack justifyContent="space-between">
186
- <SizableText color="$color11">{label}</SizableText>
187
- <SizableText>{detail}</SizableText>
347
+ <YStack gap="$1.5" role="meter" aria-label={label} aria-valuenow={percent}>
348
+ <XStack alignItems="center" gap="$2">
349
+ <Glyph size={16} weight="bold" color="$color11" />
350
+ <SizableText flex={1} size="$2" fontVariant={["tabular-nums"]} numberOfLines={1}>
351
+ {amount}
352
+ </SizableText>
353
+ <SizableText size="$2" fontWeight="700" fontVariant={["tabular-nums"]}>
354
+ {`${percent}%`}
355
+ </SizableText>
356
+ </XStack>
357
+ <XStack gap={faceplate.ledGap} aria-hidden>
358
+ {Array.from({ length: LED_COUNT }, (_, index) => (
359
+ <View
360
+ key={index}
361
+ flex={1}
362
+ height={faceplate.led}
363
+ borderRadius={1}
364
+ backgroundColor={ledInk[lampAt(index)]}
365
+ opacity={index < lit ? 1 : 0.18}
366
+ />
367
+ ))}
188
368
  </XStack>
189
- <Progress value={value} />
190
369
  </YStack>
191
370
  );
192
371
  }
193
372
 
373
+ function NodeFaceplate() {
374
+ return (
375
+ <Card padding="$0" overflow="hidden" flexDirection="row" alignItems="stretch">
376
+ <RackEar side="left" angles={[38, 121]} />
377
+ <YStack flex={1} gap="$3" padding="$3">
378
+ <XStack alignItems="center" justifyContent="space-between" gap="$2.5">
379
+ <XStack gap="$1.5" alignItems="center" aria-hidden>
380
+ <Circle size="$0.75" backgroundColor="$green9" />
381
+ <Circle size="$0.5" backgroundColor="$green9" />
382
+ </XStack>
383
+ <AddressDisplay address="10.64.96.1:4003" />
384
+ </XStack>
385
+ <XStack alignItems="center" gap="$2.5">
386
+ <SizableText size="$4" fontWeight="700" numberOfLines={1}>
387
+ incus01
388
+ </SizableText>
389
+ <SizableText size="$1" fontWeight="600">
390
+ incus
391
+ </SizableText>
392
+ </XStack>
393
+ <LedBar bay="cpu" label="CPU" percent={12} amount="2.83/24 cores" />
394
+ <LedBar bay="memory" label="Memory" percent={41} amount="25.3/61.9 GiB" />
395
+ <LedBar bay="disk" label="Disk" percent={74} amount="352/476 GiB" />
396
+ <XStack gap="$1.5" flexWrap="wrap">
397
+ <Chip size="$2">AMD Ryzen 9 7900</Chip>
398
+ <Chip size="$2">x86_64</Chip>
399
+ </XStack>
400
+ <Avatar.Group max={7} size={36} circular={false}>
401
+ {["pokemon", "keycloak", "nextcloud", "fizzy", "gitlab", "openobserve"].map((app) => (
402
+ <Avatar key={app} name={app} />
403
+ ))}
404
+ </Avatar.Group>
405
+ </YStack>
406
+ <RackEar side="right" angles={[92, 7]} />
407
+ </Card>
408
+ );
409
+ }
410
+
194
411
  // LC-112 (FZ-8665 Round 45): each icon trigger's panel covers its button with
195
412
  // no X, and a press outside closes it. The join panel's title sits on the
196
413
  // panel's padding.
@@ -209,7 +426,7 @@ function AddNode() {
209
426
  </Popover.Header>
210
427
  <YStack paddingHorizontal="$4" gap="$3">
211
428
  <Paragraph color="$color11">Run this on the new machine.</Paragraph>
212
- <SizableText fontFamily="$mono">shc node join</SizableText>
429
+ <CodeBlock language="text">shc node join</CodeBlock>
213
430
  </YStack>
214
431
  </YStack>
215
432
  </PopoverContent>
@@ -258,26 +475,9 @@ function NodesScreen() {
258
475
  }
259
476
  >
260
477
  <PageHeader title="Nodes" divider={false} actions={<AddNode />} />
261
- <XStack alignItems="center">
262
- <SizableText fontFamily="$mono" color="$color11">
263
- shc node join
264
- </SizableText>
265
- <IconButton inline icon={CopyIcon} aria-label="Copy" />
266
- </XStack>
267
- <Card gap="$4">
268
- <XStack justifyContent="space-between" alignItems="center">
269
- <SizableText size="$6" fontWeight="600">
270
- incus01
271
- </SizableText>
272
- <XStack alignItems="center">
273
- <IconButton icon={DotsThreeIcon} aria-label="More for incus01" />
274
- <IconButton error icon={TrashIcon} aria-label="Remove incus01" />
275
- </XStack>
276
- </XStack>
277
- <SizableText color="$color11">10.64.96.1:4003</SizableText>
278
- <Meter label="CPU" value={12} detail="2.83/24 cores 12%" />
279
- <Meter label="Mem" value={41} detail="25.3/61.9 GiB 41%" />
280
- </Card>
478
+ <PageSection title="Unplaced" description="1 node" headingLevel={3}>
479
+ <NodeFaceplate />
480
+ </PageSection>
281
481
  </Screen>
282
482
  );
283
483
  }
@@ -315,3 +315,93 @@ export function SettingsPageLayout({
315
315
  </Screen>
316
316
  );
317
317
  }
318
+
319
+ // ── DensePageLayout ────────────────────────────────────────────────────────
320
+
321
+ export interface DensePageLayoutProps
322
+ extends Omit<ScreenProps, "scroll" | "bleed" | "noPadding">, TemplateHeaderProps {
323
+ /** The live state beside the title (a `StatusDot`). */
324
+ status?: ReactNode;
325
+ /**
326
+ * The column the dense region keeps at regular width: `"wide"` (a log or
327
+ * terminal, the page column) or `"readable"` (a chart, the `expanded`
328
+ * measure, so a dozen bars never stretch across a desktop).
329
+ */
330
+ measure?: "wide" | "readable";
331
+ /** Names the dense region (`data-dense-surface`), e.g. "log" or "chart". */
332
+ kind?: string;
333
+ /**
334
+ * Keep the page gutter inside the region on a phone. A chart takes it so its
335
+ * axis labels sit on the page line; a `LogView` pads its own text.
336
+ */
337
+ gutter?: boolean;
338
+ children: ReactNode;
339
+ }
340
+
341
+ /**
342
+ * A page whose content is one dense surface: a log, a terminal or a chart
343
+ * (LC-215). The page keeps its head (title, live status, icon actions) and
344
+ * the surface takes the rest of the height. On a phone the surface bleeds to
345
+ * both screen edges; from `medium` up it stays in the page column, rounded
346
+ * as the page's one surface, at the measure its content reads at.
347
+ */
348
+ export function DensePageLayout({
349
+ title,
350
+ subtitle,
351
+ status,
352
+ actions,
353
+ header,
354
+ nav,
355
+ measure = "wide",
356
+ kind,
357
+ gutter = false,
358
+ maxWidth,
359
+ children,
360
+ ...rest
361
+ }: DensePageLayoutProps) {
362
+ const compact = useLayoutSizeClass() === "compact";
363
+ const { knobProps } = useResolvedKnobs({ component: "Screen" });
364
+ const head =
365
+ header !== undefined ? (
366
+ header
367
+ ) : title || subtitle || actions || nav || status ? (
368
+ <PageHeader title={title} subtitle={subtitle} status={status} actions={actions} nav={nav} />
369
+ ) : null;
370
+ const region = (
371
+ <YStack
372
+ flex={1}
373
+ minHeight={0}
374
+ minWidth={0}
375
+ data-dense-surface={kind}
376
+ {...(compact
377
+ ? gutter
378
+ ? { paddingHorizontal: knobProps.panelPadding.padding as "$4" }
379
+ : null
380
+ : { ...knobProps.containerRadius, overflow: "hidden" })}
381
+ >
382
+ {children}
383
+ </YStack>
384
+ );
385
+ if (compact)
386
+ return (
387
+ <Screen data-canonical="dense-page" scroll={false} bleed noPadding {...rest}>
388
+ {head ? (
389
+ <YStack {...knobProps.panelPadding} paddingBottom={0}>
390
+ {head}
391
+ </YStack>
392
+ ) : null}
393
+ {region}
394
+ </Screen>
395
+ );
396
+ return (
397
+ <Screen
398
+ data-canonical="dense-page"
399
+ scroll={false}
400
+ maxWidth={maxWidth ?? (measure === "readable" ? layoutBreakpoints.expanded : undefined)}
401
+ {...rest}
402
+ >
403
+ {head}
404
+ {region}
405
+ </Screen>
406
+ );
407
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * LC-157 (FZ-8442): the bar's inline title is centred on the bar while it
3
+ * fits clear of both side groups, and slides into the span between them when
4
+ * the wider group would touch it (Clay's "Inbox" beside four trailing glyphs).
5
+ */
6
+ import { describe, expect, it } from "vitest";
7
+ import { BAR_TITLE_GAP, barTitleSpan, sideExtents } from "./barTitleSpan";
8
+
9
+ describe("bar title span (LC-157, FZ-8442)", () => {
10
+ it("centres on the bar when the title fits clear of both groups", () => {
11
+ const span = barTitleSpan({ row: 358, leftEnd: 44, rightStart: 314, title: 60 });
12
+ expect(span.left).toBe(span.right);
13
+ expect(span.left).toBe(44 + BAR_TITLE_GAP);
14
+ });
15
+
16
+ it("slides between the groups when centring would hide it", () => {
17
+ const span = barTitleSpan({ row: 358, leftEnd: 44, rightStart: 130, title: 45 });
18
+ expect(span).toEqual({ left: 44 + BAR_TITLE_GAP, right: 358 - 130 + BAR_TITLE_GAP });
19
+ expect(358 - span.left - span.right).toBeGreaterThanOrEqual(45);
20
+ });
21
+
22
+ it("reads the groups by side, so a right-to-left bar mirrors", () => {
23
+ const ltr = sideExtents(358, { x: 16, width: 44 }, { x: 250, width: 92 });
24
+ const rtl = sideExtents(358, { x: 298, width: 44 }, { x: 16, width: 92 });
25
+ expect(ltr).toEqual({ leftEnd: 60, rightStart: 250 });
26
+ expect(rtl).toEqual({ leftEnd: 108, rightStart: 298 });
27
+ });
28
+
29
+ it("an empty group takes no room", () => {
30
+ expect(sideExtents(358, { x: 16, width: 44 }, { x: 342, width: 0 })).toEqual({
31
+ leftEnd: 60,
32
+ rightStart: 0,
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,44 @@
1
+ /** The space kept between the bar's inline title and either side group. */
2
+ export const BAR_TITLE_GAP = 8;
3
+
4
+ export interface BarTitleBox {
5
+ /** The bar row's width. */
6
+ row: number;
7
+ /** Where the group on the left ends, from the row's left edge; 0 when empty. */
8
+ leftEnd: number;
9
+ /** Where the group on the right starts, from the row's left edge; 0 when empty. */
10
+ rightStart: number;
11
+ /** The title's natural one-line width. */
12
+ title: number;
13
+ }
14
+
15
+ /**
16
+ * Where the bar's inline title sits. Like UIKit it is centred on the bar
17
+ * while it fits clear of both side groups; when the wider group would touch
18
+ * it, it centres in the span left between the groups instead of vanishing.
19
+ */
20
+ export function barTitleSpan(box: BarTitleBox): { left: number; right: number } {
21
+ const left = box.leftEnd + BAR_TITLE_GAP;
22
+ const right = (box.rightStart ? box.row - box.rightStart : 0) + BAR_TITLE_GAP;
23
+ const centred = Math.max(left, right);
24
+ return box.row - 2 * centred >= box.title ? { left: centred, right: centred } : { left, right };
25
+ }
26
+
27
+ type Extent = { x: number; width: number };
28
+
29
+ /** The bar's two side groups as physical left and right extents (RTL-safe). */
30
+ export function sideExtents(
31
+ row: number,
32
+ a: Extent,
33
+ b: Extent,
34
+ ): Pick<BarTitleBox, "leftEnd" | "rightStart"> {
35
+ const groups = [a, b].filter((group) => group.width > 0);
36
+ let leftEnd = 0;
37
+ let rightStart = 0;
38
+ for (const group of groups) {
39
+ if (group.x + group.width / 2 < row / 2)
40
+ leftEnd = Math.max(leftEnd, Math.ceil(group.x + group.width));
41
+ else rightStart = rightStart ? Math.min(rightStart, Math.floor(group.x)) : Math.floor(group.x);
42
+ }
43
+ return { leftEnd, rightStart };
44
+ }