@linto-ai/transcript-ui-ui 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +17 -0
- package/package.json +55 -0
- package/src/atoms/Badge.vue +26 -0
- package/src/atoms/Button.vue +264 -0
- package/src/atoms/CodeBlock.vue +98 -0
- package/src/atoms/CopyButton.vue +98 -0
- package/src/atoms/EditableText.vue +115 -0
- package/src/atoms/EditorCheckbox.vue +66 -0
- package/src/atoms/EditorIcon.vue +59 -0
- package/src/atoms/MarkdownEditor.vue +542 -0
- package/src/atoms/MarkdownView.vue +159 -0
- package/src/atoms/PopoverList.vue +92 -0
- package/src/atoms/SelectableListItem.vue +183 -0
- package/src/atoms/SpeakerIndicator.vue +19 -0
- package/src/atoms/SwitchToggle.vue +90 -0
- package/src/atoms/UserAvatar.vue +38 -0
- package/src/atoms/icons.ts +108 -0
- package/src/index.ts +36 -0
- package/src/molecules/DocumentArticle.vue +169 -0
- package/src/molecules/DownloadMenu.vue +48 -0
- package/src/molecules/FormInput.vue +510 -0
- package/src/molecules/SpeakerMenu.vue +43 -0
- package/src/molecules/Tabs.vue +119 -0
- package/src/molecules/TurnTextEditor.vue +93 -0
- package/src/styles/base.css +110 -0
- package/src/styles/fonts.css +45 -0
- package/src/styles/popover-list.css +67 -0
- package/src/styles/variables.css +131 -0
- package/src/turndown-plugin-gfm.d.ts +10 -0
- package/src/utils/computeInitials.ts +10 -0
- package/src/utils/computeSelectionOffset.ts +10 -0
- package/src/utils/computeTextOffsetInContainer.ts +54 -0
- package/src/utils/highlight.ts +46 -0
- package/src/utils/markdown.ts +56 -0
- package/src/utils/placeCaretAt.ts +23 -0
- package/src/utils/shadowAwareSelection.ts +52 -0
- package/src/utils/test/computeInitials.test.ts +15 -0
- package/src/utils/test/computeTextOffsetInContainer.test.ts +53 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome's document.getSelection() does not cross the shadow boundary: inside
|
|
3
|
+
* a shadow root the anchor/focus nodes clamp to the host. These helpers read
|
|
4
|
+
* the selection through ShadowRoot.getSelection() (Blink) or
|
|
5
|
+
* Selection.getComposedRanges() (standard) before falling back to the
|
|
6
|
+
* document selection (which crosses the boundary in Firefox).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Live, mutable Selection for `node`'s root, shadow-aware. */
|
|
10
|
+
export function getShadowAwareSelection(
|
|
11
|
+
node: Node | null | undefined,
|
|
12
|
+
): Selection | null {
|
|
13
|
+
if (!node) return null
|
|
14
|
+
const root = node.getRootNode()
|
|
15
|
+
if (root instanceof ShadowRoot) {
|
|
16
|
+
const shadowSelection = getShadowRootSelection(root)
|
|
17
|
+
if (shadowSelection) return shadowSelection
|
|
18
|
+
}
|
|
19
|
+
return node.ownerDocument?.getSelection() ?? null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Focus point of the current selection, or null when there is none. Covers
|
|
23
|
+
* browsers without ShadowRoot.getSelection() through the read-only
|
|
24
|
+
* getComposedRanges() path. */
|
|
25
|
+
export function getSelectionFocus(
|
|
26
|
+
container: HTMLElement,
|
|
27
|
+
): { node: Node; offset: number } | null {
|
|
28
|
+
const root = container.getRootNode()
|
|
29
|
+
if (root instanceof ShadowRoot) {
|
|
30
|
+
const shadowSelection = getShadowRootSelection(root)
|
|
31
|
+
if (shadowSelection?.focusNode) {
|
|
32
|
+
return {
|
|
33
|
+
node: shadowSelection.focusNode,
|
|
34
|
+
offset: shadowSelection.focusOffset,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const range = container.ownerDocument
|
|
38
|
+
.getSelection()
|
|
39
|
+
?.getComposedRanges?.({ shadowRoots: [root] })[0]
|
|
40
|
+
if (range) return { node: range.endContainer, offset: range.endOffset }
|
|
41
|
+
}
|
|
42
|
+
const selection = container.ownerDocument.getSelection()
|
|
43
|
+
if (!selection?.focusNode) return null
|
|
44
|
+
return { node: selection.focusNode, offset: selection.focusOffset }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Blink-only API, absent from lib.dom.
|
|
48
|
+
function getShadowRootSelection(root: ShadowRoot): Selection | null {
|
|
49
|
+
return (
|
|
50
|
+
(root as { getSelection?: () => Selection | null }).getSelection?.() ?? null
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test"
|
|
2
|
+
import { computeInitials } from "../computeInitials"
|
|
3
|
+
|
|
4
|
+
describe("computeInitials", () => {
|
|
5
|
+
it("takes the first letter of the first two words, uppercased", () => {
|
|
6
|
+
expect(computeInitials("Marie Dupont")).toBe("MD")
|
|
7
|
+
expect(computeInitials("marie dupont durand")).toBe("MD")
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it("handles single-word and empty names", () => {
|
|
11
|
+
expect(computeInitials("marie")).toBe("M")
|
|
12
|
+
expect(computeInitials(" ")).toBe("?")
|
|
13
|
+
expect(computeInitials("")).toBe("?")
|
|
14
|
+
})
|
|
15
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test"
|
|
2
|
+
import { computeTextOffsetInContainer } from "../computeTextOffsetInContainer"
|
|
3
|
+
|
|
4
|
+
// Minimal DOM-shaped stubs: the walk only reads nodeType, nodeValue and
|
|
5
|
+
// childNodes, so plain objects are enough (bun test has no DOM).
|
|
6
|
+
interface StubNode {
|
|
7
|
+
nodeType: number
|
|
8
|
+
nodeValue: string | null
|
|
9
|
+
childNodes: StubNode[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function text(value: string): StubNode {
|
|
13
|
+
return { nodeType: 3, nodeValue: value, childNodes: [] }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function element(...children: StubNode[]): StubNode {
|
|
17
|
+
return { nodeType: 1, nodeValue: null, childNodes: children }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function offset(container: StubNode, target: StubNode, local: number): number {
|
|
21
|
+
return computeTextOffsetInContainer(
|
|
22
|
+
container as unknown as Node,
|
|
23
|
+
target as unknown as Node,
|
|
24
|
+
local,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("computeTextOffsetInContainer", () => {
|
|
29
|
+
// Mirrors a read-view turn: <p><span>Bonjour</span> <span>tout</span> <span>le</span></p>
|
|
30
|
+
const words = [text("Bonjour"), text("tout"), text("le")]
|
|
31
|
+
const spans = words.map((w) => element(w))
|
|
32
|
+
const container = element(spans[0]!, text(" "), spans[1]!, text(" "), spans[2]!)
|
|
33
|
+
|
|
34
|
+
it("offsets inside the first text node are the local offset", () => {
|
|
35
|
+
expect(offset(container, words[0]!, 3)).toBe(3)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("offsets in later words include preceding words and separators", () => {
|
|
39
|
+
// "Bonjour tout le" — "tout" starts at 8, "le" at 13
|
|
40
|
+
expect(offset(container, words[1]!, 0)).toBe(8)
|
|
41
|
+
expect(offset(container, words[1]!, 4)).toBe(12)
|
|
42
|
+
expect(offset(container, words[2]!, 2)).toBe(15)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("resolves an element position to the text before its Nth child", () => {
|
|
46
|
+
// Caret "before child 2" of the container = before the second span
|
|
47
|
+
expect(offset(container, container, 2)).toBe(8)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("resolves an element position past the last child to the full length", () => {
|
|
51
|
+
expect(offset(container, container, container.childNodes.length)).toBe(15)
|
|
52
|
+
})
|
|
53
|
+
})
|