@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.
@@ -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
- renderedPages.value.clear()
226
- await nextTick()
227
- await renderAllPdfPages()
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: { pageNum: number, canvas: HTMLCanvasElement, scale: number }) => Promise<void>
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
- <Icon v-if="typeof currentOption.icon === 'string'" :name="currentOption.icon" />
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(measureContentWidth)
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 { computed } from 'vue'
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 = computed(() => {
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
+ }
@@ -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, type TypeResolver } from './type-resolver'
7
- import { createVolarExtractor, type VolarExtractor } from './extractors/volar-extract'
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 design-tokens.md content if it exists
548
- let designTokensContent = ''
549
+ // Read all .md files from docs/ and append them to SKILL.md
550
+ let docsSection = ''
549
551
  if (layerPath) {
550
- const designTokensSourcePath = join(layerPath, 'docs', 'design-tokens.md')
551
- if (existsSync(designTokensSourcePath)) {
552
- designTokensContent = readFileSync(designTokensSourcePath, 'utf-8')
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')}${designTokensSection}
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-zA-Z0-9-]/g, '-')
683
+ .replace(/[^a-z0-9-]/gi, '-')
679
684
  .toLowerCase()
680
- .replace(/--+/g, '-')
685
+ .replace(/-{2,}/g, '-')
681
686
  .replace(/^-+|-+$/g, '')
682
687
  }
683
688
 
684
- function wrapWithSkillFrontmatter(meta: { name: string; description: string; allowedTools?: string[] }, body: string): 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(/--+/g, '-')
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(/\[([^\]]+)\]\([^\)]+\)/g, '$1')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",
@@ -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>