@opencode/ui 2.0.23 → 2.0.25

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 (82) hide show
  1. package/dist/context/marked-parser.d.ts +2 -0
  2. package/dist/i18n/en.d.ts +1 -0
  3. package/dist/icons/catalog.d.ts +36 -0
  4. package/dist/navigation/segmented-control/segmented-control.d.ts +21 -2
  5. package/package.json +2 -1
  6. package/src/actions/button/button.css +11 -9
  7. package/src/actions/button/button.tsx +1 -0
  8. package/src/actions/icon-button/icon-button.css +1 -5
  9. package/src/actions/icon-button/icon-button.tsx +1 -0
  10. package/src/actions/split-button/split-button.tsx +3 -0
  11. package/src/actions/submit.css +14 -12
  12. package/src/components/animated-number.tsx +13 -0
  13. package/src/components/app-icon.tsx +2 -0
  14. package/src/components/card.tsx +21 -0
  15. package/src/components/collapsible.tsx +1 -0
  16. package/src/components/context-menu.tsx +34 -0
  17. package/src/components/dock-surface.tsx +3 -0
  18. package/src/components/file-icon.tsx +12 -0
  19. package/src/components/image-preview.tsx +1 -0
  20. package/src/components/list.tsx +32 -0
  21. package/src/components/motion-spring.tsx +5 -0
  22. package/src/components/popover.tsx +12 -0
  23. package/src/components/provider-icon.tsx +1 -0
  24. package/src/components/resize-handle.tsx +8 -0
  25. package/src/components/scroll-view.tsx +39 -0
  26. package/src/components/spinner.tsx +2 -0
  27. package/src/components/text-field.tsx +6 -0
  28. package/src/components/text-reveal.tsx +19 -0
  29. package/src/components/text-strikethrough.tsx +8 -0
  30. package/src/components/typewriter.tsx +4 -0
  31. package/src/context/dialog.tsx +12 -0
  32. package/src/context/file.tsx +1 -0
  33. package/src/context/helper.tsx +4 -0
  34. package/src/context/i18n.tsx +20 -0
  35. package/src/context/index.ts +3 -0
  36. package/src/context/marked-base.tsx +9 -1
  37. package/src/context/marked-parser.tsx +57 -18
  38. package/src/context/marked.tsx +3 -0
  39. package/src/context/worker-pool.tsx +2 -0
  40. package/src/data-display/avatar/avatar.tsx +4 -0
  41. package/src/data-display/badge/badge.tsx +1 -0
  42. package/src/data-display/diff-changes/diff-changes.tsx +2 -0
  43. package/src/data-display/keybind/keybind.tsx +1 -0
  44. package/src/data-display/line-comment/line-comment.tsx +22 -0
  45. package/src/data-display/project-avatar/project-avatar.tsx +10 -0
  46. package/src/feedback/loader/loader.tsx +1 -0
  47. package/src/feedback/toast/description.ts +1 -0
  48. package/src/forms/checkbox/checkbox.tsx +1 -0
  49. package/src/forms/field/field.tsx +8 -0
  50. package/src/forms/inline-input/inline-input.tsx +4 -0
  51. package/src/forms/radio/radio.tsx +2 -0
  52. package/src/forms/select/select.tsx +11 -0
  53. package/src/forms/switch/switch.tsx +1 -0
  54. package/src/forms/text-input/text-input.tsx +3 -0
  55. package/src/hooks/create-auto-scroll.tsx +34 -0
  56. package/src/hooks/index.ts +1 -0
  57. package/src/hooks/use-filtered-list.tsx +14 -0
  58. package/src/i18n/en.ts +1 -0
  59. package/src/i18n/no.ts +1 -0
  60. package/src/icons/catalog.ts +36 -0
  61. package/src/icons/icon/icon.tsx +2 -0
  62. package/src/layout/divider/divider.tsx +1 -0
  63. package/src/navigation/menu/menu.tsx +13 -0
  64. package/src/navigation/segmented-control/segmented-control.css +37 -4
  65. package/src/navigation/segmented-control/segmented-control.tsx +210 -14
  66. package/src/navigation/tab-state-indicator.tsx +1 -0
  67. package/src/navigation/tabs/tabs-current.css +1 -5
  68. package/src/navigation/tabs/tabs.css +5 -22
  69. package/src/navigation/tabs/tabs.tsx +18 -0
  70. package/src/overlays/dialog/dialog.tsx +3 -0
  71. package/src/overlays/tooltip/tooltip.tsx +15 -0
  72. package/src/storybook/scaffold.tsx +3 -0
  73. package/src/styles/theme.css +1 -1
  74. package/src/theme/color.ts +27 -0
  75. package/src/theme/context.tsx +58 -0
  76. package/src/theme/default-themes.ts +35 -0
  77. package/src/theme/index.ts +3 -0
  78. package/src/theme/loader.ts +11 -0
  79. package/src/theme/resolve.ts +28 -0
  80. package/src/theme/v2/foreground.ts +8 -0
  81. package/src/theme/v2/resolve.ts +10 -0
  82. package/src/typography/text-shimmer/text-shimmer.tsx +1 -0
@@ -47,7 +47,9 @@ const MenuAppearanceContext = createContext<{ appearance: Accessor<MenuAppearanc
47
47
 
48
48
  function useMenuContext() {
49
49
  const ctx = useContext(MenuAppearanceContext)
50
+
50
51
  if (!ctx) throw new Error("Menu components must be used inside Menu or Menu.Context")
52
+
51
53
  return ctx
52
54
  }
53
55
 
@@ -76,6 +78,7 @@ export interface MenuItemProps extends ComponentProps<typeof DropdownMenu.Item>
76
78
  function MenuItem(props: ParentProps<MenuItemProps>) {
77
79
  const ctx = useMenuContext()
78
80
  const [s, r] = splitProps(props, ["class", "classList", "children", "shortcut", "badge"])
81
+
79
82
  return (
80
83
  <DropdownMenu.Item
81
84
  {...r}
@@ -98,6 +101,7 @@ export interface MenuCheckboxItemProps extends ComponentProps<typeof DropdownMen
98
101
  function MenuCheckboxItem(props: ParentProps<MenuCheckboxItemProps>) {
99
102
  const ctx = useMenuContext()
100
103
  const [s, r] = splitProps(props, ["class", "classList", "children", "shortcut", "badge"])
104
+
101
105
  return (
102
106
  <DropdownMenu.CheckboxItem
103
107
  {...r}
@@ -128,6 +132,7 @@ export interface MenuRadioItemProps extends ComponentProps<typeof DropdownMenu.R
128
132
  function MenuRadioItem(props: ParentProps<MenuRadioItemProps>) {
129
133
  const ctx = useMenuContext()
130
134
  const [s, r] = splitProps(props, ["class", "classList", "children", "shortcut", "badge"])
135
+
131
136
  return (
132
137
  <DropdownMenu.RadioItem
133
138
  {...r}
@@ -178,6 +183,7 @@ function MenuSub(props: MenuSubProps) {
178
183
  function MenuSubTrigger(props: ParentProps<MenuSubTriggerProps>) {
179
184
  const ctx = useMenuContext()
180
185
  const [s, r] = splitProps(props, ["class", "classList", "children", "shortcut", "badge"])
186
+
181
187
  return (
182
188
  <DropdownMenu.SubTrigger
183
189
  {...r}
@@ -195,6 +201,7 @@ function MenuSubTrigger(props: ParentProps<MenuSubTriggerProps>) {
195
201
  function MenuSubContent(props: ComponentProps<typeof DropdownMenu.SubContent>) {
196
202
  const ctx = useMenuContext()
197
203
  const [s, r] = splitProps(props, ["class", "classList"])
204
+
198
205
  return (
199
206
  <DropdownMenu.SubContent
200
207
  {...r}
@@ -208,6 +215,7 @@ function MenuSubContent(props: ComponentProps<typeof DropdownMenu.SubContent>) {
208
215
  function MenuGroupLabel(props: ComponentProps<typeof DropdownMenu.GroupLabel>) {
209
216
  const ctx = useMenuContext()
210
217
  const [s, r] = splitProps(props, ["class", "classList"])
218
+
211
219
  return (
212
220
  <DropdownMenu.GroupLabel
213
221
  {...r}
@@ -221,6 +229,7 @@ function MenuGroupLabel(props: ComponentProps<typeof DropdownMenu.GroupLabel>) {
221
229
  function MenuSeparator(props: ComponentProps<typeof DropdownMenu.Separator>) {
222
230
  const ctx = useMenuContext()
223
231
  const [s, r] = splitProps(props, ["class", "classList"])
232
+
224
233
  return (
225
234
  <DropdownMenu.Separator
226
235
  {...r}
@@ -234,6 +243,7 @@ function MenuSeparator(props: ComponentProps<typeof DropdownMenu.Separator>) {
234
243
  function MenuContent(props: ComponentProps<typeof DropdownMenu.Content>) {
235
244
  const ctx = useMenuContext()
236
245
  const [s, r] = splitProps(props, ["class", "classList"])
246
+
237
247
  return (
238
248
  <DropdownMenu.Content
239
249
  {...r}
@@ -251,6 +261,7 @@ export interface MenuProps extends ComponentProps<typeof DropdownMenu> {
251
261
  function MenuRoot(props: MenuProps) {
252
262
  const [local, rest] = splitProps(props, ["appearance", "children"])
253
263
  const appearance = () => local.appearance ?? "compact"
264
+
254
265
  return (
255
266
  <MenuAppearanceContext.Provider value={{ appearance }}>
256
267
  <DropdownMenu {...rest}>{local.children}</DropdownMenu>
@@ -265,6 +276,7 @@ export interface MenuContextProps extends ComponentProps<typeof ContextMenu> {
265
276
  function MenuContextRoot(props: MenuContextProps) {
266
277
  const [local, rest] = splitProps(props, ["appearance", "children"])
267
278
  const appearance = () => local.appearance ?? "compact"
279
+
268
280
  return (
269
281
  <MenuAppearanceContext.Provider value={{ appearance }}>
270
282
  <ContextMenu {...rest}>{local.children}</ContextMenu>
@@ -275,6 +287,7 @@ function MenuContextRoot(props: MenuContextProps) {
275
287
  function MenuContextContent(props: ComponentProps<typeof ContextMenu.Content>) {
276
288
  const ctx = useMenuContext()
277
289
  const [s, r] = splitProps(props, ["class", "classList"])
290
+
278
291
  return (
279
292
  <ContextMenu.Content
280
293
  {...r}
@@ -3,6 +3,8 @@
3
3
  }
4
4
 
5
5
  [data-slot="segmented-control-v2"] {
6
+ position: relative;
7
+ isolation: isolate;
6
8
  box-sizing: border-box;
7
9
  display: flex;
8
10
  flex-direction: row;
@@ -16,9 +18,28 @@
16
18
  border-radius: 6px;
17
19
  box-shadow: 0 0 0 0.5px var(--v2-border-border-base);
18
20
  flex: none;
21
+ user-select: none;
22
+ -webkit-user-select: none;
23
+ }
24
+
25
+ [data-slot="segmented-control-v2-indicator"] {
26
+ position: absolute;
27
+ top: 0;
28
+ bottom: 0;
29
+ left: 0;
30
+ border-radius: 6px;
31
+ background: var(--v2-background-bg-base);
32
+ box-shadow: 0 0 0 0.5px var(--v2-border-border-strong);
33
+ pointer-events: none;
34
+ user-select: none;
35
+ -webkit-user-select: none;
36
+ z-index: 0;
37
+ will-change: transform, width;
19
38
  }
20
39
 
21
40
  [data-slot="segmented-control-v2-item"] {
41
+ position: relative;
42
+ z-index: 1;
22
43
  box-sizing: border-box;
23
44
  display: flex;
24
45
  flex-direction: column;
@@ -34,20 +55,26 @@
34
55
  background: transparent;
35
56
  box-shadow: none;
36
57
  cursor: pointer;
58
+ user-select: none;
59
+ -webkit-user-select: none;
37
60
  font-style: normal;
38
61
  font-weight: 440;
39
62
  font-size: 13px;
40
- line-height: var(--line-height-compact);
63
+ line-height: 100%;
41
64
  letter-spacing: -0.04px;
42
65
  font-variant-numeric: tabular-nums;
43
66
  font-variation-settings: "slnt" 0;
44
67
  color: var(--v2-text-text-muted);
45
68
  transition:
69
+ color 0.15s ease,
46
70
  background-color 0.12s ease,
47
- color 0.12s ease,
48
71
  box-shadow 0.12s ease;
49
72
  }
50
73
 
74
+ [data-slot="segmented-control-v2-item"]:where(:hover:not(:disabled):not([data-pressed])) {
75
+ color: var(--v2-text-text-base);
76
+ }
77
+
51
78
  [data-slot="segmented-control-v2-item"]:where(:disabled) {
52
79
  cursor: not-allowed;
53
80
  opacity: 0.45;
@@ -60,12 +87,15 @@
60
87
  }
61
88
 
62
89
  [data-slot="segmented-control-v2-item"]:where([data-pressed]) {
63
- background: var(--v2-background-bg-base);
64
90
  color: var(--v2-text-text-base);
65
- box-shadow: 0 0 0 0.5px var(--v2-border-border-strong);
66
91
  z-index: 1;
67
92
  }
68
93
 
94
+ [data-slot="segmented-control-v2"]:not([data-ready]) [data-slot="segmented-control-v2-item"]:where([data-pressed]) {
95
+ background: var(--v2-background-bg-base);
96
+ box-shadow: 0 0 0 0.5px var(--v2-border-border-strong);
97
+ }
98
+
69
99
  [data-slot="segmented-control-v2-item-label"] {
70
100
  display: flex;
71
101
  flex-direction: row;
@@ -77,4 +107,7 @@
77
107
  overflow: hidden;
78
108
  text-overflow: ellipsis;
79
109
  white-space: nowrap;
110
+ pointer-events: none;
111
+ user-select: none;
112
+ -webkit-user-select: none;
80
113
  }
@@ -1,35 +1,76 @@
1
+ import { animate, motion, useMotionValue, type Transition } from "@kitlangton/solid-motion"
1
2
  import {
2
3
  createContext,
4
+ createEffect,
3
5
  createMemo,
4
6
  createSignal,
5
7
  mergeProps,
8
+ onCleanup,
9
+ onMount,
10
+ Show,
6
11
  splitProps,
7
12
  useContext,
8
13
  type Accessor,
14
+ type ComponentProps,
9
15
  type JSX,
10
16
  type ParentProps,
11
17
  } from "solid-js"
12
- import type { ComponentProps } from "solid-js"
13
18
  import "./segmented-control.css"
14
19
 
15
20
  type OnChange = (value: string | null) => void
16
21
 
22
+ export const SEGMENTED_CONTROL_SPRING: Transition = {
23
+ type: "spring",
24
+ visualDuration: 0.15,
25
+ bounce: 0,
26
+ }
27
+
17
28
  type SegmentedControlContextValue = {
18
29
  selected: Accessor<string | null>
19
30
  groupDisabled: Accessor<boolean>
20
31
  select: (value: string) => void
21
32
  clearIfAllowed: (value: string) => void
22
33
  focusNext: (from: HTMLButtonElement, direction: 1 | -1) => void
34
+ registerItem: (value: string, el: HTMLButtonElement) => () => void
23
35
  }
24
36
 
25
37
  const SegmentedControlContext = createContext<SegmentedControlContextValue>()
26
38
 
27
39
  function useSegmentedControlContext() {
28
40
  const ctx = useContext(SegmentedControlContext)
41
+
29
42
  if (!ctx) throw new Error("SegmentedControlItem must be used inside SegmentedControl")
43
+
30
44
  return ctx
31
45
  }
32
46
 
47
+ export type OffsetElement = {
48
+ offsetLeft: number
49
+ offsetWidth: number
50
+ offsetParent: Element | OffsetElement | null
51
+ getBoundingClientRect: () => { left: number }
52
+ }
53
+
54
+ function isOffsetElement(node: Element | OffsetElement | null): node is OffsetElement {
55
+ return node !== null && "offsetLeft" in node && "offsetParent" in node
56
+ }
57
+
58
+ export function measureOffset(root: OffsetElement, button: OffsetElement) {
59
+ let offset = 0
60
+ let node: OffsetElement | null = button
61
+
62
+ while (node && node !== root) {
63
+ offset += node.offsetLeft
64
+ node = isOffsetElement(node.offsetParent) ? node.offsetParent : null
65
+ }
66
+
67
+ if (node !== root) {
68
+ offset = button.getBoundingClientRect().left - root.getBoundingClientRect().left
69
+ }
70
+
71
+ return { x: offset, width: button.offsetWidth }
72
+ }
73
+
33
74
  export type SegmentedControlProps = Omit<ComponentProps<"div">, "onChange"> &
34
75
  ParentProps<{
35
76
  /** Selected value when controlled (including `null` when empty). Omit key for uncontrolled. */
@@ -40,11 +81,14 @@ export type SegmentedControlProps = Omit<ComponentProps<"div">, "onChange"> &
40
81
  /** When true, clicking the active segment clears selection (`onChange(null)`). Default false. */
41
82
  allowDeselect?: boolean
42
83
  disabled?: boolean
84
+ /** Motion transition for the active indicator pill. */
85
+ transition?: Transition
43
86
  }>
44
87
 
45
88
  export function SegmentedControl(props: SegmentedControlProps) {
46
- const isControlled = createMemo(() => Object.hasOwn(props as object, "value"))
89
+ const isControlled = createMemo(() => Object.hasOwn(props, "value"))
47
90
  const merged = mergeProps({ allowDeselect: false, disabled: false }, props)
91
+
48
92
  const [local, rest] = splitProps(merged, [
49
93
  "class",
50
94
  "classList",
@@ -54,13 +98,88 @@ export function SegmentedControl(props: SegmentedControlProps) {
54
98
  "onChange",
55
99
  "allowDeselect",
56
100
  "disabled",
101
+ "transition",
57
102
  "ref",
58
103
  ])
59
104
 
60
105
  const [internal, setInternal] = createSignal<string | null>(local.defaultValue ?? null)
106
+ const [layoutVersion, setLayoutVersion] = createSignal(0)
107
+ const [ready, setReady] = createSignal(false)
61
108
 
62
109
  const selected = createMemo(() => (isControlled() ? (local.value ?? null) : internal()))
63
110
 
111
+ const reducedMotion = () => globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true
112
+
113
+ const x = useMotionValue(0)
114
+ const width = useMotionValue(0)
115
+
116
+ let rootEl: HTMLDivElement | undefined
117
+ let observer: ResizeObserver | undefined
118
+ const items = new Map<string, HTMLButtonElement>()
119
+ let target = { x: 0, width: 0 }
120
+ let lastSelected: string | null = null
121
+
122
+ const syncIndicator = (shouldAnimate: boolean) => {
123
+ const current = selected()
124
+ const button = current !== null ? items.get(current) : undefined
125
+
126
+ if (!rootEl || !button) {
127
+ setReady(false)
128
+
129
+ return
130
+ }
131
+
132
+ const next = measureOffset(rootEl, button)
133
+
134
+ if (next.width === 0) return
135
+
136
+ target = next
137
+
138
+ if (!ready() || !shouldAnimate || reducedMotion()) {
139
+ x.jump(next.x)
140
+ width.jump(next.width)
141
+ setReady(true)
142
+
143
+ return
144
+ }
145
+
146
+ const transition = local.transition ?? SEGMENTED_CONTROL_SPRING
147
+ animate(x, next.x, transition)
148
+ animate(width, next.width, transition)
149
+ }
150
+
151
+ onMount(() => {
152
+ if (!("ResizeObserver" in globalThis)) return
153
+
154
+ observer = new ResizeObserver(() => {
155
+ const current = selected()
156
+ const button = current !== null ? items.get(current) : undefined
157
+
158
+ if (!rootEl || !button) return
159
+
160
+ const next = measureOffset(rootEl, button)
161
+
162
+ if (next.width === 0) return
163
+
164
+ if (ready() && next.x === target.x && next.width === target.width) return
165
+
166
+ syncIndicator(false)
167
+ })
168
+
169
+ if (rootEl) observer.observe(rootEl)
170
+
171
+ items.forEach((button) => observer?.observe(button))
172
+ onCleanup(() => observer?.disconnect())
173
+ })
174
+
175
+ createEffect(() => {
176
+ const current = selected()
177
+ layoutVersion()
178
+ const shouldAnimate = ready() && lastSelected !== null && current !== null && current !== lastSelected
179
+ lastSelected = current
180
+ syncIndicator(shouldAnimate)
181
+ })
182
+
64
183
  const setSelected = (next: string | null) => {
65
184
  if (!isControlled()) setInternal(next)
66
185
  local.onChange?.(next)
@@ -77,27 +196,44 @@ export function SegmentedControl(props: SegmentedControlProps) {
77
196
 
78
197
  const focusNext = (from: HTMLButtonElement, direction: 1 | -1) => {
79
198
  const root = from.closest(`[data-slot="segmented-control-v2"]`)
199
+
80
200
  if (!root) return
201
+
81
202
  const buttons = Array.from(
82
203
  root.querySelectorAll<HTMLButtonElement>(`button[data-slot="segmented-control-v2-item"]`),
83
204
  ).filter((b) => !b.disabled)
205
+
84
206
  const i = buttons.indexOf(from)
85
207
  const next = buttons[i + direction]
86
208
  next?.focus()
87
209
  }
88
210
 
211
+ const registerItem = (value: string, el: HTMLButtonElement) => {
212
+ items.set(value, el)
213
+ observer?.observe(el)
214
+ setLayoutVersion((v) => v + 1)
215
+
216
+ return () => {
217
+ if (items.get(value) === el) items.delete(value)
218
+ observer?.unobserve(el)
219
+ setLayoutVersion((v) => v + 1)
220
+ }
221
+ }
222
+
89
223
  const ctx: SegmentedControlContextValue = {
90
224
  selected,
91
225
  groupDisabled: () => !!local.disabled,
92
226
  select,
93
227
  clearIfAllowed,
94
228
  focusNext,
229
+ registerItem,
95
230
  }
96
231
 
97
- const assignRef = (el: HTMLDivElement | undefined) => {
232
+ const assignRef = (el: HTMLDivElement) => {
233
+ rootEl = el
98
234
  const r = local.ref
99
- if (typeof r === "function") (r as (el: HTMLDivElement | undefined) => void)(el)
100
- else if (r != null && typeof r === "object" && "value" in r) (r as { value: HTMLDivElement | undefined }).value = el
235
+
236
+ if (r instanceof Function) r(el)
101
237
  }
102
238
 
103
239
  return (
@@ -108,11 +244,15 @@ export function SegmentedControl(props: SegmentedControlProps) {
108
244
  role="group"
109
245
  data-component="segmented-control-v2"
110
246
  data-slot="segmented-control-v2"
247
+ data-ready={ready() ? "" : undefined}
111
248
  classList={{
112
249
  ...local.classList,
113
250
  [local.class ?? ""]: !!local.class,
114
251
  }}
115
252
  >
253
+ <Show when={ready() && selected() !== null}>
254
+ <motion.span data-slot="segmented-control-v2-indicator" aria-hidden="true" style={{ x, width }} />
255
+ </Show>
116
256
  {local.children}
117
257
  </div>
118
258
  </SegmentedControlContext.Provider>
@@ -127,13 +267,22 @@ export type SegmentedControlItemProps = Omit<ComponentProps<"button">, "type" |
127
267
 
128
268
  function invokeButtonHandler<E extends Event>(
129
269
  handler: JSX.EventHandlerUnion<HTMLButtonElement, E> | undefined,
130
- e: E & { currentTarget: HTMLButtonElement },
270
+ e: Parameters<JSX.EventHandler<HTMLButtonElement, E>>[0],
131
271
  ) {
132
- if (typeof handler === "function") (handler as (ev: typeof e) => void)(e)
272
+ if (handler instanceof Function) {
273
+ handler(e)
274
+
275
+ return
276
+ }
277
+
278
+ if (Array.isArray(handler)) {
279
+ handler[0](handler[1], e)
280
+ }
133
281
  }
134
282
 
135
283
  export function SegmentedControlItem(props: SegmentedControlItemProps) {
136
284
  const merged = mergeProps({ disabled: false }, props)
285
+
137
286
  const [local, rest] = splitProps(merged, [
138
287
  "class",
139
288
  "classList",
@@ -142,55 +291,94 @@ export function SegmentedControlItem(props: SegmentedControlItemProps) {
142
291
  "disabled",
143
292
  "onClick",
144
293
  "onKeyDown",
294
+ "ref",
145
295
  ])
296
+
146
297
  const ctx = useSegmentedControlContext()
147
298
 
299
+ let buttonEl: HTMLButtonElement | undefined
300
+ createEffect(() => {
301
+ if (!buttonEl) return
302
+ const cleanup = ctx.registerItem(local.value, buttonEl)
303
+ onCleanup(cleanup)
304
+ })
305
+
148
306
  const pressed = createMemo(() => ctx.selected() === local.value)
149
307
  const disabled = createMemo(() => ctx.groupDisabled() || !!local.disabled)
150
308
 
151
309
  const onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = (e) => {
152
310
  invokeButtonHandler(local.onClick, e)
311
+
153
312
  if (e.defaultPrevented || disabled()) return
154
- if (pressed()) ctx.clearIfAllowed(local.value)
155
- else ctx.select(local.value)
313
+
314
+ if (pressed()) {
315
+ ctx.clearIfAllowed(local.value)
316
+
317
+ return
318
+ }
319
+
320
+ ctx.select(local.value)
156
321
  }
157
322
 
158
323
  const onKeyDown: JSX.EventHandlerUnion<HTMLButtonElement, KeyboardEvent> = (e) => {
159
324
  invokeButtonHandler(local.onKeyDown, e)
325
+
160
326
  if (e.defaultPrevented || disabled()) return
327
+
161
328
  const t = e.currentTarget
162
329
  const horizontal = t.closest(`[data-slot="segmented-control-v2"]`)?.matches(":dir(rtl)") ? -1 : 1
163
330
 
164
331
  if (e.key === "ArrowRight") {
165
332
  e.preventDefault()
166
333
  ctx.focusNext(t, horizontal)
167
- } else if (e.key === "ArrowLeft") {
334
+
335
+ return
336
+ }
337
+
338
+ if (e.key === "ArrowLeft") {
168
339
  e.preventDefault()
169
- ctx.focusNext(t, -horizontal as 1 | -1)
340
+ ctx.focusNext(t, horizontal === 1 ? -1 : 1)
341
+
342
+ return
170
343
  }
171
344
 
172
- // accessibility stuff
173
- else if (e.key === "Home") {
345
+ if (e.key === "Home") {
174
346
  e.preventDefault()
175
347
  const root = t.closest(`[data-slot="segmented-control-v2"]`)
348
+
176
349
  const first = root?.querySelector<HTMLButtonElement>(
177
350
  `button[data-slot="segmented-control-v2-item"]:not(:disabled)`,
178
351
  )
352
+
179
353
  first?.focus()
180
- } else if (e.key === "End") {
354
+
355
+ return
356
+ }
357
+
358
+ if (e.key === "End") {
181
359
  e.preventDefault()
182
360
  const root = t.closest(`[data-slot="segmented-control-v2"]`)
361
+
183
362
  const buttons = root?.querySelectorAll<HTMLButtonElement>(
184
363
  `button[data-slot="segmented-control-v2-item"]:not(:disabled)`,
185
364
  )
365
+
186
366
  const last = buttons?.[buttons.length - 1]
187
367
  last?.focus()
188
368
  }
189
369
  }
190
370
 
371
+ const assignRef = (el: HTMLButtonElement) => {
372
+ buttonEl = el
373
+ const r = local.ref
374
+
375
+ if (r instanceof Function) r(el)
376
+ }
377
+
191
378
  return (
192
379
  <button
193
380
  {...rest}
381
+ ref={assignRef}
194
382
  type="button"
195
383
  data-slot="segmented-control-v2-item"
196
384
  data-pressed={pressed() ? "" : undefined}
@@ -207,3 +395,11 @@ export function SegmentedControlItem(props: SegmentedControlItemProps) {
207
395
  </button>
208
396
  )
209
397
  }
398
+
399
+ export const SegmentedControlV2 = SegmentedControl
400
+
401
+ export const SegmentedControlItemV2 = SegmentedControlItem
402
+
403
+ export type SegmentedControlV2Props = SegmentedControlProps
404
+
405
+ export type SegmentedControlItemV2Props = SegmentedControlItemProps
@@ -2,6 +2,7 @@ import { splitProps, type ComponentProps } from "solid-js"
2
2
 
3
3
  export function TabStateIndicator(props: ComponentProps<"svg">) {
4
4
  const [local, rest] = splitProps(props, ["class", "classList", "width", "height"])
5
+
5
6
  return (
6
7
  <svg
7
8
  {...rest}
@@ -1,10 +1,6 @@
1
- * {
1
+ [data-component="tabs-v2"] {
2
2
  -webkit-font-smoothing: antialiased;
3
3
  -moz-osx-font-smoothing: grayscale;
4
- text-rendering: geometricPrecision;
5
- }
6
-
7
- [data-component="tabs-v2"] {
8
4
  width: 100%;
9
5
  height: 100%;
10
6
  display: flex;
@@ -568,11 +568,6 @@
568
568
  color: var(--v2-text-text-base);
569
569
  }
570
570
 
571
- .tab-fileicon-color,
572
- .tab-fileicon-mono {
573
- margin-inline-start: -2px;
574
- }
575
-
576
571
  .italic {
577
572
  font-style: italic !important;
578
573
  font-variation-settings: "slnt" -10 !important;
@@ -600,27 +595,15 @@
600
595
  flex: 1 1 0;
601
596
  min-width: 0;
602
597
  width: auto;
598
+ height: var(--tabs-bar-height);
599
+ padding-inline-end: 0;
603
600
  border-bottom: none;
604
601
  }
605
602
 
603
+ /* The end gap lives inside the sticky "+" box. Sticky offsets respect the strip's padding, so padding there
604
+ would leave a gap after the "+" where scrolled tabs show through. */
606
605
  :root body #review-panel [data-component="tabs"] .session-review-v2-tabs-bar [data-slot="tabs-list"] > .sticky {
607
- padding-inline-end: 0;
608
-
609
- [data-component="icon-button-v2"] {
610
- position: relative;
611
-
612
- &::after {
613
- content: "";
614
- position: absolute;
615
- inset-inline-start: 100%;
616
- top: 50%;
617
- transform: translateY(-50%);
618
- width: 20px;
619
- height: 28px;
620
- background-color: var(--v2-background-bg-base);
621
- pointer-events: none;
622
- }
623
- }
606
+ padding-inline-end: 12px;
624
607
  }
625
608
 
626
609
  :root