@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
|
@@ -21,15 +21,18 @@ export function TextStrikethrough(props: {
|
|
|
21
21
|
|
|
22
22
|
let baseRef: HTMLSpanElement | undefined
|
|
23
23
|
let containerRef: HTMLSpanElement | undefined
|
|
24
|
+
|
|
24
25
|
const [state, setState] = createStore({
|
|
25
26
|
textWidth: 0,
|
|
26
27
|
containerWidth: 0,
|
|
27
28
|
})
|
|
29
|
+
|
|
28
30
|
const textWidth = () => state.textWidth
|
|
29
31
|
const containerWidth = () => state.containerWidth
|
|
30
32
|
|
|
31
33
|
const measure = () => {
|
|
32
34
|
if (baseRef) setState("textWidth", baseRef.scrollWidth)
|
|
35
|
+
|
|
33
36
|
if (containerRef) setState("containerWidth", containerRef.offsetWidth)
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -39,6 +42,7 @@ export function TextStrikethrough(props: {
|
|
|
39
42
|
// Revealed pixels from left = progress * textWidth
|
|
40
43
|
const revealedPx = () => {
|
|
41
44
|
const tw = textWidth()
|
|
45
|
+
|
|
42
46
|
return tw > 0 ? progress() * tw : 0
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -46,15 +50,19 @@ export function TextStrikethrough(props: {
|
|
|
46
50
|
const overlayClip = () => {
|
|
47
51
|
const cw = containerWidth()
|
|
48
52
|
const tw = textWidth()
|
|
53
|
+
|
|
49
54
|
if (cw <= 0 || tw <= 0) return `inset(0 ${(1 - progress()) * 100}% 0 0)`
|
|
50
55
|
const remaining = Math.max(0, cw - revealedPx())
|
|
56
|
+
|
|
51
57
|
return `inset(0 ${remaining}px 0 0)`
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
// Base clip: hide everything to the left of revealed area (complementary)
|
|
55
61
|
const baseClip = () => {
|
|
56
62
|
const px = revealedPx()
|
|
63
|
+
|
|
57
64
|
if (px <= 0.5) return "none"
|
|
65
|
+
|
|
58
66
|
return `inset(0 0 0 ${px}px)`
|
|
59
67
|
}
|
|
60
68
|
|
|
@@ -11,6 +11,7 @@ export const Typewriter = <T extends ValidComponent = "p">(props: { text?: strin
|
|
|
11
11
|
|
|
12
12
|
createEffect(() => {
|
|
13
13
|
const text = props.text
|
|
14
|
+
|
|
14
15
|
if (!text) return
|
|
15
16
|
|
|
16
17
|
let i = 0
|
|
@@ -21,8 +22,11 @@ export const Typewriter = <T extends ValidComponent = "p">(props: { text?: strin
|
|
|
21
22
|
|
|
22
23
|
const getTypingDelay = () => {
|
|
23
24
|
const random = Math.random()
|
|
25
|
+
|
|
24
26
|
if (random < 0.05) return 150 + Math.random() * 100
|
|
27
|
+
|
|
25
28
|
if (random < 0.15) return 80 + Math.random() * 60
|
|
29
|
+
|
|
26
30
|
return 30 + Math.random() * 50
|
|
27
31
|
}
|
|
28
32
|
|
package/src/context/dialog.tsx
CHANGED
|
@@ -28,6 +28,7 @@ type Active = {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const Context = createContext<ReturnType<typeof init>>()
|
|
31
|
+
|
|
31
32
|
// Lets the dialog rendered in a layer opt out of closing on a backdrop click.
|
|
32
33
|
const LayerContext = createContext<{ setBackdropDismiss: (value: boolean) => void }>()
|
|
33
34
|
|
|
@@ -70,6 +71,7 @@ function init() {
|
|
|
70
71
|
current.id,
|
|
71
72
|
setTimeout(() => {
|
|
72
73
|
closing.delete(current.id)
|
|
74
|
+
|
|
73
75
|
if (closing.size === 0) lock.value = false
|
|
74
76
|
setExiting((ids) => new Set([...ids].filter((id) => id !== current.id)))
|
|
75
77
|
setStack((items) => items.filter((item) => item.id !== current.id))
|
|
@@ -84,6 +86,7 @@ function init() {
|
|
|
84
86
|
/** Programmatic close. Without an id it closes the top dialog, one at a time; with an id it never waits. */
|
|
85
87
|
const close = (id?: string) => {
|
|
86
88
|
const current = id ? stack().find((item) => item.id === id) : stack().at(-1)
|
|
89
|
+
|
|
87
90
|
if (!current || closing.has(current.id) || (!id && lock.value)) return
|
|
88
91
|
closing.set(current.id, undefined)
|
|
89
92
|
lock.value = true
|
|
@@ -93,6 +96,7 @@ function init() {
|
|
|
93
96
|
/** Escape, a backdrop click, or Kobalte dismissing: only the top dialog, and one per exit animation. */
|
|
94
97
|
const dismiss = (id?: string) => {
|
|
95
98
|
const current = stack().at(-1)
|
|
99
|
+
|
|
96
100
|
if (!current || (id && current.id !== id) || closing.has(current.id) || lock.value) return
|
|
97
101
|
closing.set(current.id, undefined)
|
|
98
102
|
lock.value = true
|
|
@@ -116,12 +120,14 @@ function init() {
|
|
|
116
120
|
// A deferred open (e.g. from a focus callback) must not mount after the provider is gone.
|
|
117
121
|
if (state.disposed) return
|
|
118
122
|
const id = key ?? Math.random().toString(36).slice(2)
|
|
123
|
+
|
|
119
124
|
// The layer follows the dialog's current place in the stack, so a new top dialog always renders above.
|
|
120
125
|
const layer = () =>
|
|
121
126
|
Math.max(
|
|
122
127
|
0,
|
|
123
128
|
stack().findIndex((item) => item.id === id),
|
|
124
129
|
)
|
|
130
|
+
|
|
125
131
|
const zIndex = () => String(50 + layer() * 10)
|
|
126
132
|
let dispose: (() => void) | undefined
|
|
127
133
|
let setClosing: ((closing: boolean) => void) | undefined
|
|
@@ -133,6 +139,7 @@ function init() {
|
|
|
133
139
|
const [closing, setClosingSignal] = createSignal(false)
|
|
134
140
|
const [backdropDismiss, setBackdropDismiss] = createSignal(true)
|
|
135
141
|
setClosing = setClosingSignal
|
|
142
|
+
|
|
136
143
|
return (
|
|
137
144
|
<Kobalte
|
|
138
145
|
modal={stack().findLast((item) => !exiting().has(item.id))?.id === id}
|
|
@@ -193,6 +200,7 @@ function init() {
|
|
|
193
200
|
push,
|
|
194
201
|
}
|
|
195
202
|
}
|
|
203
|
+
|
|
196
204
|
/** Runs a dialog callback so its throw is reported without stopping the caller's cleanup. */
|
|
197
205
|
function isolate(fn: () => void) {
|
|
198
206
|
try {
|
|
@@ -204,6 +212,7 @@ function isolate(fn: () => void) {
|
|
|
204
212
|
|
|
205
213
|
export function DialogProvider(props: ParentProps) {
|
|
206
214
|
const ctx = init()
|
|
215
|
+
|
|
207
216
|
return (
|
|
208
217
|
<Context.Provider value={ctx}>
|
|
209
218
|
{props.children}
|
|
@@ -221,6 +230,7 @@ export function useDialog() {
|
|
|
221
230
|
if (!owner) {
|
|
222
231
|
throw new Error("useDialog must be used within a DialogProvider")
|
|
223
232
|
}
|
|
233
|
+
|
|
224
234
|
if (!ctx) {
|
|
225
235
|
throw new Error("useDialog must be used within a DialogProvider")
|
|
226
236
|
}
|
|
@@ -235,6 +245,7 @@ export function useDialog() {
|
|
|
235
245
|
*/
|
|
236
246
|
show(element: DialogElement, onClose?: () => void, id?: string, signal?: AbortSignal) {
|
|
237
247
|
const base = ctx.stack().at(-1)?.owner ?? owner
|
|
248
|
+
|
|
238
249
|
return startTransition(() => {
|
|
239
250
|
if (signal?.aborted) return isolate(() => onClose?.())
|
|
240
251
|
ctx.show(element, base, onClose, id)
|
|
@@ -242,6 +253,7 @@ export function useDialog() {
|
|
|
242
253
|
},
|
|
243
254
|
push(element: DialogElement, onClose?: () => void, id?: string, signal?: AbortSignal) {
|
|
244
255
|
const base = ctx.stack().at(-1)?.owner ?? owner
|
|
256
|
+
|
|
245
257
|
return startTransition(() => {
|
|
246
258
|
if (signal?.aborted) return isolate(() => onClose?.())
|
|
247
259
|
ctx.push(element, base, onClose, id)
|
package/src/context/file.tsx
CHANGED
package/src/context/helper.tsx
CHANGED
|
@@ -21,8 +21,10 @@ export function createSimpleContext<T, Props extends Record<string, any>>(
|
|
|
21
21
|
const isReady = createMemo(() => {
|
|
22
22
|
// @ts-expect-error
|
|
23
23
|
const ready = init.ready as Accessor<boolean> | boolean | undefined
|
|
24
|
+
|
|
24
25
|
return ready === undefined || (typeof ready === "function" ? ready() : ready)
|
|
25
26
|
})
|
|
27
|
+
|
|
26
28
|
return (
|
|
27
29
|
<Show when={isReady()}>
|
|
28
30
|
<ctx.Provider value={init}>{props.children}</ctx.Provider>
|
|
@@ -31,7 +33,9 @@ export function createSimpleContext<T, Props extends Record<string, any>>(
|
|
|
31
33
|
},
|
|
32
34
|
use() {
|
|
33
35
|
const value = useContext(ctx)
|
|
36
|
+
|
|
34
37
|
if (!value) throw new Error(`${input.name} context must be used within a context provider`)
|
|
38
|
+
|
|
35
39
|
return value
|
|
36
40
|
},
|
|
37
41
|
}
|
package/src/context/i18n.tsx
CHANGED
|
@@ -4,14 +4,21 @@ import { dict as en } from "../i18n/en"
|
|
|
4
4
|
import type { Key, LocaleKey, PluralCategory, PluralKey, PluralLookupKey } from "../i18n/en"
|
|
5
5
|
|
|
6
6
|
export type UiI18nKey = Key
|
|
7
|
+
|
|
7
8
|
export type UiI18nPluralKey = PluralKey
|
|
9
|
+
|
|
8
10
|
export type UiPluralCategory = PluralCategory
|
|
11
|
+
|
|
9
12
|
export type UiI18nPluralLookupKey = PluralLookupKey
|
|
13
|
+
|
|
10
14
|
export type UiI18nLocaleKey = LocaleKey
|
|
15
|
+
|
|
11
16
|
type UiTranslationKey<Value extends string> = Value extends UiI18nPluralLookupKey ? never : Value
|
|
17
|
+
|
|
12
18
|
export type UiI18nOrdinaryKey = UiTranslationKey<UiI18nKey>
|
|
13
19
|
|
|
14
20
|
export type UiI18nParams = Record<string, string | number | boolean>
|
|
21
|
+
|
|
15
22
|
export type UiTranslate = <Value extends string>(key: UiTranslationKey<Value>, params?: UiI18nParams) => string
|
|
16
23
|
|
|
17
24
|
export type UiI18nSource = {
|
|
@@ -33,10 +40,13 @@ const rules = new Map<string, Intl.PluralRules>()
|
|
|
33
40
|
|
|
34
41
|
export function pluralCategory(locale: string, count: number): UiPluralCategory {
|
|
35
42
|
const cached = rules.get(locale)
|
|
43
|
+
|
|
36
44
|
if (cached) return cached.select(count)
|
|
37
45
|
const next = new Intl.PluralRules(locale)
|
|
46
|
+
|
|
38
47
|
if (rules.size >= 32) rules.delete(rules.keys().next().value!)
|
|
39
48
|
rules.set(locale, next)
|
|
49
|
+
|
|
40
50
|
return next.select(count)
|
|
41
51
|
}
|
|
42
52
|
|
|
@@ -46,13 +56,17 @@ export function pluralKey(key: UiI18nPluralKey, category: UiPluralCategory) {
|
|
|
46
56
|
|
|
47
57
|
export function localizedListSeparator(locale: string, index: number, count: number) {
|
|
48
58
|
if (index <= 0 || index >= count) return ""
|
|
59
|
+
|
|
49
60
|
const parts = new Intl.ListFormat(locale, { style: "long", type: "conjunction" }).formatToParts(
|
|
50
61
|
Array.from({ length: count }, (_, item) => String(item)),
|
|
51
62
|
)
|
|
63
|
+
|
|
52
64
|
const current = parts.findIndex((part) => part.type === "element" && part.value === String(index))
|
|
65
|
+
|
|
53
66
|
const previous = parts.findLastIndex(
|
|
54
67
|
(part, partIndex) => partIndex < current && part.type === "element" && part.value === String(index - 1),
|
|
55
68
|
)
|
|
69
|
+
|
|
56
70
|
return parts
|
|
57
71
|
.slice(previous + 1, current)
|
|
58
72
|
.map((part) => part.value)
|
|
@@ -61,9 +75,11 @@ export function localizedListSeparator(locale: string, index: number, count: num
|
|
|
61
75
|
|
|
62
76
|
function resolveTemplate(text: string, params?: UiI18nParams) {
|
|
63
77
|
if (!params) return text
|
|
78
|
+
|
|
64
79
|
return text.replace(/{{\s*([^}]+?)\s*}}/g, (_, rawKey) => {
|
|
65
80
|
const key = String(rawKey)
|
|
66
81
|
const value = params[key]
|
|
82
|
+
|
|
67
83
|
return value === undefined ? "" : String(value)
|
|
68
84
|
})
|
|
69
85
|
}
|
|
@@ -91,6 +107,7 @@ const fallbackSource: UiI18nSource = {
|
|
|
91
107
|
locale: () => "en",
|
|
92
108
|
t: (key, params) => {
|
|
93
109
|
const value = en[key as UiI18nKey] ?? String(key)
|
|
110
|
+
|
|
94
111
|
return resolveTemplate(value, params)
|
|
95
112
|
},
|
|
96
113
|
plural: (key, count, params) =>
|
|
@@ -98,15 +115,18 @@ const fallbackSource: UiI18nSource = {
|
|
|
98
115
|
pluralForm: (key, category, params) => {
|
|
99
116
|
const values = en as Partial<Record<UiI18nLocaleKey, string>>
|
|
100
117
|
const value = values[pluralKey(key, category)] ?? values[`${key}.other`] ?? `${key}.other`
|
|
118
|
+
|
|
101
119
|
return resolveTemplate(value, params)
|
|
102
120
|
},
|
|
103
121
|
}
|
|
122
|
+
|
|
104
123
|
const fallback = createUiI18n(fallbackSource)
|
|
105
124
|
|
|
106
125
|
const Context = createContext<UiI18n>(fallback)
|
|
107
126
|
|
|
108
127
|
function UiI18nProvider(props: ParentProps<{ value: UiI18nSource }>) {
|
|
109
128
|
const value = createUiI18n(props.value)
|
|
129
|
+
|
|
110
130
|
return (
|
|
111
131
|
<I18nProvider locale={(value.layoutLocale ?? value.locale)()}>
|
|
112
132
|
<Context.Provider value={value}>{props.children}</Context.Provider>
|
package/src/context/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export function createMarkdownBase() {
|
|
|
5
5
|
renderer: {
|
|
6
6
|
link({ href, title, text }) {
|
|
7
7
|
const titleAttr = title ? ` title="${title}"` : ""
|
|
8
|
+
|
|
8
9
|
return `<a href="${href}"${titleAttr} class="external-link" target="_blank" rel="noopener noreferrer">${text}</a>`
|
|
9
10
|
},
|
|
10
11
|
},
|
|
@@ -15,7 +16,9 @@ let smallParser: Marked | undefined
|
|
|
15
16
|
|
|
16
17
|
export function parseSmallMarkdown(text: string) {
|
|
17
18
|
// Any possible KaTeX delimiter stays on the worker, including escaped ones.
|
|
18
|
-
|
|
19
|
+
// Dollar math needs a pair, so a lone price such as `$5` can stay on this path.
|
|
20
|
+
if (text.length > 1024 || text.includes("\\(") || text.indexOf("$") !== text.lastIndexOf("$")) return
|
|
21
|
+
|
|
19
22
|
// Ordinary prose does not need the block/inline lexer's cold regular expressions.
|
|
20
23
|
// Keep every possible Markdown construct, autolink, and hard break on that lexer.
|
|
21
24
|
if (
|
|
@@ -30,8 +33,10 @@ export function parseSmallMarkdown(text: string) {
|
|
|
30
33
|
.map((paragraph) => `<p>${paragraph.replace(/["']/g, (value) => (value === '"' ? """ : "'"))}</p>\n`)
|
|
31
34
|
.join("")
|
|
32
35
|
}
|
|
36
|
+
|
|
33
37
|
const parser = (smallParser ??= createMarkdownBase())
|
|
34
38
|
const paragraphs = text.replace(/\r\n?/g, "\n")
|
|
39
|
+
|
|
35
40
|
// Inline formatting in ordinary paragraphs does not need the block lexer.
|
|
36
41
|
// Any possible block opener, HTML, table, or reference definition stays on it.
|
|
37
42
|
if (
|
|
@@ -45,11 +50,14 @@ export function parseSmallMarkdown(text: string) {
|
|
|
45
50
|
.map((paragraph) => `<p>${parser.parseInline(paragraph, { async: false })}</p>\n`)
|
|
46
51
|
.join("")
|
|
47
52
|
}
|
|
53
|
+
|
|
48
54
|
const tokens = parser.lexer(text)
|
|
49
55
|
let code = false
|
|
50
56
|
parser.walkTokens(tokens, (token) => {
|
|
51
57
|
if (token.type === "code") code = true
|
|
52
58
|
})
|
|
59
|
+
|
|
53
60
|
if (code) return
|
|
61
|
+
|
|
54
62
|
return parser.parser(tokens)
|
|
55
63
|
}
|
|
@@ -4,55 +4,94 @@ import markedShiki from "marked-shiki"
|
|
|
4
4
|
import { createMarkdownBase } from "./marked-base"
|
|
5
5
|
|
|
6
6
|
export function createMarkdownParser(highlight: (code: string, language: string) => string | Promise<string>) {
|
|
7
|
-
return createMarkdownBase().use(
|
|
7
|
+
return createMarkdownBase().use(markdownMath, markedShiki({ highlight }))
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// `$$...$$` renders as display math anywhere in a line.
|
|
11
|
+
// `$...$` follows Pandoc's boundaries so prices and shell variables stay text: the opening `$` needs a non-space
|
|
12
|
+
// after it, the closing `$` needs a non-space before it and no letter, digit, or underscore after it.
|
|
13
|
+
// Neither form spans a newline, and `$...$` cannot contain an unescaped `$`, so a failed `$...$` attempt stops at
|
|
14
|
+
// the next `$` and a failed `$$...$$` attempt stops at the end of the line.
|
|
15
|
+
const inlineDollarMathRegex =
|
|
16
|
+
/\$\$(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n$]))\$\$|\$(?=[^\s$])((?:\\.|[^\\\n$])*?(?:\\\S|[^\s\\$]))\$(?!\w)/y
|
|
12
17
|
|
|
13
|
-
const
|
|
18
|
+
const inlineParenMathRegex = /^\\\(((?:\\.|[^\\\n])*?)\\\)/
|
|
19
|
+
|
|
20
|
+
const blockMathRegex = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/
|
|
21
|
+
|
|
22
|
+
function renderMath(token: Tokens.Generic) {
|
|
23
|
+
return katex.renderToString(token.text, { throwOnError: false, displayMode: token.displayMode })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const markdownMath: MarkedExtension = {
|
|
14
27
|
extensions: [
|
|
15
28
|
{
|
|
16
|
-
name: "
|
|
29
|
+
name: "inlineDollarKatex",
|
|
30
|
+
level: "inline",
|
|
31
|
+
// marked calls `start` again for every text token with the rest of the paragraph, so it must not try the
|
|
32
|
+
// regex at each `$`; a `$` that fails the tokenizer just stays in the merged text.
|
|
33
|
+
start(src) {
|
|
34
|
+
const index = src.indexOf("$")
|
|
35
|
+
|
|
36
|
+
if (index === -1) return
|
|
37
|
+
|
|
38
|
+
return index
|
|
39
|
+
},
|
|
40
|
+
tokenizer(src) {
|
|
41
|
+
inlineDollarMathRegex.lastIndex = 0
|
|
42
|
+
const match = inlineDollarMathRegex.exec(src)
|
|
43
|
+
|
|
44
|
+
if (!match) return
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
type: "inlineDollarKatex",
|
|
48
|
+
raw: match[0],
|
|
49
|
+
text: (match[1] ?? match[2]).trim(),
|
|
50
|
+
displayMode: match[1] !== undefined,
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
renderer: renderMath,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "inlineParenKatex",
|
|
17
57
|
level: "inline",
|
|
18
58
|
start(src) {
|
|
19
59
|
const index = src.indexOf("\\(")
|
|
60
|
+
|
|
20
61
|
if (index === -1) return
|
|
62
|
+
|
|
21
63
|
return index
|
|
22
64
|
},
|
|
23
65
|
tokenizer(src) {
|
|
24
|
-
const match = src.match(
|
|
66
|
+
const match = src.match(inlineParenMathRegex)
|
|
67
|
+
|
|
25
68
|
if (!match) return
|
|
69
|
+
|
|
26
70
|
return {
|
|
27
|
-
type: "
|
|
71
|
+
type: "inlineParenKatex",
|
|
28
72
|
raw: match[0],
|
|
29
73
|
text: match[1].trim(),
|
|
30
74
|
displayMode: false,
|
|
31
75
|
}
|
|
32
76
|
},
|
|
33
|
-
renderer:
|
|
77
|
+
renderer: renderMath,
|
|
34
78
|
},
|
|
35
79
|
{
|
|
36
80
|
name: "blockKatex",
|
|
37
81
|
level: "block",
|
|
38
82
|
tokenizer(src) {
|
|
39
83
|
const match = src.match(blockMathRegex)
|
|
84
|
+
|
|
40
85
|
if (!match) return
|
|
86
|
+
|
|
41
87
|
return {
|
|
42
88
|
type: "blockKatex",
|
|
43
89
|
raw: match[0],
|
|
44
|
-
text: match[
|
|
45
|
-
displayMode:
|
|
90
|
+
text: match[2].trim(),
|
|
91
|
+
displayMode: match[1].length === 2,
|
|
46
92
|
}
|
|
47
93
|
},
|
|
48
|
-
renderer:
|
|
94
|
+
renderer: (token) => renderMath(token) + "\n",
|
|
49
95
|
},
|
|
50
96
|
],
|
|
51
97
|
}
|
|
52
|
-
|
|
53
|
-
function renderKatexToken(token: Tokens.Generic) {
|
|
54
|
-
return katex.renderToString(typeof token.text === "string" ? token.text : "", {
|
|
55
|
-
displayMode: token.displayMode === true,
|
|
56
|
-
throwOnError: false,
|
|
57
|
-
})
|
|
58
|
-
}
|
package/src/context/marked.tsx
CHANGED
|
@@ -17,8 +17,11 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext(
|
|
|
17
17
|
langs: [],
|
|
18
18
|
preferredHighlighter: "shiki-wasm",
|
|
19
19
|
})
|
|
20
|
+
|
|
20
21
|
const name = language in bundledLanguages ? language : "text"
|
|
22
|
+
|
|
21
23
|
if (!highlighter.getLoadedLanguages().includes(name)) await highlighter.loadLanguage(name as BundledLanguage)
|
|
24
|
+
|
|
22
25
|
return highlighter.codeToHtml(code, {
|
|
23
26
|
lang: name,
|
|
24
27
|
theme: "OpenCode",
|
|
@@ -8,7 +8,9 @@ const segmenter =
|
|
|
8
8
|
|
|
9
9
|
function first(value: string) {
|
|
10
10
|
if (!value) return ""
|
|
11
|
+
|
|
11
12
|
if (!segmenter) return Array.from(value)[0] ?? ""
|
|
13
|
+
|
|
12
14
|
return segmenter.segment(value)[Symbol.iterator]().next().value?.segment ?? Array.from(value)[0] ?? ""
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -33,7 +35,9 @@ export function Avatar(props: AvatarProps) {
|
|
|
33
35
|
"classList",
|
|
34
36
|
"style",
|
|
35
37
|
])
|
|
38
|
+
|
|
36
39
|
const src = split.src // did this so i can zero it out to test fallback
|
|
40
|
+
|
|
37
41
|
return (
|
|
38
42
|
<div
|
|
39
43
|
{...rest}
|
|
@@ -11,11 +11,13 @@ export function DiffChanges(props: {
|
|
|
11
11
|
? props.changes.reduce((acc, diff) => acc + (diff.additions ?? 0), 0)
|
|
12
12
|
: props.changes.additions,
|
|
13
13
|
)
|
|
14
|
+
|
|
14
15
|
const deletions = createMemo(() =>
|
|
15
16
|
Array.isArray(props.changes)
|
|
16
17
|
? props.changes.reduce((acc, diff) => acc + (diff.deletions ?? 0), 0)
|
|
17
18
|
: props.changes.deletions,
|
|
18
19
|
)
|
|
20
|
+
|
|
19
21
|
const total = createMemo(() => (additions() ?? 0) + (deletions() ?? 0))
|
|
20
22
|
|
|
21
23
|
return (
|
|
@@ -35,6 +35,7 @@ export interface LineCommentProps extends ComponentProps<"div"> {
|
|
|
35
35
|
|
|
36
36
|
export function LineComment(props: LineCommentProps) {
|
|
37
37
|
const [local, rest] = splitProps(props, ["comment", "selection", "actions", "class", "classList"])
|
|
38
|
+
|
|
38
39
|
return (
|
|
39
40
|
<div
|
|
40
41
|
{...rest}
|
|
@@ -78,11 +79,13 @@ export interface LineCommentEditorProps extends Omit<ComponentProps<"div">, "chi
|
|
|
78
79
|
|
|
79
80
|
function pathFilename(path: string) {
|
|
80
81
|
const index = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))
|
|
82
|
+
|
|
81
83
|
return index === -1 ? path : path.slice(index + 1)
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
function pathDirectory(path: string) {
|
|
85
87
|
const index = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"))
|
|
88
|
+
|
|
86
89
|
return index === -1 ? "" : path.slice(0, index + 1)
|
|
87
90
|
}
|
|
88
91
|
|
|
@@ -118,12 +121,16 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
|
|
|
118
121
|
|
|
119
122
|
const currentMention = () => {
|
|
120
123
|
const textarea = textareaRef
|
|
124
|
+
|
|
121
125
|
if (!textarea) return
|
|
126
|
+
|
|
122
127
|
if (!local.mention) return
|
|
128
|
+
|
|
123
129
|
if (textarea.selectionStart !== textarea.selectionEnd) return
|
|
124
130
|
|
|
125
131
|
const end = textarea.selectionStart
|
|
126
132
|
const match = textarea.value.slice(0, end).match(/@(\S*)$/)
|
|
133
|
+
|
|
127
134
|
if (!match) return
|
|
128
135
|
|
|
129
136
|
return {
|
|
@@ -138,6 +145,7 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
|
|
|
138
145
|
|
|
139
146
|
const textarea = textareaRef
|
|
140
147
|
const query = currentMention()
|
|
148
|
+
|
|
141
149
|
if (!textarea || !query) return
|
|
142
150
|
|
|
143
151
|
const value = `${textarea.value.slice(0, query.start)}@${item.path} ${textarea.value.slice(query.end)}`
|
|
@@ -155,8 +163,10 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
|
|
|
155
163
|
const mention = useFilteredList<{ path: string }>({
|
|
156
164
|
items: async (query) => {
|
|
157
165
|
if (!local.mention) return []
|
|
166
|
+
|
|
158
167
|
if (!query.trim()) return []
|
|
159
168
|
const paths = await local.mention.items(query)
|
|
169
|
+
|
|
160
170
|
return paths.map((path) => ({ path }))
|
|
161
171
|
},
|
|
162
172
|
key: (item) => item.path,
|
|
@@ -167,8 +177,10 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
|
|
|
167
177
|
|
|
168
178
|
const syncMention = () => {
|
|
169
179
|
const item = currentMention()
|
|
180
|
+
|
|
170
181
|
if (!item) {
|
|
171
182
|
closeMention()
|
|
183
|
+
|
|
172
184
|
return
|
|
173
185
|
}
|
|
174
186
|
|
|
@@ -178,6 +190,7 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
|
|
|
178
190
|
|
|
179
191
|
const selectActiveMention = () => {
|
|
180
192
|
const items = mention.flat()
|
|
193
|
+
|
|
181
194
|
if (items.length === 0) return
|
|
182
195
|
const active = mention.active()
|
|
183
196
|
selectMention(items.find((item) => item.path === active) ?? items[0])
|
|
@@ -185,6 +198,7 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
|
|
|
185
198
|
|
|
186
199
|
const submit = () => {
|
|
187
200
|
const v = local.value.trim()
|
|
201
|
+
|
|
188
202
|
if (!v) return
|
|
189
203
|
local.onSubmit(v)
|
|
190
204
|
}
|
|
@@ -225,12 +239,14 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
|
|
|
225
239
|
onSelect={() => syncMention()}
|
|
226
240
|
onKeyDown={(e) => {
|
|
227
241
|
e.stopPropagation()
|
|
242
|
+
|
|
228
243
|
if (e.isComposing || e.keyCode === 229) return
|
|
229
244
|
|
|
230
245
|
if (mentionOpen()) {
|
|
231
246
|
if (e.key === "Escape") {
|
|
232
247
|
e.preventDefault()
|
|
233
248
|
closeMention()
|
|
249
|
+
|
|
234
250
|
return
|
|
235
251
|
}
|
|
236
252
|
|
|
@@ -238,14 +254,17 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
|
|
|
238
254
|
if (mention.flat().length === 0) return
|
|
239
255
|
e.preventDefault()
|
|
240
256
|
selectActiveMention()
|
|
257
|
+
|
|
241
258
|
return
|
|
242
259
|
}
|
|
243
260
|
|
|
244
261
|
const nav = e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "Enter"
|
|
245
262
|
const ctrlNav = e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && (e.key === "n" || e.key === "p")
|
|
263
|
+
|
|
246
264
|
if ((nav || ctrlNav) && mention.flat().length > 0) {
|
|
247
265
|
mention.onKeyDown(e)
|
|
248
266
|
e.preventDefault()
|
|
267
|
+
|
|
249
268
|
return
|
|
250
269
|
}
|
|
251
270
|
}
|
|
@@ -254,8 +273,10 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
|
|
|
254
273
|
e.preventDefault()
|
|
255
274
|
e.currentTarget.blur()
|
|
256
275
|
local.onCancel()
|
|
276
|
+
|
|
257
277
|
return
|
|
258
278
|
}
|
|
279
|
+
|
|
259
280
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
260
281
|
e.preventDefault()
|
|
261
282
|
submit()
|
|
@@ -268,6 +289,7 @@ export function LineCommentEditor(props: LineCommentEditorProps) {
|
|
|
268
289
|
{(item) => {
|
|
269
290
|
const directory = item.path.endsWith("/") ? item.path : pathDirectory(item.path)
|
|
270
291
|
const name = item.path.endsWith("/") ? "" : pathFilename(item.path)
|
|
292
|
+
|
|
271
293
|
return (
|
|
272
294
|
<button
|
|
273
295
|
type="button"
|