@crewlethq/ui 0.9.0 → 0.10.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/README.md CHANGED
@@ -191,6 +191,21 @@ places:
191
191
  - A surface that pans or zooms dispatches `LAYER_REPOSITION_EVENT` on its host
192
192
  when the content beneath it moves, and every panel anchored inside follows.
193
193
 
194
+ ## Breaking changes in 0.10.0
195
+
196
+ 0.10.0 moves an agent's state off the ring round its badge and onto a small badge on its corner.
197
+
198
+ **A state is a status, never a ring.** `Avatar` takes `status`: `working`, `needs` (waiting on a person) or `stopped`, and none for idle. It draws a round badge on the lower-right corner, the state's fill carrying a glyph whose shape says it (play, a question mark, a square) in `--color-text-inverse`, which the tokens' palette suite now holds at 3:1 on each fill (`MARK_ON_FILL`). The badge sits in a notch cut out of the face with a mask, so it reads on any ground without knowing its colour. A badge with a status is drawn inside a `.crewlet-avatar-frame` of the face's own size, which takes your `className` and `style`; a badge that names itself appends the state to its accessible name (`Software Engineer avatar, stopped`), and a decorative one says nothing, so say the state in words beside it. `AVATAR_STATUSES` and `AVATAR_STATUS_LABELS` list the statuses and their words. A ring of a state's hue round a plate in the agent's own hue put two colours on one small mark, and framed what the agent was doing louder than who it is.
199
+
200
+ | Was | Is |
201
+ |---|---|
202
+ | `<Avatar ring="info">`, `ring="warning"`, `ring="danger"` for what a seat is doing | `status="working"`, `status="needs"`, `status="stopped"` |
203
+ | `<Avatar ring="success">` | no status; say a finished state in words beside the badge |
204
+ | `AvatarRing` naming `info`, `warning`, `danger` or `success` | `AvatarRing` is `'brand'` alone, the selected ring; a state is an `AvatarStatus` |
205
+ | an `AvatarStack` member's `ring` | nothing: each member overlaps the one before it, so a corner badge would be hidden; say what the members are doing beside the stack |
206
+ | a stylesheet of your own reading `.crewlet-avatar--ring-info`, `-warning`, `-danger` or `-success` | `.crewlet-avatar__status--working`, `--needs` or `--stopped` |
207
+ | a `ring="brand"` and a `status` on one badge | the status alone; the notch is cut from the outline the ring would follow, so a badge with a status draws no ring |
208
+
194
209
  ## Changes in 0.9.0
195
210
 
196
211
  0.9.0 gives an agent a face of its own: one of the thirty Crewlet characters in `@crewlethq/icons/characters`, drawn in one of the six node hues an operator chooses for it.
@@ -1,7 +1,6 @@
1
1
  import type { HTMLAttributes } from 'react';
2
2
  import { type CrewletCharacterId } from '@crewlethq/icons/characters';
3
3
  import type { NodeHue } from '../utils/nodeHue.js';
4
- import type { Tone } from '../utils/tone.js';
5
4
  /**
6
5
  * WHAT THE BADGE STANDS FOR, drawn as its outline.
7
6
  *
@@ -37,16 +36,37 @@ export type AvatarKind = 'agent' | 'human';
37
36
  */
38
37
  export type AvatarTone = 'neutral' | 'seeded';
39
38
  /**
40
- * The STATE ring: a 1.5px line 2px outside the badge, in one of the state
41
- * fills or the accent.
42
- *
43
- * `info`, `warning`, `danger` and `success` are what the thing is DOING, and
44
- * `brand` is SELECTED. Which state each one means is the consumer's to decide,
45
- * and to SAY in words beside the badge: the ring is never the only carrier of
46
- * a state, because in forced-colors mode every ring is the same system colour.
47
- * No ring at all is the resting state.
39
+ * The SELECTED ring: a 1.5px line 2px outside the badge, in the accent.
40
+ *
41
+ * It is the one ring there is. A ring used to carry a state too (`info`,
42
+ * `warning`, `danger`, `success`), and on a character plate that already
43
+ * wears its own hue it put a second colour round a 32px mark, framing what
44
+ * the agent was doing louder than who it is. A state is a [AvatarStatus]
45
+ * now, a small badge on the corner whose SHAPE says it.
46
+ */
47
+ export type AvatarRing = 'brand';
48
+ /**
49
+ * WHAT AN AGENT IS DOING, drawn as a small round badge on the lower-right
50
+ * corner: `working`, `needs` (it is waiting on a person) or `stopped`. An idle
51
+ * badge carries none, because a mark that means "nothing is happening" is one
52
+ * a reader learns to skip.
53
+ *
54
+ * SHAPE FIRST, colour second. Each badge is a state's fill step carrying a
55
+ * glyph, play for working, a question for needs and a square for stopped, so
56
+ * it reads to somebody who cannot separate the hues and in forced-colors mode,
57
+ * where every fill is one system colour. The glyph is in the inverse ink,
58
+ * which the tokens' palette suite holds at 3:1 on each fill (`MARK_ON_FILL`).
59
+ *
60
+ * THE BADGE IS NOT THE ONLY CARRIER. A row that draws a badge with a status
61
+ * says the state in words as well; a badge drawn alone (a breadcrumb, a stack)
62
+ * is the case the glyph exists for. A badge that names itself appends the
63
+ * state to its accessible name.
48
64
  */
49
- export type AvatarRing = Exclude<Tone, 'neutral'>;
65
+ export type AvatarStatus = 'working' | 'needs' | 'stopped';
66
+ /** Every status, in the order a legend lists them. */
67
+ export declare const AVATAR_STATUSES: readonly ["working", "needs", "stopped"];
68
+ /** What each status is called in a sentence: the accessible name's last words. */
69
+ export declare const AVATAR_STATUS_LABELS: Readonly<Record<AvatarStatus, string>>;
50
70
  /** The four steps, in px. A number is accepted for anything outside them. */
51
71
  export declare const AVATAR_SIZES: {
52
72
  readonly xs: 20;
@@ -81,8 +101,13 @@ export interface AvatarProps extends HTMLAttributes<HTMLElement> {
81
101
  /** An agent (the default) is a squircle; a human is a circle. */
82
102
  kind?: AvatarKind | undefined;
83
103
  tone?: AvatarTone | undefined;
84
- /** A state ring, or none. `brand` is selected. */
104
+ /**
105
+ * The selected ring, or none. Not drawn on a badge with a `status`: the
106
+ * corner badge is cut out of the outline the ring would follow.
107
+ */
85
108
  ring?: AvatarRing | undefined;
109
+ /** What the agent is doing, as a corner badge; none is idle. */
110
+ status?: AvatarStatus | undefined;
86
111
  /**
87
112
  * The name is already printed beside this badge, so the badge itself says
88
113
  * nothing. Without it a row reads "Carlos Diaz avatar, Carlos Diaz".
@@ -164,7 +189,13 @@ export declare function avatarCorner(pixels: number): number;
164
189
  *
165
190
  * If the image fails to load (a stale CDN URL, an expired signature) it
166
191
  * degrades to the initials rather than to a broken-image glyph. Either way it
167
- * keeps its kind's outline and its ring: whether somebody has uploaded a photo
168
- * says nothing about what they are or what they are doing.
192
+ * keeps its kind's outline, its ring and its status: whether somebody has
193
+ * uploaded a photo says nothing about what they are or what they are doing.
194
+ *
195
+ * A BADGE WITH A STATUS IS FRAMED: the face (character, image or initials)
196
+ * stands in a box of its own size beside the corner badge, and the face has a
197
+ * notch cut where the badge sits, so the badge reads on any ground without
198
+ * knowing what colour that ground is. The frame takes the caller's class and
199
+ * style, which is where a layout reaches for the badge.
169
200
  */
170
- export declare const Avatar: ({ src, name, size, kind, tone, ring, decorative, colorSeed, character, hue, className, style, ...rest }: AvatarProps) => import("react").JSX.Element;
201
+ export declare const Avatar: ({ src, name, size, kind, tone, ring, status, decorative, colorSeed, character, hue, className, style, ...rest }: AvatarProps) => import("react").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  import type { HTMLAttributes } from 'react';
2
2
  import type { CrewletCharacterId } from '@crewlethq/icons/characters';
3
3
  import type { NodeHue } from '../utils/nodeHue.js';
4
- import { type AvatarKind, type AvatarRing, type AvatarSizeStep } from './Avatar.js';
4
+ import { type AvatarKind, type AvatarSizeStep } from './Avatar.js';
5
5
  /** One badge in a stack. */
6
6
  export interface AvatarStackMember {
7
7
  /** Who: the initials, and a word of the stack's accessible name. */
@@ -9,8 +9,6 @@ export interface AvatarStackMember {
9
9
  /** An agent (the default) is a squircle; a human is a circle. */
10
10
  kind?: AvatarKind | undefined;
11
11
  src?: string | undefined;
12
- /** A state ring on this badge, as `Avatar` draws it. */
13
- ring?: AvatarRing | undefined;
14
12
  /** The Crewlet character an agent member is drawn as, as `Avatar` draws it. */
15
13
  character?: CrewletCharacterId | undefined;
16
14
  /** The hue an agent member's character is drawn in, as `Avatar` draws it. */
@@ -1,6 +1,6 @@
1
1
  import './Avatar.css';
2
2
  import './AvatarStack.css';
3
- export { AVATAR_CHARACTER_FULL_DETAIL, AVATAR_CORNER_RATIO, AVATAR_SIZES, Avatar, avatarCorner, avatarPixels, avatarTint, getInitials, } from './Avatar.js';
4
- export type { AvatarKind, AvatarProps, AvatarRing, AvatarSize, AvatarSizeStep, AvatarTone } from './Avatar.js';
3
+ export { AVATAR_CHARACTER_FULL_DETAIL, AVATAR_CORNER_RATIO, AVATAR_SIZES, AVATAR_STATUS_LABELS, AVATAR_STATUSES, Avatar, avatarCorner, avatarPixels, avatarTint, getInitials, } from './Avatar.js';
4
+ export type { AvatarKind, AvatarProps, AvatarRing, AvatarSize, AvatarSizeStep, AvatarStatus, AvatarTone, } from './Avatar.js';
5
5
  export { AvatarStack, avatarStackLabel } from './AvatarStack.js';
6
6
  export type { AvatarStackMember, AvatarStackProps } from './AvatarStack.js';
@@ -1,3 +1,3 @@
1
- export { AvatarStack, avatarStackLabel } from '../chunk-OAH4IEQJ.js';
2
- export { AVATAR_CHARACTER_FULL_DETAIL, AVATAR_CORNER_RATIO, AVATAR_SIZES, Avatar, avatarCorner, avatarPixels, avatarTint, getInitials } from '../chunk-UDW3DAP5.js';
1
+ export { AvatarStack, avatarStackLabel } from '../chunk-3UK4VFSK.js';
2
+ export { AVATAR_CHARACTER_FULL_DETAIL, AVATAR_CORNER_RATIO, AVATAR_SIZES, AVATAR_STATUSES, AVATAR_STATUS_LABELS, Avatar, avatarCorner, avatarPixels, avatarTint, getInitials } from '../chunk-HI6Q2UXQ.js';
3
3
  import '../chunk-ODEQTPFM.js';
@@ -10,8 +10,10 @@
10
10
  * OUTLINE (a squircle for an agent, a circle for a person), the monogram and
11
11
  * the name, and for an agent an operator gave one, its character and its hue.
12
12
  *
13
- * THE ONE HUE A BADGE CARRIES AS A LINE IS A STATE, as a ring round it, and the
14
- * ring is the component's only coloured line outside its own plate.
13
+ * A STATE IS A CORNER BADGE, NEVER A LINE ROUND THE PLATE. A ring in a state's
14
+ * hue round a plate in the agent's own hue put two colours on a 32px mark and
15
+ * framed what the agent was doing louder than who it is. The one ring left is
16
+ * SELECTED, in the accent.
15
17
  *
16
18
  * The seeded tints stay for the one case that earns them, a roster somebody
17
19
  * scans, and their grounds are TOKENS: measured under white initials and held
@@ -211,11 +213,12 @@
211
213
  color: var(--color-node-blue);
212
214
  }
213
215
 
214
- /* ── the state ring ────────────────────────────────────────────────────── */
216
+ /* ── the selected ring ────────────────────────────────────────────────── */
215
217
 
216
218
  /*
217
219
  * A 1.5px LINE 2px OUTSIDE THE BADGE, which is the design's ring: it draws a
218
220
  * 3.5px shadow over a 2px one in the card's colour, and 3.5 less 2 is 1.5.
221
+ * Longhands, so nothing else that sets an outline resets the ring's geometry.
219
222
  *
220
223
  * AN OUTLINE RATHER THAN THAT SHADOW PAIR, for three reasons. The gap is
221
224
  * TRANSPARENT, so the ring reads on whatever the badge stands on, where the
@@ -223,42 +226,97 @@
223
226
  * other ground. An outline follows the corner, the squircle's included, in
224
227
  * every engine this package supports. And forced-colors mode keeps an outline
225
228
  * and drops every shadow, so the ring is still drawn for the readers who most
226
- * need a boundary; it is drawn in the system colour there, which is why the
227
- * state is always said in words beside the badge as well.
229
+ * need a boundary.
228
230
  *
229
- * LONGHANDS, so the tone rules below set the colour alone and cannot reset the
230
- * geometry, whichever order they are read in. Each fill is measured at 3:1 as
231
- * a mark on every opaque surface by @crewlethq/tokens, which is the floor a
232
- * boundary a reader has to see is held to.
231
+ * SELECTED, in the accent, which is what the accent means: where the reader
232
+ * is. It is the only place a badge spends the accent, and the accent is
233
+ * measured at 3:1 as a mark on every opaque surface by @crewlethq/tokens.
233
234
  */
234
- .crewlet-avatar--ring-info,
235
- .crewlet-avatar--ring-warning,
236
- .crewlet-avatar--ring-danger,
237
- .crewlet-avatar--ring-success,
238
235
  .crewlet-avatar--ring-brand {
239
236
  outline-style: solid;
240
237
  outline-width: 1.5px;
241
238
  outline-offset: 2px;
239
+ outline-color: var(--color-brand-accent);
240
+ }
241
+
242
+ /* ── the status badge ─────────────────────────────────────────────────── */
243
+
244
+ /*
245
+ * THE FRAME is the face's own box, so a badge with a status takes exactly the
246
+ * room one without takes: the corner badge hangs past it rather than growing
247
+ * it, and a row lays out the same either way. The badge is 0.42 of the box,
248
+ * never under 10px, which is the size its glyph stays a shape at.
249
+ */
250
+ .crewlet-avatar-frame {
251
+ --crewlet-avatar-status-size: max(10px, calc(var(--crewlet-avatar-size) * 0.42));
252
+ --crewlet-avatar-status-overhang: 2px;
253
+ --crewlet-avatar-status-gap: 1.5px;
254
+ position: relative;
255
+ display: inline-flex;
256
+ flex: none;
257
+ width: var(--crewlet-avatar-size);
258
+ height: var(--crewlet-avatar-size);
259
+ vertical-align: middle;
242
260
  }
243
261
 
244
- .crewlet-avatar--ring-info {
245
- outline-color: var(--color-feedback-info);
262
+ /*
263
+ * THE NOTCH: the face is masked by a circle a gap wider than the badge, centred
264
+ * where the badge sits, so the gap shows whatever the badge stands on. A ring
265
+ * of the ground's colour would need to know that ground, and a badge on a card,
266
+ * a hovered row and the page would each need a different one. A mask reads only
267
+ * alpha, so its black is opacity rather than a colour.
268
+ */
269
+ .crewlet-avatar--notched {
270
+ --crewlet-avatar-status-centre: calc(
271
+ 100% + var(--crewlet-avatar-status-overhang) - var(--crewlet-avatar-status-size) / 2
272
+ );
273
+ --crewlet-avatar-status-cut: calc(var(--crewlet-avatar-status-size) / 2 + var(--crewlet-avatar-status-gap));
274
+ -webkit-mask-image: radial-gradient(
275
+ circle at var(--crewlet-avatar-status-centre) var(--crewlet-avatar-status-centre),
276
+ transparent var(--crewlet-avatar-status-cut),
277
+ #000 calc(var(--crewlet-avatar-status-cut) + 0.5px)
278
+ );
279
+ mask-image: radial-gradient(
280
+ circle at var(--crewlet-avatar-status-centre) var(--crewlet-avatar-status-centre),
281
+ transparent var(--crewlet-avatar-status-cut),
282
+ #000 calc(var(--crewlet-avatar-status-cut) + 0.5px)
283
+ );
284
+ }
285
+
286
+ /*
287
+ * THE BADGE: a state's fill step carrying its glyph in the inverse ink, which
288
+ * the tokens' palette suite holds at 3:1 on each fill (`MARK_ON_FILL`). The
289
+ * transparent outline is drawn only in forced-colors mode, where the fill
290
+ * becomes the page's own colour and the outline is what keeps a boundary.
291
+ */
292
+ .crewlet-avatar__status {
293
+ position: absolute;
294
+ right: calc(var(--crewlet-avatar-status-overhang) * -1);
295
+ bottom: calc(var(--crewlet-avatar-status-overhang) * -1);
296
+ display: grid;
297
+ place-items: center;
298
+ width: var(--crewlet-avatar-status-size);
299
+ height: var(--crewlet-avatar-status-size);
300
+ border-radius: var(--radius-circle);
301
+ outline: 1px solid transparent;
302
+ color: var(--color-text-inverse);
303
+ pointer-events: none;
246
304
  }
247
305
 
248
- .crewlet-avatar--ring-warning {
249
- outline-color: var(--color-feedback-warning);
306
+ .crewlet-avatar__status > svg {
307
+ display: block;
308
+ width: 100%;
309
+ height: 100%;
250
310
  }
251
311
 
252
- .crewlet-avatar--ring-danger {
253
- outline-color: var(--color-feedback-danger);
312
+ .crewlet-avatar__status--working {
313
+ background: var(--color-feedback-info);
254
314
  }
255
315
 
256
- .crewlet-avatar--ring-success {
257
- outline-color: var(--color-feedback-success);
316
+ .crewlet-avatar__status--needs {
317
+ background: var(--color-feedback-warning);
258
318
  }
259
319
 
260
- /* SELECTED, in the accent, which is what the accent means: where the reader
261
- is. It is the only place a badge spends the accent. */
262
- .crewlet-avatar--ring-brand {
263
- outline-color: var(--color-brand-accent);
320
+ .crewlet-avatar__status--stopped {
321
+ background: var(--color-feedback-danger);
264
322
  }
@@ -1,4 +1,4 @@
1
- export { CHARACTER_PICKER_COLUMNS, CharacterPicker } from '../chunk-IKH3Y4MP.js';
1
+ export { CHARACTER_PICKER_COLUMNS, CharacterPicker } from '../chunk-UO5UK7V2.js';
2
2
  import '../chunk-CBOICT64.js';
3
- import '../chunk-UDW3DAP5.js';
3
+ import '../chunk-HI6Q2UXQ.js';
4
4
  import '../chunk-ODEQTPFM.js';
@@ -1,4 +1,4 @@
1
- export { EntityChip } from '../chunk-Y2V3TSQC.js';
2
- import '../chunk-OAH4IEQJ.js';
3
- import '../chunk-UDW3DAP5.js';
1
+ export { EntityChip } from '../chunk-DDTDN5L2.js';
2
+ import '../chunk-3UK4VFSK.js';
3
+ import '../chunk-HI6Q2UXQ.js';
4
4
  import '../chunk-ODEQTPFM.js';
@@ -1,4 +1,4 @@
1
- export { ImageUpload } from '../chunk-E3HDS4F7.js';
2
- import '../chunk-OAH4IEQJ.js';
3
- import '../chunk-UDW3DAP5.js';
1
+ export { ImageUpload } from '../chunk-LAQK4V3Z.js';
2
+ import '../chunk-3UK4VFSK.js';
3
+ import '../chunk-HI6Q2UXQ.js';
4
4
  import '../chunk-ODEQTPFM.js';
@@ -1,4 +1,4 @@
1
- export { OrgLabel } from '../chunk-J3IS2TOA.js';
2
- import '../chunk-OAH4IEQJ.js';
3
- import '../chunk-UDW3DAP5.js';
1
+ export { OrgLabel } from '../chunk-6OF2DBKY.js';
2
+ import '../chunk-3UK4VFSK.js';
3
+ import '../chunk-HI6Q2UXQ.js';
4
4
  import '../chunk-ODEQTPFM.js';
@@ -1,5 +1,5 @@
1
- export { OrgNodeDisclosure, OrgNodeLabel, OrgNodeLead } from '../chunk-K7OS4O2U.js';
2
- import '../chunk-J3IS2TOA.js';
3
- import '../chunk-OAH4IEQJ.js';
4
- import '../chunk-UDW3DAP5.js';
1
+ export { OrgNodeDisclosure, OrgNodeLabel, OrgNodeLead } from '../chunk-IHGKPEEV.js';
2
+ import '../chunk-6OF2DBKY.js';
3
+ import '../chunk-3UK4VFSK.js';
4
+ import '../chunk-HI6Q2UXQ.js';
5
5
  import '../chunk-ODEQTPFM.js';
@@ -1,13 +1,13 @@
1
- export { OrgTable, OrgTableActions, OrgTableAdd, OrgTableName } from '../chunk-JB6AN4ZC.js';
1
+ export { OrgTable, OrgTableActions, OrgTableAdd, OrgTableName } from '../chunk-7FFDJQ23.js';
2
2
  import '../chunk-SX2DFTQV.js';
3
3
  import '../chunk-S3BCVWTU.js';
4
- import '../chunk-J3IS2TOA.js';
4
+ import '../chunk-6OF2DBKY.js';
5
5
  import '../chunk-53DVJCVW.js';
6
6
  import '../chunk-74WGARJE.js';
7
7
  import '../chunk-7RPGY3JE.js';
8
8
  import '../chunk-R25RGPUB.js';
9
9
  import '../chunk-4FQT6ZAY.js';
10
- import '../chunk-OAH4IEQJ.js';
11
- import '../chunk-UDW3DAP5.js';
10
+ import '../chunk-3UK4VFSK.js';
11
+ import '../chunk-HI6Q2UXQ.js';
12
12
  import '../chunk-BZFWDIV6.js';
13
13
  import '../chunk-ODEQTPFM.js';
@@ -1,6 +1,6 @@
1
- import { AVATAR_SIZES, Avatar } from './chunk-UDW3DAP5.js';
1
+ import { AVATAR_SIZES, Avatar } from './chunk-HI6Q2UXQ.js';
2
2
  import { cx } from './chunk-ODEQTPFM.js';
3
- import './Avatar-JAU77SHZ.css';
3
+ import './Avatar-QHYOFID3.css';
4
4
  import './AvatarStack-GEJBNV7E.css';
5
5
  import { jsxs, jsx } from 'react/jsx-runtime';
6
6
 
@@ -48,7 +48,6 @@ function AvatarStack({
48
48
  name: member.name,
49
49
  kind: member.kind,
50
50
  src: member.src,
51
- ring: member.ring,
52
51
  character: member.character,
53
52
  hue: member.hue,
54
53
  size,
@@ -1,4 +1,4 @@
1
- import { Avatar } from './chunk-UDW3DAP5.js';
1
+ import { Avatar } from './chunk-HI6Q2UXQ.js';
2
2
  import { cx } from './chunk-ODEQTPFM.js';
3
3
  import './OrgLabel-6WSKJQOT.css';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -1,6 +1,6 @@
1
1
  import { TreeGrid } from './chunk-SX2DFTQV.js';
2
2
  import { createTreeModel, treeParent } from './chunk-S3BCVWTU.js';
3
- import { OrgLabel } from './chunk-J3IS2TOA.js';
3
+ import { OrgLabel } from './chunk-6OF2DBKY.js';
4
4
  import { AddPill } from './chunk-74WGARJE.js';
5
5
  import { IconButton } from './chunk-4FQT6ZAY.js';
6
6
  import { cx } from './chunk-ODEQTPFM.js';
@@ -1,4 +1,4 @@
1
- import { Avatar } from './chunk-UDW3DAP5.js';
1
+ import { Avatar } from './chunk-HI6Q2UXQ.js';
2
2
  import { cx } from './chunk-ODEQTPFM.js';
3
3
  import './EntityChip-LUCPZBBG.css';
4
4
  import { Children, isValidElement, cloneElement } from 'react';
@@ -0,0 +1,169 @@
1
+ import { cx } from './chunk-ODEQTPFM.js';
2
+ import { useState, useEffect } from 'react';
3
+ import { CrewletCharacter } from '@crewlethq/icons/characters';
4
+ import { jsxs, jsx } from 'react/jsx-runtime';
5
+
6
+ var AVATAR_STATUSES = ["working", "needs", "stopped"];
7
+ var AVATAR_STATUS_LABELS = {
8
+ working: "working",
9
+ needs: "needs you",
10
+ stopped: "stopped"
11
+ };
12
+ var AVATAR_SIZES = { xs: 20, sm: 26, md: 32, lg: 40 };
13
+ var AVATAR_CORNER_RATIO = 0.29;
14
+ var AVATAR_CHARACTER_FULL_DETAIL = 56;
15
+ var getInitials = (name) => {
16
+ if (!name) return "?";
17
+ const words = name.trim().split(/[\s\-_.]+/).filter(Boolean);
18
+ if (words.length === 0) return "?";
19
+ const first = (word2) => (Array.from(word2)[0] ?? "").toUpperCase();
20
+ if (words.length >= 2) return words.slice(0, 2).map(first).join("") || "?";
21
+ const word = words[0];
22
+ const humps = word.split(/(?<=\p{Ll})(?=\p{Lu})/u);
23
+ if (humps.length >= 2) return humps.slice(0, 2).map(first).join("");
24
+ return Array.from(word).slice(0, 2).map((character) => character.toUpperCase()).join("") || "?";
25
+ };
26
+ var TINT_COUNT = 10;
27
+ var avatarTint = (seed) => {
28
+ let hash = 0;
29
+ for (let i = 0; i < seed.length; i += 1) {
30
+ hash = hash * 31 + seed.charCodeAt(i) >>> 0;
31
+ }
32
+ return hash % TINT_COUNT;
33
+ };
34
+ function avatarPixels(size) {
35
+ return typeof size === "number" ? size : AVATAR_SIZES[size];
36
+ }
37
+ function avatarCorner(pixels) {
38
+ return pixels * AVATAR_CORNER_RATIO;
39
+ }
40
+ var Avatar = ({
41
+ src,
42
+ name,
43
+ size = "md",
44
+ kind = "agent",
45
+ tone = "neutral",
46
+ ring,
47
+ status,
48
+ decorative = false,
49
+ colorSeed,
50
+ character,
51
+ hue,
52
+ className,
53
+ style,
54
+ ...rest
55
+ }) => {
56
+ const [imageFailed, setImageFailed] = useState(false);
57
+ useEffect(() => {
58
+ setImageFailed(false);
59
+ }, [src]);
60
+ const step = typeof size === "number" ? null : size;
61
+ const pixels = avatarPixels(size);
62
+ const measured = step === null ? {
63
+ "--crewlet-avatar-size": `${pixels}px`,
64
+ fontSize: Math.max(9, Math.round(pixels * 0.36))
65
+ } : {};
66
+ const seeded = tone === "seeded" ? avatarTint(colorSeed ?? name ?? "") : null;
67
+ const framed = status !== void 0;
68
+ const label = name ? `${name} avatar` : "Avatar";
69
+ const spoken = framed ? `${label}, ${AVATAR_STATUS_LABELS[status]}` : label;
70
+ const classes = cx(
71
+ "crewlet-avatar",
72
+ `crewlet-avatar--${kind}`,
73
+ step === null ? null : `crewlet-avatar--${step}`,
74
+ framed ? "crewlet-avatar--notched" : null,
75
+ ring === void 0 || framed ? null : `crewlet-avatar--ring-${ring}`,
76
+ framed ? null : className
77
+ );
78
+ const faceStyle = framed ? measured : { ...measured, ...style };
79
+ function drawFace() {
80
+ if (character !== void 0 && kind === "agent") {
81
+ return /* @__PURE__ */ jsx(
82
+ "span",
83
+ {
84
+ ...rest,
85
+ className: cx(
86
+ classes,
87
+ "crewlet-avatar--character",
88
+ hue === void 0 ? "crewlet-avatar--neutral" : `crewlet-avatar--hue-${hue}`
89
+ ),
90
+ style: faceStyle,
91
+ role: decorative ? void 0 : "img",
92
+ "aria-hidden": decorative ? true : void 0,
93
+ "aria-label": decorative ? void 0 : spoken,
94
+ children: /* @__PURE__ */ jsx(
95
+ CrewletCharacter,
96
+ {
97
+ className: "crewlet-avatar__character",
98
+ character,
99
+ detail: pixels >= AVATAR_CHARACTER_FULL_DETAIL ? "full" : "compact"
100
+ }
101
+ )
102
+ }
103
+ );
104
+ }
105
+ if (src && !imageFailed) {
106
+ return /* @__PURE__ */ jsx(
107
+ "img",
108
+ {
109
+ ...rest,
110
+ className: cx(classes, "crewlet-avatar--image"),
111
+ style: faceStyle,
112
+ src,
113
+ alt: decorative ? "" : framed ? `${name ?? ""}, ${AVATAR_STATUS_LABELS[status]}` : name ?? "",
114
+ "aria-hidden": decorative ? true : void 0,
115
+ onError: () => setImageFailed(true)
116
+ }
117
+ );
118
+ }
119
+ return /* @__PURE__ */ jsx(
120
+ "span",
121
+ {
122
+ ...rest,
123
+ className: cx(
124
+ classes,
125
+ "crewlet-avatar--fallback",
126
+ `crewlet-avatar--${tone}`,
127
+ seeded === null ? null : `crewlet-avatar--tint-${seeded}`
128
+ ),
129
+ style: faceStyle,
130
+ role: decorative ? void 0 : "img",
131
+ "aria-hidden": decorative ? true : void 0,
132
+ "aria-label": decorative ? void 0 : spoken,
133
+ children: getInitials(name)
134
+ }
135
+ );
136
+ }
137
+ const face = drawFace();
138
+ if (!framed) return face;
139
+ return /* @__PURE__ */ jsxs(
140
+ "span",
141
+ {
142
+ className: cx("crewlet-avatar-frame", className),
143
+ style: { "--crewlet-avatar-size": `${pixels}px`, ...style },
144
+ children: [
145
+ face,
146
+ /* @__PURE__ */ jsx("span", { className: `crewlet-avatar__status crewlet-avatar__status--${status}`, "aria-hidden": "true", children: STATUS_GLYPHS[status] })
147
+ ]
148
+ }
149
+ );
150
+ };
151
+ var STATUS_GLYPHS = {
152
+ working: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 14 14", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M5.4 3.9 10.2 7l-4.8 3.1z", fill: "currentColor" }) }),
153
+ needs: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 14 14", focusable: "false", children: [
154
+ /* @__PURE__ */ jsx(
155
+ "path",
156
+ {
157
+ d: "M5.1 5.3a1.9 1.9 0 1 1 2.7 1.7c-.55.27-.8.62-.8 1.15v.3",
158
+ fill: "none",
159
+ stroke: "currentColor",
160
+ strokeWidth: "1.5",
161
+ strokeLinecap: "round"
162
+ }
163
+ ),
164
+ /* @__PURE__ */ jsx("circle", { cx: "7", cy: "10.45", r: ".85", fill: "currentColor" })
165
+ ] }),
166
+ stopped: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 14 14", focusable: "false", children: /* @__PURE__ */ jsx("rect", { x: "4.6", y: "4.6", width: "4.8", height: "4.8", rx: ".7", fill: "currentColor" }) })
167
+ };
168
+
169
+ export { AVATAR_CHARACTER_FULL_DETAIL, AVATAR_CORNER_RATIO, AVATAR_SIZES, AVATAR_STATUSES, AVATAR_STATUS_LABELS, Avatar, avatarCorner, avatarPixels, avatarTint, getInitials };
@@ -1,4 +1,4 @@
1
- import { OrgLabel } from './chunk-J3IS2TOA.js';
1
+ import { OrgLabel } from './chunk-6OF2DBKY.js';
2
2
  import { cx } from './chunk-ODEQTPFM.js';
3
3
  import './OrgNode-TCHJH5FO.css';
4
4
  import { jsx, jsxs } from 'react/jsx-runtime';
@@ -1,4 +1,4 @@
1
- import { avatarCorner, Avatar } from './chunk-UDW3DAP5.js';
1
+ import { avatarCorner, Avatar } from './chunk-HI6Q2UXQ.js';
2
2
  import './ImageUpload-L2GE4JUM.css';
3
3
  import { useRef } from 'react';
4
4
  import { CameraGlyph, XGlyph } from '@crewlethq/icons/glyphs';
@@ -1,5 +1,5 @@
1
1
  import { useRoving, tabStop } from './chunk-CBOICT64.js';
2
- import { Avatar } from './chunk-UDW3DAP5.js';
2
+ import { Avatar } from './chunk-HI6Q2UXQ.js';
3
3
  import { cx } from './chunk-ODEQTPFM.js';
4
4
  import './CharacterPicker-ZEYIKUNQ.css';
5
5
  import { CREWLET_CHARACTERS, CREWLET_CHARACTER_GEOMETRY } from '@crewlethq/icons/characters';
@@ -26,11 +26,11 @@ export { SegmentedMeter, segmentedMeterLabel } from '../chunk-4AK52VHB.js';
26
26
  export { Select } from '../chunk-SEFMO2EV.js';
27
27
  export { matchesShortcut, useShortcut } from '../chunk-2FPENTCF.js';
28
28
  export { NewItemsNotice } from '../chunk-PXU6HOZB.js';
29
- export { OrgNodeDisclosure, OrgNodeLabel, OrgNodeLead } from '../chunk-K7OS4O2U.js';
30
- export { OrgTable, OrgTableActions, OrgTableAdd, OrgTableName } from '../chunk-JB6AN4ZC.js';
29
+ export { OrgNodeDisclosure, OrgNodeLabel, OrgNodeLead } from '../chunk-IHGKPEEV.js';
30
+ export { OrgTable, OrgTableActions, OrgTableAdd, OrgTableName } from '../chunk-7FFDJQ23.js';
31
31
  export { TreeGrid } from '../chunk-SX2DFTQV.js';
32
32
  export { SIZE_EPSILON, TYPE_AHEAD_RESET_MS, createTreeModel, isTypeAheadKey, layoutConnectors, layoutForest, placeGroups, treeAllExpandable, treeAncestors, treeCollapseOrAscend, treeExpandOrDescend, treeExpandTo, treeExpandable, treeFirst, treeFirstChild, treeItemAction, treeLast, treeLevel, treeNext, treeParent, treePosInSet, treePrevious, treeSetSize, treeStep, treeTypeAhead, treeVisible, typeAheadBuffer, useLayoutAnchor, useMeasuredSizes, useTreeState } from '../chunk-S3BCVWTU.js';
33
- export { OrgLabel } from '../chunk-J3IS2TOA.js';
33
+ export { OrgLabel } from '../chunk-6OF2DBKY.js';
34
34
  export { PricingCard } from '../chunk-UCYIRDND.js';
35
35
  export { Tag } from '../chunk-LOECRMHA.js';
36
36
  export { StatusDot } from '../chunk-WIATRXEJ.js';
@@ -45,13 +45,13 @@ export { HuePicker } from '../chunk-26TXVHN2.js';
45
45
  export { NODE_HUES, NODE_HUE_NAMES, isNodeHue } from '../chunk-AYKXMIOR.js';
46
46
  export { SegmentedControl } from '../chunk-LPHHDCN3.js';
47
47
  export { TabPanel, Tabs, tabId } from '../chunk-HEOVQZL7.js';
48
+ export { ImageUpload } from '../chunk-LAQK4V3Z.js';
48
49
  export { InlineCode } from '../chunk-NN62JQLR.js';
49
- export { ImageUpload } from '../chunk-E3HDS4F7.js';
50
50
  export { DescriptionList, DescriptionListItem } from '../chunk-XN4JBAN3.js';
51
51
  export { DIFF_LIST_LABELS, DiffList } from '../chunk-LFHCXZO5.js';
52
52
  export { Disclosure } from '../chunk-MICH6QSJ.js';
53
53
  export { EmptyState } from '../chunk-RA2ULG6H.js';
54
- export { EntityChip } from '../chunk-Y2V3TSQC.js';
54
+ export { EntityChip } from '../chunk-DDTDN5L2.js';
55
55
  export { ErrorBoundary } from '../chunk-XX2VQE55.js';
56
56
  export { Eyebrow } from '../chunk-RK4XPHVG.js';
57
57
  export { Combobox } from '../chunk-UYK72NOS.js';
@@ -76,7 +76,7 @@ export { CANVAS_COMPOSE_CONTEXT, CANVAS_FIT_MAX_ZOOM, CANVAS_FIT_PADDING, CANVAS
76
76
  import '../chunk-53DVJCVW.js';
77
77
  export { Card } from '../chunk-CEOXFO3E.js';
78
78
  export { useCardHeaderSlot } from '../chunk-AV2DXAEL.js';
79
- export { CHARACTER_PICKER_COLUMNS, CharacterPicker } from '../chunk-IKH3Y4MP.js';
79
+ export { CHARACTER_PICKER_COLUMNS, CharacterPicker } from '../chunk-UO5UK7V2.js';
80
80
  import '../chunk-CBOICT64.js';
81
81
  export { ActivityStrip, BarList, ChartTooltip, DATA_COLORS, DATA_COLOR_OTHER, Legend, Sparkline, StackedBar, StackedColumns, TimeSeries, dataColor, niceScale, useChartHover } from '../chunk-4WNNEDKO.js';
82
82
  export { Checkbox } from '../chunk-YCTMKKRA.js';
@@ -93,8 +93,8 @@ export { AppShell, AppShellRail, AppShellRailRow, AppShellToggle, AppShellTopbar
93
93
  export { LAYER_GAP, LAYER_REPOSITION_EVENT, LayerHost, outsideBounds, placePopup, useLayerContainer, viewportBounds } from '../chunk-7RPGY3JE.js';
94
94
  export { focusables, isComposing, isModalLayerOpen, layerCount, useBodyScrollLock, useModalLayer, usePopupLayer } from '../chunk-R25RGPUB.js';
95
95
  export { IconButton } from '../chunk-4FQT6ZAY.js';
96
- export { AvatarStack, avatarStackLabel } from '../chunk-OAH4IEQJ.js';
97
- export { AVATAR_CHARACTER_FULL_DETAIL, AVATAR_CORNER_RATIO, AVATAR_SIZES, Avatar, avatarCorner, avatarPixels, avatarTint, getInitials } from '../chunk-UDW3DAP5.js';
96
+ export { AvatarStack, avatarStackLabel } from '../chunk-3UK4VFSK.js';
97
+ export { AVATAR_CHARACTER_FULL_DETAIL, AVATAR_CORNER_RATIO, AVATAR_SIZES, AVATAR_STATUSES, AVATAR_STATUS_LABELS, Avatar, avatarCorner, avatarPixels, avatarTint, getInitials } from '../chunk-HI6Q2UXQ.js';
98
98
  export { BrandLockup } from '../chunk-7KBNFFM3.js';
99
99
  export { Button, ButtonLink } from '../chunk-OG6CL3EB.js';
100
100
  export { VisuallyHidden } from '../chunk-BZFWDIV6.js';
package/dist/index.js CHANGED
@@ -26,11 +26,11 @@ export { SegmentedMeter, segmentedMeterLabel } from './chunk-4AK52VHB.js';
26
26
  export { Select } from './chunk-SEFMO2EV.js';
27
27
  export { matchesShortcut, useShortcut } from './chunk-2FPENTCF.js';
28
28
  export { NewItemsNotice } from './chunk-PXU6HOZB.js';
29
- export { OrgNodeDisclosure, OrgNodeLabel, OrgNodeLead } from './chunk-K7OS4O2U.js';
30
- export { OrgTable, OrgTableActions, OrgTableAdd, OrgTableName } from './chunk-JB6AN4ZC.js';
29
+ export { OrgNodeDisclosure, OrgNodeLabel, OrgNodeLead } from './chunk-IHGKPEEV.js';
30
+ export { OrgTable, OrgTableActions, OrgTableAdd, OrgTableName } from './chunk-7FFDJQ23.js';
31
31
  export { TreeGrid } from './chunk-SX2DFTQV.js';
32
32
  export { SIZE_EPSILON, TYPE_AHEAD_RESET_MS, createTreeModel, isTypeAheadKey, layoutConnectors, layoutForest, placeGroups, treeAllExpandable, treeAncestors, treeCollapseOrAscend, treeExpandOrDescend, treeExpandTo, treeExpandable, treeFirst, treeFirstChild, treeItemAction, treeLast, treeLevel, treeNext, treeParent, treePosInSet, treePrevious, treeSetSize, treeStep, treeTypeAhead, treeVisible, typeAheadBuffer, useLayoutAnchor, useMeasuredSizes, useTreeState } from './chunk-S3BCVWTU.js';
33
- export { OrgLabel } from './chunk-J3IS2TOA.js';
33
+ export { OrgLabel } from './chunk-6OF2DBKY.js';
34
34
  export { PricingCard } from './chunk-UCYIRDND.js';
35
35
  export { Tag } from './chunk-LOECRMHA.js';
36
36
  export { StatusDot } from './chunk-WIATRXEJ.js';
@@ -45,13 +45,13 @@ export { HuePicker } from './chunk-26TXVHN2.js';
45
45
  export { NODE_HUES, NODE_HUE_NAMES, isNodeHue } from './chunk-AYKXMIOR.js';
46
46
  export { SegmentedControl } from './chunk-LPHHDCN3.js';
47
47
  export { TabPanel, Tabs, tabId } from './chunk-HEOVQZL7.js';
48
+ export { ImageUpload } from './chunk-LAQK4V3Z.js';
48
49
  export { InlineCode } from './chunk-NN62JQLR.js';
49
- export { ImageUpload } from './chunk-E3HDS4F7.js';
50
50
  export { DescriptionList, DescriptionListItem } from './chunk-XN4JBAN3.js';
51
51
  export { DIFF_LIST_LABELS, DiffList } from './chunk-LFHCXZO5.js';
52
52
  export { Disclosure } from './chunk-MICH6QSJ.js';
53
53
  export { EmptyState } from './chunk-RA2ULG6H.js';
54
- export { EntityChip } from './chunk-Y2V3TSQC.js';
54
+ export { EntityChip } from './chunk-DDTDN5L2.js';
55
55
  export { ErrorBoundary } from './chunk-XX2VQE55.js';
56
56
  export { Eyebrow } from './chunk-RK4XPHVG.js';
57
57
  export { Combobox } from './chunk-UYK72NOS.js';
@@ -76,7 +76,7 @@ export { CANVAS_COMPOSE_CONTEXT, CANVAS_FIT_MAX_ZOOM, CANVAS_FIT_PADDING, CANVAS
76
76
  import './chunk-53DVJCVW.js';
77
77
  export { Card } from './chunk-CEOXFO3E.js';
78
78
  export { useCardHeaderSlot } from './chunk-AV2DXAEL.js';
79
- export { CHARACTER_PICKER_COLUMNS, CharacterPicker } from './chunk-IKH3Y4MP.js';
79
+ export { CHARACTER_PICKER_COLUMNS, CharacterPicker } from './chunk-UO5UK7V2.js';
80
80
  import './chunk-CBOICT64.js';
81
81
  export { ActivityStrip, BarList, ChartTooltip, DATA_COLORS, DATA_COLOR_OTHER, Legend, Sparkline, StackedBar, StackedColumns, TimeSeries, dataColor, niceScale, useChartHover } from './chunk-4WNNEDKO.js';
82
82
  export { Checkbox } from './chunk-YCTMKKRA.js';
@@ -93,8 +93,8 @@ export { AppShell, AppShellRail, AppShellRailRow, AppShellToggle, AppShellTopbar
93
93
  export { LAYER_GAP, LAYER_REPOSITION_EVENT, LayerHost, outsideBounds, placePopup, useLayerContainer, viewportBounds } from './chunk-7RPGY3JE.js';
94
94
  export { focusables, isComposing, isModalLayerOpen, layerCount, useBodyScrollLock, useModalLayer, usePopupLayer } from './chunk-R25RGPUB.js';
95
95
  export { IconButton } from './chunk-4FQT6ZAY.js';
96
- export { AvatarStack, avatarStackLabel } from './chunk-OAH4IEQJ.js';
97
- export { AVATAR_CHARACTER_FULL_DETAIL, AVATAR_CORNER_RATIO, AVATAR_SIZES, Avatar, avatarCorner, avatarPixels, avatarTint, getInitials } from './chunk-UDW3DAP5.js';
96
+ export { AvatarStack, avatarStackLabel } from './chunk-3UK4VFSK.js';
97
+ export { AVATAR_CHARACTER_FULL_DETAIL, AVATAR_CORNER_RATIO, AVATAR_SIZES, AVATAR_STATUSES, AVATAR_STATUS_LABELS, Avatar, avatarCorner, avatarPixels, avatarTint, getInitials } from './chunk-HI6Q2UXQ.js';
98
98
  export { BrandLockup } from './chunk-7KBNFFM3.js';
99
99
  export { Button, ButtonLink } from './chunk-OG6CL3EB.js';
100
100
  export { VisuallyHidden } from './chunk-BZFWDIV6.js';
package/dist/styles.css CHANGED
@@ -1088,7 +1088,7 @@
1088
1088
  }
1089
1089
 
1090
1090
 
1091
- /* Avatar-JAU77SHZ.css */
1091
+ /* Avatar-QHYOFID3.css */
1092
1092
  /*
1093
1093
  * The identity badge.
1094
1094
  *
@@ -1101,8 +1101,10 @@
1101
1101
  * OUTLINE (a squircle for an agent, a circle for a person), the monogram and
1102
1102
  * the name, and for an agent an operator gave one, its character and its hue.
1103
1103
  *
1104
- * THE ONE HUE A BADGE CARRIES AS A LINE IS A STATE, as a ring round it, and the
1105
- * ring is the component's only coloured line outside its own plate.
1104
+ * A STATE IS A CORNER BADGE, NEVER A LINE ROUND THE PLATE. A ring in a state's
1105
+ * hue round a plate in the agent's own hue put two colours on a 32px mark and
1106
+ * framed what the agent was doing louder than who it is. The one ring left is
1107
+ * SELECTED, in the accent.
1106
1108
  *
1107
1109
  * The seeded tints stay for the one case that earns them, a roster somebody
1108
1110
  * scans, and their grounds are TOKENS: measured under white initials and held
@@ -1302,11 +1304,12 @@
1302
1304
  color: var(--color-node-blue);
1303
1305
  }
1304
1306
 
1305
- /* ── the state ring ────────────────────────────────────────────────────── */
1307
+ /* ── the selected ring ────────────────────────────────────────────────── */
1306
1308
 
1307
1309
  /*
1308
1310
  * A 1.5px LINE 2px OUTSIDE THE BADGE, which is the design's ring: it draws a
1309
1311
  * 3.5px shadow over a 2px one in the card's colour, and 3.5 less 2 is 1.5.
1312
+ * Longhands, so nothing else that sets an outline resets the ring's geometry.
1310
1313
  *
1311
1314
  * AN OUTLINE RATHER THAN THAT SHADOW PAIR, for three reasons. The gap is
1312
1315
  * TRANSPARENT, so the ring reads on whatever the badge stands on, where the
@@ -1314,44 +1317,99 @@
1314
1317
  * other ground. An outline follows the corner, the squircle's included, in
1315
1318
  * every engine this package supports. And forced-colors mode keeps an outline
1316
1319
  * and drops every shadow, so the ring is still drawn for the readers who most
1317
- * need a boundary; it is drawn in the system colour there, which is why the
1318
- * state is always said in words beside the badge as well.
1319
- *
1320
- * LONGHANDS, so the tone rules below set the colour alone and cannot reset the
1321
- * geometry, whichever order they are read in. Each fill is measured at 3:1 as
1322
- * a mark on every opaque surface by @crewlethq/tokens, which is the floor a
1323
- * boundary a reader has to see is held to.
1324
- */
1325
- .crewlet-avatar--ring-info,
1326
- .crewlet-avatar--ring-warning,
1327
- .crewlet-avatar--ring-danger,
1328
- .crewlet-avatar--ring-success,
1320
+ * need a boundary.
1321
+ *
1322
+ * SELECTED, in the accent, which is what the accent means: where the reader
1323
+ * is. It is the only place a badge spends the accent, and the accent is
1324
+ * measured at 3:1 as a mark on every opaque surface by @crewlethq/tokens.
1325
+ */
1329
1326
  .crewlet-avatar--ring-brand {
1330
1327
  outline-style: solid;
1331
1328
  outline-width: 1.5px;
1332
1329
  outline-offset: 2px;
1330
+ outline-color: var(--color-brand-accent);
1333
1331
  }
1334
1332
 
1335
- .crewlet-avatar--ring-info {
1336
- outline-color: var(--color-feedback-info);
1333
+ /* ── the status badge ─────────────────────────────────────────────────── */
1334
+
1335
+ /*
1336
+ * THE FRAME is the face's own box, so a badge with a status takes exactly the
1337
+ * room one without takes: the corner badge hangs past it rather than growing
1338
+ * it, and a row lays out the same either way. The badge is 0.42 of the box,
1339
+ * never under 10px, which is the size its glyph stays a shape at.
1340
+ */
1341
+ .crewlet-avatar-frame {
1342
+ --crewlet-avatar-status-size: max(10px, calc(var(--crewlet-avatar-size) * 0.42));
1343
+ --crewlet-avatar-status-overhang: 2px;
1344
+ --crewlet-avatar-status-gap: 1.5px;
1345
+ position: relative;
1346
+ display: inline-flex;
1347
+ flex: none;
1348
+ width: var(--crewlet-avatar-size);
1349
+ height: var(--crewlet-avatar-size);
1350
+ vertical-align: middle;
1337
1351
  }
1338
1352
 
1339
- .crewlet-avatar--ring-warning {
1340
- outline-color: var(--color-feedback-warning);
1353
+ /*
1354
+ * THE NOTCH: the face is masked by a circle a gap wider than the badge, centred
1355
+ * where the badge sits, so the gap shows whatever the badge stands on. A ring
1356
+ * of the ground's colour would need to know that ground, and a badge on a card,
1357
+ * a hovered row and the page would each need a different one. A mask reads only
1358
+ * alpha, so its black is opacity rather than a colour.
1359
+ */
1360
+ .crewlet-avatar--notched {
1361
+ --crewlet-avatar-status-centre: calc(
1362
+ 100% + var(--crewlet-avatar-status-overhang) - var(--crewlet-avatar-status-size) / 2
1363
+ );
1364
+ --crewlet-avatar-status-cut: calc(var(--crewlet-avatar-status-size) / 2 + var(--crewlet-avatar-status-gap));
1365
+ -webkit-mask-image: radial-gradient(
1366
+ circle at var(--crewlet-avatar-status-centre) var(--crewlet-avatar-status-centre),
1367
+ transparent var(--crewlet-avatar-status-cut),
1368
+ #000 calc(var(--crewlet-avatar-status-cut) + 0.5px)
1369
+ );
1370
+ mask-image: radial-gradient(
1371
+ circle at var(--crewlet-avatar-status-centre) var(--crewlet-avatar-status-centre),
1372
+ transparent var(--crewlet-avatar-status-cut),
1373
+ #000 calc(var(--crewlet-avatar-status-cut) + 0.5px)
1374
+ );
1341
1375
  }
1342
1376
 
1343
- .crewlet-avatar--ring-danger {
1344
- outline-color: var(--color-feedback-danger);
1377
+ /*
1378
+ * THE BADGE: a state's fill step carrying its glyph in the inverse ink, which
1379
+ * the tokens' palette suite holds at 3:1 on each fill (`MARK_ON_FILL`). The
1380
+ * transparent outline is drawn only in forced-colors mode, where the fill
1381
+ * becomes the page's own colour and the outline is what keeps a boundary.
1382
+ */
1383
+ .crewlet-avatar__status {
1384
+ position: absolute;
1385
+ right: calc(var(--crewlet-avatar-status-overhang) * -1);
1386
+ bottom: calc(var(--crewlet-avatar-status-overhang) * -1);
1387
+ display: grid;
1388
+ place-items: center;
1389
+ width: var(--crewlet-avatar-status-size);
1390
+ height: var(--crewlet-avatar-status-size);
1391
+ border-radius: var(--radius-circle);
1392
+ outline: 1px solid transparent;
1393
+ color: var(--color-text-inverse);
1394
+ pointer-events: none;
1395
+ }
1396
+
1397
+ .crewlet-avatar__status > svg {
1398
+ display: block;
1399
+ width: 100%;
1400
+ height: 100%;
1345
1401
  }
1346
1402
 
1347
- .crewlet-avatar--ring-success {
1348
- outline-color: var(--color-feedback-success);
1403
+ .crewlet-avatar__status--working {
1404
+ background: var(--color-feedback-info);
1349
1405
  }
1350
1406
 
1351
- /* SELECTED, in the accent, which is what the accent means: where the reader
1352
- is. It is the only place a badge spends the accent. */
1353
- .crewlet-avatar--ring-brand {
1354
- outline-color: var(--color-brand-accent);
1407
+ .crewlet-avatar__status--needs {
1408
+ background: var(--color-feedback-warning);
1409
+ }
1410
+
1411
+ .crewlet-avatar__status--stopped {
1412
+ background: var(--color-feedback-danger);
1355
1413
  }
1356
1414
 
1357
1415
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewlethq/ui",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Crewlet shared React components built on @crewlethq/tokens and @crewlethq/icons.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,8 +42,8 @@
42
42
  "clean": "rm -rf dist src/generated"
43
43
  },
44
44
  "dependencies": {
45
- "@crewlethq/icons": "0.9.0",
46
- "@crewlethq/tokens": "0.9.0"
45
+ "@crewlethq/icons": "0.10.0",
46
+ "@crewlethq/tokens": "0.10.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18",
@@ -1,125 +0,0 @@
1
- import { cx } from './chunk-ODEQTPFM.js';
2
- import { useState, useEffect } from 'react';
3
- import { CrewletCharacter } from '@crewlethq/icons/characters';
4
- import { jsx } from 'react/jsx-runtime';
5
-
6
- var AVATAR_SIZES = { xs: 20, sm: 26, md: 32, lg: 40 };
7
- var AVATAR_CORNER_RATIO = 0.29;
8
- var AVATAR_CHARACTER_FULL_DETAIL = 56;
9
- var getInitials = (name) => {
10
- if (!name) return "?";
11
- const words = name.trim().split(/[\s\-_.]+/).filter(Boolean);
12
- if (words.length === 0) return "?";
13
- const first = (word2) => (Array.from(word2)[0] ?? "").toUpperCase();
14
- if (words.length >= 2) return words.slice(0, 2).map(first).join("") || "?";
15
- const word = words[0];
16
- const humps = word.split(/(?<=\p{Ll})(?=\p{Lu})/u);
17
- if (humps.length >= 2) return humps.slice(0, 2).map(first).join("");
18
- return Array.from(word).slice(0, 2).map((character) => character.toUpperCase()).join("") || "?";
19
- };
20
- var TINT_COUNT = 10;
21
- var avatarTint = (seed) => {
22
- let hash = 0;
23
- for (let i = 0; i < seed.length; i += 1) {
24
- hash = hash * 31 + seed.charCodeAt(i) >>> 0;
25
- }
26
- return hash % TINT_COUNT;
27
- };
28
- function avatarPixels(size) {
29
- return typeof size === "number" ? size : AVATAR_SIZES[size];
30
- }
31
- function avatarCorner(pixels) {
32
- return pixels * AVATAR_CORNER_RATIO;
33
- }
34
- var Avatar = ({
35
- src,
36
- name,
37
- size = "md",
38
- kind = "agent",
39
- tone = "neutral",
40
- ring,
41
- decorative = false,
42
- colorSeed,
43
- character,
44
- hue,
45
- className,
46
- style,
47
- ...rest
48
- }) => {
49
- const [imageFailed, setImageFailed] = useState(false);
50
- useEffect(() => {
51
- setImageFailed(false);
52
- }, [src]);
53
- const step = typeof size === "number" ? null : size;
54
- const pixels = avatarPixels(size);
55
- const measured = step === null ? {
56
- "--crewlet-avatar-size": `${pixels}px`,
57
- fontSize: Math.max(9, Math.round(pixels * 0.36))
58
- } : {};
59
- const seeded = tone === "seeded" ? avatarTint(colorSeed ?? name ?? "") : null;
60
- const classes = cx(
61
- "crewlet-avatar",
62
- `crewlet-avatar--${kind}`,
63
- step === null ? null : `crewlet-avatar--${step}`,
64
- ring === void 0 ? null : `crewlet-avatar--ring-${ring}`,
65
- className
66
- );
67
- if (character !== void 0 && kind === "agent") {
68
- return /* @__PURE__ */ jsx(
69
- "span",
70
- {
71
- ...rest,
72
- className: cx(
73
- classes,
74
- "crewlet-avatar--character",
75
- hue === void 0 ? "crewlet-avatar--neutral" : `crewlet-avatar--hue-${hue}`
76
- ),
77
- style: { ...measured, ...style },
78
- role: decorative ? void 0 : "img",
79
- "aria-hidden": decorative ? true : void 0,
80
- "aria-label": decorative ? void 0 : name ? `${name} avatar` : "Avatar",
81
- children: /* @__PURE__ */ jsx(
82
- CrewletCharacter,
83
- {
84
- className: "crewlet-avatar__character",
85
- character,
86
- detail: pixels >= AVATAR_CHARACTER_FULL_DETAIL ? "full" : "compact"
87
- }
88
- )
89
- }
90
- );
91
- }
92
- if (src && !imageFailed) {
93
- return /* @__PURE__ */ jsx(
94
- "img",
95
- {
96
- ...rest,
97
- className: cx(classes, "crewlet-avatar--image"),
98
- style: { ...measured, ...style },
99
- src,
100
- alt: decorative ? "" : name ?? "",
101
- "aria-hidden": decorative ? true : void 0,
102
- onError: () => setImageFailed(true)
103
- }
104
- );
105
- }
106
- return /* @__PURE__ */ jsx(
107
- "span",
108
- {
109
- ...rest,
110
- className: cx(
111
- classes,
112
- "crewlet-avatar--fallback",
113
- `crewlet-avatar--${tone}`,
114
- seeded === null ? null : `crewlet-avatar--tint-${seeded}`
115
- ),
116
- style: { ...measured, ...style },
117
- role: decorative ? void 0 : "img",
118
- "aria-hidden": decorative ? true : void 0,
119
- "aria-label": decorative ? void 0 : name ? `${name} avatar` : "Avatar",
120
- children: getInitials(name)
121
- }
122
- );
123
- };
124
-
125
- export { AVATAR_CHARACTER_FULL_DETAIL, AVATAR_CORNER_RATIO, AVATAR_SIZES, Avatar, avatarCorner, avatarPixels, avatarTint, getInitials };