@meistrari/tela-build 1.21.0 → 1.23.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/components/tela/card/card.mdx +62 -0
- package/components/tela/card/card.stories.ts +143 -0
- package/components/tela/card/card.vue +33 -0
- package/components/tela/preview/preview-content.vue +83 -3
- package/components/tela/preview/preview.vue +9 -0
- package/components/tela/preview/types.ts +9 -1
- package/components/tela/scroll-area/scroll-area.stories.ts +130 -0
- package/components/tela/segment-toggle/segment-toggle.mdx +89 -0
- package/components/tela/select-menu/select-menu.vue +1 -1
- package/components/tela/status/status.vue +5 -1
- package/components/tela/tooltip/tooltip-content.vue +2 -6
- package/composables/citation-navigation.ts +88 -0
- package/lib/doc-generator.ts +22 -17
- package/package.json +1 -1
- package/components/tela/card.vue +0 -28
- /package/components/tela/{segment-toggle.stories.ts → segment-toggle/segment-toggle.stories.ts} +0 -0
- /package/components/tela/{segment-toggle.vue → segment-toggle/segment-toggle.vue} +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# TelaCard
|
|
2
|
+
|
|
3
|
+
A surface container component used to group related content with consistent visual boundaries.
|
|
4
|
+
|
|
5
|
+
## Size Prop
|
|
6
|
+
|
|
7
|
+
Always use the `size` prop to control card padding — it maps directly to the standardized values. Never apply padding manually to `<TelaCard>` or its inner elements.
|
|
8
|
+
|
|
9
|
+
| `size` | Applied classes | Use for |
|
|
10
|
+
|--------|------------------|---------|
|
|
11
|
+
| `md` *(default)* | `p-32px sm:p-48px rounded-24px` | Standard and large cards |
|
|
12
|
+
| `sm` | `p-24px rounded-12px` | Small cards, inner containers |
|
|
13
|
+
|
|
14
|
+
```vue
|
|
15
|
+
<!-- Correct — use size prop -->
|
|
16
|
+
<TelaCard size="md">...</TelaCard>
|
|
17
|
+
<TelaCard size="sm">...</TelaCard>
|
|
18
|
+
<TelaCard>...</TelaCard> <!-- md is the default -->
|
|
19
|
+
|
|
20
|
+
<!-- Incorrect — never apply padding manually -->
|
|
21
|
+
<TelaCard class="p-20px">...</TelaCard>
|
|
22
|
+
<TelaCard>
|
|
23
|
+
<div class="p-20px">...</div>
|
|
24
|
+
</TelaCard>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
|
|
29
|
+
### Standard Card (default)
|
|
30
|
+
|
|
31
|
+
```vue
|
|
32
|
+
<TelaCard>
|
|
33
|
+
<h2 class="heading-h4-semibold text-contrast">Title</h2>
|
|
34
|
+
<p class="body-14-regular text-secondary">Supporting description text.</p>
|
|
35
|
+
</TelaCard>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Minor / Inner Card
|
|
39
|
+
|
|
40
|
+
```vue
|
|
41
|
+
<TelaCard size="sm">
|
|
42
|
+
<span class="body-12-medium text-secondary">Compact content</span>
|
|
43
|
+
</TelaCard>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Card Grid
|
|
47
|
+
|
|
48
|
+
```vue
|
|
49
|
+
<div class="grid grid-cols-3 gap-4">
|
|
50
|
+
<TelaCard v-for="item in items" :key="item.id">...</TelaCard>
|
|
51
|
+
</div>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Props
|
|
55
|
+
|
|
56
|
+
| Prop | Type | Default | Description |
|
|
57
|
+
|------|------|---------|-------------|
|
|
58
|
+
| `size` | `'sm' \| 'md'` | `'md'` | `md` = `p-32px sm:p-48px rounded-24px`, `sm` = `p-24px rounded-12px` |
|
|
59
|
+
|
|
60
|
+
## Slots
|
|
61
|
+
|
|
62
|
+
- `default` — Card body content
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import Card from './card.vue'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Card> = {
|
|
5
|
+
title: 'Core/Card',
|
|
6
|
+
component: Card,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'centered',
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component: 'A surface container that groups related content with consistent visual boundaries. Use the `size` prop to control padding — `md` for standard cards, `sm` for minor or inner containers.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
argTypes: {
|
|
16
|
+
size: {
|
|
17
|
+
control: 'select',
|
|
18
|
+
options: ['md', 'sm'],
|
|
19
|
+
description: '`md` applies standard card padding. `sm` applies minor/inner container padding.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default meta
|
|
25
|
+
|
|
26
|
+
type Story = StoryObj<typeof meta>
|
|
27
|
+
|
|
28
|
+
export const Default: Story = {
|
|
29
|
+
render: args => ({
|
|
30
|
+
components: { Card },
|
|
31
|
+
setup() {
|
|
32
|
+
return { args }
|
|
33
|
+
},
|
|
34
|
+
template: `
|
|
35
|
+
<Card :size="args.size">
|
|
36
|
+
<div style="display: flex; flex-direction: column; gap: 8px; min-width: 280px;">
|
|
37
|
+
<p class="heading-h4-semibold text-contrast">Card Title</p>
|
|
38
|
+
<p class="body-14-regular text-secondary">This is supporting content inside the card. It adapts to the selected size.</p>
|
|
39
|
+
</div>
|
|
40
|
+
</Card>
|
|
41
|
+
`,
|
|
42
|
+
}),
|
|
43
|
+
args: {
|
|
44
|
+
size: 'md',
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const Standard: Story = {
|
|
49
|
+
render: () => ({
|
|
50
|
+
components: { Card },
|
|
51
|
+
template: `
|
|
52
|
+
<Card size="md">
|
|
53
|
+
<div style="display: flex; flex-direction: column; gap: 8px; min-width: 280px;">
|
|
54
|
+
<p class="heading-h4-semibold text-contrast">Standard Card</p>
|
|
55
|
+
<p class="body-14-regular text-secondary">Used for large, standard, and medium cards.</p>
|
|
56
|
+
</div>
|
|
57
|
+
</Card>
|
|
58
|
+
`,
|
|
59
|
+
}),
|
|
60
|
+
parameters: {
|
|
61
|
+
docs: {
|
|
62
|
+
description: {
|
|
63
|
+
story: 'Default size (`md`). Use for primary content surfaces.',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const Minor: Story = {
|
|
70
|
+
render: () => ({
|
|
71
|
+
components: { Card },
|
|
72
|
+
template: `
|
|
73
|
+
<Card size="sm">
|
|
74
|
+
<div style="display: flex; flex-direction: column; gap: 8px; min-width: 240px;">
|
|
75
|
+
<p class="heading-h5-semibold text-contrast">Minor Card</p>
|
|
76
|
+
<p class="body-12-regular text-secondary">Used for small cards and inner containers.</p>
|
|
77
|
+
</div>
|
|
78
|
+
</Card>
|
|
79
|
+
`,
|
|
80
|
+
}),
|
|
81
|
+
parameters: {
|
|
82
|
+
docs: {
|
|
83
|
+
description: {
|
|
84
|
+
story: 'Small size (`sm`). Use for compact cards or nested inner containers.',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const SizeComparison: Story = {
|
|
91
|
+
render: () => ({
|
|
92
|
+
components: { Card },
|
|
93
|
+
template: `
|
|
94
|
+
<div style="display: flex; gap: 16px; align-items: flex-start;">
|
|
95
|
+
<Card size="md">
|
|
96
|
+
<div style="display: flex; flex-direction: column; gap: 6px; min-width: 200px;">
|
|
97
|
+
<p class="body-12-medium text-secondary">size="md"</p>
|
|
98
|
+
<p class="heading-h4-semibold text-contrast">Standard</p>
|
|
99
|
+
<p class="body-14-regular text-tertiary">p-32px sm:p-48px padding</p>
|
|
100
|
+
</div>
|
|
101
|
+
</Card>
|
|
102
|
+
<Card size="sm">
|
|
103
|
+
<div style="display: flex; flex-direction: column; gap: 6px; min-width: 200px;">
|
|
104
|
+
<p class="body-12-medium text-secondary">size="sm"</p>
|
|
105
|
+
<p class="heading-h4-semibold text-contrast">Minor</p>
|
|
106
|
+
<p class="body-14-regular text-tertiary">p-24px padding</p>
|
|
107
|
+
</div>
|
|
108
|
+
</Card>
|
|
109
|
+
</div>
|
|
110
|
+
`,
|
|
111
|
+
}),
|
|
112
|
+
parameters: {
|
|
113
|
+
docs: {
|
|
114
|
+
description: {
|
|
115
|
+
story: 'Side-by-side comparison of both size variants.',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const Grid: Story = {
|
|
122
|
+
render: () => ({
|
|
123
|
+
components: { Card },
|
|
124
|
+
template: `
|
|
125
|
+
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; width: 640px;">
|
|
126
|
+
<Card v-for="n in 3" :key="n">
|
|
127
|
+
<div style="display: flex; flex-direction: column; gap: 4px;">
|
|
128
|
+
<p class="heading-h5-semibold text-contrast">Card {{ n }}</p>
|
|
129
|
+
<p class="body-12-regular text-secondary">Grid item</p>
|
|
130
|
+
</div>
|
|
131
|
+
</Card>
|
|
132
|
+
</div>
|
|
133
|
+
`,
|
|
134
|
+
}),
|
|
135
|
+
parameters: {
|
|
136
|
+
layout: 'padded',
|
|
137
|
+
docs: {
|
|
138
|
+
description: {
|
|
139
|
+
story: 'Cards used in a grid layout. No padding override needed — `size="md"` is the default.',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<{
|
|
5
|
+
size?: 'sm' | 'md'
|
|
6
|
+
class?: HTMLAttributes['class']
|
|
7
|
+
/** @deprecated Use `class` instead */
|
|
8
|
+
contentPadding?: HTMLAttributes['class']
|
|
9
|
+
/** @deprecated Use `class` instead */
|
|
10
|
+
borderRadius?: HTMLAttributes['class']
|
|
11
|
+
}>(), {
|
|
12
|
+
size: 'md',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const sizeStyles = computed(() => (({
|
|
16
|
+
sm: { padding: 'p-24px', rounded: 'rounded-12px' },
|
|
17
|
+
md: { padding: 'p-32px sm:p-48px', rounded: 'rounded-24px' },
|
|
18
|
+
}) as Record<string, { padding: string, rounded: string }>)[props.size ?? 'md'] ?? { padding: '', rounded: '' })
|
|
19
|
+
|
|
20
|
+
const paddingClass = computed(() => props.contentPadding ?? sizeStyles.value.padding)
|
|
21
|
+
const borderRadiusClass = computed(() => props.borderRadius ?? sizeStyles.value.rounded)
|
|
22
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
23
|
+
|
|
24
|
+
defineExpose({
|
|
25
|
+
el: rootEl,
|
|
26
|
+
})
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<div ref="rootEl" :class="cn('bg border-0.5px border', paddingClass, borderRadiusClass, props.class)">
|
|
31
|
+
<slot />
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
@@ -16,10 +16,19 @@ const props = withDefaults(
|
|
|
16
16
|
/** Load PDF document from URL. Required for PDF preview. */
|
|
17
17
|
pdfLoader?: (url: string) => Promise<PdfDocumentHandle | null>
|
|
18
18
|
labels?: PreviewContentLabels
|
|
19
|
+
/** Text to highlight on the PDF pages. When set, a text layer is rendered with yellow highlights. */
|
|
20
|
+
highlightText?: string | null
|
|
21
|
+
/** The page to scroll to and highlight on. When set, scrolls to this page and restricts highlights to it. */
|
|
22
|
+
highlightPage?: number | null
|
|
23
|
+
/** When true, uses exact matching instead of fuzzy word-based matching for highlights. */
|
|
24
|
+
highlightExact?: boolean
|
|
19
25
|
}>(),
|
|
20
26
|
{
|
|
21
27
|
variant: 'default',
|
|
22
28
|
segmentTab: undefined,
|
|
29
|
+
highlightText: undefined,
|
|
30
|
+
highlightPage: undefined,
|
|
31
|
+
highlightExact: false,
|
|
23
32
|
},
|
|
24
33
|
)
|
|
25
34
|
|
|
@@ -80,7 +89,11 @@ const pdfDocHandle = ref<PdfDocumentHandle | null>(null)
|
|
|
80
89
|
const pdfLoadError = ref<string | null>(null)
|
|
81
90
|
|
|
82
91
|
const pageRefs = ref<Map<number, HTMLElement>>(new Map())
|
|
92
|
+
const textLayerRefs = ref<Map<number, HTMLDivElement>>(new Map())
|
|
83
93
|
const renderedPages = ref<Set<number>>(new Set())
|
|
94
|
+
let isRendering = false
|
|
95
|
+
let pendingReRender = false
|
|
96
|
+
let pendingScrollPage: number | null = null
|
|
84
97
|
|
|
85
98
|
function zoomIn() {
|
|
86
99
|
if (scale.value < 3) {
|
|
@@ -185,6 +198,15 @@ function setPageRef(pageNum: number, el: HTMLElement | null) {
|
|
|
185
198
|
}
|
|
186
199
|
}
|
|
187
200
|
|
|
201
|
+
function setTextLayerRef(pageNum: number, el: HTMLDivElement | null) {
|
|
202
|
+
if (el) {
|
|
203
|
+
textLayerRefs.value.set(pageNum, el)
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
textLayerRefs.value.delete(pageNum)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
188
210
|
async function renderPdfPage(pageNum: number) {
|
|
189
211
|
const handle = pdfDocHandle.value
|
|
190
212
|
|
|
@@ -201,10 +223,16 @@ async function renderPdfPage(pageNum: number) {
|
|
|
201
223
|
if (!canvas)
|
|
202
224
|
return
|
|
203
225
|
|
|
226
|
+
const textLayer = textLayerRefs.value.get(pageNum) ?? null
|
|
227
|
+
|
|
204
228
|
await handle.renderPage({
|
|
205
229
|
pageNum,
|
|
206
230
|
canvas: canvas as HTMLCanvasElement,
|
|
207
231
|
scale: scale.value,
|
|
232
|
+
textLayer,
|
|
233
|
+
highlight: props.highlightText,
|
|
234
|
+
highlightPage: props.highlightPage,
|
|
235
|
+
highlightExact: props.highlightExact,
|
|
208
236
|
})
|
|
209
237
|
|
|
210
238
|
renderedPages.value.add(pageNum)
|
|
@@ -222,9 +250,32 @@ async function renderAllPdfPages() {
|
|
|
222
250
|
}
|
|
223
251
|
|
|
224
252
|
async function reRenderAllPdfPages() {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
253
|
+
if (isRendering) {
|
|
254
|
+
pendingReRender = true
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
isRendering = true
|
|
258
|
+
try {
|
|
259
|
+
renderedPages.value.clear()
|
|
260
|
+
await nextTick()
|
|
261
|
+
await renderAllPdfPages()
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
isRendering = false
|
|
265
|
+
if (pendingReRender) {
|
|
266
|
+
pendingReRender = false
|
|
267
|
+
void reRenderAllPdfPages().then(() => {
|
|
268
|
+
if (pendingScrollPage !== null) {
|
|
269
|
+
scrollToPage(pendingScrollPage)
|
|
270
|
+
pendingScrollPage = null
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
else if (pendingScrollPage !== null) {
|
|
275
|
+
scrollToPage(pendingScrollPage)
|
|
276
|
+
pendingScrollPage = null
|
|
277
|
+
}
|
|
278
|
+
}
|
|
228
279
|
}
|
|
229
280
|
|
|
230
281
|
watch(() => props.file, async (newFile) => {
|
|
@@ -275,6 +326,15 @@ watch(() => props.file, async (newFile) => {
|
|
|
275
326
|
|
|
276
327
|
await nextTick()
|
|
277
328
|
await renderAllPdfPages()
|
|
329
|
+
|
|
330
|
+
const scrollTarget = (props.highlightPage && props.highlightPage > 0)
|
|
331
|
+
? props.highlightPage
|
|
332
|
+
: pendingScrollPage
|
|
333
|
+
if (scrollTarget) {
|
|
334
|
+
pendingScrollPage = null
|
|
335
|
+
await nextTick()
|
|
336
|
+
scrollToPage(scrollTarget)
|
|
337
|
+
}
|
|
278
338
|
}
|
|
279
339
|
else {
|
|
280
340
|
pdfLoadError.value = labels.value.failedToLoadFile
|
|
@@ -392,6 +452,20 @@ function handleHotkey(e: KeyboardEvent) {
|
|
|
392
452
|
}
|
|
393
453
|
|
|
394
454
|
useEventListener('keydown', handleHotkey)
|
|
455
|
+
|
|
456
|
+
watch([() => props.highlightText, () => props.highlightPage, () => props.highlightExact], async () => {
|
|
457
|
+
if (props.highlightPage && props.highlightPage > 0) {
|
|
458
|
+
pendingScrollPage = props.highlightPage
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
pendingScrollPage = null
|
|
462
|
+
}
|
|
463
|
+
if (pdfDocHandle.value) {
|
|
464
|
+
await reRenderAllPdfPages()
|
|
465
|
+
}
|
|
466
|
+
// When doc isn't loaded yet, keep pendingScrollPage — it will be
|
|
467
|
+
// consumed by reRenderAllPdfPages().finally once the PDF renders.
|
|
468
|
+
})
|
|
395
469
|
</script>
|
|
396
470
|
|
|
397
471
|
<template>
|
|
@@ -615,6 +689,12 @@ useEventListener('keydown', handleHotkey)
|
|
|
615
689
|
:class="cn(variant === 'minimal' && 'w-256px pdf-page-card--minimal')"
|
|
616
690
|
>
|
|
617
691
|
<canvas />
|
|
692
|
+
<div
|
|
693
|
+
:ref="(el) => setTextLayerRef(pageNum, el as HTMLDivElement)"
|
|
694
|
+
data-text-layer
|
|
695
|
+
class="absolute top-0 left-0 w-full h-full z-1"
|
|
696
|
+
style="pointer-events: none; mix-blend-mode: multiply; line-height: 1; opacity: 0.4;"
|
|
697
|
+
/>
|
|
618
698
|
<div
|
|
619
699
|
data-pdf-page-badge
|
|
620
700
|
absolute z-2 top-8px left-8px
|
|
@@ -29,6 +29,12 @@ const props = withDefaults(
|
|
|
29
29
|
pdfLoader?: (url: string) => Promise<import('./types').PdfDocumentHandle | null>
|
|
30
30
|
/** Labels for PreviewContent (failedToLoadFile, page, download, etc.) */
|
|
31
31
|
contentLabels?: import('./types').PreviewContentLabels
|
|
32
|
+
/** Text to highlight on the PDF pages. Passed to PreviewContent. */
|
|
33
|
+
highlightText?: string | null
|
|
34
|
+
/** Page to scroll to and highlight on. Passed to PreviewContent. */
|
|
35
|
+
highlightPage?: number | null
|
|
36
|
+
/** When true, uses exact matching instead of fuzzy word-based matching. */
|
|
37
|
+
highlightExact?: boolean
|
|
32
38
|
}>(),
|
|
33
39
|
{
|
|
34
40
|
variant: 'default',
|
|
@@ -120,6 +126,9 @@ const fileReaderKey = computed(() => {
|
|
|
120
126
|
:pdf-loader="pdfLoader"
|
|
121
127
|
:labels="contentLabels"
|
|
122
128
|
:variant="variant"
|
|
129
|
+
:highlight-text="highlightText"
|
|
130
|
+
:highlight-page="highlightPage"
|
|
131
|
+
:highlight-exact="highlightExact"
|
|
123
132
|
class="h-full"
|
|
124
133
|
@fullscreen="emit('fullscreen')"
|
|
125
134
|
/>
|
|
@@ -60,6 +60,14 @@ export interface PreviewSelectVariableLabels {
|
|
|
60
60
|
|
|
61
61
|
export interface PdfDocumentHandle {
|
|
62
62
|
numPages: number
|
|
63
|
-
renderPage: (opts: {
|
|
63
|
+
renderPage: (opts: {
|
|
64
|
+
pageNum: number
|
|
65
|
+
canvas: HTMLCanvasElement
|
|
66
|
+
scale: number
|
|
67
|
+
textLayer?: HTMLDivElement | null
|
|
68
|
+
highlight?: string | null
|
|
69
|
+
highlightPage?: number | null
|
|
70
|
+
highlightExact?: boolean
|
|
71
|
+
}) => Promise<void>
|
|
64
72
|
destroy: () => void
|
|
65
73
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import ScrollArea from './scroll-area.vue'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ScrollArea> = {
|
|
5
|
+
title: 'Utility/ScrollArea',
|
|
6
|
+
component: ScrollArea,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'centered',
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component: 'A scroll area component that provides custom-styled scrollbars. Built on reka-ui for consistent scrollbar appearance across browsers. Useful for creating scrollable containers with better visual consistency than native scrollbars.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
argTypes: {
|
|
17
|
+
type: {
|
|
18
|
+
control: 'select',
|
|
19
|
+
options: ['auto', 'always', 'scroll', 'hover'],
|
|
20
|
+
description: 'Scrollbar visibility behavior.',
|
|
21
|
+
},
|
|
22
|
+
orientation: {
|
|
23
|
+
control: 'select',
|
|
24
|
+
options: ['vertical', 'horizontal', 'both'],
|
|
25
|
+
description: 'Direction(s) of scrolling.',
|
|
26
|
+
},
|
|
27
|
+
scrollHideDelay: {
|
|
28
|
+
control: 'number',
|
|
29
|
+
description: 'Delay in ms before scrollbars are hidden (when type is hover or scroll).',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
args: {
|
|
33
|
+
type: 'hover',
|
|
34
|
+
orientation: 'vertical',
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default meta
|
|
39
|
+
|
|
40
|
+
type Story = StoryObj<typeof meta>
|
|
41
|
+
|
|
42
|
+
export const Default: Story = {
|
|
43
|
+
render: args => ({
|
|
44
|
+
components: { ScrollArea },
|
|
45
|
+
setup() {
|
|
46
|
+
const items = Array.from({ length: 30 }, (_, i) => `Item ${i + 1}`)
|
|
47
|
+
return { args, items }
|
|
48
|
+
},
|
|
49
|
+
template: `
|
|
50
|
+
<ScrollArea v-bind="args" class="h-200px w-300px border border-gray-200 rounded-md">
|
|
51
|
+
<div class="p-4 flex flex-col gap-2">
|
|
52
|
+
<div v-for="item in items" :key="item" class="text-sm py-1 border-b border-gray-100 last:border-0">
|
|
53
|
+
{{ item }}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</ScrollArea>
|
|
57
|
+
`,
|
|
58
|
+
}),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const AlwaysVisible: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
type: 'always',
|
|
64
|
+
},
|
|
65
|
+
render: args => ({
|
|
66
|
+
components: { ScrollArea },
|
|
67
|
+
setup() {
|
|
68
|
+
const items = Array.from({ length: 30 }, (_, i) => `Item ${i + 1}`)
|
|
69
|
+
return { args, items }
|
|
70
|
+
},
|
|
71
|
+
template: `
|
|
72
|
+
<ScrollArea v-bind="args" class="h-200px w-300px border border-gray-200 rounded-md">
|
|
73
|
+
<div class="p-4 flex flex-col gap-2">
|
|
74
|
+
<div v-for="item in items" :key="item" class="text-sm py-1 border-b border-gray-100 last:border-0">
|
|
75
|
+
{{ item }}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</ScrollArea>
|
|
79
|
+
`,
|
|
80
|
+
}),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const Horizontal: Story = {
|
|
84
|
+
args: {
|
|
85
|
+
orientation: 'horizontal',
|
|
86
|
+
type: 'always',
|
|
87
|
+
},
|
|
88
|
+
render: args => ({
|
|
89
|
+
components: { ScrollArea },
|
|
90
|
+
setup() {
|
|
91
|
+
const items = Array.from({ length: 20 }, (_, i) => `Card ${i + 1}`)
|
|
92
|
+
return { args, items }
|
|
93
|
+
},
|
|
94
|
+
template: `
|
|
95
|
+
<ScrollArea v-bind="args" class="w-400px border border-gray-200 rounded-md">
|
|
96
|
+
<div class="flex gap-3 p-4" style="width: 1200px">
|
|
97
|
+
<div v-for="item in items" :key="item" class="flex-shrink-0 w-120px h-80px bg-gray-100 rounded flex items-center justify-center text-sm text-gray-600">
|
|
98
|
+
{{ item }}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</ScrollArea>
|
|
102
|
+
`,
|
|
103
|
+
}),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const Both: Story = {
|
|
107
|
+
args: {
|
|
108
|
+
orientation: 'both',
|
|
109
|
+
type: 'always',
|
|
110
|
+
},
|
|
111
|
+
render: args => ({
|
|
112
|
+
components: { ScrollArea },
|
|
113
|
+
setup() {
|
|
114
|
+
const rows = Array.from({ length: 20 }, (_, i) => `Row ${i + 1}`)
|
|
115
|
+
return { args, rows }
|
|
116
|
+
},
|
|
117
|
+
template: `
|
|
118
|
+
<ScrollArea v-bind="args" class="h-250px w-400px border border-gray-200 rounded-md">
|
|
119
|
+
<div style="width: 900px">
|
|
120
|
+
<div v-for="row in rows" :key="row" class="flex items-center px-4 py-2 border-b border-gray-100 text-sm gap-4">
|
|
121
|
+
<span class="w-100px flex-shrink-0 font-medium">{{ row }}</span>
|
|
122
|
+
<span class="w-200px flex-shrink-0 text-gray-500">Column B content here</span>
|
|
123
|
+
<span class="w-200px flex-shrink-0 text-gray-500">Column C content here</span>
|
|
124
|
+
<span class="w-200px flex-shrink-0 text-gray-500">Column D content here</span>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</ScrollArea>
|
|
128
|
+
`,
|
|
129
|
+
}),
|
|
130
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
|
|
2
|
+
import * as SegmentToggleStories from './segment-toggle.stories.ts';
|
|
3
|
+
|
|
4
|
+
<Meta of={SegmentToggleStories} />
|
|
5
|
+
|
|
6
|
+
# TelaSegmentToggle
|
|
7
|
+
|
|
8
|
+
A segment toggle component that displays multiple options as connected segments. Only one option can be selected at a time. Supports v-model binding, different sizes, and disabled states. Useful for mutually exclusive option selection with a modern segmented control interface.
|
|
9
|
+
|
|
10
|
+
## Examples
|
|
11
|
+
|
|
12
|
+
### Default
|
|
13
|
+
|
|
14
|
+
<Canvas of={SegmentToggleStories.Default} />
|
|
15
|
+
|
|
16
|
+
### Small Size
|
|
17
|
+
|
|
18
|
+
<Canvas of={SegmentToggleStories.Small} />
|
|
19
|
+
|
|
20
|
+
### Disabled
|
|
21
|
+
|
|
22
|
+
<Canvas of={SegmentToggleStories.Disabled} />
|
|
23
|
+
|
|
24
|
+
### Two Options
|
|
25
|
+
|
|
26
|
+
<Canvas of={SegmentToggleStories.TwoOptions} />
|
|
27
|
+
|
|
28
|
+
### Preselected Middle
|
|
29
|
+
|
|
30
|
+
<Canvas of={SegmentToggleStories.PreselectedMiddle} />
|
|
31
|
+
|
|
32
|
+
## Basic Usage
|
|
33
|
+
|
|
34
|
+
```vue
|
|
35
|
+
<script setup>
|
|
36
|
+
import { ref } from 'vue'
|
|
37
|
+
|
|
38
|
+
const alignment = ref('left')
|
|
39
|
+
const options = [
|
|
40
|
+
{ label: 'Left', value: 'left' },
|
|
41
|
+
{ label: 'Center', value: 'center' },
|
|
42
|
+
{ label: 'Right', value: 'right' },
|
|
43
|
+
]
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<TelaSegmentToggle v-model="alignment" :options="options" />
|
|
48
|
+
</template>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Small Size
|
|
52
|
+
|
|
53
|
+
```vue
|
|
54
|
+
<TelaSegmentToggle v-model="value" :options="options" size="small" />
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Disabled State
|
|
58
|
+
|
|
59
|
+
```vue
|
|
60
|
+
<TelaSegmentToggle v-model="value" :options="options" disabled />
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Props
|
|
64
|
+
|
|
65
|
+
<ArgTypes />
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
interface Option {
|
|
69
|
+
label: string
|
|
70
|
+
value: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type SegmentToggleProps = {
|
|
74
|
+
modelValue: string
|
|
75
|
+
options: Option[]
|
|
76
|
+
size?: 'small' | 'medium'
|
|
77
|
+
disabled?: boolean
|
|
78
|
+
class?: string
|
|
79
|
+
buttonsClass?: string
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Features
|
|
84
|
+
|
|
85
|
+
- **Two-way Binding**: Full v-model support for reactive state
|
|
86
|
+
- **Multiple Sizes**: Small and medium size options
|
|
87
|
+
- **Animated Selection**: Spring-animated indicator for smooth transitions
|
|
88
|
+
- **Disabled State**: Proper disabled styling and interaction prevention
|
|
89
|
+
- **Accessible**: Keyboard navigation support via native button elements
|
|
@@ -110,7 +110,7 @@ function handleOpenChange(open: boolean) {
|
|
|
110
110
|
v-if="currentOption?.icon && !(currentOption.value === '')"
|
|
111
111
|
shrink-0 rounded-4px flex items-center justify-center
|
|
112
112
|
>
|
|
113
|
-
<
|
|
113
|
+
<TelaIcon v-if="typeof currentOption.icon === 'string'" :name="currentOption.icon" />
|
|
114
114
|
<Component :is="currentOption.icon" v-else />
|
|
115
115
|
</div>
|
|
116
116
|
<div v-if="!currentOption?.icon && currentOption?.externalIconSrc" mr-4px flex h-20px w-16px shrink-0 items-center justify-center rounded-4px>
|
|
@@ -336,7 +336,11 @@ watch(
|
|
|
336
336
|
{ immediate: true },
|
|
337
337
|
)
|
|
338
338
|
|
|
339
|
-
onMounted(
|
|
339
|
+
onMounted(() => {
|
|
340
|
+
requestAnimationFrame(() => {
|
|
341
|
+
requestAnimationFrame(measureContentWidth)
|
|
342
|
+
})
|
|
343
|
+
})
|
|
340
344
|
|
|
341
345
|
const shineClass = computed(() => {
|
|
342
346
|
const baseColor = currentStatus.value.textColor.replace('text-', '')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui'
|
|
3
3
|
import type { TooltipContentEmits, TooltipContentProps } from 'reka-ui'
|
|
4
|
-
import {
|
|
4
|
+
import { reactiveOmit } from '@vueuse/core'
|
|
5
5
|
import type { HTMLAttributes } from 'vue'
|
|
6
6
|
|
|
7
7
|
defineOptions({
|
|
@@ -16,11 +16,7 @@ const emits = defineEmits<TooltipContentEmits & {
|
|
|
16
16
|
click: [event: Event]
|
|
17
17
|
}>()
|
|
18
18
|
|
|
19
|
-
const delegatedProps =
|
|
20
|
-
const { class: _, ...delegated } = props
|
|
21
|
-
|
|
22
|
-
return delegated
|
|
23
|
-
})
|
|
19
|
+
const delegatedProps = reactiveOmit(props, 'class')
|
|
24
20
|
|
|
25
21
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|
26
22
|
</script>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter, Ref } from 'vue'
|
|
2
|
+
import { nextTick, ref, toValue } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface CitationTarget {
|
|
5
|
+
file: string
|
|
6
|
+
page: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function useCitationNavigation(options: {
|
|
10
|
+
citations: MaybeRefOrGetter<Record<string, any> | null | undefined>
|
|
11
|
+
}) {
|
|
12
|
+
const highlightText = ref<string | null>(null)
|
|
13
|
+
const highlightPage = ref<number | null>(null)
|
|
14
|
+
const highlightExact = ref(false)
|
|
15
|
+
const activeFile = ref<string | null>(null)
|
|
16
|
+
|
|
17
|
+
function lookupCitation(path: string): CitationTarget | null {
|
|
18
|
+
const citations = toValue(options.citations)
|
|
19
|
+
if (!citations)
|
|
20
|
+
return null
|
|
21
|
+
|
|
22
|
+
const parts = path.split('.')
|
|
23
|
+
let current: any = citations
|
|
24
|
+
|
|
25
|
+
for (const part of parts) {
|
|
26
|
+
if (current == null)
|
|
27
|
+
return null
|
|
28
|
+
if (Array.isArray(current)) {
|
|
29
|
+
const idx = Number.parseInt(part, 10)
|
|
30
|
+
if (Number.isNaN(idx))
|
|
31
|
+
return null
|
|
32
|
+
current = current[idx]
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
current = current[part]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!current || typeof current !== 'object' || !('page' in current))
|
|
40
|
+
return null
|
|
41
|
+
|
|
42
|
+
if ('file' in current && current.file)
|
|
43
|
+
return { file: current.file, page: current.page }
|
|
44
|
+
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function navigateToCitation(path: string, value?: any): void {
|
|
49
|
+
const citation = lookupCitation(path)
|
|
50
|
+
if (!citation) {
|
|
51
|
+
clearCitation()
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Reset then set via nextTick to force watchers to re-trigger
|
|
56
|
+
// even when clicking the same citation field twice
|
|
57
|
+
highlightText.value = null
|
|
58
|
+
highlightPage.value = null
|
|
59
|
+
highlightExact.value = false
|
|
60
|
+
activeFile.value = null
|
|
61
|
+
|
|
62
|
+
nextTick(() => {
|
|
63
|
+
activeFile.value = citation.file
|
|
64
|
+
highlightPage.value = citation.page
|
|
65
|
+
highlightText.value = value != null
|
|
66
|
+
? (typeof value === 'string' ? value : String(value))
|
|
67
|
+
: null
|
|
68
|
+
highlightExact.value = true
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function clearCitation(): void {
|
|
73
|
+
highlightText.value = null
|
|
74
|
+
highlightPage.value = null
|
|
75
|
+
highlightExact.value = false
|
|
76
|
+
activeFile.value = null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
highlightText: highlightText as Ref<string | null>,
|
|
81
|
+
highlightPage: highlightPage as Ref<number | null>,
|
|
82
|
+
highlightExact: highlightExact as Ref<boolean>,
|
|
83
|
+
activeFile: activeFile as Ref<string | null>,
|
|
84
|
+
lookupCitation,
|
|
85
|
+
navigateToCitation,
|
|
86
|
+
clearCitation,
|
|
87
|
+
}
|
|
88
|
+
}
|
package/lib/doc-generator.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
import { join, resolve, relative, dirname, basename } from 'pathe'
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs'
|
|
4
4
|
import { parse as parseVue } from 'vue-docgen-api'
|
|
5
5
|
// Load glob dynamically for compatibility across versions (v8 vs v11)
|
|
6
|
-
import { getTypeResolver
|
|
7
|
-
import {
|
|
6
|
+
import { getTypeResolver } from './type-resolver'
|
|
7
|
+
import type { TypeResolver } from './type-resolver'
|
|
8
|
+
import { createVolarExtractor } from './extractors/volar-extract'
|
|
9
|
+
import type { VolarExtractor } from './extractors/volar-extract'
|
|
8
10
|
|
|
9
11
|
const colors = {
|
|
10
12
|
gray: '\x1B[90m',
|
|
@@ -544,20 +546,23 @@ export function generateDocsToDirectory(componentDocs: ComponentDoc[], typeResol
|
|
|
544
546
|
componentLinks.push(`- [${title}](components/${groupSlug}.md)`)
|
|
545
547
|
}
|
|
546
548
|
|
|
547
|
-
// Read
|
|
548
|
-
let
|
|
549
|
+
// Read all .md files from docs/ and append them to SKILL.md
|
|
550
|
+
let docsSection = ''
|
|
549
551
|
if (layerPath) {
|
|
550
|
-
const
|
|
551
|
-
if (existsSync(
|
|
552
|
-
|
|
552
|
+
const docsDir = join(layerPath, 'docs')
|
|
553
|
+
if (existsSync(docsDir)) {
|
|
554
|
+
const docFiles = readdirSync(docsDir)
|
|
555
|
+
.filter(f => f.endsWith('.md'))
|
|
556
|
+
.sort()
|
|
557
|
+
for (const file of docFiles) {
|
|
558
|
+
const content = readFileSync(join(docsDir, file), 'utf-8')
|
|
559
|
+
docsSection += `\n\n---\n\n${content}`
|
|
560
|
+
}
|
|
553
561
|
}
|
|
554
562
|
}
|
|
555
563
|
|
|
556
564
|
// Create SKILL.md describing Tela Build with links to supporting md
|
|
557
565
|
const skillDescription = buildTelaBuildSkillDescription()
|
|
558
|
-
const designTokensSection = designTokensContent
|
|
559
|
-
? `\n\n---\n\n${designTokensContent}`
|
|
560
|
-
: ''
|
|
561
566
|
const body = dedent`
|
|
562
567
|
# Tela Build
|
|
563
568
|
|
|
@@ -574,7 +579,7 @@ Use it when building, refactoring, or using Tela components — props, events, s
|
|
|
574
579
|
|
|
575
580
|
## Components Index
|
|
576
581
|
|
|
577
|
-
${componentLinks.join('\n')}${
|
|
582
|
+
${componentLinks.join('\n')}${docsSection}
|
|
578
583
|
`
|
|
579
584
|
|
|
580
585
|
const skillMd = wrapWithSkillFrontmatter({ name: 'tela-build', description: skillDescription }, body)
|
|
@@ -675,13 +680,13 @@ function toKebabFromTag(tagName: string): string {
|
|
|
675
680
|
return tagName
|
|
676
681
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
677
682
|
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
678
|
-
.replace(/[^a-
|
|
683
|
+
.replace(/[^a-z0-9-]/gi, '-')
|
|
679
684
|
.toLowerCase()
|
|
680
|
-
.replace(
|
|
685
|
+
.replace(/-{2,}/g, '-')
|
|
681
686
|
.replace(/^-+|-+$/g, '')
|
|
682
687
|
}
|
|
683
688
|
|
|
684
|
-
function wrapWithSkillFrontmatter(meta: { name: string
|
|
689
|
+
function wrapWithSkillFrontmatter(meta: { name: string, description: string, allowedTools?: string[] }, body: string): string {
|
|
685
690
|
const { name, description, allowedTools } = meta
|
|
686
691
|
const lines: string[] = []
|
|
687
692
|
lines.push('---')
|
|
@@ -700,7 +705,7 @@ function wrapWithSkillFrontmatter(meta: { name: string; description: string; all
|
|
|
700
705
|
|
|
701
706
|
function sanitizeSkillName(name: string): string {
|
|
702
707
|
// Lowercase + hyphens + digits only, max 64 chars
|
|
703
|
-
return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 64).replace(
|
|
708
|
+
return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 64).replace(/-{2,}/g, '-')
|
|
704
709
|
}
|
|
705
710
|
|
|
706
711
|
function sanitizeDescription(desc: string): string {
|
|
@@ -804,7 +809,7 @@ function sanitizeInlineComment(value?: string): string {
|
|
|
804
809
|
if (!value)
|
|
805
810
|
return ''
|
|
806
811
|
|
|
807
|
-
const withoutMarkdown = value.replace(/\[([^\]]+)\]\([
|
|
812
|
+
const withoutMarkdown = value.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
808
813
|
const singleLine = withoutMarkdown.replace(/\s+/g, ' ').trim()
|
|
809
814
|
if (!singleLine)
|
|
810
815
|
return ''
|
package/package.json
CHANGED
package/components/tela/card.vue
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
defineProps<{
|
|
3
|
-
contentPadding?: string
|
|
4
|
-
borderRadius?: string
|
|
5
|
-
}>()
|
|
6
|
-
|
|
7
|
-
const rootEl = ref<HTMLElement | null>(null)
|
|
8
|
-
|
|
9
|
-
defineExpose({
|
|
10
|
-
el: rootEl,
|
|
11
|
-
})
|
|
12
|
-
</script>
|
|
13
|
-
|
|
14
|
-
<template>
|
|
15
|
-
<div
|
|
16
|
-
ref="rootEl"
|
|
17
|
-
bg-white
|
|
18
|
-
:class="borderRadius ? `rounded-${borderRadius}` : 'rounded-16px'"
|
|
19
|
-
b=".5 gray-200"
|
|
20
|
-
>
|
|
21
|
-
<div :class="contentPadding ?? 'p-32px'">
|
|
22
|
-
<slot />
|
|
23
|
-
</div>
|
|
24
|
-
<div v-if="$slots.footer" p-19px b="t-1 #EBEBEB">
|
|
25
|
-
<slot name="footer" />
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
28
|
-
</template>
|
/package/components/tela/{segment-toggle.stories.ts → segment-toggle/segment-toggle.stories.ts}
RENAMED
|
File without changes
|
|
File without changes
|