@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.
- package/dist/context/marked-parser.d.ts +2 -0
- package/dist/i18n/en.d.ts +1 -0
- package/dist/icons/catalog.d.ts +36 -0
- package/dist/navigation/segmented-control/segmented-control.d.ts +21 -2
- package/package.json +2 -1
- package/src/actions/button/button.css +11 -9
- package/src/actions/button/button.tsx +1 -0
- package/src/actions/icon-button/icon-button.css +1 -5
- package/src/actions/icon-button/icon-button.tsx +1 -0
- package/src/actions/split-button/split-button.tsx +3 -0
- package/src/actions/submit.css +14 -12
- package/src/components/animated-number.tsx +13 -0
- package/src/components/app-icon.tsx +2 -0
- package/src/components/card.tsx +21 -0
- package/src/components/collapsible.tsx +1 -0
- package/src/components/context-menu.tsx +34 -0
- package/src/components/dock-surface.tsx +3 -0
- package/src/components/file-icon.tsx +12 -0
- package/src/components/image-preview.tsx +1 -0
- package/src/components/list.tsx +32 -0
- package/src/components/motion-spring.tsx +5 -0
- package/src/components/popover.tsx +12 -0
- package/src/components/provider-icon.tsx +1 -0
- package/src/components/resize-handle.tsx +8 -0
- package/src/components/scroll-view.tsx +39 -0
- package/src/components/spinner.tsx +2 -0
- package/src/components/text-field.tsx +6 -0
- package/src/components/text-reveal.tsx +19 -0
- package/src/components/text-strikethrough.tsx +8 -0
- package/src/components/typewriter.tsx +4 -0
- package/src/context/dialog.tsx +12 -0
- package/src/context/file.tsx +1 -0
- package/src/context/helper.tsx +4 -0
- package/src/context/i18n.tsx +20 -0
- package/src/context/index.ts +3 -0
- package/src/context/marked-base.tsx +9 -1
- package/src/context/marked-parser.tsx +57 -18
- package/src/context/marked.tsx +3 -0
- package/src/context/worker-pool.tsx +2 -0
- package/src/data-display/avatar/avatar.tsx +4 -0
- package/src/data-display/badge/badge.tsx +1 -0
- package/src/data-display/diff-changes/diff-changes.tsx +2 -0
- package/src/data-display/keybind/keybind.tsx +1 -0
- package/src/data-display/line-comment/line-comment.tsx +22 -0
- package/src/data-display/project-avatar/project-avatar.tsx +10 -0
- package/src/feedback/loader/loader.tsx +1 -0
- package/src/feedback/toast/description.ts +1 -0
- package/src/forms/checkbox/checkbox.tsx +1 -0
- package/src/forms/field/field.tsx +8 -0
- package/src/forms/inline-input/inline-input.tsx +4 -0
- package/src/forms/radio/radio.tsx +2 -0
- package/src/forms/select/select.tsx +11 -0
- package/src/forms/switch/switch.tsx +1 -0
- package/src/forms/text-input/text-input.tsx +3 -0
- package/src/hooks/create-auto-scroll.tsx +34 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-filtered-list.tsx +14 -0
- package/src/i18n/en.ts +1 -0
- package/src/i18n/no.ts +1 -0
- package/src/icons/catalog.ts +36 -0
- package/src/icons/icon/icon.tsx +2 -0
- package/src/layout/divider/divider.tsx +1 -0
- package/src/navigation/menu/menu.tsx +13 -0
- package/src/navigation/segmented-control/segmented-control.css +37 -4
- package/src/navigation/segmented-control/segmented-control.tsx +210 -14
- package/src/navigation/tab-state-indicator.tsx +1 -0
- package/src/navigation/tabs/tabs-current.css +1 -5
- package/src/navigation/tabs/tabs.css +5 -22
- package/src/navigation/tabs/tabs.tsx +18 -0
- package/src/overlays/dialog/dialog.tsx +3 -0
- package/src/overlays/tooltip/tooltip.tsx +15 -0
- package/src/storybook/scaffold.tsx +3 -0
- package/src/styles/theme.css +1 -1
- package/src/theme/color.ts +27 -0
- package/src/theme/context.tsx +58 -0
- package/src/theme/default-themes.ts +35 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/loader.ts +11 -0
- package/src/theme/resolve.ts +28 -0
- package/src/theme/v2/foreground.ts +8 -0
- package/src/theme/v2/resolve.ts +10 -0
- 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:
|
|
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
|
|
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
|
|
232
|
+
const assignRef = (el: HTMLDivElement) => {
|
|
233
|
+
rootEl = el
|
|
98
234
|
const r = local.ref
|
|
99
|
-
|
|
100
|
-
|
|
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:
|
|
270
|
+
e: Parameters<JSX.EventHandler<HTMLButtonElement, E>>[0],
|
|
131
271
|
) {
|
|
132
|
-
if (
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
334
|
+
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (e.key === "ArrowLeft") {
|
|
168
339
|
e.preventDefault()
|
|
169
|
-
ctx.focusNext(t,
|
|
340
|
+
ctx.focusNext(t, horizontal === 1 ? -1 : 1)
|
|
341
|
+
|
|
342
|
+
return
|
|
170
343
|
}
|
|
171
344
|
|
|
172
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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:
|
|
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
|