chore: merge dev into v2 (#36770)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Jay <air@live.ca> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: James Long <jlongster@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com> Co-authored-by: Victor Navarro <vn4varro@gmail.com> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Nabs <nabil@instafork.com> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: AidenGeunGeun <eastlandwyvern@gmail.com> Co-authored-by: Mark <geraint0923@users.noreply.github.com>
This commit is contained in:
parent
26e27c7a7f
commit
634386fe0f
116 changed files with 3764 additions and 1016 deletions
|
|
@ -698,3 +698,104 @@ body[data-new-layout] #terminal-panel [data-component="tabs"][data-variant="norm
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
body[data-new-layout] #review-panel [data-component="tabs"][data-variant="normal"][data-orientation="horizontal"] {
|
||||
[data-slot="tabs-list"] {
|
||||
height: 52px;
|
||||
padding-right: 12px;
|
||||
gap: 8px;
|
||||
--tabs-review-fade: 8px;
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger-wrapper"] {
|
||||
height: 28px;
|
||||
padding-inline: 10px;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger"] {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
&:has([data-slot="tabs-trigger-close-button"]) {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
&:has([data-selected]) {
|
||||
background-color: var(--v2-background-bg-layer-02);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="tabs-trigger"] {
|
||||
font-family: Inter, sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 440;
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
letter-spacing: -0.04px;
|
||||
color: var(--v2-text-text-muted);
|
||||
font-variation-settings: "slnt" 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
&[data-selected] {
|
||||
color: var(--v2-text-text-base);
|
||||
}
|
||||
|
||||
.tab-fileicon-color,
|
||||
.tab-fileicon-mono,
|
||||
[data-slot="icon-svg"] {
|
||||
margin-left: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Preview/temporary tabs: beat trigger + .text-14-medium upright defaults */
|
||||
[data-slot="tabs-trigger"] .italic {
|
||||
font-style: italic !important;
|
||||
font-variation-settings: "slnt" -10 !important;
|
||||
}
|
||||
}
|
||||
|
||||
body[data-new-layout] #review-panel [data-component="tabs"] .session-review-v2-tabs-bar {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
gap: 8px;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--border-weaker-base, var(--v2-border-border-weak));
|
||||
background-color: var(--v2-background-bg-base);
|
||||
}
|
||||
|
||||
body[data-new-layout] #review-panel [data-component="tabs"] .session-review-v2-tabs-bar [data-slot="tabs-list"],
|
||||
body[data-new-layout]
|
||||
#review-panel
|
||||
[data-component="tabs"][data-variant="normal"][data-orientation="horizontal"]
|
||||
.session-review-v2-tabs-bar
|
||||
[data-slot="tabs-list"] {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
body[data-new-layout]
|
||||
#review-panel
|
||||
[data-component="tabs"]
|
||||
.session-review-v2-tabs-bar
|
||||
[data-slot="tabs-list"]
|
||||
> .sticky {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
body[data-new-layout] #review-panel [data-component="tabs"] .session-review-v2-open-in-app-slot {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
body[data-new-layout] #review-panel [data-component="tabs"] .session-review-v2-open-in-app {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
|
|||
13
packages/ui/src/context/marked-code-span.test.ts
Normal file
13
packages/ui/src/context/marked-code-span.test.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { Marked } from "marked"
|
||||
import { markedCodeSpanBoundary } from "./marked-code-span"
|
||||
|
||||
test("preserves code spans adjacent to tildes", async () => {
|
||||
const marked = new Marked(markedCodeSpanBoundary)
|
||||
|
||||
expect(await marked.parse("~`0.1576` to measurement-window-only `0.00092`")).toBe(
|
||||
"<p>~<code>0.1576</code> to measurement-window-only <code>0.00092</code></p>\n",
|
||||
)
|
||||
expect(await marked.parse("`before`~`after`")).toBe("<p><code>before</code>~<code>after</code></p>\n")
|
||||
expect(await marked.parse("~~`deleted code`~~")).toBe("<p><del><code>deleted code</code></del></p>\n")
|
||||
})
|
||||
17
packages/ui/src/context/marked-code-span.ts
Normal file
17
packages/ui/src/context/marked-code-span.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { MarkedExtension } from "marked"
|
||||
|
||||
// Keep adjacent tilde and backtick runs separate until markedjs/marked#4011 is released.
|
||||
export const markedCodeSpanBoundary = {
|
||||
tokenizer: {
|
||||
inlineText(src) {
|
||||
const match = /^(`+(?=~)|~+(?=`))/.exec(src)
|
||||
if (!match) return false
|
||||
return {
|
||||
type: "text",
|
||||
raw: match[0],
|
||||
text: match[0],
|
||||
escaped: this.lexer.state.inRawBlock,
|
||||
}
|
||||
},
|
||||
},
|
||||
} satisfies MarkedExtension
|
||||
|
|
@ -3,6 +3,7 @@ import markedShiki from "marked-shiki"
|
|||
import katex from "katex"
|
||||
import { bundledLanguages, type BundledLanguage } from "shiki"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { markedCodeSpanBoundary } from "./marked-code-span"
|
||||
import { getSharedHighlighter, registerCustomTheme, ThemeRegistrationResolved } from "@pierre/diffs"
|
||||
|
||||
export const OpenCodeTheme = {
|
||||
|
|
@ -521,6 +522,7 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext(
|
|||
name: "Marked",
|
||||
init: (props: { nativeParser?: NativeMarkdownParser }) => {
|
||||
const jsParser = marked.use(
|
||||
markedCodeSpanBoundary,
|
||||
{
|
||||
renderer: {
|
||||
link({ href, title, text }) {
|
||||
|
|
|
|||
|
|
@ -14,12 +14,14 @@
|
|||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
padding-right: 8px;
|
||||
overflow: visible;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background-color: transparent;
|
||||
color: var(--v2-text-text-muted);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
scroll-margin-block: 8px;
|
||||
transition:
|
||||
background-color 120ms ease,
|
||||
color 120ms ease;
|
||||
|
|
@ -42,6 +44,21 @@
|
|||
background-color: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-component="file-tree-v2"] [data-slot="file-tree-v2-guide"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
margin-top: -2px;
|
||||
height: calc(100% + 2px);
|
||||
width: 1px;
|
||||
pointer-events: none;
|
||||
background-color: var(--v2-border-border-muted);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-component="file-tree-v2"]:hover [data-slot="file-tree-v2-guide"] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
[data-component="file-tree-v2"] .filetree-icon--mono {
|
||||
color: var(--v2-icon-icon-muted);
|
||||
}
|
||||
|
|
@ -108,7 +125,7 @@
|
|||
}
|
||||
|
||||
[data-component="file-tree-v2"] [data-slot="file-tree-v2-change"][data-change="modified"] {
|
||||
opacity: 0;
|
||||
color: var(--v2-state-fg-info);
|
||||
}
|
||||
|
||||
[data-component="file-tree-v2"] [data-slot="file-tree-v2-change"][data-change="added"] {
|
||||
|
|
|
|||
|
|
@ -89,6 +89,10 @@ const icons = {
|
|||
viewBox: "0 0 16 16",
|
||||
body: `<path d="M4.25 11.75L11.75 4.25M11.75 11.75L4.25 4.25" stroke="currentColor"/>`,
|
||||
},
|
||||
"outline-xmark": {
|
||||
viewBox: "0 0 16 16",
|
||||
body: `<path fill-rule="evenodd" clip-rule="evenodd" d="M4.99487 5.70186L7.29297 7.99995L4.99487 10.2981L5.70198 11.0052L8.00008 8.70706L10.2982 11.0052L11.0053 10.2981L8.70718 7.99995L11.0053 5.70186L10.2982 4.99475L8.00008 7.29285L5.70198 4.99475L4.99487 5.70186Z" fill="currentColor"/>`,
|
||||
},
|
||||
"outline-chevron-down": {
|
||||
viewBox: "0 0 16 16",
|
||||
body: `<path d="M5 6.5L8 9.5L11 6.5" stroke="currentColor"/>`,
|
||||
|
|
@ -129,6 +133,10 @@ const icons = {
|
|||
viewBox: "0 0 16 16",
|
||||
body: `<path d="M13.5555 6.66656V2.44434H9.33326M13.5555 2.44434L7.99993 7.99989M13.5555 9.33324V13.5555C13.5555 13.5555 12.7599 13.5555 11.7777 13.5555H2.44438C2.44438 13.5555 2.44438 12.7599 2.44438 11.7777V4.22213C2.44438 3.2399 2.44434 2.44435 2.44434 2.44435H6.66661" stroke="currentColor"/>`,
|
||||
},
|
||||
"outline-share": {
|
||||
viewBox: "0 0 16 16",
|
||||
body: `<path d="M13.5554 10.4445V13.5556C13.5554 13.5556 12.7599 13.5556 11.7777 13.5556H4.22211C3.23989 13.5556 2.44434 13.5556 2.44434 13.5556V10.4445M4.88878 5.55557L7.99989 2.44446L11.111 5.55557M7.99989 2.44446L7.99989 9.11112" stroke="currentColor"/>`,
|
||||
},
|
||||
reset: {
|
||||
viewBox: "0 0 20 20",
|
||||
body: `<path d="M5.83333 4.16406L2.5 7.4974L5.83333 10.8307M3.33333 7.4974H17.9167V15.4141H10" stroke="currentColor" stroke-linecap="square"/>`,
|
||||
|
|
|
|||
|
|
@ -203,3 +203,56 @@
|
|||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-component="line-comment-v2"][data-variant="editor"] [data-slot="line-comment-v2-mention-list"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
margin-top: 8px;
|
||||
padding: 4px;
|
||||
border: 0.5px solid var(--v2-border-border-muted);
|
||||
border-radius: 6px;
|
||||
background: var(--v2-background-bg-layer-02);
|
||||
}
|
||||
|
||||
[data-component="line-comment-v2"][data-variant="editor"] [data-slot="line-comment-v2-mention-item"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 6px 8px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--v2-text-text-base);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-component="line-comment-v2"][data-variant="editor"] [data-slot="line-comment-v2-mention-item"][data-active] {
|
||||
background: var(--v2-background-bg-layer-03);
|
||||
}
|
||||
|
||||
[data-component="line-comment-v2"][data-variant="editor"] [data-slot="line-comment-v2-mention-path"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
[data-component="line-comment-v2"][data-variant="editor"] [data-slot="line-comment-v2-mention-dir"] {
|
||||
min-width: 0;
|
||||
color: var(--v2-text-text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
[data-component="line-comment-v2"][data-variant="editor"] [data-slot="line-comment-v2-mention-file"] {
|
||||
color: var(--v2-text-text-base);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { type ComponentProps, type JSX, Show, onMount, splitProps } from "solid-js"
|
||||
import { For, Show, createSignal, onMount, splitProps, type ComponentProps, type JSX } from "solid-js"
|
||||
import { FileIcon } from "../../components/file-icon"
|
||||
import { useFilteredList } from "../../hooks"
|
||||
import { ButtonV2 } from "./button-v2"
|
||||
import "./line-comment-v2.css"
|
||||
|
||||
|
|
@ -53,6 +55,10 @@ export function LineCommentV2(props: LineCommentV2Props) {
|
|||
)
|
||||
}
|
||||
|
||||
export type LineCommentEditorV2Mention = {
|
||||
items: (query: string) => string[] | Promise<string[]>
|
||||
}
|
||||
|
||||
export interface LineCommentEditorV2Props extends Omit<ComponentProps<"div">, "children" | "onInput" | "onSubmit"> {
|
||||
/** Visible field label above the textarea (default: “Comment”). */
|
||||
heading?: JSX.Element | string
|
||||
|
|
@ -66,10 +72,22 @@ export interface LineCommentEditorV2Props extends Omit<ComponentProps<"div">, "c
|
|||
cancelLabel?: string
|
||||
submitLabel?: string
|
||||
autofocus?: boolean
|
||||
mention?: LineCommentEditorV2Mention
|
||||
}
|
||||
|
||||
function pathFilename(path: string) {
|
||||
const index = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))
|
||||
return index === -1 ? path : path.slice(index + 1)
|
||||
}
|
||||
|
||||
function pathDirectory(path: string) {
|
||||
const index = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))
|
||||
return index === -1 ? "" : path.slice(0, index + 1)
|
||||
}
|
||||
|
||||
export function LineCommentEditorV2(props: LineCommentEditorV2Props) {
|
||||
let textareaRef: HTMLTextAreaElement | undefined
|
||||
const [mentionOpen, setMentionOpen] = createSignal(false)
|
||||
|
||||
const [local, rest] = splitProps(props, [
|
||||
"heading",
|
||||
|
|
@ -83,6 +101,7 @@ export function LineCommentEditorV2(props: LineCommentEditorV2Props) {
|
|||
"cancelLabel",
|
||||
"submitLabel",
|
||||
"autofocus",
|
||||
"mention",
|
||||
"class",
|
||||
"classList",
|
||||
])
|
||||
|
|
@ -90,6 +109,78 @@ export function LineCommentEditorV2(props: LineCommentEditorV2Props) {
|
|||
const heading = () => local.heading ?? "Comment"
|
||||
const canSubmit = () => local.value.trim().length > 0
|
||||
|
||||
const closeMention = () => {
|
||||
setMentionOpen(false)
|
||||
mention.clear()
|
||||
}
|
||||
|
||||
const currentMention = () => {
|
||||
const textarea = textareaRef
|
||||
if (!textarea) return
|
||||
if (!local.mention) return
|
||||
if (textarea.selectionStart !== textarea.selectionEnd) return
|
||||
|
||||
const end = textarea.selectionStart
|
||||
const match = textarea.value.slice(0, end).match(/@(\S*)$/)
|
||||
if (!match) return
|
||||
|
||||
return {
|
||||
query: match[1] ?? "",
|
||||
start: end - match[0].length,
|
||||
end,
|
||||
}
|
||||
}
|
||||
|
||||
function selectMention(item: { path: string } | undefined) {
|
||||
if (!item) return
|
||||
|
||||
const textarea = textareaRef
|
||||
const query = currentMention()
|
||||
if (!textarea || !query) return
|
||||
|
||||
const value = `${textarea.value.slice(0, query.start)}@${item.path} ${textarea.value.slice(query.end)}`
|
||||
const cursor = query.start + item.path.length + 2
|
||||
|
||||
local.onInput(value)
|
||||
closeMention()
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
textarea.focus()
|
||||
textarea.setSelectionRange(cursor, cursor)
|
||||
})
|
||||
}
|
||||
|
||||
const mention = useFilteredList<{ path: string }>({
|
||||
items: async (query) => {
|
||||
if (!local.mention) return []
|
||||
if (!query.trim()) return []
|
||||
const paths = await local.mention.items(query)
|
||||
return paths.map((path) => ({ path }))
|
||||
},
|
||||
key: (item) => item.path,
|
||||
filterKeys: ["path"],
|
||||
skipFilter: () => true,
|
||||
onSelect: selectMention,
|
||||
})
|
||||
|
||||
const syncMention = () => {
|
||||
const item = currentMention()
|
||||
if (!item) {
|
||||
closeMention()
|
||||
return
|
||||
}
|
||||
|
||||
setMentionOpen(true)
|
||||
mention.onInput(item.query)
|
||||
}
|
||||
|
||||
const selectActiveMention = () => {
|
||||
const items = mention.flat()
|
||||
if (items.length === 0) return
|
||||
const active = mention.active()
|
||||
selectMention(items.find((item) => item.path === active) ?? items[0])
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
const v = local.value.trim()
|
||||
if (!v) return
|
||||
|
|
@ -122,9 +213,39 @@ export function LineCommentEditorV2(props: LineCommentEditorV2Props) {
|
|||
rows={local.rows ?? 3}
|
||||
placeholder={local.placeholder ?? "Add context for this change"}
|
||||
value={local.value}
|
||||
onInput={(e) => local.onInput(e.currentTarget.value)}
|
||||
onInput={(e) => {
|
||||
local.onInput(e.currentTarget.value)
|
||||
syncMention()
|
||||
}}
|
||||
onClick={() => syncMention()}
|
||||
onSelect={() => syncMention()}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation()
|
||||
if (e.isComposing || e.keyCode === 229) return
|
||||
|
||||
if (mentionOpen()) {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
closeMention()
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === "Tab") {
|
||||
if (mention.flat().length === 0) return
|
||||
e.preventDefault()
|
||||
selectActiveMention()
|
||||
return
|
||||
}
|
||||
|
||||
const nav = e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Enter"
|
||||
const ctrlNav = e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && (e.key === "n" || e.key === "p")
|
||||
if ((nav || ctrlNav) && mention.flat().length > 0) {
|
||||
mention.onKeyDown(e)
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault()
|
||||
e.currentTarget.blur()
|
||||
|
|
@ -137,6 +258,34 @@ export function LineCommentEditorV2(props: LineCommentEditorV2Props) {
|
|||
}
|
||||
}}
|
||||
/>
|
||||
<Show when={mentionOpen() && mention.flat().length > 0}>
|
||||
<div data-slot="line-comment-v2-mention-list">
|
||||
<For each={mention.flat().slice(0, 10)}>
|
||||
{(item) => {
|
||||
const directory = item.path.endsWith("/") ? item.path : pathDirectory(item.path)
|
||||
const name = item.path.endsWith("/") ? "" : pathFilename(item.path)
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-slot="line-comment-v2-mention-item"
|
||||
data-active={mention.active() === item.path ? "" : undefined}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onMouseEnter={() => mention.setActive(item.path)}
|
||||
onClick={() => selectMention(item)}
|
||||
>
|
||||
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-4" />
|
||||
<div data-slot="line-comment-v2-mention-path">
|
||||
<span data-slot="line-comment-v2-mention-dir">{directory}</span>
|
||||
<Show when={name}>
|
||||
<span data-slot="line-comment-v2-mention-file">{name}</span>
|
||||
</Show>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div data-slot="line-comment-v2-footer">
|
||||
<div data-slot="line-comment-v2-footer-meta">{local.selection}</div>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ export interface ProjectAvatarProps extends ComponentProps<"div"> {
|
|||
|
||||
export function ProjectAvatar(props: ProjectAvatarProps) {
|
||||
const [split, rest] = splitProps(props, ["fallback", "src", "variant", "unread", "class", "classList", "style"])
|
||||
const src = split.src
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
|
|
@ -53,9 +52,9 @@ export function ProjectAvatar(props: ProjectAvatarProps) {
|
|||
<div
|
||||
data-slot="project-avatar-surface"
|
||||
data-variant={split.variant ?? "gray"}
|
||||
data-has-image={src ? "" : undefined}
|
||||
data-has-image={split.src ? "" : undefined}
|
||||
>
|
||||
<Show when={src} fallback={first(split.fallback)}>
|
||||
<Show when={split.src} fallback={first(split.fallback)}>
|
||||
{(value) => <img src={value()} draggable={false} data-slot="project-avatar-image" />}
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
99
packages/ui/src/v2/components/split-button-v2.css
Normal file
99
packages/ui/src/v2/components/split-button-v2.css
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
[data-component="split-button-v2"] {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
flex-shrink: 0;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-component="split-button-v2"]:is(:hover, :has([data-component="split-button-v2-menu-trigger"][data-expanded])) {
|
||||
box-shadow: inset 0 0 0 1px var(--v2-border-border-muted);
|
||||
}
|
||||
|
||||
[data-component="split-button-v2-action"],
|
||||
[data-component="split-button-v2-menu-trigger"] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 28px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--v2-icon-icon-base);
|
||||
cursor: default;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-component="split-button-v2-action"] {
|
||||
width: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-component="split-button-v2-action"]:hover:not(:disabled) {
|
||||
background-color: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
[data-component="split-button-v2-action"]:is(:active, [data-pressed]):not(:disabled) {
|
||||
background-color: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-component="split-button-v2-action"]:is(:focus-visible, [data-focus]) {
|
||||
outline: 2px solid var(--v2-border-border-focus);
|
||||
outline-offset: -2px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
[data-component="split-button-v2-action"]:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
[data-component="split-button-v2-menu-trigger"] {
|
||||
width: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-component="split-button-v2-menu-trigger"]:is(:hover, [data-expanded]):not(:disabled) {
|
||||
background-color: var(--v2-overlay-simple-overlay-hover);
|
||||
}
|
||||
|
||||
[data-component="split-button-v2-menu-trigger"]:is(:active, [data-pressed]):not(:disabled) {
|
||||
background-color: var(--v2-overlay-simple-overlay-pressed);
|
||||
}
|
||||
|
||||
[data-component="split-button-v2-menu-trigger"]:is(:focus-visible, [data-focus]) {
|
||||
outline: 2px solid var(--v2-border-border-focus);
|
||||
outline-offset: -2px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
[data-component="split-button-v2-menu-trigger"]:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
[data-component="split-button-v2-action"] [data-component="app-icon"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
[data-component="menu-v2-content"].open-in-app-v2-menu [data-component="app-icon"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-component="menu-v2-content"].open-in-app-v2-menu [data-component="icon"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-component="menu-v2-content"].open-in-app-v2-menu [data-component="icon"] [data-slot="icon-svg"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
48
packages/ui/src/v2/components/split-button-v2.tsx
Normal file
48
packages/ui/src/v2/components/split-button-v2.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { splitProps, type ComponentProps, type ParentProps } from "solid-js"
|
||||
import "./split-button-v2.css"
|
||||
|
||||
export function SplitButtonV2(props: ParentProps<ComponentProps<"div">>) {
|
||||
const [split, rest] = splitProps(props, ["class", "classList", "children"])
|
||||
return (
|
||||
<div
|
||||
data-component="split-button-v2"
|
||||
classList={{
|
||||
...split.classList,
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{split.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SplitButtonV2Action(props: ComponentProps<"button">) {
|
||||
const [split, rest] = splitProps(props, ["class", "classList"])
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-component="split-button-v2-action"
|
||||
classList={{
|
||||
...split.classList,
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function SplitButtonV2MenuTrigger(props: ComponentProps<"button">) {
|
||||
const [split, rest] = splitProps(props, ["class", "classList"])
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-component="split-button-v2-menu-trigger"
|
||||
classList={{
|
||||
...split.classList,
|
||||
[split.class ?? ""]: !!split.class,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue