@frockbot/client-ui 0.3.10 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/client-ui",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -12,7 +12,7 @@
12
12
  "typecheck": "vue-tsc --noEmit -p tsconfig.json"
13
13
  },
14
14
  "dependencies": {
15
- "@frockbot/client-core": "0.3.10",
15
+ "@frockbot/client-core": "0.3.11",
16
16
  "markdown-it": "15.0.1",
17
17
  "vue": "3.5.41"
18
18
  },
@@ -0,0 +1,156 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * A thin stroke drawn around an avatar while that Bot is working.
4
+ *
5
+ * It says two things without words: that something is still happening — it
6
+ * breathes — and that steps are going by, because the stroke advances as the
7
+ * caller's `progress` does. It never names what those steps were; the point of
8
+ * the ring is that the transcript stays a conversation.
9
+ *
10
+ * The element positions itself over its parent, which must be `position:
11
+ * relative`, and sits behind pointer events. `laps` draws a faint completed
12
+ * ring behind the live one so a long Turn reads as further along without the
13
+ * ring growing.
14
+ */
15
+ const props = withDefaults(
16
+ defineProps<{
17
+ /** Fraction of the ring the live stroke draws, `0 … 1`. */
18
+ progress?: number;
19
+ /** Whether the ring breathes. A settled ring is still, and full. */
20
+ running?: boolean;
21
+ /** Faint rings behind the live stroke. Bounded by the caller. */
22
+ laps?: number;
23
+ /** What a screen reader is told this ring means. */
24
+ label?: string;
25
+ }>(),
26
+ { progress: 0, running: true, laps: 0, label: "Working" },
27
+ );
28
+
29
+ /*
30
+ * A 100-unit viewBox makes the geometry readable: the circumference below is
31
+ * the only number the stroke maths needs, and `stroke-dasharray` splits it
32
+ * into the drawn arc and the gap.
33
+ */
34
+ const RADIUS = 46;
35
+ const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
36
+
37
+ const arc = () => {
38
+ const fraction = Math.min(1, Math.max(0, props.progress));
39
+ // A ring with nothing drawn on it is a ring nobody sees, and the first
40
+ // moment of a Turn — before a single step has settled — is exactly when
41
+ // somebody needs to see one. The minimum arc is a fifth of the circle: big
42
+ // enough to read as a moving head, small enough that the first real tick is
43
+ // still an advance.
44
+ const drawn = Math.max(0.2, fraction) * CIRCUMFERENCE;
45
+ return `${drawn} ${CIRCUMFERENCE - drawn}`;
46
+ };
47
+ </script>
48
+
49
+ <template>
50
+ <span
51
+ class="ui-activity-ring"
52
+ :class="{ 'ui-activity-ring--running': running }"
53
+ role="status"
54
+ :aria-label="label"
55
+ >
56
+ <svg viewBox="0 0 100 100" aria-hidden="true" focusable="false">
57
+ <circle
58
+ v-if="laps > 0"
59
+ class="ui-activity-ring__lap"
60
+ cx="50"
61
+ cy="50"
62
+ :r="RADIUS"
63
+ />
64
+ <circle class="ui-activity-ring__track" cx="50" cy="50" :r="RADIUS" />
65
+ <circle
66
+ class="ui-activity-ring__arc"
67
+ cx="50"
68
+ cy="50"
69
+ :r="RADIUS"
70
+ :stroke-dasharray="arc()"
71
+ />
72
+ </svg>
73
+ </span>
74
+ </template>
75
+
76
+ <style scoped>
77
+ .ui-activity-ring {
78
+ position: absolute;
79
+
80
+ /*
81
+ * Clear of the avatar rather than on top of it: an avatar is usually a
82
+ * rounded square, and a circle inscribed in its own box would run under the
83
+ * art at the sides and vanish. `z-index` keeps it above a positioned avatar,
84
+ * which is what the sidebar rows use.
85
+ */
86
+ z-index: 1;
87
+ inset: -7px;
88
+ pointer-events: none;
89
+ }
90
+
91
+ .ui-activity-ring svg {
92
+ width: 100%;
93
+ height: 100%;
94
+ /* Twelve o'clock, so the stroke advances the way a clock hand does. */
95
+ transform: rotate(-90deg);
96
+ overflow: visible;
97
+ }
98
+
99
+ /*
100
+ * The viewBox is 100 units across and the element is roughly 36px, so 6 units
101
+ * of stroke is the ~2px hairline this is meant to be at the size an avatar is
102
+ * actually drawn.
103
+ */
104
+ .ui-activity-ring circle {
105
+ fill: none;
106
+ stroke-width: 6;
107
+ stroke-linecap: round;
108
+ }
109
+
110
+ /* The unfilled part of the ring. Present enough to read as a circle. */
111
+ .ui-activity-ring__track {
112
+ stroke: var(--frock-border-strong);
113
+ opacity: 0.75;
114
+ }
115
+
116
+ .ui-activity-ring__lap {
117
+ stroke: var(--frock-action-primary);
118
+ opacity: 0.28;
119
+ }
120
+
121
+ .ui-activity-ring__arc {
122
+ stroke: var(--frock-action-primary);
123
+ /*
124
+ * The tick itself: a step settles, `progress` changes, and the dash grows
125
+ * into its new length rather than jumping there.
126
+ */
127
+ transition: stroke-dasharray 420ms ease-out;
128
+ }
129
+
130
+ .ui-activity-ring--running .ui-activity-ring__arc {
131
+ animation: frock-ring-pulse 2200ms ease-in-out infinite;
132
+ }
133
+
134
+ @keyframes frock-ring-pulse {
135
+ 0%,
136
+ 100% {
137
+ opacity: 0.7;
138
+ }
139
+
140
+ 50% {
141
+ opacity: 1;
142
+ }
143
+ }
144
+
145
+ /*
146
+ * Reduced motion keeps the information and drops the movement: the ring still
147
+ * ticks forward for every step, it simply does not breathe or animate there.
148
+ */
149
+ @media (prefers-reduced-motion: reduce) {
150
+ .ui-activity-ring__arc {
151
+ transition: none;
152
+ animation: none;
153
+ opacity: 1;
154
+ }
155
+ }
156
+ </style>
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export {
6
6
  UI_ANCHOR_HIGHLIGHT_MS,
7
7
  type UiAnchorEvent,
8
8
  } from "./anchors.js";
9
+ export { default as UiActivityRing } from "./UiActivityRing.vue";
9
10
  export { default as UiButton } from "./UiButton.vue";
10
11
  export { default as UiField } from "./UiField.vue";
11
12
  export { default as UiIcon } from "./UiIcon.vue";