@adea-ai/ui 0.22.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +6 -3
  2. package/dist/components/account-drawer.d.ts +3 -3
  3. package/dist/components/account-drawer.js +119 -19
  4. package/dist/components/on-screen-controls.d.ts +2 -1
  5. package/dist/components/on-screen-controls.js +158 -46
  6. package/dist/components/scene-settings.d.ts +3 -3
  7. package/dist/components/scene-settings.js +247 -28
  8. package/dist/components/theme-provider.d.ts +27 -5
  9. package/dist/components/theme-provider.js +96 -7
  10. package/dist/components/theme-toggle.d.ts +4 -4
  11. package/dist/components/theme-toggle.js +62 -18
  12. package/dist/components/ui/badge.d.ts +4 -3
  13. package/dist/components/ui/badge.js +31 -30
  14. package/dist/components/ui/button.d.ts +4 -3
  15. package/dist/components/ui/button.js +49 -32
  16. package/dist/components/ui/card.d.ts +3 -3
  17. package/dist/components/ui/card.js +25 -6
  18. package/dist/components/ui/dialog.d.ts +13 -13
  19. package/dist/components/ui/dialog.js +94 -26
  20. package/dist/components/ui/drawer.d.ts +23 -15
  21. package/dist/components/ui/drawer.js +109 -64
  22. package/dist/components/ui/dropdown-menu.d.ts +28 -22
  23. package/dist/components/ui/dropdown-menu.js +165 -36
  24. package/dist/components/ui/empty.d.ts +7 -6
  25. package/dist/components/ui/empty.js +78 -26
  26. package/dist/components/ui/field.d.ts +15 -14
  27. package/dist/components/ui/field.js +177 -55
  28. package/dist/components/ui/input.d.ts +2 -2
  29. package/dist/components/ui/input.js +20 -5
  30. package/dist/components/ui/label.d.ts +2 -2
  31. package/dist/components/ui/label.js +15 -5
  32. package/dist/components/ui/radio-group.d.ts +4 -4
  33. package/dist/components/ui/radio-group.js +36 -9
  34. package/dist/components/ui/separator.d.ts +6 -2
  35. package/dist/components/ui/separator.js +18 -6
  36. package/dist/components/ui/skeleton.d.ts +2 -1
  37. package/dist/components/ui/skeleton.js +15 -4
  38. package/dist/components/ui/spinner.d.ts +4 -3
  39. package/dist/components/ui/spinner.js +15 -5
  40. package/dist/components/ui/switch.d.ts +6 -3
  41. package/dist/components/ui/switch.js +57 -6
  42. package/dist/components/ui/tabs.d.ts +6 -5
  43. package/dist/components/ui/tabs.js +52 -24
  44. package/dist/components/ui/toggle-group.d.ts +10 -8
  45. package/dist/components/ui/toggle-group.js +91 -20
  46. package/dist/components/ui/toggle.d.ts +4 -2
  47. package/dist/components/ui/toggle.js +38 -23
  48. package/dist/components/ui/tooltip.d.ts +13 -6
  49. package/dist/components/ui/tooltip.js +51 -14
  50. package/dist/components/version-dialog.d.ts +2 -2
  51. package/dist/components/version-dialog.js +322 -129
  52. package/dist/components/workspace-brand.d.ts +1 -1
  53. package/dist/components/workspace-brand.js +12 -3
  54. package/dist/components/workspace-logo.d.ts +2 -2
  55. package/dist/components/workspace-logo.js +15 -3
  56. package/dist/index.d.ts +1 -1
  57. package/dist/index.js +20 -19
  58. package/dist/lib/utils.js +7 -4
  59. package/dist/lib/version-notes.js +14 -29
  60. package/package.json +18 -16
  61. package/src/components/account-drawer.tsx +48 -67
  62. package/src/components/on-screen-controls.tsx +55 -62
  63. package/src/components/scene-settings.tsx +97 -100
  64. package/src/components/theme-provider.tsx +122 -22
  65. package/src/components/theme-toggle.tsx +29 -54
  66. package/src/components/ui/badge.tsx +14 -23
  67. package/src/components/ui/button.tsx +9 -11
  68. package/src/components/ui/card.tsx +9 -6
  69. package/src/components/ui/dialog.tsx +58 -55
  70. package/src/components/ui/drawer.tsx +84 -125
  71. package/src/components/ui/dropdown-menu.tsx +130 -153
  72. package/src/components/ui/empty.tsx +29 -26
  73. package/src/components/ui/field.tsx +83 -87
  74. package/src/components/ui/input.tsx +8 -8
  75. package/src/components/ui/label.tsx +6 -7
  76. package/src/components/ui/radio-group.tsx +22 -19
  77. package/src/components/ui/separator.tsx +16 -9
  78. package/src/components/ui/skeleton.tsx +6 -3
  79. package/src/components/ui/spinner.tsx +9 -5
  80. package/src/components/ui/switch.tsx +47 -20
  81. package/src/components/ui/tabs.tsx +31 -30
  82. package/src/components/ui/toggle-group.tsx +65 -54
  83. package/src/components/ui/toggle.tsx +9 -12
  84. package/src/components/ui/tooltip.tsx +41 -36
  85. package/src/components/version-dialog.tsx +144 -144
  86. package/src/components/workspace-brand.tsx +5 -5
  87. package/src/components/workspace-logo.tsx +9 -8
  88. package/src/index.tsx +7 -1
  89. package/src/styles/base.css +637 -0
  90. package/src/styles/globals.css +1 -1
  91. package/tsconfig.json +2 -1
  92. package/components.json +0 -19
@@ -1,31 +1,16 @@
1
- export function plainTextFromMarkdown(markdown) {
2
- return markdown
3
- .replace(/\r\n/g, '\n')
4
- .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
5
- .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
6
- .replace(/^[ \t]*#{1,6}[ \t]*/gm, '')
7
- .replace(/^[ \t]*>[ \t]?/gm, '')
8
- .replace(/^[ \t]*---+[ \t]*$/gm, '')
9
- .replace(/^[ \t]*[-*+][ \t]+/gm, '• ')
10
- .replace(/^[ \t]*\d+\.[ \t]+/gm, '')
11
- .replace(/```[^\n]*\n?/g, '')
12
- .replace(/`([^`]+)`/g, '$1')
13
- .replace(/\*\*([^*]+)\*\*/g, '$1')
14
- .replace(/__([^_]+)__/g, '$1')
15
- .replace(/\*([^*]+)\*/g, '$1')
16
- .replace(/_([^_]+)_/g, '$1')
17
- .replace(/\n{3,}/g, '\n\n')
18
- .trim();
1
+ //#region src/lib/version-notes.ts
2
+ function plainTextFromMarkdown(markdown) {
3
+ return markdown.replace(/\r\n/g, "\n").replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/^[ \t]*#{1,6}[ \t]*/gm, "").replace(/^[ \t]*>[ \t]?/gm, "").replace(/^[ \t]*---+[ \t]*$/gm, "").replace(/^[ \t]*[-*+][ \t]+/gm, "• ").replace(/^[ \t]*\d+\.[ \t]+/gm, "").replace(/```[^\n]*\n?/g, "").replace(/`([^`]+)`/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/__([^_]+)__/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/_([^_]+)_/g, "$1").replace(/\n{3,}/g, "\n\n").trim();
19
4
  }
20
- export function formatReleaseDate(value) {
21
- if (!value)
22
- return null;
23
- const date = new Date(value);
24
- if (Number.isNaN(date.getTime()))
25
- return value;
26
- return new Intl.DateTimeFormat(undefined, {
27
- day: 'numeric',
28
- month: 'short',
29
- year: 'numeric',
30
- }).format(date);
5
+ function formatReleaseDate(value) {
6
+ if (!value) return null;
7
+ const date = new Date(value);
8
+ if (Number.isNaN(date.getTime())) return value;
9
+ return new Intl.DateTimeFormat(void 0, {
10
+ day: "numeric",
11
+ month: "short",
12
+ year: "numeric"
13
+ }).format(date);
31
14
  }
15
+ //#endregion
16
+ export { formatReleaseDate, plainTextFromMarkdown };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.22.1",
4
- "description": "Shared UI kit for Adea: Base UI-backed shadcn primitives, workspace shells, and theme styles.",
3
+ "version": "0.23.0",
4
+ "description": "Shared UI kit for Adea: Kobalte/corvu-backed Solid primitives, workspace shells, and theme styles.",
5
5
  "keywords": [
6
- "base-ui",
7
6
  "components",
8
- "react",
9
- "shadcn",
7
+ "corvu",
8
+ "kobalte",
9
+ "solid",
10
10
  "tailwind",
11
11
  "workspace"
12
12
  ],
@@ -19,7 +19,6 @@
19
19
  "files": [
20
20
  "dist",
21
21
  "src",
22
- "components.json",
23
22
  "tsconfig.json",
24
23
  "README.md"
25
24
  ],
@@ -33,16 +32,19 @@
33
32
  "exports": {
34
33
  ".": {
35
34
  "types": "./dist/index.d.ts",
35
+ "solid": "./src/index.tsx",
36
36
  "development": "./src/index.tsx",
37
37
  "import": "./dist/index.js"
38
38
  },
39
39
  "./components/*": {
40
40
  "types": "./dist/components/*.d.ts",
41
+ "solid": "./src/components/*",
41
42
  "development": "./src/components/*",
42
43
  "import": "./dist/components/*.js"
43
44
  },
44
45
  "./lib/*": {
45
46
  "types": "./dist/lib/*.d.ts",
47
+ "solid": "./src/lib/*",
46
48
  "development": "./src/lib/*",
47
49
  "import": "./dist/lib/*.js"
48
50
  },
@@ -50,27 +52,27 @@
50
52
  "./auth-shell.css": "./src/styles/auth-shell.css",
51
53
  "./conventional-workspace.css": "./src/styles/conventional-workspace.css",
52
54
  "./theme.css": "./src/styles/theme.css",
53
- "./workspace-shell.css": "./src/styles/workspace-shell.css"
55
+ "./workspace-shell.css": "./src/styles/workspace-shell.css",
56
+ "./base.css": "./src/styles/base.css"
54
57
  },
55
58
  "scripts": {
56
59
  "lint": "oxlint",
57
- "build": "tsc -p tsconfig.json",
58
- "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
60
+ "build": "vite build && tsc -p tsconfig.json --emitDeclarationOnly",
61
+ "dev": "vite build --watch",
59
62
  "typecheck": "tsc -p tsconfig.json --noEmit"
60
63
  },
61
64
  "dependencies": {
62
- "@base-ui/react": "^1.7.0",
65
+ "@corvu/drawer": "^0.2.4",
66
+ "@kobalte/core": "^0.13.14",
63
67
  "class-variance-authority": "^0.7.1",
64
68
  "clsx": "^2.1.1",
65
- "lucide-react": "^1.33.0",
66
- "next-themes": "^0.4.6",
67
- "react": "19.3.0",
68
- "react-dom": "19.3.0",
69
+ "lucide-solid": "^1.45.0",
70
+ "solid-js": "^1.9.15",
69
71
  "tailwind-merge": "^3.6.0",
70
72
  "tw-animate-css": "^1.4.0"
71
73
  },
72
74
  "devDependencies": {
73
- "@types/react": "^19.2.18",
74
- "@types/react-dom": "^19.2.4"
75
+ "vite": "^8.0.14",
76
+ "vite-plugin-solid": "^2.11.14"
75
77
  }
76
78
  }
@@ -1,9 +1,8 @@
1
- 'use client'
1
+ import { UserRound } from 'lucide-solid'
2
+ import { createEffect, createSignal, Show, type JSX } from 'solid-js'
3
+ import { Portal } from 'solid-js/web'
2
4
 
3
- import { createPortal } from 'react-dom'
4
- import { useEffect, useState, type ReactNode } from 'react'
5
- import { UserRound } from 'lucide-react'
6
- import { Button } from '#components/ui/button'
5
+ import { buttonVariants } from '#components/ui/button'
7
6
  import {
8
7
  Drawer,
9
8
  DrawerClose,
@@ -19,111 +18,93 @@ export type AccountDrawerProps = {
19
18
  accountLabel?: string
20
19
  authenticated?: boolean
21
20
  busy?: boolean
22
- musicControl?: ReactNode
21
+ musicControl?: JSX.Element
23
22
  triggerTargetId?: string
24
23
  onSignIn?: () => void
25
24
  onSignOut?: () => void
26
25
  }
27
26
 
28
- export function AccountDrawer({
29
- accountLabel = 'Sign in',
30
- authenticated = false,
31
- busy = false,
32
- musicControl,
33
- triggerTargetId,
34
- onSignIn,
35
- onSignOut,
36
- }: AccountDrawerProps) {
37
- const [triggerTarget, setTriggerTarget] = useState<HTMLElement | null>(null)
27
+ export function AccountDrawer(props: AccountDrawerProps) {
28
+ const accountLabel = () => props.accountLabel ?? 'Sign in'
29
+ const [triggerTarget, setTriggerTarget] = createSignal<HTMLElement | null>(null)
38
30
 
39
- useEffect(() => {
40
- setTriggerTarget(triggerTargetId ? document.getElementById(triggerTargetId) : null)
41
- }, [triggerTargetId])
31
+ createEffect(() =>
32
+ setTriggerTarget(props.triggerTargetId ? document.getElementById(props.triggerTargetId) : null)
33
+ )
42
34
 
43
35
  const trigger = (
44
36
  <DrawerTrigger
45
- render={
46
- <Button
47
- variant="outline"
48
- size="sm"
49
- className="workspace-account-trigger"
50
- aria-label={`Open user menu for ${accountLabel}`}
51
- aria-haspopup="dialog"
52
- />
53
- }
37
+ class={buttonVariants({ variant: 'outline', size: 'sm', class: 'workspace-account-trigger' })}
38
+ aria-label={`Open user menu for ${accountLabel()}`}
39
+ aria-haspopup="dialog"
54
40
  >
55
41
  <UserRound aria-hidden="true" />
56
- <span>{accountLabel}</span>
42
+ <span>{accountLabel()}</span>
57
43
  </DrawerTrigger>
58
44
  )
59
45
 
60
46
  return (
61
47
  <Drawer swipeDirection="right">
62
- {triggerTarget ? (
63
- createPortal(trigger, triggerTarget)
64
- ) : (
65
- <div className="fixed right-4 top-4 z-40">{trigger}</div>
66
- )}
67
- <DrawerContent className="w-[min(24rem,90vw)]">
68
- <DrawerHeader className="border-b px-5 pb-4 pt-5 text-left">
69
- <div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4">
70
- <div className="flex min-w-0 items-center gap-3">
71
- <span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary text-primary-foreground">
72
- <UserRound className="size-5" aria-hidden="true" />
48
+ <Show when={triggerTarget()} fallback={<div class="fixed right-4 top-4 z-40">{trigger}</div>}>
49
+ <Portal mount={triggerTarget()!}>{trigger}</Portal>
50
+ </Show>
51
+ <DrawerContent class="w-[min(24rem,90vw)]">
52
+ <DrawerHeader class="border-b px-5 pb-4 pt-5 text-left">
53
+ <div class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4">
54
+ <div class="flex min-w-0 items-center gap-3">
55
+ <span class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary text-primary-foreground">
56
+ <UserRound class="size-5" aria-hidden="true" />
73
57
  </span>
74
- <div className="min-w-0">
75
- <DrawerTitle className="truncate">{accountLabel}</DrawerTitle>
58
+ <div class="min-w-0">
59
+ <DrawerTitle class="truncate">{accountLabel()}</DrawerTitle>
76
60
  <DrawerDescription>
77
- {authenticated
61
+ {props.authenticated
78
62
  ? 'Signed in · workspace saved'
79
63
  : 'Guest workspace · sign in anytime'}
80
64
  </DrawerDescription>
81
65
  </div>
82
66
  </div>
83
- <ThemeToggle className="shrink-0" />
67
+ <ThemeToggle class="shrink-0" />
84
68
  </div>
85
69
  </DrawerHeader>
86
- <div className="flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto p-5">
87
- {musicControl ? (
70
+ <div class="flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto p-5">
71
+ <Show when={props.musicControl}>
88
72
  <section
89
- className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4 border-t pt-5"
73
+ class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4 border-t pt-5"
90
74
  aria-labelledby="account-music-title"
91
75
  >
92
- <div className="min-w-0">
93
- <h2 id="account-music-title" className="text-sm font-semibold">
76
+ <div class="min-w-0">
77
+ <h2 id="account-music-title" class="text-sm font-semibold">
94
78
  Music
95
79
  </h2>
96
- <p className="text-sm text-muted-foreground">Control the workspace soundtrack.</p>
80
+ <p class="text-sm text-muted-foreground">Control the workspace soundtrack.</p>
97
81
  </div>
98
- <div className="flex shrink-0 items-center justify-end">{musicControl}</div>
82
+ <div class="flex shrink-0 items-center justify-end">{props.musicControl}</div>
99
83
  </section>
100
- ) : null}
84
+ </Show>
101
85
  <section
102
- className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4 border-t pt-5"
86
+ class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-4 border-t pt-5"
103
87
  aria-labelledby="account-session-title"
104
88
  >
105
- <div className="min-w-0">
106
- <h2 id="account-session-title" className="text-sm font-semibold">
89
+ <div class="min-w-0">
90
+ <h2 id="account-session-title" class="text-sm font-semibold">
107
91
  Account
108
92
  </h2>
109
- <p className="text-sm text-muted-foreground">
110
- {authenticated
93
+ <p class="text-sm text-muted-foreground">
94
+ {props.authenticated
111
95
  ? 'Sign out on this device without removing your saved workspace.'
112
96
  : 'Sign in or create an account to keep this workspace across devices.'}
113
97
  </p>
114
98
  </div>
115
99
  <DrawerClose
116
- render={
117
- <Button
118
- type="button"
119
- className="shrink-0"
120
- variant={authenticated ? 'outline' : 'default'}
121
- disabled={busy}
122
- onClick={authenticated ? onSignOut : onSignIn}
123
- />
124
- }
100
+ class={buttonVariants({
101
+ variant: props.authenticated ? 'outline' : 'default',
102
+ class: 'shrink-0',
103
+ })}
104
+ disabled={props.busy}
105
+ onClick={props.authenticated ? props.onSignOut : props.onSignIn}
125
106
  >
126
- {authenticated ? 'Sign out' : 'Sign in'}
107
+ {props.authenticated ? 'Sign out' : 'Sign in'}
127
108
  </DrawerClose>
128
109
  </section>
129
110
  </div>
@@ -1,15 +1,14 @@
1
- 'use client'
1
+ import { ZoomIn, ZoomOut } from 'lucide-solid'
2
+ import { onCleanup, Show, type JSX } from 'solid-js'
2
3
 
3
- import { useCallback, useEffect, useRef } from 'react'
4
- import type { PointerEvent, ReactNode } from 'react'
5
- import { ZoomIn, ZoomOut } from 'lucide-react'
6
4
  import { Button } from '#components/ui/button'
7
5
  import { cn } from '#lib/utils'
6
+
8
7
  type ControlButtonProps = {
9
8
  code: string
10
9
  label: string
11
- children: ReactNode
12
- className?: string
10
+ children: JSX.Element
11
+ class?: string
13
12
  }
14
13
 
15
14
  function sendKeyEvent(type: 'keydown' | 'keyup', code: string): void {
@@ -18,52 +17,49 @@ function sendKeyEvent(type: 'keydown' | 'keyup', code: string): void {
18
17
  )
19
18
  }
20
19
 
21
- function ControlButton({ code, label, children, className }: ControlButtonProps) {
22
- const pressed = useRef(false)
23
- const pressedPointer = useRef<number | null>(null)
20
+ function ControlButton(props: ControlButtonProps) {
21
+ let pressed = false
22
+ let pressedPointer: number | null = null
24
23
 
25
- const release = useCallback(() => {
26
- if (!pressed.current) return
27
- pressed.current = false
28
- pressedPointer.current = null
29
- sendKeyEvent('keyup', code)
30
- }, [code])
24
+ const release = () => {
25
+ if (!pressed) return
26
+ pressed = false
27
+ pressedPointer = null
28
+ sendKeyEvent('keyup', props.code)
29
+ }
31
30
 
32
31
  // Self-heal: release the key if the pointerup lands elsewhere (e.g. the
33
32
  // scene DOM changes mid-teleport) or the window loses focus. PointerId
34
33
  // matching preserves multi-touch.
35
- useEffect(() => {
36
- const onWindowPointerUp = (event: globalThis.PointerEvent) => {
37
- if (pressedPointer.current === event.pointerId) release()
34
+ if (typeof window !== 'undefined') {
35
+ const onWindowPointerUp = (event: PointerEvent) => {
36
+ if (pressedPointer === event.pointerId) release()
38
37
  }
39
38
  const onBlur = () => release()
40
39
  window.addEventListener('pointerup', onWindowPointerUp)
41
40
  window.addEventListener('blur', onBlur)
42
- return () => {
41
+ onCleanup(() => {
43
42
  window.removeEventListener('pointerup', onWindowPointerUp)
44
43
  window.removeEventListener('blur', onBlur)
45
- }
46
- }, [release])
44
+ })
45
+ }
47
46
 
48
- const press = useCallback(
49
- (event: PointerEvent<HTMLButtonElement>) => {
50
- event.preventDefault()
51
- if (pressed.current) return
52
- pressed.current = true
53
- pressedPointer.current = event.pointerId
54
- event.currentTarget.setPointerCapture(event.pointerId)
55
- sendKeyEvent('keydown', code)
56
- },
57
- [code]
58
- )
47
+ const press = (event: PointerEvent & { currentTarget: HTMLButtonElement }) => {
48
+ event.preventDefault()
49
+ if (pressed) return
50
+ pressed = true
51
+ pressedPointer = event.pointerId
52
+ event.currentTarget.setPointerCapture(event.pointerId)
53
+ sendKeyEvent('keydown', props.code)
54
+ }
59
55
 
60
56
  return (
61
57
  <button
62
58
  type="button"
63
- aria-label={label}
64
- className={cn(
59
+ aria-label={props.label}
60
+ class={cn(
65
61
  'flex size-14 touch-none select-none items-center justify-center rounded-2xl border border-white/25 bg-slate-950/65 p-2 text-white shadow-lg backdrop-blur-sm transition active:scale-95 [-webkit-touch-callout:none] [-webkit-user-select:none]',
66
- className
62
+ props.class
67
63
  )}
68
64
  onContextMenu={(event) => event.preventDefault()}
69
65
  onPointerCancel={release}
@@ -71,7 +67,7 @@ function ControlButton({ code, label, children, className }: ControlButtonProps)
71
67
  onPointerLeave={release}
72
68
  onPointerUp={release}
73
69
  >
74
- {children}
70
+ {props.children}
75
71
  </button>
76
72
  )
77
73
  }
@@ -83,41 +79,38 @@ export type OnScreenControlsProps = {
83
79
  showJumpControl?: boolean
84
80
  }
85
81
 
86
- export function OnScreenControls({
87
- onZoomIn,
88
- onZoomOut,
89
- showMovementControls = true,
90
- showJumpControl = true,
91
- }: OnScreenControlsProps = {}) {
92
- const showZoomControls = Boolean(onZoomIn && onZoomOut)
82
+ export function OnScreenControls(props: OnScreenControlsProps) {
83
+ const showMovementControls = () => props.showMovementControls ?? true
84
+ const showJumpControl = () => props.showJumpControl ?? true
85
+ const showZoomControls = () => Boolean(props.onZoomIn && props.onZoomOut)
93
86
 
94
87
  return (
95
88
  <div
96
89
  data-agent-hq-on-screen-controls
97
- className="pointer-events-none fixed inset-x-4 bottom-4 z-30 flex select-none items-end justify-between gap-4 pb-[env(safe-area-inset-bottom)] sm:inset-x-6 sm:bottom-6 [-webkit-touch-callout:none] [-webkit-user-select:none]"
90
+ class="pointer-events-none fixed inset-x-4 bottom-4 z-30 flex select-none items-end justify-between gap-4 pb-[env(safe-area-inset-bottom)] sm:inset-x-6 sm:bottom-6 [-webkit-touch-callout:none] [-webkit-user-select:none]"
98
91
  >
99
- {showMovementControls ? (
100
- <div className="pointer-events-auto grid grid-cols-3 gap-1.5 rounded-3xl bg-slate-950/20 p-2 backdrop-blur-[2px]">
92
+ <Show when={showMovementControls()}>
93
+ <div class="pointer-events-auto grid grid-cols-3 gap-1.5 rounded-3xl bg-slate-950/20 p-2 backdrop-blur-[2px]">
101
94
  <span />
102
- <ControlButton code="KeyW" label="Move forward" className="size-12 rounded-xl text-xl">
95
+ <ControlButton code="KeyW" label="Move forward" class="size-12 rounded-xl text-xl">
103
96
  ▲
104
97
  </ControlButton>
105
98
  <span />
106
- <ControlButton code="KeyA" label="Move left" className="size-12 rounded-xl text-xl">
99
+ <ControlButton code="KeyA" label="Move left" class="size-12 rounded-xl text-xl">
107
100
  ◀
108
101
  </ControlButton>
109
- <ControlButton code="KeyS" label="Move backward" className="size-12 rounded-xl text-xl">
102
+ <ControlButton code="KeyS" label="Move backward" class="size-12 rounded-xl text-xl">
110
103
  ▼
111
104
  </ControlButton>
112
- <ControlButton code="KeyD" label="Move right" className="size-12 rounded-xl text-xl">
105
+ <ControlButton code="KeyD" label="Move right" class="size-12 rounded-xl text-xl">
113
106
  ▶
114
107
  </ControlButton>
115
108
  </div>
116
- ) : null}
117
- <div className="pointer-events-auto ml-auto flex flex-col items-end gap-2">
118
- {showZoomControls ? (
109
+ </Show>
110
+ <div class="pointer-events-auto ml-auto flex flex-col items-end gap-2">
111
+ <Show when={showZoomControls()}>
119
112
  <div
120
- className="flex items-center gap-1.5 rounded-2xl bg-slate-950/20 p-1.5 backdrop-blur-[2px]"
113
+ class="flex items-center gap-1.5 rounded-2xl bg-slate-950/20 p-1.5 backdrop-blur-[2px]"
121
114
  role="group"
122
115
  aria-label="Camera zoom"
123
116
  >
@@ -127,8 +120,8 @@ export function OnScreenControls({
127
120
  size="icon-lg"
128
121
  aria-label="Zoom out"
129
122
  title="Zoom out"
130
- className="border border-white/25 bg-slate-950/65 text-white shadow-lg backdrop-blur-sm hover:bg-slate-900/80"
131
- onClick={onZoomOut}
123
+ class="border border-white/25 bg-slate-950/65 text-white shadow-lg backdrop-blur-sm hover:bg-slate-900/80"
124
+ onClick={() => props.onZoomOut?.()}
132
125
  >
133
126
  <ZoomOut aria-hidden="true" />
134
127
  </Button>
@@ -138,18 +131,18 @@ export function OnScreenControls({
138
131
  size="icon-lg"
139
132
  aria-label="Zoom in"
140
133
  title="Zoom in"
141
- className="border border-white/25 bg-slate-950/65 text-white shadow-lg backdrop-blur-sm hover:bg-slate-900/80"
142
- onClick={onZoomIn}
134
+ class="border border-white/25 bg-slate-950/65 text-white shadow-lg backdrop-blur-sm hover:bg-slate-900/80"
135
+ onClick={() => props.onZoomIn?.()}
143
136
  >
144
137
  <ZoomIn aria-hidden="true" />
145
138
  </Button>
146
139
  </div>
147
- ) : null}
148
- {showJumpControl ? (
149
- <ControlButton code="Space" label="Jump" className="text-xs font-semibold">
140
+ </Show>
141
+ <Show when={showJumpControl()}>
142
+ <ControlButton code="Space" label="Jump" class="text-xs font-semibold">
150
143
  JUMP
151
144
  </ControlButton>
152
- ) : null}
145
+ </Show>
153
146
  </div>
154
147
  </div>
155
148
  )