@viamrobotics/test-widgets 0.10.2 → 0.11.1

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 (31) hide show
  1. package/dist/components/audio-properties.svelte +3 -3
  2. package/dist/components/connection-status.svelte +1 -3
  3. package/dist/components/error.svelte +15 -10
  4. package/dist/components/readings-list.svelte +30 -30
  5. package/dist/components/refetch-controller.svelte +2 -2
  6. package/dist/components/widgets/audio-input/audio-input.svelte +85 -83
  7. package/dist/components/widgets/audio-output/audio-output.svelte +93 -91
  8. package/dist/components/widgets/base/base.svelte +39 -37
  9. package/dist/components/widgets/base/move-straight.svelte +1 -1
  10. package/dist/components/widgets/base/set-power.svelte +1 -1
  11. package/dist/components/widgets/base/set-velocity.svelte +1 -1
  12. package/dist/components/widgets/base/spin.svelte +1 -1
  13. package/dist/components/widgets/board/pin-selector.svelte +1 -1
  14. package/dist/components/widgets/board/read-write-pins.svelte +1 -1
  15. package/dist/components/widgets/camera/camera.svelte +6 -5
  16. package/dist/components/widgets/camera/live-or-polling-video.svelte +2 -2
  17. package/dist/components/widgets/discovery/resources-list.svelte +35 -33
  18. package/dist/components/widgets/do-command/do-command.svelte +44 -41
  19. package/dist/components/widgets/gantry/gantry.svelte +102 -98
  20. package/dist/components/widgets/gantry/move-to-position.svelte +1 -1
  21. package/dist/components/widgets/gripper/gripper.svelte +41 -39
  22. package/dist/components/widgets/motor/go-for.svelte +1 -1
  23. package/dist/components/widgets/motor/go-to.svelte +1 -1
  24. package/dist/components/widgets/motor/motor.svelte +88 -86
  25. package/dist/components/widgets/pcd/inputs.svelte +1 -1
  26. package/dist/components/widgets/pcd/pcd-widget.svelte +19 -17
  27. package/dist/components/widgets/servo/servo.svelte +69 -64
  28. package/dist/components/widgets/vision-service/image.svelte +10 -3
  29. package/dist/components/widgets/vision-service/object-point-clouds.svelte +1 -1
  30. package/dist/components/widgets/vision-service/vision-service.svelte +5 -5
  31. package/package.json +16 -6
@@ -10,17 +10,17 @@
10
10
 
11
11
  <dl class="font-roboto-mono flex flex-col gap-y-2 text-sm">
12
12
  <div class="flex flex-col gap-0.5">
13
- <dt class="text-default w-50 min-w-50 pr-2 font-medium">Supported Codecs</dt>
13
+ <dt class="text-default pr-2 font-medium">Supported Codecs</dt>
14
14
  <dd class="text-subtle-1">
15
15
  {supportedCodecs.length > 0 ? supportedCodecs.join(', ') : 'None'}
16
16
  </dd>
17
17
  </div>
18
18
  <div class="flex flex-col gap-0.5">
19
- <dt class="text-default w-50 min-w-50 pr-2 font-medium">Sample Rate</dt>
19
+ <dt class="text-default pr-2 font-medium">Sample Rate</dt>
20
20
  <dd class="text-subtle-1">{sampleRateHz} Hz</dd>
21
21
  </div>
22
22
  <div class="flex flex-col gap-0.5">
23
- <dt class="text-default w-50 min-w-50 pr-2 font-medium">Channels</dt>
23
+ <dt class="text-default pr-2 font-medium">Channels</dt>
24
24
  <dd class="text-subtle-1">{numChannels}</dd>
25
25
  </div>
26
26
  </dl>
@@ -78,9 +78,7 @@
78
78
  }}>{isErrorExpanded ? 'Hide' : 'Show'} error</button
79
79
  >
80
80
  {#if isErrorExpanded}
81
- <pre class="font-mono text-xs text-wrap">
82
- {error}
83
- </pre>
81
+ <pre class="font-mono text-xs text-wrap">{error}</pre>
84
82
  {/if}
85
83
  </div>
86
84
  {/snippet}
@@ -39,18 +39,23 @@
39
39
  <div class="flex items-center justify-between gap-1">
40
40
  <p
41
41
  {id}
42
- class={twMerge('font-roboto-mono text-danger-dark text-xs', className)}
42
+ class={twMerge(
43
+ 'font-roboto-mono text-danger-dark min-w-0 overflow-auto text-xs wrap-break-word',
44
+ className
45
+ )}
43
46
  >
44
47
  {errorText}
45
48
  </p>
46
- <Tooltip let:tooltipID>
47
- <IconButton
48
- aria-describedby={tooltipID}
49
- icon={showCopySuccess ? 'check' : 'content-copy'}
50
- label="Copy error"
51
- on:click={copyErrorToClipboard}
52
- />
53
- <div slot="description">Copy error</div>
54
- </Tooltip>
49
+ <div class="shrink-0">
50
+ <Tooltip let:tooltipID>
51
+ <IconButton
52
+ aria-describedby={tooltipID}
53
+ icon={showCopySuccess ? 'check' : 'content-copy'}
54
+ label="Copy error"
55
+ on:click={copyErrorToClipboard}
56
+ />
57
+ <div slot="description">Copy error</div>
58
+ </Tooltip>
59
+ </div>
55
60
  </div>
56
61
  {/if}
@@ -39,35 +39,35 @@
39
39
  <div slot="description">Copy JSON</div>
40
40
  </Tooltip>
41
41
  </div>
42
- <dl class="font-roboto-mono flex flex-col gap-y-2">
43
- {#each sortedData as [key, value] (key)}
44
- {@const valueString = JSON.stringify(value, null, 2)}
45
- <div class="flex flex-row">
46
- <dt
47
- class="text-default w-50 min-w-50 truncate pr-2 font-medium"
48
- title={key}
49
- >
50
- {key}
51
- </dt>
52
- <dd class="text-subtle-1 min-w-50">
53
- {#if typeof value === 'object'}
54
- <div class="border-light bg-extralight min-w-50 border px-3 py-2">
55
- <pre class="whitespace-pre-wrap">
56
- <code>{valueString}</code>
57
- </pre>
58
- </div>
59
- {:else if typeof value === 'string' && value.startsWith('data:image/')}
60
- <img
61
- class="max-h-80"
62
- src={value}
63
- alt={key}
64
- />
65
- {:else}
66
- {truncate(valueString, { length: 300 })}
67
- {/if}
68
- </dd>
69
- </div>
70
- {/each}
71
- </dl>
42
+ <div class="@container">
43
+ <dl class="font-roboto-mono flex flex-col gap-y-2">
44
+ {#each sortedData as [key, value] (key)}
45
+ {@const valueString = JSON.stringify(value, null, 2)}
46
+ <div class="flex flex-col gap-y-1 @lg:flex-row @lg:gap-y-0">
47
+ <dt
48
+ class="text-default truncate pr-2 font-medium @lg:w-50 @lg:min-w-50"
49
+ title={key}
50
+ >
51
+ {key}
52
+ </dt>
53
+ <dd class="text-subtle-1 min-w-0 grow wrap-break-word">
54
+ {#if value !== null && typeof value === 'object'}
55
+ <div class="border-light bg-extralight border px-3 py-2">
56
+ <pre class="whitespace-pre-wrap"><code>{valueString}</code></pre>
57
+ </div>
58
+ {:else if typeof value === 'string' && value.startsWith('data:image/')}
59
+ <img
60
+ class="max-h-80 max-w-full"
61
+ src={value}
62
+ alt={key}
63
+ />
64
+ {:else}
65
+ {truncate(valueString, { length: 300 })}
66
+ {/if}
67
+ </dd>
68
+ </div>
69
+ {/each}
70
+ </dl>
71
+ </div>
72
72
  </div>
73
73
  {/if}
@@ -91,8 +91,8 @@
91
91
  })
92
92
  </script>
93
93
 
94
- <div class="text-default flex flex-row gap-2 text-xs">
95
- <div class="w-50">
94
+ <div class="text-default flex flex-wrap gap-2 text-xs">
95
+ <div class="w-50 max-w-full">
96
96
  <Select on:change={onChange}>
97
97
  {#each refetchOptions as option (option)}
98
98
  <option selected={selectedOption === option}>{option}</option>
@@ -58,97 +58,99 @@
58
58
  />
59
59
  </div>
60
60
 
61
- <div class="flex flex-row divide-x">
62
- <div class="flex w-full flex-col divide-y">
63
- <MutationSection
64
- title="GetAudio"
65
- api="rdk:component:audio_input"
66
- description="Capture audio from the device"
67
- lastError={capture.error}
68
- >
69
- <div class="flex flex-col gap-2">
70
- <Label cx="gap-1 text-xs">
71
- Codec
72
- <Select
73
- slot="input"
74
- value={selectedCodec}
75
- on:change={(e) => {
76
- captureCodec = (e.target as HTMLSelectElement).value
77
- }}
78
- >
79
- {#each availableCodecs as codec (codec)}
80
- <option value={codec}>{codec}</option>
81
- {/each}
82
- </Select>
83
- </Label>
84
- <Label cx="gap-1 text-xs">
85
- Duration (seconds, 0 = stream until stopped)
86
- <NumericInput
87
- slot="input"
88
- value={captureDuration}
89
- on:change={(e) => {
90
- captureDuration = numberValueFromEvent(e) ?? 3
91
- }}
92
- />
93
- </Label>
94
- </div>
61
+ <div class="@container">
62
+ <div class="flex flex-col divide-y @2xl:flex-row @2xl:divide-x @2xl:divide-y-0">
63
+ <div class="flex w-full flex-col divide-y">
64
+ <MutationSection
65
+ title="GetAudio"
66
+ api="rdk:component:audio_input"
67
+ description="Capture audio from the device"
68
+ lastError={capture.error}
69
+ >
70
+ <div class="flex flex-col gap-2">
71
+ <Label cx="gap-1 text-xs">
72
+ Codec
73
+ <Select
74
+ slot="input"
75
+ value={selectedCodec}
76
+ on:change={(e) => {
77
+ captureCodec = (e.target as HTMLSelectElement).value
78
+ }}
79
+ >
80
+ {#each availableCodecs as codec (codec)}
81
+ <option value={codec}>{codec}</option>
82
+ {/each}
83
+ </Select>
84
+ </Label>
85
+ <Label cx="gap-1 text-xs">
86
+ Duration (seconds, 0 = stream until stopped)
87
+ <NumericInput
88
+ slot="input"
89
+ value={captureDuration}
90
+ on:change={(e) => {
91
+ captureDuration = numberValueFromEvent(e) ?? 3
92
+ }}
93
+ />
94
+ </Label>
95
+ </div>
95
96
 
96
- <div class="mt-auto flex flex-col items-start gap-2">
97
- {#if capture.status === 'recording'}
98
- <p class="font-roboto-mono text-subtle-1 text-xs">
99
- {(capture.totalBytes / 1024).toFixed(1)} kB captured
100
- </p>
101
- <Button
102
- icon="stop-circle-outline"
103
- onclick={capture.stop}
104
- >
105
- Stop
106
- </Button>
107
- {:else}
108
- {#if capture.downloadUrl}
97
+ <div class="mt-auto flex flex-col items-start gap-2">
98
+ {#if capture.status === 'recording'}
109
99
  <p class="font-roboto-mono text-subtle-1 text-xs">
110
100
  {(capture.totalBytes / 1024).toFixed(1)} kB captured
111
101
  </p>
112
- <a
113
- href={capture.downloadUrl}
114
- download="audio-capture.{selectedCodec}"
115
- rel="external"
102
+ <Button
103
+ icon="stop-circle-outline"
104
+ onclick={capture.stop}
105
+ >
106
+ Stop
107
+ </Button>
108
+ {:else}
109
+ {#if capture.downloadUrl}
110
+ <p class="font-roboto-mono text-subtle-1 text-xs">
111
+ {(capture.totalBytes / 1024).toFixed(1)} kB captured
112
+ </p>
113
+ <a
114
+ href={capture.downloadUrl}
115
+ download="audio-capture.{selectedCodec}"
116
+ rel="external"
117
+ >
118
+ <Button icon="download">Download</Button>
119
+ </a>
120
+ {/if}
121
+ <Button
122
+ icon="play-circle-outline"
123
+ onclick={() => capture.start(selectedCodec, captureDuration)}
124
+ disabled={!client.current}
116
125
  >
117
- <Button icon="download">Download</Button>
118
- </a>
126
+ {capture.status === 'done' ? 'Capture Again' : 'Start Capture'}
127
+ </Button>
119
128
  {/if}
120
- <Button
121
- icon="play-circle-outline"
122
- onclick={() => capture.start(selectedCodec, captureDuration)}
123
- disabled={!client.current}
124
- >
125
- {capture.status === 'done' ? 'Capture Again' : 'Start Capture'}
126
- </Button>
127
- {/if}
128
- </div>
129
- </MutationSection>
130
- </div>
129
+ </div>
130
+ </MutationSection>
131
+ </div>
131
132
 
132
- <div class="ml-auto flex w-full max-w-1/2 flex-col divide-y sm:max-w-1/3">
133
- <ApiSection
134
- title="GetProperties"
135
- api="rdk:component:audio_input"
136
- description="Audio input properties"
137
- class="relative"
138
- >
139
- <Query
140
- query={propertiesQuery}
141
- contentCx="h-6"
133
+ <div class="flex w-full flex-col divide-y @2xl:ml-auto @2xl:max-w-1/2 @4xl:max-w-1/3">
134
+ <ApiSection
135
+ title="GetProperties"
136
+ api="rdk:component:audio_input"
137
+ description="Audio input properties"
138
+ class="relative"
142
139
  >
143
- {#if propertiesQuery.data !== undefined}
144
- <Properties
145
- supportedCodecs={propertiesQuery.data.supportedCodecs}
146
- sampleRateHz={propertiesQuery.data.sampleRateHz}
147
- numChannels={propertiesQuery.data.numChannels}
148
- />
149
- {/if}
150
- </Query>
151
- </ApiSection>
140
+ <Query
141
+ query={propertiesQuery}
142
+ contentCx="h-6"
143
+ >
144
+ {#if propertiesQuery.data !== undefined}
145
+ <Properties
146
+ supportedCodecs={propertiesQuery.data.supportedCodecs}
147
+ sampleRateHz={propertiesQuery.data.sampleRateHz}
148
+ numChannels={propertiesQuery.data.numChannels}
149
+ />
150
+ {/if}
151
+ </Query>
152
+ </ApiSection>
153
+ </div>
152
154
  </div>
153
155
  </div>
154
156
  {/snippet}
@@ -103,99 +103,101 @@
103
103
  />
104
104
  </div>
105
105
 
106
- <div class="flex flex-row divide-x">
107
- <div class="flex w-full flex-col divide-y">
108
- <MutationSection
109
- title="Play"
110
- api="rdk:component:audio_output"
111
- description="Send audio data to the device"
112
- lastError={playContext.error}
113
- >
114
- <div class="flex flex-col gap-2">
115
- <Label cx="gap-1 text-xs">
116
- Audio file
117
- <input
118
- slot="input"
119
- type="file"
120
- accept={availableCodecs.map((c) => `.${c}`).join(',')}
121
- class="text-xs"
122
- onchange={handleFileChange}
123
- />
124
- </Label>
125
- {#if fileInputError}
126
- <p class="text-xs text-red-500">{fileInputError}</p>
127
- {/if}
128
- {#if selectedFile}
129
- <p class="font-roboto-mono text-subtle-1 text-xs">{selectedFile.name}</p>
130
- {/if}
131
- <Label cx="gap-1 text-xs">
132
- Codec
133
- <Select
134
- slot="input"
135
- value={selectedCodec}
136
- on:change={(e) => {
137
- playCodec = (e.target as HTMLSelectElement).value
138
- }}
106
+ <div class="@container">
107
+ <div class="flex flex-col divide-y @2xl:flex-row @2xl:divide-x @2xl:divide-y-0">
108
+ <div class="flex w-full flex-col divide-y">
109
+ <MutationSection
110
+ title="Play"
111
+ api="rdk:component:audio_output"
112
+ description="Send audio data to the device"
113
+ lastError={playContext.error}
114
+ >
115
+ <div class="flex flex-col gap-2">
116
+ <Label cx="gap-1 text-xs">
117
+ Audio file
118
+ <input
119
+ slot="input"
120
+ type="file"
121
+ accept={availableCodecs.map((c) => `.${c}`).join(',')}
122
+ class="text-xs"
123
+ onchange={handleFileChange}
124
+ />
125
+ </Label>
126
+ {#if fileInputError}
127
+ <p class="text-xs text-red-500">{fileInputError}</p>
128
+ {/if}
129
+ {#if selectedFile}
130
+ <p class="font-roboto-mono text-subtle-1 text-xs">{selectedFile.name}</p>
131
+ {/if}
132
+ <Label cx="gap-1 text-xs">
133
+ Codec
134
+ <Select
135
+ slot="input"
136
+ value={selectedCodec}
137
+ on:change={(e) => {
138
+ playCodec = (e.target as HTMLSelectElement).value
139
+ }}
140
+ >
141
+ {#each availableCodecs as codec (codec)}
142
+ <option value={codec}>{codec}</option>
143
+ {/each}
144
+ </Select>
145
+ </Label>
146
+ <Label cx="gap-1 text-xs">
147
+ Sample rate (Hz)
148
+ <NumericInput
149
+ slot="input"
150
+ value={selectedSampleRateHz}
151
+ on:change={(e) => {
152
+ playSampleRateHz = numberValueFromEvent(e) ?? null
153
+ }}
154
+ />
155
+ </Label>
156
+ <Label cx="gap-1 text-xs">
157
+ Channels
158
+ <NumericInput
159
+ slot="input"
160
+ value={selectedNumChannels}
161
+ on:change={(e) => {
162
+ playNumChannels = numberValueFromEvent(e) ?? null
163
+ }}
164
+ />
165
+ </Label>
166
+ </div>
167
+
168
+ <div class="mt-auto">
169
+ <Button
170
+ icon="play-circle-outline"
171
+ onclick={play}
172
+ disabled={!client.current || playContext.status === 'playing'}
139
173
  >
140
- {#each availableCodecs as codec (codec)}
141
- <option value={codec}>{codec}</option>
142
- {/each}
143
- </Select>
144
- </Label>
145
- <Label cx="gap-1 text-xs">
146
- Sample rate (Hz)
147
- <NumericInput
148
- slot="input"
149
- value={selectedSampleRateHz}
150
- on:change={(e) => {
151
- playSampleRateHz = numberValueFromEvent(e) ?? null
152
- }}
153
- />
154
- </Label>
155
- <Label cx="gap-1 text-xs">
156
- Channels
157
- <NumericInput
158
- slot="input"
159
- value={selectedNumChannels}
160
- on:change={(e) => {
161
- playNumChannels = numberValueFromEvent(e) ?? null
162
- }}
163
- />
164
- </Label>
165
- </div>
166
-
167
- <div class="mt-auto">
168
- <Button
169
- icon="play-circle-outline"
170
- onclick={play}
171
- disabled={!client.current || playContext.status === 'playing'}
172
- >
173
- {playButtonLabel}
174
- </Button>
175
- </div>
176
- </MutationSection>
177
- </div>
178
-
179
- <div class="ml-auto flex w-full max-w-1/2 flex-col divide-y sm:max-w-1/3">
180
- <ApiSection
181
- title="GetProperties"
182
- api="rdk:component:audio_output"
183
- description="Audio output properties"
184
- class="relative"
185
- >
186
- <Query
187
- query={propertiesQuery}
188
- contentCx="h-6"
174
+ {playButtonLabel}
175
+ </Button>
176
+ </div>
177
+ </MutationSection>
178
+ </div>
179
+
180
+ <div class="flex w-full flex-col divide-y @2xl:ml-auto @2xl:max-w-1/2 @4xl:max-w-1/3">
181
+ <ApiSection
182
+ title="GetProperties"
183
+ api="rdk:component:audio_output"
184
+ description="Audio output properties"
185
+ class="relative"
189
186
  >
190
- {#if propertiesQuery.data !== undefined}
191
- <Properties
192
- supportedCodecs={propertiesQuery.data.supportedCodecs}
193
- sampleRateHz={propertiesQuery.data.sampleRateHz}
194
- numChannels={propertiesQuery.data.numChannels}
195
- />
196
- {/if}
197
- </Query>
198
- </ApiSection>
187
+ <Query
188
+ query={propertiesQuery}
189
+ contentCx="h-6"
190
+ >
191
+ {#if propertiesQuery.data !== undefined}
192
+ <Properties
193
+ supportedCodecs={propertiesQuery.data.supportedCodecs}
194
+ sampleRateHz={propertiesQuery.data.sampleRateHz}
195
+ numChannels={propertiesQuery.data.numChannels}
196
+ />
197
+ {/if}
198
+ </Query>
199
+ </ApiSection>
200
+ </div>
199
201
  </div>
200
202
  </div>
201
203
  {/snippet}
@@ -60,46 +60,48 @@
60
60
 
61
61
  <ConnectionStatus {partID}>
62
62
  {#snippet connected()}
63
- <div class="flex flex-row divide-x">
64
- <MutationSection
65
- title="Quick move"
66
- lastError={quickSetPowerMutation.error}
67
- >
68
- {#snippet titleInput()}
69
- <Label>
70
- Keyboard control
63
+ <div class="@container">
64
+ <div class="flex flex-col divide-y @4xl:flex-row @4xl:divide-x @4xl:divide-y-0">
65
+ <MutationSection
66
+ title="Quick move"
67
+ lastError={quickSetPowerMutation.error}
68
+ >
69
+ {#snippet titleInput()}
70
+ <Label>
71
+ Keyboard control
71
72
 
72
- <Switch
73
- slot="input"
74
- on={quickMoveKeyboardControl}
75
- on:change={() => (quickMoveKeyboardControl = !quickMoveKeyboardControl)}
76
- />
77
- </Label>
78
- {/snippet}
73
+ <Switch
74
+ slot="input"
75
+ on={quickMoveKeyboardControl}
76
+ on:change={() => (quickMoveKeyboardControl = !quickMoveKeyboardControl)}
77
+ />
78
+ </Label>
79
+ {/snippet}
79
80
 
80
- <QuickMove
81
- isKeyboardEnabled={quickMoveKeyboardControl}
82
- setPower={quickSetPower}
83
- />
84
- </MutationSection>
85
- <div class="ml-auto flex w-full max-w-40 grow flex-col divide-y">
86
- <ApiSection
87
- title="Stop"
88
- api="rdk:component:base"
89
- >
90
- <StopButton
91
- error={stopMutation.error}
92
- onStop={() => {
93
- stopMutation.mutate([])
94
- }}
81
+ <QuickMove
82
+ isKeyboardEnabled={quickMoveKeyboardControl}
83
+ setPower={quickSetPower}
84
+ />
85
+ </MutationSection>
86
+ <div class="flex grow flex-col divide-y @4xl:ml-auto @4xl:w-full @4xl:max-w-40">
87
+ <ApiSection
88
+ title="Stop"
89
+ api="rdk:component:base"
90
+ >
91
+ <StopButton
92
+ error={stopMutation.error}
93
+ onStop={() => {
94
+ stopMutation.mutate([])
95
+ }}
96
+ />
97
+ </ApiSection>
98
+ <IsMoving
99
+ client={BaseClient}
100
+ api="rdk:component:base"
101
+ {partID}
102
+ {resourceName}
95
103
  />
96
- </ApiSection>
97
- <IsMoving
98
- client={BaseClient}
99
- api="rdk:component:base"
100
- {partID}
101
- {resourceName}
102
- />
104
+ </div>
103
105
  </div>
104
106
  </div>
105
107
  <MutationSection
@@ -13,7 +13,7 @@
13
13
  let mmPerSec = $state(100)
14
14
  </script>
15
15
 
16
- <div class="flex flex-row gap-4">
16
+ <div class="flex flex-wrap gap-4">
17
17
  <Label cx="max-w-[96px] gap-1 text-xs whitespace-nowrap">
18
18
  <span class="flex gap-1">
19
19
  Distance
@@ -15,7 +15,7 @@
15
15
  let angularPower = $state<Vector3>({ x: 0, y: 0, z: 0.75 })
16
16
  </script>
17
17
 
18
- <div class="flex flex-row gap-4">
18
+ <div class="flex flex-wrap gap-4">
19
19
  <Vec3Builder
20
20
  title="Linear power"
21
21
  titleTooltip="The percentage of max power of the base’s linear propulsion. In the range of –1.0 to 1.0, with 1.0 meaning 100% power."
@@ -15,7 +15,7 @@
15
15
  let angularVelocity = $state<Vector3>({ x: 0, y: 0, z: 15 })
16
16
  </script>
17
17
 
18
- <div class="flex flex-row gap-4">
18
+ <div class="flex flex-wrap gap-4">
19
19
  <Vec3Builder
20
20
  title="Linear velocity"
21
21
  titleUnits="(mm/s)"
@@ -13,7 +13,7 @@
13
13
  let degsPerSec = $state(45)
14
14
  </script>
15
15
 
16
- <div class="flex flex-row gap-4">
16
+ <div class="flex flex-wrap gap-4">
17
17
  <Label cx="max-w-[96px] gap-1 text-xs">
18
18
  <span class="flex gap-1">
19
19
  Angle