@vit-foundation/ui 0.32.1 → 0.33.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
@@ -27,7 +27,7 @@ Everything exports flat from the root, and again grouped by role:
27
27
 
28
28
  | Entry point | What lives there |
29
29
  | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
- | [`/primitives`](./docs/components/primitives.md) | Button, GhostButton, Link, CardMedia, CardTitle, CopyIntro, DateText, DecorShapes, FilterChips, Pagination, RichText, SearchInput, ShareRow |
30
+ | [`/primitives`](./docs/components/primitives.md) | Button, GhostButton, Link, CardMedia, CardTitle, CopyIntro, DateText, DecorShapes, FigureBody, FigureHead, FilterChips, Pagination, PersonFigure, RichText, SearchInput, ShareRow |
31
31
  | [`/forms`](./docs/forms.md) | Field, SelectField, TextField, FormFeedback, FormErrorFeedback, FormResultSlot, Honeypot, LocaleField, NewsletterIntentField — plus the remote-form seam: structural types, transport constants, field bounds |
32
32
  | [`/chrome`](./docs/components/chrome.md) | PageShell, Nav, Footer |
33
33
  | [`/content`](./docs/components/content.md) | WeeklieCard, ProjectCard, Timeline, TimelineMilestone, TeamMemberCard, CollaboratorList, JobList, SortSelect, the nine page modules (HomePage … WeeklyPage) — plus the data shapes, the page-copy vocabulary, helpers and the two list rules (createWeeklyList, createUrlFilters) |
@@ -145,7 +145,7 @@
145
145
 
146
146
  .vit-inline-select__trigger {
147
147
  display: inline;
148
- padding: 0 0 0.1em;
148
+ padding: 0;
149
149
  margin: 0;
150
150
  border: 0;
151
151
  border-bottom: 2px solid currentColor;
@@ -183,8 +183,10 @@
183
183
  position: absolute;
184
184
  left: 0;
185
185
  z-index: var(--vit-madlib-menu-z, 40);
186
- margin: 0.25em 0 0;
187
- padding: 0.25em 0;
186
+ /* Pixel spacing, not em: the menu sits beside sentence text that can be
187
+ 24px, and em-scaled rows read as a different, roomier control there. */
188
+ margin: 4px 0 0;
189
+ padding: 4px 0;
188
190
  list-style: none;
189
191
  width: max-content;
190
192
  max-width: 42rem;
@@ -197,7 +199,7 @@
197
199
  }
198
200
 
199
201
  .vit-inline-select__option {
200
- padding: 0.375em 1em;
202
+ padding: 6px 16px;
201
203
  font-weight: var(--vit-madlib-control-weight, 700);
202
204
  color: var(--vit-madlib-muted-color, color-mix(in srgb, currentColor 35%, transparent));
203
205
  cursor: pointer;
@@ -233,12 +233,12 @@
233
233
  }
234
234
 
235
235
  .vit-madlib__separator {
236
- padding: 0 0.375em;
236
+ padding: 0 6px;
237
237
  color: var(--vit-madlib-muted-color, color-mix(in srgb, currentColor 35%, transparent));
238
238
  }
239
239
 
240
240
  .vit-madlib__toggle {
241
- padding: 0 0 0.125em;
241
+ padding: 0 0 2px;
242
242
  margin: 0;
243
243
  border: 0;
244
244
  border-bottom: 2px solid transparent;
@@ -0,0 +1,63 @@
1
+ <!--
2
+ @component FigureBody
3
+
4
+ The line body: a shoulder line with two hanging arms and a pair of legs in
5
+ a pose, drawn in the composed figure box (`FIGURE_VIEWBOX` in `./paths`).
6
+ No head — a `FigureHead`, a marker or a callout goes in as `children`, which
7
+ render INSIDE the svg and so share its coordinates (neck at `NECK`, shoulder
8
+ line at `SHOULDER`).
9
+
10
+ Decorative: `aria-hidden`, and it never names itself — whatever composes it
11
+ carries the text. Sized by `--vit-figure-width`; the strokes scale with it
12
+ because `--vit-figure-stroke` is in viewBox units.
13
+
14
+ @property arms - Arm pose
15
+ @property legs - Leg pose
16
+ @property class - Extra classes on the svg
17
+ @property children - Drawn inside the svg, after the body
18
+ -->
19
+ <script lang="ts">
20
+ import type { Snippet } from 'svelte';
21
+ import { ARMS, ARMS_AT, FIGURE_VIEWBOX, LEGS, LEGS_AT } from './paths.js';
22
+ import type { ArmsPose, LegsPose } from './paths.js';
23
+
24
+ interface Props {
25
+ arms?: ArmsPose;
26
+ legs?: LegsPose;
27
+ class?: string;
28
+ children?: Snippet;
29
+ }
30
+
31
+ let { arms = 'down', legs = 'standing', class: className = '', children }: Props = $props();
32
+ </script>
33
+
34
+ <svg
35
+ class="vit-figure-body {className}"
36
+ viewBox={FIGURE_VIEWBOX}
37
+ aria-hidden="true"
38
+ focusable="false"
39
+ >
40
+ <g class="vit-figure-body__strokes">
41
+ <path d={ARMS[arms]} transform={ARMS_AT} />
42
+ <path d={LEGS[legs]} transform={LEGS_AT} />
43
+ </g>
44
+ {@render children?.()}
45
+ </svg>
46
+
47
+ <style>
48
+ .vit-figure-body {
49
+ display: block;
50
+ width: var(--vit-figure-width, 10rem);
51
+ height: auto;
52
+ /* A callout, or a raised arm at a small width, may leave the box. */
53
+ overflow: visible;
54
+ }
55
+
56
+ .vit-figure-body__strokes {
57
+ fill: none;
58
+ stroke: var(--vit-figure-ink, var(--color-ink));
59
+ stroke-width: var(--vit-figure-stroke, 4);
60
+ stroke-linecap: round;
61
+ stroke-linejoin: round;
62
+ }
63
+ </style>
@@ -0,0 +1,29 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ArmsPose, LegsPose } from './paths.js';
3
+ interface Props {
4
+ arms?: ArmsPose;
5
+ legs?: LegsPose;
6
+ class?: string;
7
+ children?: Snippet;
8
+ }
9
+ /**
10
+ * FigureBody
11
+ *
12
+ * The line body: a shoulder line with two hanging arms and a pair of legs in
13
+ * a pose, drawn in the composed figure box (`FIGURE_VIEWBOX` in `./paths`).
14
+ * No head — a `FigureHead`, a marker or a callout goes in as `children`, which
15
+ * render INSIDE the svg and so share its coordinates (neck at `NECK`, shoulder
16
+ * line at `SHOULDER`).
17
+ *
18
+ * Decorative: `aria-hidden`, and it never names itself — whatever composes it
19
+ * carries the text. Sized by `--vit-figure-width`; the strokes scale with it
20
+ * because `--vit-figure-stroke` is in viewBox units.
21
+ *
22
+ * @property arms - Arm pose
23
+ * @property legs - Leg pose
24
+ * @property class - Extra classes on the svg
25
+ * @property children - Drawn inside the svg, after the body
26
+ */
27
+ declare const FigureBody: import("svelte").Component<Props, {}, "">;
28
+ type FigureBody = ReturnType<typeof FigureBody>;
29
+ export default FigureBody;
@@ -0,0 +1,93 @@
1
+ <!--
2
+ @component FigureHead
3
+
4
+ The head, three ways: a cut-out photo with a transparent background sat on
5
+ the neck, a portrait masked into a circle, or — with no photo, or one that
6
+ fails to load — the survey's drawn outline. An svg FRAGMENT: it renders
7
+ inside a `FigureBody` (or any svg in the figure box) and places itself on
8
+ `NECK`.
9
+
10
+ The photo carries no alt and the outline no title: the head is part of a
11
+ decorative drawing, and the name belongs to whatever composes it.
12
+
13
+ @property photo - The photo; absent or failing to load, the head is drawn
14
+ @property photoShape - `cutout` sits a transparent cut-out on the neck; `circle` masks a portrait
15
+ @property head - The drawn outline, used when there is no photo
16
+ @property headScale - Scales the photo about the neck point, so the chin stays put
17
+ -->
18
+ <svelte:options namespace="svg" />
19
+
20
+ <script lang="ts">
21
+ import { HEAD_AT, HEADS, NECK } from './paths.js';
22
+ import type { HeadShape } from './paths.js';
23
+
24
+ interface Props {
25
+ photo?: string | null;
26
+ photoShape?: 'cutout' | 'circle';
27
+ head?: HeadShape;
28
+ headScale?: number;
29
+ }
30
+
31
+ let { photo, photoShape = 'cutout', head = 'round', headScale = 1 }: Props = $props();
32
+
33
+ /**
34
+ * The photo that failed to load, so a broken URL degrades to the drawn head
35
+ * rather than an empty neck. Keyed by value, not a boolean (the CardMedia
36
+ * rule): a new URL gets a fresh attempt.
37
+ */
38
+ let failedSrc = $state<string | null>(null);
39
+ const showPhoto = $derived(Boolean(photo) && photo !== failedSrc);
40
+
41
+ // A clipPath is referenced by id, and a team page renders many heads.
42
+ const uid = $props.id();
43
+ const clipId = `${uid}-head`;
44
+ const transform = $derived(
45
+ headScale === 1
46
+ ? undefined
47
+ : `translate(${NECK.x} ${NECK.y}) scale(${headScale}) translate(${-NECK.x} ${-NECK.y})`
48
+ );
49
+ </script>
50
+
51
+ {#if !showPhoto}
52
+ <path class="vit-figure-head" d={HEADS[head]} transform={HEAD_AT} />
53
+ {:else if photoShape === 'circle'}
54
+ <clipPath id={clipId}>
55
+ <circle cx={NECK.x} cy="48" r="34" />
56
+ </clipPath>
57
+ <image
58
+ class="vit-figure-head__photo"
59
+ href={photo}
60
+ x={NECK.x - 34}
61
+ y="14"
62
+ width="68"
63
+ height="68"
64
+ preserveAspectRatio="xMidYMid slice"
65
+ clip-path="url(#{clipId})"
66
+ {transform}
67
+ onerror={() => (failedSrc = photo ?? null)}
68
+ />
69
+ {:else}
70
+ <!-- Bottom-anchored (`xMidYMax`): whatever the cut-out's proportions, its
71
+ chin lands on the neck, 8 over the shoulder line. The box is the
72
+ shoulders' width — a head that wide is the mockup's proportion. -->
73
+ <image
74
+ class="vit-figure-head__photo"
75
+ href={photo}
76
+ x={NECK.x - 38}
77
+ y="0"
78
+ width="76"
79
+ height="84"
80
+ preserveAspectRatio="xMidYMax meet"
81
+ {transform}
82
+ onerror={() => (failedSrc = photo ?? null)}
83
+ />
84
+ {/if}
85
+
86
+ <style>
87
+ .vit-figure-head {
88
+ fill: var(--color-surface);
89
+ stroke: var(--vit-figure-ink, var(--color-ink));
90
+ stroke-width: var(--vit-figure-stroke, 4);
91
+ stroke-linejoin: round;
92
+ }
93
+ </style>
@@ -0,0 +1,27 @@
1
+ import type { HeadShape } from './paths.js';
2
+ interface Props {
3
+ photo?: string | null;
4
+ photoShape?: 'cutout' | 'circle';
5
+ head?: HeadShape;
6
+ headScale?: number;
7
+ }
8
+ /**
9
+ * FigureHead
10
+ *
11
+ * The head, three ways: a cut-out photo with a transparent background sat on
12
+ * the neck, a portrait masked into a circle, or — with no photo, or one that
13
+ * fails to load — the survey's drawn outline. An svg FRAGMENT: it renders
14
+ * inside a `FigureBody` (or any svg in the figure box) and places itself on
15
+ * `NECK`.
16
+ *
17
+ * The photo carries no alt and the outline no title: the head is part of a
18
+ * decorative drawing, and the name belongs to whatever composes it.
19
+ *
20
+ * @property photo - The photo; absent or failing to load, the head is drawn
21
+ * @property photoShape - `cutout` sits a transparent cut-out on the neck; `circle` masks a portrait
22
+ * @property head - The drawn outline, used when there is no photo
23
+ * @property headScale - Scales the photo about the neck point, so the chin stays put
24
+ */
25
+ declare const FigureHead: import("svelte").Component<Props, {}, "">;
26
+ type FigureHead = ReturnType<typeof FigureHead>;
27
+ export default FigureHead;
@@ -0,0 +1,253 @@
1
+ <!--
2
+ @component PersonFigure
3
+
4
+ A person: a `FigureBody` in a pose, a `FigureHead` on its neck, and beside
5
+ them the name and role over a short rule, joined to the shoulder by a
6
+ diagonal callout line. A bio, when there is one, reveals under the rule on
7
+ hover, keyboard focus or tap.
8
+
9
+ This is the composition; the parts stand alone. `./paths` holds the drawing
10
+ and the callout arithmetic, `FigureBody` the strokes, `FigureHead` the head's
11
+ three modes. What is here is what only the whole needs: the grid that pins
12
+ the caption's baseline to the rule, and the reveal.
13
+
14
+ ## Accessibility
15
+ The drawing is decorative — the svg is `aria-hidden`, the photo has no alt —
16
+ and the name in the caption is the only text: the `Logo` rule, a mark never
17
+ names itself. The bio is hidden by opacity, not removed, so a screen reader
18
+ reads it without hovering; the figure takes `tabindex="0"` only when there
19
+ is a bio to reveal.
20
+
21
+ ## Sizing
22
+ Set `--vit-figure-width` on the figure or an ancestor. The art, the callout
23
+ run and the height of the rule above the baseline all derive from it; the
24
+ text is in rem. Below about 8rem the two label lines outgrow the room above
25
+ a top-aligned rule — lower `--vit-figure-name-size`/`--vit-figure-role-size`
26
+ or align the label `bottom`. The callout leaves the art's box on purpose
27
+ (`overflow: visible`), so an ancestor clipping its overflow hides it.
28
+
29
+ | property | default |
30
+ |-----------------------------|---------------------|
31
+ | `--vit-figure-width` | `10rem` |
32
+ | `--vit-figure-ink` | `var(--color-ink)` |
33
+ | `--vit-figure-stroke` | `4` (viewBox units) |
34
+ | `--vit-figure-callout-ink` | `var(--color-ink)` |
35
+ | `--vit-figure-name-size` | `var(--text-base)` |
36
+ | `--vit-figure-role-size` | `var(--text-sm)` |
37
+ | `--vit-figure-bio-width` | `14rem` |
38
+
39
+ @property name - The person's name; the figure's accessible text
40
+ @property role - The line under the name
41
+ @property bio - Revealed under the rule on hover, focus or tap
42
+ @property photo - The head photo; absent or failing to load, the head is drawn
43
+ @property photoShape - `cutout` sits a transparent cut-out on the neck; `circle` masks a portrait
44
+ @property head - Drawn head outline used when there is no photo
45
+ @property arms - Arm pose
46
+ @property legs - Leg pose
47
+ @property headScale - Scales the photo head about the neck point
48
+ @property labelSide - Which side of the figure the label sits on
49
+ @property labelAlign - `top` puts the rule beside the head, `bottom` beside the legs
50
+ @property class - Extra classes on the figure
51
+ @property children - An extra marker laid over the body (position it absolutely)
52
+ -->
53
+ <script lang="ts">
54
+ import type { Snippet } from 'svelte';
55
+ import FigureBody from './FigureBody.svelte';
56
+ import FigureHead from './FigureHead.svelte';
57
+ import { calloutPath } from './paths.js';
58
+ import type { ArmsPose, HeadShape, LabelAlign, LabelSide, LegsPose } from './paths.js';
59
+
60
+ interface Props {
61
+ /** The figure's only accessible text — the drawing never names itself. */
62
+ name: string;
63
+ role?: string;
64
+ /** Revealed under the rule on hover, focus or tap. Absent: nothing to reveal, no tabindex. */
65
+ bio?: string | null;
66
+ /** Absent, or failing to load, the head is drawn instead. */
67
+ photo?: string | null;
68
+ /** `cutout` = a transparent-background cut-out sat on the neck; `circle` = a portrait masked into a circle. */
69
+ photoShape?: 'cutout' | 'circle';
70
+ /** The drawn head, used when there is no photo. */
71
+ head?: HeadShape;
72
+ arms?: ArmsPose;
73
+ legs?: LegsPose;
74
+ /** Scales the photo head about the neck point, so the chin stays put. */
75
+ headScale?: number;
76
+ labelSide?: LabelSide;
77
+ /** `top` puts the rule beside the head, `bottom` beside the legs. */
78
+ labelAlign?: LabelAlign;
79
+ class?: string;
80
+ /** An extra marker over the body; position it absolutely inside the art's box. */
81
+ children?: Snippet;
82
+ }
83
+
84
+ let {
85
+ name,
86
+ role,
87
+ bio,
88
+ photo,
89
+ photoShape = 'cutout',
90
+ head = 'round',
91
+ arms = 'down',
92
+ legs = 'standing',
93
+ headScale = 1,
94
+ labelSide = 'right',
95
+ labelAlign = 'top',
96
+ class: className = '',
97
+ children
98
+ }: Props = $props();
99
+
100
+ const callout = $derived(calloutPath(labelSide, labelAlign));
101
+ </script>
102
+
103
+ <!-- Focusable only when there is a bio to reveal; `figure` is not interactive
104
+ otherwise, and a focus stop with nothing behind it is noise. -->
105
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
106
+ <figure
107
+ class="vit-figure {className}"
108
+ data-side={labelSide}
109
+ data-align={labelAlign}
110
+ tabindex={bio ? 0 : undefined}
111
+ >
112
+ <FigureBody {arms} {legs} class="vit-figure__art">
113
+ <!-- After the body, so the head covers the middle of the shoulder line. -->
114
+ <FigureHead {photo} {photoShape} {head} {headScale} />
115
+ <path class="vit-figure__callout" d={callout} vector-effect="non-scaling-stroke" />
116
+ </FigureBody>
117
+ {#if children}
118
+ <div class="vit-figure__extra">{@render children()}</div>
119
+ {/if}
120
+ <figcaption class="vit-figure__label">
121
+ <strong class="vit-figure__name">{name}</strong>
122
+ {#if role}<span class="vit-figure__role">{role}</span>{/if}
123
+ {#if bio}<p class="vit-figure__bio">{bio}</p>{/if}
124
+ </figcaption>
125
+ </figure>
126
+
127
+ <style>
128
+ .vit-figure {
129
+ /* One viewBox unit, so the run and the rule's row follow the width.
130
+ The 24 and 90 are `CALLOUT_RUN` and `CALLOUT_RULE` in paths.ts. */
131
+ --_w: var(--vit-figure-width, 10rem);
132
+ --_u: calc(var(--_w) / 153);
133
+ --_run: calc(var(--_u) * 24);
134
+ --_rule-y: 50;
135
+ position: relative;
136
+ display: inline-grid;
137
+ align-items: start;
138
+ /* The first row ends exactly where the callout's rule is drawn, and the
139
+ caption sits on that edge — so the rule underlines the role whatever
140
+ the font size, and the text grows upward from it. */
141
+ grid-template-rows: calc(var(--_u) * var(--_rule-y)) auto;
142
+ margin: 0;
143
+ color: var(--color-ink);
144
+ }
145
+
146
+ .vit-figure[data-align='bottom'] {
147
+ --_rule-y: 200;
148
+ }
149
+
150
+ .vit-figure[data-side='right'] {
151
+ grid-template-columns: var(--_w) var(--_run) auto;
152
+ grid-template-areas:
153
+ 'art run label'
154
+ 'art run .';
155
+ }
156
+
157
+ .vit-figure[data-side='left'] {
158
+ grid-template-columns: auto var(--_run) var(--_w);
159
+ grid-template-areas:
160
+ 'label run art'
161
+ '. run art';
162
+ }
163
+
164
+ /* The body's svg is another component's element, hence the :global. */
165
+ .vit-figure > :global(.vit-figure__art) {
166
+ grid-area: art;
167
+ }
168
+
169
+ .vit-figure__callout {
170
+ fill: none;
171
+ stroke: var(--vit-figure-callout-ink, var(--color-ink));
172
+ stroke-width: 1;
173
+ }
174
+
175
+ .vit-figure__label {
176
+ grid-area: label;
177
+ position: relative;
178
+ display: flex;
179
+ flex-direction: column;
180
+ align-self: end;
181
+ min-width: calc(var(--_u) * 90);
182
+ padding-bottom: var(--space-1);
183
+ line-height: var(--leading-tight);
184
+ }
185
+
186
+ .vit-figure[data-side='left'] .vit-figure__label {
187
+ align-items: flex-end;
188
+ text-align: right;
189
+ }
190
+
191
+ .vit-figure__name {
192
+ font-size: var(--vit-figure-name-size, var(--text-base));
193
+ font-weight: 700;
194
+ }
195
+
196
+ .vit-figure__role {
197
+ font-size: var(--vit-figure-role-size, var(--text-sm));
198
+ color: var(--color-ink-secondary);
199
+ }
200
+
201
+ .vit-figure__bio {
202
+ position: absolute;
203
+ top: 100%;
204
+ inset-inline-start: 0;
205
+ width: var(--vit-figure-bio-width, 14rem);
206
+ margin: var(--space-1) 0 0;
207
+ font-size: var(--text-sm);
208
+ line-height: var(--leading-body);
209
+ color: var(--color-ink-secondary);
210
+ opacity: 0;
211
+ transform: translateY(-0.25rem);
212
+ pointer-events: none;
213
+ transition:
214
+ opacity var(--transition-fast),
215
+ transform var(--transition-fast);
216
+ }
217
+
218
+ .vit-figure[data-side='left'] .vit-figure__bio {
219
+ inset-inline-start: auto;
220
+ inset-inline-end: 0;
221
+ }
222
+
223
+ .vit-figure:hover .vit-figure__bio,
224
+ .vit-figure:focus-within .vit-figure__bio {
225
+ opacity: 1;
226
+ transform: none;
227
+ pointer-events: auto;
228
+ }
229
+
230
+ .vit-figure:focus-visible {
231
+ outline: 2px solid var(--color-brand);
232
+ outline-offset: 4px;
233
+ }
234
+
235
+ .vit-figure__extra {
236
+ grid-area: art;
237
+ position: relative;
238
+ width: var(--_w);
239
+ height: 100%;
240
+ pointer-events: none;
241
+ }
242
+
243
+ .vit-figure__extra > :global(*) {
244
+ pointer-events: auto;
245
+ }
246
+
247
+ @media (prefers-reduced-motion: reduce) {
248
+ .vit-figure__bio {
249
+ transition: none;
250
+ transform: none;
251
+ }
252
+ }
253
+ </style>
@@ -0,0 +1,80 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ArmsPose, HeadShape, LabelAlign, LabelSide, LegsPose } from './paths.js';
3
+ interface Props {
4
+ /** The figure's only accessible text — the drawing never names itself. */
5
+ name: string;
6
+ role?: string;
7
+ /** Revealed under the rule on hover, focus or tap. Absent: nothing to reveal, no tabindex. */
8
+ bio?: string | null;
9
+ /** Absent, or failing to load, the head is drawn instead. */
10
+ photo?: string | null;
11
+ /** `cutout` = a transparent-background cut-out sat on the neck; `circle` = a portrait masked into a circle. */
12
+ photoShape?: 'cutout' | 'circle';
13
+ /** The drawn head, used when there is no photo. */
14
+ head?: HeadShape;
15
+ arms?: ArmsPose;
16
+ legs?: LegsPose;
17
+ /** Scales the photo head about the neck point, so the chin stays put. */
18
+ headScale?: number;
19
+ labelSide?: LabelSide;
20
+ /** `top` puts the rule beside the head, `bottom` beside the legs. */
21
+ labelAlign?: LabelAlign;
22
+ class?: string;
23
+ /** An extra marker over the body; position it absolutely inside the art's box. */
24
+ children?: Snippet;
25
+ }
26
+ /**
27
+ * PersonFigure
28
+ *
29
+ * A person: a `FigureBody` in a pose, a `FigureHead` on its neck, and beside
30
+ * them the name and role over a short rule, joined to the shoulder by a
31
+ * diagonal callout line. A bio, when there is one, reveals under the rule on
32
+ * hover, keyboard focus or tap.
33
+ *
34
+ * This is the composition; the parts stand alone. `./paths` holds the drawing
35
+ * and the callout arithmetic, `FigureBody` the strokes, `FigureHead` the head's
36
+ * three modes. What is here is what only the whole needs: the grid that pins
37
+ * the caption's baseline to the rule, and the reveal.
38
+ *
39
+ * ## Accessibility
40
+ * The drawing is decorative — the svg is `aria-hidden`, the photo has no alt —
41
+ * and the name in the caption is the only text: the `Logo` rule, a mark never
42
+ * names itself. The bio is hidden by opacity, not removed, so a screen reader
43
+ * reads it without hovering; the figure takes `tabindex="0"` only when there
44
+ * is a bio to reveal.
45
+ *
46
+ * ## Sizing
47
+ * Set `--vit-figure-width` on the figure or an ancestor. The art, the callout
48
+ * run and the height of the rule above the baseline all derive from it; the
49
+ * text is in rem. Below about 8rem the two label lines outgrow the room above
50
+ * a top-aligned rule — lower `--vit-figure-name-size`/`--vit-figure-role-size`
51
+ * or align the label `bottom`. The callout leaves the art's box on purpose
52
+ * (`overflow: visible`), so an ancestor clipping its overflow hides it.
53
+ *
54
+ * | property | default |
55
+ * |-----------------------------|---------------------|
56
+ * | `--vit-figure-width` | `10rem` |
57
+ * | `--vit-figure-ink` | `var(--color-ink)` |
58
+ * | `--vit-figure-stroke` | `4` (viewBox units) |
59
+ * | `--vit-figure-callout-ink` | `var(--color-ink)` |
60
+ * | `--vit-figure-name-size` | `var(--text-base)` |
61
+ * | `--vit-figure-role-size` | `var(--text-sm)` |
62
+ * | `--vit-figure-bio-width` | `14rem` |
63
+ *
64
+ * @property name - The person's name; the figure's accessible text
65
+ * @property role - The line under the name
66
+ * @property bio - Revealed under the rule on hover, focus or tap
67
+ * @property photo - The head photo; absent or failing to load, the head is drawn
68
+ * @property photoShape - `cutout` sits a transparent cut-out on the neck; `circle` masks a portrait
69
+ * @property head - Drawn head outline used when there is no photo
70
+ * @property arms - Arm pose
71
+ * @property legs - Leg pose
72
+ * @property headScale - Scales the photo head about the neck point
73
+ * @property labelSide - Which side of the figure the label sits on
74
+ * @property labelAlign - `top` puts the rule beside the head, `bottom` beside the legs
75
+ * @property class - Extra classes on the figure
76
+ * @property children - An extra marker laid over the body (position it absolutely)
77
+ */
78
+ declare const PersonFigure: import("svelte").Component<Props, {}, "">;
79
+ type PersonFigure = ReturnType<typeof PersonFigure>;
80
+ export default PersonFigure;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The drawing behind `FigureBody`, `FigureHead` and `PersonFigure`: the
3
+ * survey's stroke paths, where each part sits in the composed box, and the
4
+ * arithmetic of the label's callout. Data and arithmetic only, so it tests in
5
+ * node and a host that wants line people without heads or labels — a chart, a
6
+ * decorative band — can draw from the same set.
7
+ *
8
+ * The paths are the ones `mwc-enquesta-bretxa-digital` built its survey
9
+ * avatars from (`paths.json`). They are closed sets: extending one is a
10
+ * package change, so every consumer's `ArmsPose` stays honest (the `Icon`
11
+ * rule).
12
+ */
13
+ /** Shoulder line and two hanging arms, in the survey's 153×111 torso box. */
14
+ export declare const ARMS: {
15
+ readonly down: "M27 107L39.375 8H113.625L126 107";
16
+ readonly raised: "M7 25L32.9376 57.5L39.125 8H113.375L119.563 57.5L145.5 25";
17
+ readonly 'one-bent': "M51.899 91.3976L19 53.7671L38.9562 8H113.565L126 107";
18
+ };
19
+ /** Two legs in the survey's 91×187 box (its adult set). */
20
+ export declare const LEGS: {
21
+ readonly standing: "M21 7V179H8M70 7V179H82";
22
+ readonly walking: "M69 7L85 129.857L69 179H81.4218M21 7L36 129.857L21 179H32.6453";
23
+ readonly stride: "M21 7V80.7143L38 179H25.0024M71 7V80.7143L54 179H65.3333";
24
+ readonly step: "M21 7V179H8M70 7L85 129.857L70 179H81.6545";
25
+ readonly kneel: "M21 7V179H9M71 7V130.087L46 168.589V182";
26
+ readonly sit: "M21 7V130.087L45 168.589V182M69 7L54 129.857L69 179H57.3546";
27
+ };
28
+ /** Drawn head outlines in the survey's 49×48 box, for a figure with no photo. */
29
+ export declare const HEADS: {
30
+ readonly round: "M25 41C33.8366 41 41 33.6127 41 24.5C41 15.3873 33.8366 8 25 8C16.1634 8 9 15.3873 9 24.5C9 33.6127 16.1634 41 25 41Z";
31
+ readonly cup: "M8 8V24.4952C7.99874 26.6662 8.4255 28.8161 9.25592 30.8216C10.0863 32.8271 11.3042 34.6488 12.8393 36.1822C14.3745 37.7155 16.1968 38.9304 18.2019 39.7571C20.207 40.5839 22.3554 41.0062 24.5239 40.9999C28.8936 40.9999 33.0843 39.262 36.1742 36.1686C39.2641 33.0751 41 28.8795 41 24.5047V8.00955L8 8Z";
32
+ readonly d: "M27.571 8H38V41H27.571C23.1726 40.9949 18.9563 39.2535 15.8489 36.1585C12.7415 33.0636 10.9975 28.8684 11 24.4952C11 20.1237 12.7453 15.931 15.8524 12.8381C18.9595 9.74519 23.1743 8.00507 27.571 8V8Z";
33
+ };
34
+ export type ArmsPose = keyof typeof ARMS;
35
+ export type LegsPose = keyof typeof LEGS;
36
+ export type HeadShape = keyof typeof HEADS;
37
+ /**
38
+ * The composed box. The survey stacked three svgs on a grid (rows 48 / 111 /
39
+ * 100, the legs' 187 hanging out of the last row); this is that stack
40
+ * flattened into one viewBox, plus 20 of headroom so a photo can rise above
41
+ * where the drawn head would be. Landmarks: shoulder line y 76 from x 39 to
42
+ * 114, legs from y 99, feet at y 271.
43
+ */
44
+ export declare const FIGURE_WIDTH = 153;
45
+ export declare const FIGURE_HEIGHT = 280;
46
+ export declare const FIGURE_VIEWBOX = "0 0 153 280";
47
+ export declare const HEAD_AT = "translate(52 20)";
48
+ export declare const ARMS_AT = "translate(0 68)";
49
+ export declare const LEGS_AT = "translate(31 92)";
50
+ /** The neck: a photo's chin lands here, and `headScale` scales about it. */
51
+ export declare const NECK: {
52
+ readonly x: 76.5;
53
+ readonly y: 84;
54
+ };
55
+ /** Where the callout leaves the body — the ends of the shoulder line. */
56
+ export declare const SHOULDER: {
57
+ readonly y: 76;
58
+ readonly left: 39.5;
59
+ readonly right: 113.5;
60
+ };
61
+ export type LabelSide = 'left' | 'right';
62
+ export type LabelAlign = 'top' | 'bottom';
63
+ /**
64
+ * The height of the label's rule: beside the head, or beside the legs.
65
+ * `PersonFigure` sizes the caption's grid row to it, so the caption's
66
+ * baseline meets the rule whatever the font size.
67
+ */
68
+ export declare const RULE_Y: {
69
+ readonly top: 50;
70
+ readonly bottom: 200;
71
+ };
72
+ /** How far the callout runs out of the art before the rule, and the rule's length. */
73
+ export declare const CALLOUT_RUN = 24;
74
+ export declare const CALLOUT_RULE = 90;
75
+ /**
76
+ * Shoulder → the rule's inner end → along the rule. Mirrored for a label on
77
+ * the left, where it runs into negative x — the art's svg draws with
78
+ * `overflow: visible` for exactly this.
79
+ */
80
+ export declare function calloutPath(side: LabelSide, align: LabelAlign): string;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The drawing behind `FigureBody`, `FigureHead` and `PersonFigure`: the
3
+ * survey's stroke paths, where each part sits in the composed box, and the
4
+ * arithmetic of the label's callout. Data and arithmetic only, so it tests in
5
+ * node and a host that wants line people without heads or labels — a chart, a
6
+ * decorative band — can draw from the same set.
7
+ *
8
+ * The paths are the ones `mwc-enquesta-bretxa-digital` built its survey
9
+ * avatars from (`paths.json`). They are closed sets: extending one is a
10
+ * package change, so every consumer's `ArmsPose` stays honest (the `Icon`
11
+ * rule).
12
+ */
13
+ /** Shoulder line and two hanging arms, in the survey's 153×111 torso box. */
14
+ export const ARMS = {
15
+ down: 'M27 107L39.375 8H113.625L126 107',
16
+ raised: 'M7 25L32.9376 57.5L39.125 8H113.375L119.563 57.5L145.5 25',
17
+ 'one-bent': 'M51.899 91.3976L19 53.7671L38.9562 8H113.565L126 107'
18
+ };
19
+ /** Two legs in the survey's 91×187 box (its adult set). */
20
+ export const LEGS = {
21
+ standing: 'M21 7V179H8M70 7V179H82',
22
+ walking: 'M69 7L85 129.857L69 179H81.4218M21 7L36 129.857L21 179H32.6453',
23
+ stride: 'M21 7V80.7143L38 179H25.0024M71 7V80.7143L54 179H65.3333',
24
+ step: 'M21 7V179H8M70 7L85 129.857L70 179H81.6545',
25
+ kneel: 'M21 7V179H9M71 7V130.087L46 168.589V182',
26
+ sit: 'M21 7V130.087L45 168.589V182M69 7L54 129.857L69 179H57.3546'
27
+ };
28
+ /** Drawn head outlines in the survey's 49×48 box, for a figure with no photo. */
29
+ export const HEADS = {
30
+ round: 'M25 41C33.8366 41 41 33.6127 41 24.5C41 15.3873 33.8366 8 25 8C16.1634 8 9 15.3873 9 24.5C9 33.6127 16.1634 41 25 41Z',
31
+ cup: 'M8 8V24.4952C7.99874 26.6662 8.4255 28.8161 9.25592 30.8216C10.0863 32.8271 11.3042 34.6488 12.8393 36.1822C14.3745 37.7155 16.1968 38.9304 18.2019 39.7571C20.207 40.5839 22.3554 41.0062 24.5239 40.9999C28.8936 40.9999 33.0843 39.262 36.1742 36.1686C39.2641 33.0751 41 28.8795 41 24.5047V8.00955L8 8Z',
32
+ d: 'M27.571 8H38V41H27.571C23.1726 40.9949 18.9563 39.2535 15.8489 36.1585C12.7415 33.0636 10.9975 28.8684 11 24.4952C11 20.1237 12.7453 15.931 15.8524 12.8381C18.9595 9.74519 23.1743 8.00507 27.571 8V8Z'
33
+ };
34
+ /**
35
+ * The composed box. The survey stacked three svgs on a grid (rows 48 / 111 /
36
+ * 100, the legs' 187 hanging out of the last row); this is that stack
37
+ * flattened into one viewBox, plus 20 of headroom so a photo can rise above
38
+ * where the drawn head would be. Landmarks: shoulder line y 76 from x 39 to
39
+ * 114, legs from y 99, feet at y 271.
40
+ */
41
+ export const FIGURE_WIDTH = 153;
42
+ export const FIGURE_HEIGHT = 280;
43
+ export const FIGURE_VIEWBOX = `0 0 ${FIGURE_WIDTH} ${FIGURE_HEIGHT}`;
44
+ export const HEAD_AT = 'translate(52 20)';
45
+ export const ARMS_AT = 'translate(0 68)';
46
+ export const LEGS_AT = 'translate(31 92)';
47
+ /** The neck: a photo's chin lands here, and `headScale` scales about it. */
48
+ export const NECK = { x: 76.5, y: 84 };
49
+ /** Where the callout leaves the body — the ends of the shoulder line. */
50
+ export const SHOULDER = { y: 76, left: 39.5, right: 113.5 };
51
+ /**
52
+ * The height of the label's rule: beside the head, or beside the legs.
53
+ * `PersonFigure` sizes the caption's grid row to it, so the caption's
54
+ * baseline meets the rule whatever the font size.
55
+ */
56
+ export const RULE_Y = { top: 50, bottom: 200 };
57
+ /** How far the callout runs out of the art before the rule, and the rule's length. */
58
+ export const CALLOUT_RUN = 24;
59
+ export const CALLOUT_RULE = 90;
60
+ /**
61
+ * Shoulder → the rule's inner end → along the rule. Mirrored for a label on
62
+ * the left, where it runs into negative x — the art's svg draws with
63
+ * `overflow: visible` for exactly this.
64
+ */
65
+ export function calloutPath(side, align) {
66
+ const y = RULE_Y[align];
67
+ if (side === 'right') {
68
+ const inner = FIGURE_WIDTH + CALLOUT_RUN;
69
+ return `M${SHOULDER.right} ${SHOULDER.y}L${inner} ${y}H${inner + CALLOUT_RULE}`;
70
+ }
71
+ const inner = -CALLOUT_RUN;
72
+ return `M${SHOULDER.left} ${SHOULDER.y}L${inner} ${y}H${inner - CALLOUT_RULE}`;
73
+ }
@@ -15,6 +15,11 @@ export { default as IconButton } from './components/ui/IconButton.svelte';
15
15
  export { default as Link } from './components/ui/Link.svelte';
16
16
  export { default as Logo } from './components/ui/Logo.svelte';
17
17
  export { default as Modal } from './components/ui/Modal.svelte';
18
+ export { default as FigureBody } from './components/ui/figure/FigureBody.svelte';
19
+ export { default as FigureHead } from './components/ui/figure/FigureHead.svelte';
20
+ export { default as PersonFigure } from './components/ui/figure/PersonFigure.svelte';
21
+ export { ARMS, LEGS, HEADS, NECK, SHOULDER, FIGURE_VIEWBOX, calloutPath } from './components/ui/figure/paths.js';
22
+ export type { ArmsPose, HeadShape, LabelAlign, LabelSide, LegsPose } from './components/ui/figure/paths.js';
18
23
  export { default as Pagination } from './components/ui/Pagination.svelte';
19
24
  export { default as RichText } from './components/ui/RichText.svelte';
20
25
  export { default as SearchInput } from './components/ui/SearchInput.svelte';
@@ -15,6 +15,12 @@ export { default as IconButton } from './components/ui/IconButton.svelte';
15
15
  export { default as Link } from './components/ui/Link.svelte';
16
16
  export { default as Logo } from './components/ui/Logo.svelte';
17
17
  export { default as Modal } from './components/ui/Modal.svelte';
18
+ // The figure, in parts: the drawing and the callout arithmetic, the line
19
+ // body, the head's three modes, and the person that composes them.
20
+ export { default as FigureBody } from './components/ui/figure/FigureBody.svelte';
21
+ export { default as FigureHead } from './components/ui/figure/FigureHead.svelte';
22
+ export { default as PersonFigure } from './components/ui/figure/PersonFigure.svelte';
23
+ export { ARMS, LEGS, HEADS, NECK, SHOULDER, FIGURE_VIEWBOX, calloutPath } from './components/ui/figure/paths.js';
18
24
  export { default as Pagination } from './components/ui/Pagination.svelte';
19
25
  export { default as RichText } from './components/ui/RichText.svelte';
20
26
  export { default as SearchInput } from './components/ui/SearchInput.svelte';
@@ -115,4 +115,16 @@
115
115
  --vit-madlib-menu-shadow: none;
116
116
  --vit-madlib-menu-max-height: 15rem;
117
117
  --vit-madlib-menu-z: 40;
118
+
119
+ /* Person figure (`PersonFigure`, on `./primitives`) */
120
+ /* The drawing's width; everything in the figure — body, callout run, the
121
+ rule's height above the baseline — derives from it. Text stays in rem. */
122
+ --vit-figure-width: 10rem;
123
+ --vit-figure-ink: var(--color-ink);
124
+ /* Body stroke in viewBox units (153 across), so it scales with the width. */
125
+ --vit-figure-stroke: 4;
126
+ --vit-figure-callout-ink: var(--color-ink);
127
+ --vit-figure-name-size: var(--text-base);
128
+ --vit-figure-role-size: var(--text-sm);
129
+ --vit-figure-bio-width: 14rem;
118
130
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vit-foundation/ui",
3
- "version": "0.32.1",
3
+ "version": "0.33.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",