@scalar/components 0.21.3 → 0.22.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.
Files changed (59) hide show
  1. package/dist/components/ScalarCard/ScalarCard.vue.d.ts.map +1 -1
  2. package/dist/components/ScalarCard/ScalarCard.vue.js.map +1 -1
  3. package/dist/components/ScalarCard/ScalarCard.vue.script.js +1 -1
  4. package/dist/components/ScalarCard/ScalarCard.vue.script.js.map +1 -1
  5. package/dist/components/ScalarCard/ScalarCardSection.vue.d.ts.map +1 -1
  6. package/dist/components/ScalarCard/ScalarCardSection.vue.js.map +1 -1
  7. package/dist/components/ScalarCard/ScalarCardSection.vue.script.js +1 -1
  8. package/dist/components/ScalarCard/ScalarCardSection.vue.script.js.map +1 -1
  9. package/dist/components/ScalarIcon/variants.d.ts +2 -50
  10. package/dist/components/ScalarIcon/variants.d.ts.map +1 -1
  11. package/dist/components/ScalarMarkdown/ScalarMarkdownSummary.vue.d.ts +6 -0
  12. package/dist/components/ScalarMarkdown/ScalarMarkdownSummary.vue.d.ts.map +1 -1
  13. package/dist/components/ScalarMarkdown/ScalarMarkdownSummary.vue.js.map +1 -1
  14. package/dist/components/ScalarMarkdown/ScalarMarkdownSummary.vue.script.js +12 -7
  15. package/dist/components/ScalarMarkdown/ScalarMarkdownSummary.vue.script.js.map +1 -1
  16. package/dist/components/ScalarMenu/ScalarMenuLink.vue.d.ts +2 -0
  17. package/dist/components/ScalarMenu/ScalarMenuLink.vue.d.ts.map +1 -1
  18. package/dist/components/ScalarMenu/ScalarMenuLink.vue.js.map +1 -1
  19. package/dist/components/ScalarMenu/ScalarMenuLink.vue.script.js +7 -6
  20. package/dist/components/ScalarMenu/ScalarMenuLink.vue.script.js.map +1 -1
  21. package/dist/components/ScalarMenu/ScalarMenuTeamPicker.vue.js.map +1 -1
  22. package/dist/components/ScalarMenu/ScalarMenuTeamPicker.vue.script.js +4 -4
  23. package/dist/components/ScalarMenu/ScalarMenuTeamPicker.vue.script.js.map +1 -1
  24. package/dist/components/ScalarMenu/ScalarMenuWorkspacePicker.vue.js.map +1 -1
  25. package/dist/components/ScalarMenu/ScalarMenuWorkspacePicker.vue.script.js +4 -4
  26. package/dist/components/ScalarMenu/ScalarMenuWorkspacePicker.vue.script.js.map +1 -1
  27. package/dist/components/ScalarSidebar/ScalarSidebarFooter.vue.js.map +1 -1
  28. package/dist/components/ScalarSidebar/ScalarSidebarFooter.vue.script.js +1 -1
  29. package/dist/components/ScalarSidebar/ScalarSidebarFooter.vue.script.js.map +1 -1
  30. package/dist/components/ScalarSidebar/ScalarSidebarItems.vue.js.map +1 -1
  31. package/dist/components/ScalarSidebar/ScalarSidebarItems.vue.script.js +1 -1
  32. package/dist/components/ScalarSidebar/ScalarSidebarItems.vue.script.js.map +1 -1
  33. package/dist/components/ScalarSidebar/ScalarSidebarNestedItems.vue.d.ts.map +1 -1
  34. package/dist/components/ScalarSidebar/ScalarSidebarNestedItems.vue.js +1 -0
  35. package/dist/components/ScalarSidebar/ScalarSidebarNestedItems.vue.js.map +1 -1
  36. package/dist/components/ScalarSidebar/ScalarSidebarNestedItems.vue.script.js +11 -14
  37. package/dist/components/ScalarSidebar/ScalarSidebarNestedItems.vue.script.js.map +1 -1
  38. package/dist/components/ScalarSidebar/ScalarSidebarPlayground.vue.d.ts +20 -0
  39. package/dist/components/ScalarSidebar/ScalarSidebarPlayground.vue.d.ts.map +1 -1
  40. package/dist/components/ScalarVirtualText/ScalarVirtualText.vue.d.ts +10 -1
  41. package/dist/components/ScalarVirtualText/ScalarVirtualText.vue.d.ts.map +1 -1
  42. package/dist/components/ScalarVirtualText/ScalarVirtualText.vue.js +3 -1
  43. package/dist/components/ScalarVirtualText/ScalarVirtualText.vue.js.map +1 -1
  44. package/dist/components/ScalarVirtualText/ScalarVirtualText.vue.script.js +183 -37
  45. package/dist/components/ScalarVirtualText/ScalarVirtualText.vue.script.js.map +1 -1
  46. package/dist/components/ScalarVirtualText/ScalarVirtualTextSearch.vue.d.ts +45 -0
  47. package/dist/components/ScalarVirtualText/ScalarVirtualTextSearch.vue.d.ts.map +1 -0
  48. package/dist/components/ScalarVirtualText/ScalarVirtualTextSearch.vue.js +9 -0
  49. package/dist/components/ScalarVirtualText/ScalarVirtualTextSearch.vue.js.map +1 -0
  50. package/dist/components/ScalarVirtualText/ScalarVirtualTextSearch.vue.script.js +101 -0
  51. package/dist/components/ScalarVirtualText/ScalarVirtualTextSearch.vue.script.js.map +1 -0
  52. package/dist/components/ScalarWrappingText/ScalarWrappingText.vue.d.ts.map +1 -1
  53. package/dist/components/ScalarWrappingText/ScalarWrappingText.vue.js.map +1 -1
  54. package/dist/components/ScalarWrappingText/ScalarWrappingText.vue.script.js +4 -2
  55. package/dist/components/ScalarWrappingText/ScalarWrappingText.vue.script.js.map +1 -1
  56. package/dist/docs/useBindCx/MockComponents.d.ts +4 -100
  57. package/dist/docs/useBindCx/MockComponents.d.ts.map +1 -1
  58. package/dist/style.css +137 -11
  59. package/package.json +14 -14
@@ -1 +1 @@
1
- {"version":3,"file":"ScalarSidebarNestedItems.vue.d.ts","sourceRoot":"","sources":["../../../src/components/ScalarSidebar/ScalarSidebarNestedItems.vue"],"names":[],"mappings":"AA8LA;;;;;;;;;;;;;;;GAeG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAGzC,QAAA,MAAM,YAAY;;;;WA+FT,OAAO;;;;;;;;;;WAAP,OAAO;;;;;;IA9Ed,qCAAqC;cACzB,MAAM,OAAO;IACzB,iCAAiC;aACtB,MAAM,OAAO;IACxB,wBAAwB;WACf,MAAM,OAAO;IACtB,8BAA8B;YACpB,MAAM,OAAO;IACvB,+BAA+B;WACtB,MAAM,OAAO;IACtB,qCAAqC;mBACtB,MAAM,OAAO;IAC5B,mCAAmC;YACzB,MAAM,OAAO;EAoSrB,CAAC;AACL,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
1
+ {"version":3,"file":"ScalarSidebarNestedItems.vue.d.ts","sourceRoot":"","sources":["../../../src/components/ScalarSidebar/ScalarSidebarNestedItems.vue"],"names":[],"mappings":"AAoNA;;;;;;;;;;;;;;;GAeG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAGzC,QAAA,MAAM,YAAY;;;;WA+FT,OAAO;;;;;;;;;;WAAP,OAAO;;;;;;IA9Ed,qCAAqC;cACzB,MAAM,OAAO;IACzB,iCAAiC;aACtB,MAAM,OAAO;IACxB,wBAAwB;WACf,MAAM,OAAO;IACtB,8BAA8B;YACpB,MAAM,OAAO;IACvB,+BAA+B;WACtB,MAAM,OAAO;IACtB,qCAAqC;mBACtB,MAAM,OAAO;IAC5B,mCAAmC;YACzB,MAAM,OAAO;EAoTrB,CAAC;AACL,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import ScalarSidebarNestedItems_vue_vue_type_script_setup_true_lang_default from "./ScalarSidebarNestedItems.vue.script.js";
2
+ /* empty css */
2
3
  //#region src/components/ScalarSidebar/ScalarSidebarNestedItems.vue
3
4
  var ScalarSidebarNestedItems_default = ScalarSidebarNestedItems_vue_vue_type_script_setup_true_lang_default;
4
5
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"ScalarSidebarNestedItems.vue.js","names":[],"sources":["../../../src/components/ScalarSidebar/ScalarSidebarNestedItems.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar Sidebar Nested Items component\n *\n * A provides list of items thats presented over the parent list\n * Needs to be nested inside a ScalarSidebarItems component\n *\n * @example\n * <ScalarSidebarNestedItems v-model=\"open\">\n * <!-- Item text -->\n * <template #items>\n * <ScalarSidebarItem>...</ScalarSidebarItem>\n * <ScalarSidebarItem>...</ScalarSidebarItem>\n * <ScalarSidebarItem>...</ScalarSidebarItem>\n * </template>\n * </ScalarSidebarNestedItems>\n */\nexport default {}\n</script>\n<script setup lang=\"ts\">\nimport { ScalarIconArrowRight, ScalarIconCaretLeft } from '@scalar/icons'\nimport { nextTick, ref, useTemplateRef } from 'vue'\n\nimport ScalarSidebarButton from './ScalarSidebarButton.vue'\nimport ScalarSidebarItems from './ScalarSidebarItems.vue'\nimport ScalarSidebarSpacer from './ScalarSidebarSpacer.vue'\nimport { findScrollContainer } from './findScrollContainer'\nimport type { ScalarSidebarGroupProps } from './types'\nimport { useSidebarGroups } from './useSidebarGroups'\nimport { useSidebarNestedItem } from './useSidebarNestedItems'\n\nconst { controlled } = defineProps<ScalarSidebarGroupProps>()\n\nconst emit = defineEmits<{\n /** Emitted when the nested item button is clicked */\n (e: 'click', event: MouseEvent): void\n /** Emitted when the back button is clicked */\n (e: 'back', event: MouseEvent): void\n}>()\n\nconst open = defineModel<boolean>('open', { default: false })\nuseSidebarNestedItem(open)\n\ndefineSlots<{\n /** The text content of the button */\n 'default'?: () => unknown\n /** Override the entire button */\n 'button'?: () => unknown\n /** Override the icon */\n 'icon'?: () => unknown\n /** Override the aside slot */\n 'aside'?: () => unknown\n /** Override the back button */\n 'back'?: () => unknown\n /** Override the back button label */\n 'back-label'?: () => unknown\n /** The list of sidebar subitems */\n 'items'?: () => unknown\n}>()\n\nconst { level } = useSidebarGroups({ reset: true })\n\n/** The root element ref */\nconst el = useTemplateRef('el')\n\n/** How far down to offset the nested items by when animating them in */\nconst offset = ref(0)\n\n// Transition hooks\n\nfunction onOpen() {\n // Calculate how far down the nearest scroll container is scrolled\n // We offset the nested items by this amount when animating them\n // in so they appear in line with the parent items list.\n offset.value = findScrollContainer(el.value).scrollTop ?? 0\n\n // Focus the first button in the nested items\n nextTick(() =>\n el.value\n ?.querySelector<HTMLButtonElement>('button:not([aria-expanded=\"true\"])')\n ?.focus({ preventScroll: true }),\n )\n}\nfunction onOpened() {\n /** Reset the scroll position of the scroll container after opening */\n findScrollContainer(el.value).scrollTop = 0\n}\nfunction onClose() {\n nextTick(() => {\n el.value?.querySelector('button')?.focus({ preventScroll: true })\n findScrollContainer(el.value).scrollTop = offset.value\n })\n}\n\ndefineOptions({ inheritAttrs: false })\n\n/** Handle the click event for the nested items button */\nconst handleClick = (event: MouseEvent) => {\n // Bubble up the click event\n emit('click', event)\n if (!controlled) {\n // Only toggle the open state if the group is uncontrolled\n open.value = true\n }\n}\n\n/** Handle the back button click event */\nconst handleBack = (event: MouseEvent) => {\n // Bubble up the back event\n emit('back', event)\n if (!controlled) {\n // Only toggle the open state if the group is uncontrolled\n open.value = false\n }\n}\n</script>\n<template>\n <li\n ref=\"el\"\n class=\"group/item group/nested-items contents\"\n :class=\"{ 'group/nested-items-open': open }\"\n :style=\"{ '--nested-items-offset': `${offset}px` }\">\n <slot name=\"button\">\n <ScalarSidebarButton\n is=\"button\"\n :aria-expanded=\"open\"\n :disabled\n :icon\n :indent=\"level\"\n :selected\n v-bind=\"$attrs\"\n @click=\"handleClick\">\n <template\n v-if=\"$slots.icon\"\n #icon>\n <slot name=\"icon\" />\n </template>\n <slot />\n <template #aside>\n <slot name=\"aside\">\n <ScalarIconArrowRight class=\"size-4 text-sidebar-c-2\" />\n </slot>\n </template>\n </ScalarSidebarButton>\n </slot>\n <!-- Make sure the div is around for the entire transition -->\n <Transition\n :duration=\"300\"\n enterActiveClass=\"top-(--nested-items-offset)!\"\n leaveActiveClass=\"top-(--nested-items-offset)!\"\n @afterEnter=\"onOpened\"\n @enter=\"onOpen\"\n @leave=\"onClose\">\n <div\n v-if=\"open\"\n class=\"absolute inset-0 translate-x-full\">\n <ScalarSidebarItems v-bind=\"$attrs\">\n <slot name=\"back\">\n <ScalarSidebarButton\n is=\"button\"\n class=\"text-sidebar-c-1 font-sidebar-active hover:text-sidebar-c-1\"\n @click=\"handleBack\">\n <template #icon>\n <ScalarIconCaretLeft class=\"size-4 -m-px text-sidebar-c-2\" />\n </template>\n <slot name=\"back-label\">Back</slot>\n </ScalarSidebarButton>\n </slot>\n <ScalarSidebarSpacer class=\"h-3\" />\n <slot name=\"items\" />\n </ScalarSidebarItems>\n </div>\n </Transition>\n </li>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"ScalarSidebarNestedItems.vue.js","names":[],"sources":["../../../src/components/ScalarSidebar/ScalarSidebarNestedItems.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar Sidebar Nested Items component\n *\n * A provides list of items thats presented over the parent list\n * Needs to be nested inside a ScalarSidebarItems component\n *\n * @example\n * <ScalarSidebarNestedItems v-model=\"open\">\n * <!-- Item text -->\n * <template #items>\n * <ScalarSidebarItem>...</ScalarSidebarItem>\n * <ScalarSidebarItem>...</ScalarSidebarItem>\n * <ScalarSidebarItem>...</ScalarSidebarItem>\n * </template>\n * </ScalarSidebarNestedItems>\n */\nexport default {}\n</script>\n<script setup lang=\"ts\">\nimport { ScalarIconArrowRight, ScalarIconCaretLeft } from '@scalar/icons'\nimport { nextTick, ref, useTemplateRef } from 'vue'\n\nimport ScalarSidebarButton from './ScalarSidebarButton.vue'\nimport ScalarSidebarItems from './ScalarSidebarItems.vue'\nimport ScalarSidebarSpacer from './ScalarSidebarSpacer.vue'\nimport { findScrollContainer } from './findScrollContainer'\nimport type { ScalarSidebarGroupProps } from './types'\nimport { useSidebarGroups } from './useSidebarGroups'\nimport { useSidebarNestedItem } from './useSidebarNestedItems'\n\nconst { controlled } = defineProps<ScalarSidebarGroupProps>()\n\nconst emit = defineEmits<{\n /** Emitted when the nested item button is clicked */\n (e: 'click', event: MouseEvent): void\n /** Emitted when the back button is clicked */\n (e: 'back', event: MouseEvent): void\n}>()\n\nconst open = defineModel<boolean>('open', { default: false })\nuseSidebarNestedItem(open)\n\ndefineSlots<{\n /** The text content of the button */\n 'default'?: () => unknown\n /** Override the entire button */\n 'button'?: () => unknown\n /** Override the icon */\n 'icon'?: () => unknown\n /** Override the aside slot */\n 'aside'?: () => unknown\n /** Override the back button */\n 'back'?: () => unknown\n /** Override the back button label */\n 'back-label'?: () => unknown\n /** The list of sidebar subitems */\n 'items'?: () => unknown\n}>()\n\nconst { level } = useSidebarGroups({ reset: true })\n\n/** The root element ref */\nconst el = useTemplateRef('el')\n\n/** How far down to offset the nested items by when animating them in */\nconst offset = ref(0)\n\n// Transition hooks\n\nfunction onOpen() {\n // Calculate how far down the nearest scroll container is scrolled\n // We offset the nested items by this amount when animating them\n // in so they appear in line with the parent items list.\n offset.value = findScrollContainer(el.value).scrollTop ?? 0\n\n // Focus the first button in the nested items\n nextTick(() =>\n el.value\n ?.querySelector<HTMLButtonElement>('button:not([aria-expanded=\"true\"])')\n ?.focus({ preventScroll: true }),\n )\n}\nfunction onOpened() {\n /** Reset the scroll position of the scroll container after opening */\n findScrollContainer(el.value).scrollTop = 0\n}\nfunction onClose() {\n nextTick(() => {\n el.value?.querySelector('button')?.focus({ preventScroll: true })\n findScrollContainer(el.value).scrollTop = offset.value\n })\n}\n\ndefineOptions({ inheritAttrs: false })\n\n/** Handle the click event for the nested items button */\nconst handleClick = (event: MouseEvent) => {\n // Bubble up the click event\n emit('click', event)\n if (!controlled) {\n // Only toggle the open state if the group is uncontrolled\n open.value = true\n }\n}\n\n/** Handle the back button click event */\nconst handleBack = (event: MouseEvent) => {\n // Bubble up the back event\n emit('back', event)\n if (!controlled) {\n // Only toggle the open state if the group is uncontrolled\n open.value = false\n }\n}\n</script>\n<template>\n <li\n ref=\"el\"\n class=\"group/item group/nested-items contents\"\n :class=\"{ 'group/nested-items-open': open }\"\n :style=\"{ '--nested-items-offset': `${offset}px` }\">\n <slot name=\"button\">\n <ScalarSidebarButton\n is=\"button\"\n :aria-expanded=\"open\"\n :disabled\n :icon\n :indent=\"level\"\n :selected\n v-bind=\"$attrs\"\n @click=\"handleClick\">\n <template\n v-if=\"$slots.icon\"\n #icon>\n <slot name=\"icon\" />\n </template>\n <slot />\n <template #aside>\n <slot name=\"aside\">\n <ScalarIconArrowRight class=\"size-4 text-sidebar-c-2\" />\n </slot>\n </template>\n </ScalarSidebarButton>\n </slot>\n <!-- Make sure the div is around for the entire transition -->\n <Transition\n :duration=\"300\"\n enterActiveClass=\"top-(--nested-items-offset)!\"\n leaveActiveClass=\"top-(--nested-items-offset)!\"\n @afterEnter=\"onOpened\"\n @enter=\"onOpen\"\n @leave=\"onClose\">\n <div\n v-if=\"open\"\n class=\"absolute inset-0 translate-x-full\">\n <ScalarSidebarItems v-bind=\"$attrs\">\n <div\n class=\"flex flex-col gap-px sticky top-(--scalar-sidebar-sticky-offset,0) p-(--scalar-sidebar-padding) -m-(--scalar-sidebar-padding) mb-0 pb-0 bg-sidebar-b-1 z-1 animate-sidebar-border-bottom border-b-sidebar-border\">\n <slot name=\"back\">\n <ScalarSidebarButton\n is=\"button\"\n class=\"text-sidebar-c-1 font-sidebar-active hover:text-sidebar-c-1\"\n @click=\"handleBack\">\n <template #icon>\n <ScalarIconCaretLeft class=\"size-4 -m-px text-sidebar-c-2\" />\n </template>\n <slot name=\"back-label\">Back</slot>\n </ScalarSidebarButton>\n </slot>\n <ScalarSidebarSpacer class=\"h-3\" />\n </div>\n <slot name=\"items\" />\n </ScalarSidebarItems>\n </div>\n </Transition>\n </li>\n</template>\n\n<style>\n@reference \"../../style.css\";\n\n.animate-sidebar-border-bottom {\n animation: border-bottom forwards;\n animation-timeline: scroll();\n animation-range-end: 1px;\n}\n\n@keyframes border-bottom {\n from {\n border-bottom-width: 0;\n }\n to {\n border-bottom-width: var(--scalar-border-width);\n }\n}\n</style>\n"],"mappings":""}
@@ -4,13 +4,14 @@ import { useSidebarNestedItem } from "./useSidebarNestedItems.js";
4
4
  import ScalarSidebarItems_default from "./ScalarSidebarItems.vue.js";
5
5
  import ScalarSidebarSpacer_default from "./ScalarSidebarSpacer.vue.js";
6
6
  import { findScrollContainer } from "./findScrollContainer.js";
7
- import { Transition, createCommentVNode, createElementBlock, createSlots, createTextVNode, createVNode, defineComponent, guardReactiveProps, mergeModels, mergeProps, nextTick, normalizeClass, normalizeProps, normalizeStyle, openBlock, ref, renderSlot, unref, useModel, useTemplateRef, withCtx } from "vue";
7
+ import { Transition, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, guardReactiveProps, mergeModels, mergeProps, nextTick, normalizeClass, normalizeProps, normalizeStyle, openBlock, ref, renderSlot, unref, useModel, useTemplateRef, withCtx } from "vue";
8
8
  import { ScalarIconArrowRight, ScalarIconCaretLeft } from "@scalar/icons";
9
9
  //#region src/components/ScalarSidebar/ScalarSidebarNestedItems.vue?vue&type=script&setup=true&lang.ts
10
10
  var _hoisted_1 = {
11
11
  key: 0,
12
12
  class: "absolute inset-0 translate-x-full"
13
13
  };
14
+ var _hoisted_2 = { class: "flex flex-col gap-px sticky top-(--scalar-sidebar-sticky-offset,0) p-(--scalar-sidebar-padding) -m-(--scalar-sidebar-padding) mb-0 pb-0 bg-sidebar-b-1 z-1 animate-sidebar-border-bottom border-b-sidebar-border" };
14
15
  var ScalarSidebarNestedItems_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
15
16
  inheritAttrs: false,
16
17
  __name: "ScalarSidebarNestedItems",
@@ -100,19 +101,15 @@ var ScalarSidebarNestedItems_vue_vue_type_script_setup_true_lang_default = /* @_
100
101
  onLeave: onClose
101
102
  }, {
102
103
  default: withCtx(() => [open.value ? (openBlock(), createElementBlock("div", _hoisted_1, [createVNode(ScalarSidebarItems_default, normalizeProps(guardReactiveProps(_ctx.$attrs)), {
103
- default: withCtx(() => [
104
- renderSlot(_ctx.$slots, "back", {}, () => [createVNode(ScalarSidebarButton_default, {
105
- is: "button",
106
- class: "text-sidebar-c-1 font-sidebar-active hover:text-sidebar-c-1",
107
- onClick: handleBack
108
- }, {
109
- icon: withCtx(() => [createVNode(unref(ScalarIconCaretLeft), { class: "size-4 -m-px text-sidebar-c-2" })]),
110
- default: withCtx(() => [renderSlot(_ctx.$slots, "back-label", {}, () => [_cache[0] || (_cache[0] = createTextVNode("Back", -1))])]),
111
- _: 3
112
- })]),
113
- createVNode(ScalarSidebarSpacer_default, { class: "h-3" }),
114
- renderSlot(_ctx.$slots, "items")
115
- ]),
104
+ default: withCtx(() => [createElementVNode("div", _hoisted_2, [renderSlot(_ctx.$slots, "back", {}, () => [createVNode(ScalarSidebarButton_default, {
105
+ is: "button",
106
+ class: "text-sidebar-c-1 font-sidebar-active hover:text-sidebar-c-1",
107
+ onClick: handleBack
108
+ }, {
109
+ icon: withCtx(() => [createVNode(unref(ScalarIconCaretLeft), { class: "size-4 -m-px text-sidebar-c-2" })]),
110
+ default: withCtx(() => [renderSlot(_ctx.$slots, "back-label", {}, () => [_cache[0] || (_cache[0] = createTextVNode("Back", -1))])]),
111
+ _: 3
112
+ })]), createVNode(ScalarSidebarSpacer_default, { class: "h-3" })]), renderSlot(_ctx.$slots, "items")]),
116
113
  _: 3
117
114
  }, 16)])) : createCommentVNode("", true)]),
118
115
  _: 3
@@ -1 +1 @@
1
- {"version":3,"file":"ScalarSidebarNestedItems.vue.script.js","names":["$attrs","$slots"],"sources":["../../../src/components/ScalarSidebar/ScalarSidebarNestedItems.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar Sidebar Nested Items component\n *\n * A provides list of items thats presented over the parent list\n * Needs to be nested inside a ScalarSidebarItems component\n *\n * @example\n * <ScalarSidebarNestedItems v-model=\"open\">\n * <!-- Item text -->\n * <template #items>\n * <ScalarSidebarItem>...</ScalarSidebarItem>\n * <ScalarSidebarItem>...</ScalarSidebarItem>\n * <ScalarSidebarItem>...</ScalarSidebarItem>\n * </template>\n * </ScalarSidebarNestedItems>\n */\nexport default {}\n</script>\n<script setup lang=\"ts\">\nimport { ScalarIconArrowRight, ScalarIconCaretLeft } from '@scalar/icons'\nimport { nextTick, ref, useTemplateRef } from 'vue'\n\nimport ScalarSidebarButton from './ScalarSidebarButton.vue'\nimport ScalarSidebarItems from './ScalarSidebarItems.vue'\nimport ScalarSidebarSpacer from './ScalarSidebarSpacer.vue'\nimport { findScrollContainer } from './findScrollContainer'\nimport type { ScalarSidebarGroupProps } from './types'\nimport { useSidebarGroups } from './useSidebarGroups'\nimport { useSidebarNestedItem } from './useSidebarNestedItems'\n\nconst { controlled } = defineProps<ScalarSidebarGroupProps>()\n\nconst emit = defineEmits<{\n /** Emitted when the nested item button is clicked */\n (e: 'click', event: MouseEvent): void\n /** Emitted when the back button is clicked */\n (e: 'back', event: MouseEvent): void\n}>()\n\nconst open = defineModel<boolean>('open', { default: false })\nuseSidebarNestedItem(open)\n\ndefineSlots<{\n /** The text content of the button */\n 'default'?: () => unknown\n /** Override the entire button */\n 'button'?: () => unknown\n /** Override the icon */\n 'icon'?: () => unknown\n /** Override the aside slot */\n 'aside'?: () => unknown\n /** Override the back button */\n 'back'?: () => unknown\n /** Override the back button label */\n 'back-label'?: () => unknown\n /** The list of sidebar subitems */\n 'items'?: () => unknown\n}>()\n\nconst { level } = useSidebarGroups({ reset: true })\n\n/** The root element ref */\nconst el = useTemplateRef('el')\n\n/** How far down to offset the nested items by when animating them in */\nconst offset = ref(0)\n\n// Transition hooks\n\nfunction onOpen() {\n // Calculate how far down the nearest scroll container is scrolled\n // We offset the nested items by this amount when animating them\n // in so they appear in line with the parent items list.\n offset.value = findScrollContainer(el.value).scrollTop ?? 0\n\n // Focus the first button in the nested items\n nextTick(() =>\n el.value\n ?.querySelector<HTMLButtonElement>('button:not([aria-expanded=\"true\"])')\n ?.focus({ preventScroll: true }),\n )\n}\nfunction onOpened() {\n /** Reset the scroll position of the scroll container after opening */\n findScrollContainer(el.value).scrollTop = 0\n}\nfunction onClose() {\n nextTick(() => {\n el.value?.querySelector('button')?.focus({ preventScroll: true })\n findScrollContainer(el.value).scrollTop = offset.value\n })\n}\n\ndefineOptions({ inheritAttrs: false })\n\n/** Handle the click event for the nested items button */\nconst handleClick = (event: MouseEvent) => {\n // Bubble up the click event\n emit('click', event)\n if (!controlled) {\n // Only toggle the open state if the group is uncontrolled\n open.value = true\n }\n}\n\n/** Handle the back button click event */\nconst handleBack = (event: MouseEvent) => {\n // Bubble up the back event\n emit('back', event)\n if (!controlled) {\n // Only toggle the open state if the group is uncontrolled\n open.value = false\n }\n}\n</script>\n<template>\n <li\n ref=\"el\"\n class=\"group/item group/nested-items contents\"\n :class=\"{ 'group/nested-items-open': open }\"\n :style=\"{ '--nested-items-offset': `${offset}px` }\">\n <slot name=\"button\">\n <ScalarSidebarButton\n is=\"button\"\n :aria-expanded=\"open\"\n :disabled\n :icon\n :indent=\"level\"\n :selected\n v-bind=\"$attrs\"\n @click=\"handleClick\">\n <template\n v-if=\"$slots.icon\"\n #icon>\n <slot name=\"icon\" />\n </template>\n <slot />\n <template #aside>\n <slot name=\"aside\">\n <ScalarIconArrowRight class=\"size-4 text-sidebar-c-2\" />\n </slot>\n </template>\n </ScalarSidebarButton>\n </slot>\n <!-- Make sure the div is around for the entire transition -->\n <Transition\n :duration=\"300\"\n enterActiveClass=\"top-(--nested-items-offset)!\"\n leaveActiveClass=\"top-(--nested-items-offset)!\"\n @afterEnter=\"onOpened\"\n @enter=\"onOpen\"\n @leave=\"onClose\">\n <div\n v-if=\"open\"\n class=\"absolute inset-0 translate-x-full\">\n <ScalarSidebarItems v-bind=\"$attrs\">\n <slot name=\"back\">\n <ScalarSidebarButton\n is=\"button\"\n class=\"text-sidebar-c-1 font-sidebar-active hover:text-sidebar-c-1\"\n @click=\"handleBack\">\n <template #icon>\n <ScalarIconCaretLeft class=\"size-4 -m-px text-sidebar-c-2\" />\n </template>\n <slot name=\"back-label\">Back</slot>\n </ScalarSidebarButton>\n </slot>\n <ScalarSidebarSpacer class=\"h-3\" />\n <slot name=\"items\" />\n </ScalarSidebarItems>\n </div>\n </Transition>\n </li>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiCA,MAAM,OAAO;EAOb,MAAM,OAAO,SAAoB,SAAC,OAA0B;AAC5D,uBAAqB,KAAI;EAmBzB,MAAM,EAAE,UAAU,iBAAiB,EAAE,OAAO,MAAM,CAAA;;EAGlD,MAAM,KAAK,eAAe,KAAI;;EAG9B,MAAM,SAAS,IAAI,EAAC;EAIpB,SAAS,SAAS;AAIhB,UAAO,QAAQ,oBAAoB,GAAG,MAAM,CAAC,aAAa;AAG1D,kBACE,GAAG,OACC,cAAiC,uCAAoC,EACrE,MAAM,EAAE,eAAe,MAAM,CAAC,CACpC;;EAEF,SAAS,WAAW;;AAElB,uBAAoB,GAAG,MAAM,CAAC,YAAY;;EAE5C,SAAS,UAAU;AACjB,kBAAe;AACb,OAAG,OAAO,cAAc,SAAS,EAAE,MAAM,EAAE,eAAe,MAAM,CAAA;AAChE,wBAAoB,GAAG,MAAM,CAAC,YAAY,OAAO;KAClD;;;EAMH,MAAM,eAAe,UAAsB;AAEzC,QAAK,SAAS,MAAK;AACnB,OAAI,CAAC,QAAA,WAEH,MAAK,QAAQ;;;EAKjB,MAAM,cAAc,UAAsB;AAExC,QAAK,QAAQ,MAAK;AAClB,OAAI,CAAC,QAAA,WAEH,MAAK,QAAQ;;;uBAKf,mBAwDK,MAAA;aAvDC;IAAJ,KAAI;IACJ,OAAK,eAAA,CAAC,0CAAwC,EAAA,2BACT,KAAA,OAAI,CAAA,CAAA;IACxC,OAAK,eAAA,EAAA,yBAAA,GAAgC,OAAA,MAAM,KAAA,CAAA;OAC5C,WAsBO,KAAA,QAAA,UAAA,EAAA,QAAA,CArBL,YAoBsB,6BApBtB,WAoBsB;IAnBpB,IAAG;IACF,iBAAe,KAAA;IACf,UAAA,QAAA;IACA,MAAA,QAAA;IACA,QAAQ,MAAA,MAAK;IACb,UAAA,QAAA;MACOA,KAAAA,QAAM,EACb,SAAO,aAAW,CAAA,EAAA,YAAA;IAOR,OAAK,cAGP,CAFP,WAEO,KAAA,QAAA,SAAA,EAAA,QAAA,CADL,YAAwD,MAAA,qBAAA,EAAA,EAAlC,OAAM,2BAAyB,CAAA,CAAA,CAAA,CAAA,CAAA;2BAHjD,CAAR,WAAQ,KAAA,QAAA,UAAA,CAAA,CAAA;;OAJAC,KAAAA,OAAO,OAAA;UACZ;sBACmB,CAApB,WAAoB,KAAA,QAAA,OAAA,CAAA,CAAA;;;;;;;;SAW1B,YA0Ba,YAAA;IAzBV,UAAU;IACX,kBAAiB;IACjB,kBAAiB;IAChB,cAAY;IACZ,SAAO;IACP,SAAO;;2BAmBF,CAjBE,KAAA,SAAA,WAAA,EADR,mBAkBM,OAlBN,YAkBM,CAfJ,YAcqB,4BAAA,eAAA,mBAdOD,KAAAA,OAAM,CAAA,EAAA;4BAWzB;MAVP,WAUO,KAAA,QAAA,QAAA,EAAA,QAAA,CATL,YAQsB,6BAAA;OAPpB,IAAG;OACH,OAAM;OACL,SAAO;;OACG,MAAI,cACgD,CAA7D,YAA6D,MAAA,oBAAA,EAAA,EAAxC,OAAM,iCAA+B,CAAA,CAAA,CAAA;8BAEzB,CAAnC,WAAmC,KAAA,QAAA,cAAA,EAAA,QAAA,CAAA,OAAA,OAAA,OAAA,KAAA,gBAAX,QAAI,GAAA,EAAA,CAAA,CAAA,CAAA;;;MAGhC,YAAmC,6BAAA,EAAd,OAAM,OAAK,CAAA;MAChC,WAAqB,KAAA,QAAA,QAAA"}
1
+ {"version":3,"file":"ScalarSidebarNestedItems.vue.script.js","names":["$attrs","$slots"],"sources":["../../../src/components/ScalarSidebar/ScalarSidebarNestedItems.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar Sidebar Nested Items component\n *\n * A provides list of items thats presented over the parent list\n * Needs to be nested inside a ScalarSidebarItems component\n *\n * @example\n * <ScalarSidebarNestedItems v-model=\"open\">\n * <!-- Item text -->\n * <template #items>\n * <ScalarSidebarItem>...</ScalarSidebarItem>\n * <ScalarSidebarItem>...</ScalarSidebarItem>\n * <ScalarSidebarItem>...</ScalarSidebarItem>\n * </template>\n * </ScalarSidebarNestedItems>\n */\nexport default {}\n</script>\n<script setup lang=\"ts\">\nimport { ScalarIconArrowRight, ScalarIconCaretLeft } from '@scalar/icons'\nimport { nextTick, ref, useTemplateRef } from 'vue'\n\nimport ScalarSidebarButton from './ScalarSidebarButton.vue'\nimport ScalarSidebarItems from './ScalarSidebarItems.vue'\nimport ScalarSidebarSpacer from './ScalarSidebarSpacer.vue'\nimport { findScrollContainer } from './findScrollContainer'\nimport type { ScalarSidebarGroupProps } from './types'\nimport { useSidebarGroups } from './useSidebarGroups'\nimport { useSidebarNestedItem } from './useSidebarNestedItems'\n\nconst { controlled } = defineProps<ScalarSidebarGroupProps>()\n\nconst emit = defineEmits<{\n /** Emitted when the nested item button is clicked */\n (e: 'click', event: MouseEvent): void\n /** Emitted when the back button is clicked */\n (e: 'back', event: MouseEvent): void\n}>()\n\nconst open = defineModel<boolean>('open', { default: false })\nuseSidebarNestedItem(open)\n\ndefineSlots<{\n /** The text content of the button */\n 'default'?: () => unknown\n /** Override the entire button */\n 'button'?: () => unknown\n /** Override the icon */\n 'icon'?: () => unknown\n /** Override the aside slot */\n 'aside'?: () => unknown\n /** Override the back button */\n 'back'?: () => unknown\n /** Override the back button label */\n 'back-label'?: () => unknown\n /** The list of sidebar subitems */\n 'items'?: () => unknown\n}>()\n\nconst { level } = useSidebarGroups({ reset: true })\n\n/** The root element ref */\nconst el = useTemplateRef('el')\n\n/** How far down to offset the nested items by when animating them in */\nconst offset = ref(0)\n\n// Transition hooks\n\nfunction onOpen() {\n // Calculate how far down the nearest scroll container is scrolled\n // We offset the nested items by this amount when animating them\n // in so they appear in line with the parent items list.\n offset.value = findScrollContainer(el.value).scrollTop ?? 0\n\n // Focus the first button in the nested items\n nextTick(() =>\n el.value\n ?.querySelector<HTMLButtonElement>('button:not([aria-expanded=\"true\"])')\n ?.focus({ preventScroll: true }),\n )\n}\nfunction onOpened() {\n /** Reset the scroll position of the scroll container after opening */\n findScrollContainer(el.value).scrollTop = 0\n}\nfunction onClose() {\n nextTick(() => {\n el.value?.querySelector('button')?.focus({ preventScroll: true })\n findScrollContainer(el.value).scrollTop = offset.value\n })\n}\n\ndefineOptions({ inheritAttrs: false })\n\n/** Handle the click event for the nested items button */\nconst handleClick = (event: MouseEvent) => {\n // Bubble up the click event\n emit('click', event)\n if (!controlled) {\n // Only toggle the open state if the group is uncontrolled\n open.value = true\n }\n}\n\n/** Handle the back button click event */\nconst handleBack = (event: MouseEvent) => {\n // Bubble up the back event\n emit('back', event)\n if (!controlled) {\n // Only toggle the open state if the group is uncontrolled\n open.value = false\n }\n}\n</script>\n<template>\n <li\n ref=\"el\"\n class=\"group/item group/nested-items contents\"\n :class=\"{ 'group/nested-items-open': open }\"\n :style=\"{ '--nested-items-offset': `${offset}px` }\">\n <slot name=\"button\">\n <ScalarSidebarButton\n is=\"button\"\n :aria-expanded=\"open\"\n :disabled\n :icon\n :indent=\"level\"\n :selected\n v-bind=\"$attrs\"\n @click=\"handleClick\">\n <template\n v-if=\"$slots.icon\"\n #icon>\n <slot name=\"icon\" />\n </template>\n <slot />\n <template #aside>\n <slot name=\"aside\">\n <ScalarIconArrowRight class=\"size-4 text-sidebar-c-2\" />\n </slot>\n </template>\n </ScalarSidebarButton>\n </slot>\n <!-- Make sure the div is around for the entire transition -->\n <Transition\n :duration=\"300\"\n enterActiveClass=\"top-(--nested-items-offset)!\"\n leaveActiveClass=\"top-(--nested-items-offset)!\"\n @afterEnter=\"onOpened\"\n @enter=\"onOpen\"\n @leave=\"onClose\">\n <div\n v-if=\"open\"\n class=\"absolute inset-0 translate-x-full\">\n <ScalarSidebarItems v-bind=\"$attrs\">\n <div\n class=\"flex flex-col gap-px sticky top-(--scalar-sidebar-sticky-offset,0) p-(--scalar-sidebar-padding) -m-(--scalar-sidebar-padding) mb-0 pb-0 bg-sidebar-b-1 z-1 animate-sidebar-border-bottom border-b-sidebar-border\">\n <slot name=\"back\">\n <ScalarSidebarButton\n is=\"button\"\n class=\"text-sidebar-c-1 font-sidebar-active hover:text-sidebar-c-1\"\n @click=\"handleBack\">\n <template #icon>\n <ScalarIconCaretLeft class=\"size-4 -m-px text-sidebar-c-2\" />\n </template>\n <slot name=\"back-label\">Back</slot>\n </ScalarSidebarButton>\n </slot>\n <ScalarSidebarSpacer class=\"h-3\" />\n </div>\n <slot name=\"items\" />\n </ScalarSidebarItems>\n </div>\n </Transition>\n </li>\n</template>\n\n<style>\n@reference \"../../style.css\";\n\n.animate-sidebar-border-bottom {\n animation: border-bottom forwards;\n animation-timeline: scroll();\n animation-range-end: 1px;\n}\n\n@keyframes border-bottom {\n from {\n border-bottom-width: 0;\n }\n to {\n border-bottom-width: var(--scalar-border-width);\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiCA,MAAM,OAAO;EAOb,MAAM,OAAO,SAAoB,SAAC,OAA0B;AAC5D,uBAAqB,KAAI;EAmBzB,MAAM,EAAE,UAAU,iBAAiB,EAAE,OAAO,MAAM,CAAA;;EAGlD,MAAM,KAAK,eAAe,KAAI;;EAG9B,MAAM,SAAS,IAAI,EAAC;EAIpB,SAAS,SAAS;AAIhB,UAAO,QAAQ,oBAAoB,GAAG,MAAM,CAAC,aAAa;AAG1D,kBACE,GAAG,OACC,cAAiC,uCAAoC,EACrE,MAAM,EAAE,eAAe,MAAM,CAAC,CACpC;;EAEF,SAAS,WAAW;;AAElB,uBAAoB,GAAG,MAAM,CAAC,YAAY;;EAE5C,SAAS,UAAU;AACjB,kBAAe;AACb,OAAG,OAAO,cAAc,SAAS,EAAE,MAAM,EAAE,eAAe,MAAM,CAAA;AAChE,wBAAoB,GAAG,MAAM,CAAC,YAAY,OAAO;KAClD;;;EAMH,MAAM,eAAe,UAAsB;AAEzC,QAAK,SAAS,MAAK;AACnB,OAAI,CAAC,QAAA,WAEH,MAAK,QAAQ;;;EAKjB,MAAM,cAAc,UAAsB;AAExC,QAAK,QAAQ,MAAK;AAClB,OAAI,CAAC,QAAA,WAEH,MAAK,QAAQ;;;uBAKf,mBA2DK,MAAA;aA1DC;IAAJ,KAAI;IACJ,OAAK,eAAA,CAAC,0CAAwC,EAAA,2BACT,KAAA,OAAI,CAAA,CAAA;IACxC,OAAK,eAAA,EAAA,yBAAA,GAAgC,OAAA,MAAM,KAAA,CAAA;OAC5C,WAsBO,KAAA,QAAA,UAAA,EAAA,QAAA,CArBL,YAoBsB,6BApBtB,WAoBsB;IAnBpB,IAAG;IACF,iBAAe,KAAA;IACf,UAAA,QAAA;IACA,MAAA,QAAA;IACA,QAAQ,MAAA,MAAK;IACb,UAAA,QAAA;MACOA,KAAAA,QAAM,EACb,SAAO,aAAW,CAAA,EAAA,YAAA;IAOR,OAAK,cAGP,CAFP,WAEO,KAAA,QAAA,SAAA,EAAA,QAAA,CADL,YAAwD,MAAA,qBAAA,EAAA,EAAlC,OAAM,2BAAyB,CAAA,CAAA,CAAA,CAAA,CAAA;2BAHjD,CAAR,WAAQ,KAAA,QAAA,UAAA,CAAA,CAAA;;OAJAC,KAAAA,OAAO,OAAA;UACZ;sBACmB,CAApB,WAAoB,KAAA,QAAA,OAAA,CAAA,CAAA;;;;;;;;SAW1B,YA6Ba,YAAA;IA5BV,UAAU;IACX,kBAAiB;IACjB,kBAAiB;IAChB,cAAY;IACZ,SAAO;IACP,SAAO;;2BAsBF,CApBE,KAAA,SAAA,WAAA,EADR,mBAqBM,OArBN,YAqBM,CAlBJ,YAiBqB,4BAAA,eAAA,mBAjBOD,KAAAA,OAAM,CAAA,EAAA;4BAe1B,CAdN,mBAcM,OAdN,YAcM,CAZJ,WAUO,KAAA,QAAA,QAAA,EAAA,QAAA,CATL,YAQsB,6BAAA;MAPpB,IAAG;MACH,OAAM;MACL,SAAO;;MACG,MAAI,cACgD,CAA7D,YAA6D,MAAA,oBAAA,EAAA,EAAxC,OAAM,iCAA+B,CAAA,CAAA,CAAA;6BAEzB,CAAnC,WAAmC,KAAA,QAAA,cAAA,EAAA,QAAA,CAAA,OAAA,OAAA,OAAA,KAAA,gBAAX,QAAI,GAAA,EAAA,CAAA,CAAA,CAAA;;WAGhC,YAAmC,6BAAA,EAAd,OAAM,OAAK,CAAA,CAAA,CAAA,EAElC,WAAqB,KAAA,QAAA,QAAA,CAAA,CAAA"}
@@ -12,6 +12,16 @@ declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<{
12
12
  * @default 20 (px)
13
13
  */
14
14
  indent?: number;
15
+ /**
16
+ * Overrides --scalar-sidebar-padding
17
+ * @default 12 (px)
18
+ */
19
+ padding?: number;
20
+ /**
21
+ * Overrides --scalar-sidebar-width
22
+ * @default 288 (px)
23
+ */
24
+ width?: number;
15
25
  /**
16
26
  * Sets --scalar-sidebar-background-1
17
27
  * @default var(--scalar-background-1)
@@ -92,6 +102,16 @@ declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<{
92
102
  * @default 20 (px)
93
103
  */
94
104
  indent?: number;
105
+ /**
106
+ * Overrides --scalar-sidebar-padding
107
+ * @default 12 (px)
108
+ */
109
+ padding?: number;
110
+ /**
111
+ * Overrides --scalar-sidebar-width
112
+ * @default 288 (px)
113
+ */
114
+ width?: number;
95
115
  /**
96
116
  * Sets --scalar-sidebar-background-1
97
117
  * @default var(--scalar-background-1)
@@ -1 +1 @@
1
- {"version":3,"file":"ScalarSidebarPlayground.vue.d.ts","sourceRoot":"","sources":["../../../src/components/ScalarSidebar/ScalarSidebarPlayground.vue"],"names":[],"mappings":"AAiJA;;;;;GAKG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAGzC,QAAA,MAAM,YAAY;IAEhB;;;OAGG;aACM,MAAM;IACf;;;OAGG;oBACa,MAAM;IACtB;;;OAGG;eACQ,MAAM;IACjB;;;OAGG;eACQ,MAAM;IACjB;;;OAGG;kBACW,MAAM;IACpB;;;OAGG;0BACmB,MAAM;IAC5B;;;OAGG;qBACc,MAAM;IACvB;;;OAGG;2BACoB,MAAM;IAC7B;;;OAGG;sBACe,MAAM;IACxB;;;OAGG;mBACY,MAAM;IACrB;;;OAGG;wBACiB,MAAM;IAC1B;;;OAGG;yBACkB,MAAM;IAC3B;;;OAGG;uBACgB,MAAM;IACzB;;;OAGG;kBACW,MAAM;IACpB;;;OAGG;wBACiB,MAAM;;eAQf,MAAM;;;;IAlFjB;;;OAGG;aACM,MAAM;IACf;;;OAGG;oBACa,MAAM;IACtB;;;OAGG;eACQ,MAAM;IACjB;;;OAGG;eACQ,MAAM;IACjB;;;OAGG;kBACW,MAAM;IACpB;;;OAGG;0BACmB,MAAM;IAC5B;;;OAGG;qBACc,MAAM;IACvB;;;OAGG;2BACoB,MAAM;IAC7B;;;OAGG;sBACe,MAAM;IACxB;;;OAGG;mBACY,MAAM;IACrB;;;OAGG;wBACiB,MAAM;IAC1B;;;OAGG;yBACkB,MAAM;IAC3B;;;OAGG;uBACgB,MAAM;IACzB;;;OAGG;kBACW,MAAM;IACpB;;;OAGG;wBACiB,MAAM;;eAQf,MAAM;;;;aA0IN,CAAC,KAAK,IAAgB,KAAK,GAAG;;cAC7B,CAAC,KAAK,IAAiB,KAAK,GAAG;;aAChC,CAAC,KAAK,IAAiB,KAAK,GAAG;EAUxC,CAAC;AACL,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
1
+ {"version":3,"file":"ScalarSidebarPlayground.vue.d.ts","sourceRoot":"","sources":["../../../src/components/ScalarSidebar/ScalarSidebarPlayground.vue"],"names":[],"mappings":"AAgKA;;;;;GAKG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAGzC,QAAA,MAAM,YAAY;IAEhB;;;OAGG;aACM,MAAM;IACf;;;OAGG;cACO,MAAM;IAChB;;;OAGG;YACK,MAAM;IACd;;;OAGG;oBACa,MAAM;IACtB;;;OAGG;eACQ,MAAM;IACjB;;;OAGG;eACQ,MAAM;IACjB;;;OAGG;kBACW,MAAM;IACpB;;;OAGG;0BACmB,MAAM;IAC5B;;;OAGG;qBACc,MAAM;IACvB;;;OAGG;2BACoB,MAAM;IAC7B;;;OAGG;sBACe,MAAM;IACxB;;;OAGG;mBACY,MAAM;IACrB;;;OAGG;wBACiB,MAAM;IAC1B;;;OAGG;yBACkB,MAAM;IAC3B;;;OAGG;uBACgB,MAAM;IACzB;;;OAGG;kBACW,MAAM;IACpB;;;OAGG;wBACiB,MAAM;;eAQf,MAAM;;;;IA5FjB;;;OAGG;aACM,MAAM;IACf;;;OAGG;cACO,MAAM;IAChB;;;OAGG;YACK,MAAM;IACd;;;OAGG;oBACa,MAAM;IACtB;;;OAGG;eACQ,MAAM;IACjB;;;OAGG;eACQ,MAAM;IACjB;;;OAGG;kBACW,MAAM;IACpB;;;OAGG;0BACmB,MAAM;IAC5B;;;OAGG;qBACc,MAAM;IACvB;;;OAGG;2BACoB,MAAM;IAC7B;;;OAGG;sBACe,MAAM;IACxB;;;OAGG;mBACY,MAAM;IACrB;;;OAGG;wBACiB,MAAM;IAC1B;;;OAGG;yBACkB,MAAM;IAC3B;;;OAGG;uBACgB,MAAM;IACzB;;;OAGG;kBACW,MAAM;IACpB;;;OAGG;wBACiB,MAAM;;eAQf,MAAM;;;;aAmJN,CAAC,KAAK,IAAgB,KAAK,GAAG;;cAC7B,CAAC,KAAK,IAAiB,KAAK,GAAG;;aAChC,CAAC,KAAK,IAAiB,KAAK,GAAG;EAUxC,CAAC;AACL,KAAK,eAAe,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG;IAChC,QAAO;QACN,MAAM,EAAE,CAAC,CAAC;KACV,CAAA;CACD,CAAC"}
@@ -2,10 +2,14 @@
2
2
  * Scalar Virtual Text component
3
3
  *
4
4
  * Renders large blocks of text efficiently using virtualized scrolling.
5
- * Only the visible lines are rendered to the DOM.
5
+ * Only the visible lines are rendered to the DOM. Supports in-content
6
+ * search with match highlighting and navigation (Cmd/Ctrl+F).
6
7
  *
7
8
  * @example
8
9
  * <ScalarVirtualText text="line1\nline2\nline3" :lineHeight="20" />
10
+ *
11
+ * @example
12
+ * <ScalarVirtualText text="lots of text" searchable />
9
13
  */
10
14
  declare const _default: typeof __VLS_export;
11
15
  export default _default;
@@ -20,6 +24,8 @@ declare const __VLS_export: import("vue").DefineComponent<{
20
24
  containerClass?: string;
21
25
  contentClass?: string;
22
26
  lineClass?: string;
27
+ /** Enable built-in search with Cmd/Ctrl+F */
28
+ searchable?: boolean;
23
29
  }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{
24
30
  /** Text to display */
25
31
  text: string;
@@ -31,10 +37,13 @@ declare const __VLS_export: import("vue").DefineComponent<{
31
37
  containerClass?: string;
32
38
  contentClass?: string;
33
39
  lineClass?: string;
40
+ /** Enable built-in search with Cmd/Ctrl+F */
41
+ searchable?: boolean;
34
42
  }> & Readonly<{}>, {
35
43
  lineHeight: number;
36
44
  containerClass: string;
37
45
  contentClass: string;
38
46
  lineClass: string;
47
+ searchable: boolean;
39
48
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
40
49
  //# sourceMappingURL=ScalarVirtualText.vue.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ScalarVirtualText.vue.d.ts","sourceRoot":"","sources":["../../../src/components/ScalarVirtualText/ScalarVirtualText.vue"],"names":[],"mappings":"AA0IA;;;;;;;;GAQG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAGzC,QAAA,MAAM,YAAY;IAEd,sBAAsB;UAChB,MAAM;IACZ;;;OAGG;iBACU,MAAM;qBACF,MAAM;mBACR,MAAM;gBACT,MAAM;;IATlB,sBAAsB;UAChB,MAAM;IACZ;;;OAGG;iBACU,MAAM;qBACF,MAAM;mBACR,MAAM;gBACT,MAAM;;gBAHL,MAAM;oBACF,MAAM;kBACR,MAAM;eACT,MAAM;6EAqJlB,CAAC"}
1
+ {"version":3,"file":"ScalarVirtualText.vue.d.ts","sourceRoot":"","sources":["../../../src/components/ScalarVirtualText/ScalarVirtualText.vue"],"names":[],"mappings":"AAkXA;;;;;;;;;;;;GAYG;wBACkB,OAAO,YAAY;AAAxC,wBAAyC;AAGzC,QAAA,MAAM,YAAY;IAEd,sBAAsB;UAChB,MAAM;IACZ;;;OAGG;iBACU,MAAM;qBACF,MAAM;mBACR,MAAM;gBACT,MAAM;IAClB,6CAA6C;iBAChC,OAAO;;IAXpB,sBAAsB;UAChB,MAAM;IACZ;;;OAGG;iBACU,MAAM;qBACF,MAAM;mBACR,MAAM;gBACT,MAAM;IAClB,6CAA6C;iBAChC,OAAO;;gBALP,MAAM;oBACF,MAAM;kBACR,MAAM;eACT,MAAM;gBAEL,OAAO;6EA+ZpB,CAAC"}
@@ -1,6 +1,8 @@
1
+ import _plugin_vue_export_helper_default from "../../_virtual/_plugin-vue_export-helper.js";
1
2
  import ScalarVirtualText_vue_vue_type_script_setup_true_lang_default from "./ScalarVirtualText.vue.script.js";
3
+ /* empty css */
2
4
  //#region src/components/ScalarVirtualText/ScalarVirtualText.vue
3
- var ScalarVirtualText_default = ScalarVirtualText_vue_vue_type_script_setup_true_lang_default;
5
+ var ScalarVirtualText_default = /* @__PURE__ */ _plugin_vue_export_helper_default(ScalarVirtualText_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-b50a85e1"]]);
4
6
  //#endregion
5
7
  export { ScalarVirtualText_default as default };
6
8
 
@@ -1 +1 @@
1
- {"version":3,"file":"ScalarVirtualText.vue.js","names":[],"sources":["../../../src/components/ScalarVirtualText/ScalarVirtualText.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar Virtual Text component\n *\n * Renders large blocks of text efficiently using virtualized scrolling.\n * Only the visible lines are rendered to the DOM.\n *\n * @example\n * <ScalarVirtualText text=\"line1\\nline2\\nline3\" :lineHeight=\"20\" />\n */\nexport default {}\n</script>\n<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watchEffect } from 'vue'\n\nconst props = withDefaults(\n defineProps<{\n /** Text to display */\n text: string\n /**\n * Height of each line\n * @default 20\n */\n lineHeight?: number\n containerClass?: string\n contentClass?: string\n lineClass?: string\n }>(),\n {\n lineHeight: 20,\n containerClass: '',\n contentClass: '',\n lineClass: '',\n },\n)\n\nconst containerRef = ref<HTMLElement | null>(null)\nconst contentRef = ref<HTMLElement | null>(null)\nconst scrollPosition = ref(0)\nconst containerHeight = ref(0)\n\n/** Array of text broken into lines */\nconst lines = computed(() => props.text.split('\\n'))\n/**\n * Total height of all lines combined\n *\n * TODO: there is a problem with this calculation which screws up the scrollbar a bit\n */\nconst totalHeight = computed(() => lines.value.length * props.lineHeight)\n\n/** Index of the first visible line */\nconst visibleStartIndex = computed(() =>\n Math.floor(scrollPosition.value / props.lineHeight),\n)\n\n/** Index of the last visible line */\nconst visibleEndIndex = computed(() =>\n Math.min(\n Math.ceil(\n (scrollPosition.value + containerHeight.value) / props.lineHeight,\n ),\n lines.value.length,\n ),\n)\n\n/** Array of visible lines, including buffer for smooth scrolling */\nconst visibleLines = computed(() => {\n const buffer = 10\n const start = Math.max(0, visibleStartIndex.value - buffer)\n const end = Math.min(lines.value.length, visibleEndIndex.value + buffer)\n\n return lines.value.slice(start, end)\n})\n\n/** Style object for the content container, controls \"scrolling\" */\nconst contentStyle = computed(() => ({\n height: `${totalHeight.value}px`,\n transform: `translateY(${Math.max(0, visibleStartIndex.value - 10) * props.lineHeight}px)`,\n}))\n\n/** Updates the scroll position when the container is scrolled */\nconst handleScroll = () =>\n containerRef.value && (scrollPosition.value = containerRef.value.scrollTop)\n\n/** Updates the container height when the window is resized */\nconst updateContainerHeight = () =>\n containerRef.value &&\n (containerHeight.value = containerRef.value.clientHeight)\n\nonMounted(() => {\n updateContainerHeight()\n window.addEventListener('resize', updateContainerHeight)\n})\n\nonBeforeUnmount(() => {\n window.removeEventListener('resize', updateContainerHeight)\n})\n\nwatchEffect(() => {\n if (!contentRef.value) {\n return\n }\n\n contentRef.value.style.transform = `translateY(${Math.max(0, visibleStartIndex.value - 10) * props.lineHeight}px)`\n})\n</script>\n\n<template>\n <div\n ref=\"containerRef\"\n class=\"scalar-virtual-text overflow-auto\"\n :class=\"containerClass\"\n @scroll=\"handleScroll\">\n <code\n ref=\"contentRef\"\n class=\"scalar-virtual-text-content\"\n :class=\"contentClass\"\n :style=\"contentStyle\">\n <div\n v-for=\"(line, index) in visibleLines\"\n :key=\"visibleStartIndex + index\"\n class=\"scalar-virtual-text-line\"\n :class=\"lineClass\"\n :style=\"{\n height: `${props.lineHeight}px`,\n lineHeight: `${props.lineHeight}px`,\n }\">\n {{ line }}\n </div>\n </code>\n </div>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"ScalarVirtualText.vue.js","names":[],"sources":["../../../src/components/ScalarVirtualText/ScalarVirtualText.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar Virtual Text component\n *\n * Renders large blocks of text efficiently using virtualized scrolling.\n * Only the visible lines are rendered to the DOM. Supports in-content\n * search with match highlighting and navigation (Cmd/Ctrl+F).\n *\n * @example\n * <ScalarVirtualText text=\"line1\\nline2\\nline3\" :lineHeight=\"20\" />\n *\n * @example\n * <ScalarVirtualText text=\"lots of text\" searchable />\n */\nexport default {}\n</script>\n<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport ScalarVirtualTextSearch from './ScalarVirtualTextSearch.vue'\n\nconst props = withDefaults(\n defineProps<{\n /** Text to display */\n text: string\n /**\n * Height of each line\n * @default 20\n */\n lineHeight?: number\n containerClass?: string\n contentClass?: string\n lineClass?: string\n /** Enable built-in search with Cmd/Ctrl+F */\n searchable?: boolean\n }>(),\n {\n lineHeight: 20,\n containerClass: '',\n contentClass: '',\n lineClass: '',\n searchable: false,\n },\n)\n\nconst containerRef = ref<HTMLElement | null>(null)\nconst searchRef = ref<InstanceType<typeof ScalarVirtualTextSearch> | null>(null)\nconst scrollPosition = ref(0)\nconst containerHeight = ref(0)\n\nconst searchQuery = ref('')\nconst searchOpen = ref(false)\nconst activeMatchIndex = ref(0)\n\n/** Array of text broken into lines */\nconst lines = computed(() => props.text.split('\\n'))\n/** Total height of all lines combined */\nconst totalHeight = computed(() => lines.value.length * props.lineHeight)\n\n/** Index of the first visible line */\nconst visibleStartIndex = computed(() =>\n Math.floor(scrollPosition.value / props.lineHeight),\n)\n\n/** Index of the last visible line */\nconst visibleEndIndex = computed(() =>\n Math.min(\n Math.ceil(\n (scrollPosition.value + containerHeight.value) / props.lineHeight,\n ),\n lines.value.length,\n ),\n)\n\nconst BUFFER = 10\n\n/** Index of the first rendered line (clamped, shared between slice and offset) */\nconst renderStartIndex = computed(() =>\n Math.max(0, visibleStartIndex.value - BUFFER),\n)\n\n/** Array of visible lines, including buffer for smooth scrolling */\nconst visibleLines = computed(() => {\n const end = Math.min(lines.value.length, visibleEndIndex.value + BUFFER)\n return lines.value.slice(renderStartIndex.value, end)\n})\n\n/** Pixel offset for the visible content slice */\nconst contentOffset = computed(() => renderStartIndex.value * props.lineHeight)\n\n// ── Search ────────────────────────────────────────────────────────────\n\ntype SearchMatch = { line: number; start: number; end: number }\n\n/** All matches across the full text, computed only when the query changes */\nconst searchMatches = computed<SearchMatch[]>(() => {\n const query = searchQuery.value\n if (!query) return []\n\n const lowerQuery = query.toLowerCase()\n const matches: SearchMatch[] = []\n\n for (let i = 0; i < lines.value.length; i++) {\n const line = lines.value[i]\n if (!line) continue\n const lowerLine = line.toLowerCase()\n let pos = 0\n while (pos < lowerLine.length) {\n const idx = lowerLine.indexOf(lowerQuery, pos)\n if (idx === -1) break\n matches.push({ line: i, start: idx, end: idx + query.length })\n pos = idx + lowerQuery.length\n }\n }\n return matches\n})\n\n/** Set of line indices that contain at least one match, for quick lookup */\nconst matchLineSet = computed(() => {\n const set = new Set<number>()\n for (const m of searchMatches.value) set.add(m.line)\n return set\n})\n\n/** Clamp active index when match count changes */\nwatch(\n () => searchMatches.value.length,\n (count) => {\n if (count === 0) {\n activeMatchIndex.value = 0\n } else if (activeMatchIndex.value >= count) {\n activeMatchIndex.value = count - 1\n }\n },\n)\n\n/** Scroll the container so the active match's line is visible */\nconst scrollToMatch = (match: SearchMatch) => {\n if (!containerRef.value) return\n\n const targetScroll = match.line * props.lineHeight\n const viewTop = containerRef.value.scrollTop\n const viewBottom = viewTop + containerRef.value.clientHeight\n\n if (targetScroll < viewTop || targetScroll + props.lineHeight > viewBottom) {\n containerRef.value.scrollTop =\n targetScroll - containerRef.value.clientHeight / 2\n }\n}\n\nconst goToMatch = (index: number) => {\n const match = searchMatches.value[index]\n if (!match) return\n activeMatchIndex.value = index\n scrollToMatch(match)\n}\n\nconst nextMatch = () => {\n if (searchMatches.value.length === 0) return\n goToMatch((activeMatchIndex.value + 1) % searchMatches.value.length)\n}\n\nconst prevMatch = () => {\n if (searchMatches.value.length === 0) return\n goToMatch(\n (activeMatchIndex.value - 1 + searchMatches.value.length) %\n searchMatches.value.length,\n )\n}\n\nconst openSearch = () => {\n searchOpen.value = true\n void nextTick(() => searchRef.value?.focus())\n}\n\nconst closeSearch = () => {\n searchOpen.value = false\n searchQuery.value = ''\n activeMatchIndex.value = 0\n}\n\n/** Intercepts Cmd/Ctrl+F when searchable and the container has focus */\nconst handleKeydown = (e: KeyboardEvent) => {\n if (!props.searchable) return\n\n const modifier = e.metaKey || e.ctrlKey\n if (modifier && e.key === 'f') {\n e.preventDefault()\n e.stopPropagation()\n openSearch()\n }\n if (e.key === 'Escape' && searchOpen.value) {\n e.preventDefault()\n closeSearch()\n }\n}\n\n/** Jump to the first match whenever the query changes */\nwatch(searchQuery, () => {\n activeMatchIndex.value = 0\n const first = searchMatches.value[0]\n if (first) {\n scrollToMatch(first)\n }\n})\n\n// ── Scroll / resize ───────────────────────────────────────────────────\n\n/** Updates the scroll position when the container is scrolled */\nconst handleScroll = () =>\n containerRef.value && (scrollPosition.value = containerRef.value.scrollTop)\n\n/** Updates the container height when the window is resized */\nconst updateContainerHeight = () =>\n containerRef.value &&\n (containerHeight.value = containerRef.value.clientHeight)\n\nonMounted(() => {\n updateContainerHeight()\n window.addEventListener('resize', updateContainerHeight)\n})\n\nonBeforeUnmount(() => {\n window.removeEventListener('resize', updateContainerHeight)\n})\n\n// ── Line rendering with highlights ────────────────────────────────────\n\ntype LineSegment = { text: string; highlight: boolean; active: boolean }\n\n/**\n * For a given absolute line index, splits the line text into segments\n * with match highlighting metadata.\n */\nconst getLineSegments = (absoluteIndex: number): LineSegment[] => {\n const lineText = lines.value[absoluteIndex] ?? ''\n if (!searchQuery.value || !matchLineSet.value.has(absoluteIndex)) {\n return [{ text: lineText, highlight: false, active: false }]\n }\n\n const lineMatches = searchMatches.value.filter(\n (m) => m.line === absoluteIndex,\n )\n const segments: LineSegment[] = []\n let cursor = 0\n\n for (const match of lineMatches) {\n if (match.start > cursor) {\n segments.push({\n text: lineText.slice(cursor, match.start),\n highlight: false,\n active: false,\n })\n }\n\n const isActive =\n searchMatches.value.indexOf(match) === activeMatchIndex.value\n\n segments.push({\n text: lineText.slice(match.start, match.end),\n highlight: true,\n active: isActive,\n })\n cursor = match.end\n }\n\n if (cursor < lineText.length) {\n segments.push({\n text: lineText.slice(cursor),\n highlight: false,\n active: false,\n })\n }\n\n return segments\n}\n\n/**\n * Converts a relative visible-line index to the absolute line index\n * in the full text.\n */\nconst toAbsoluteIndex = (relativeIndex: number): number =>\n renderStartIndex.value + relativeIndex\n</script>\n\n<template>\n <div\n ref=\"containerRef\"\n class=\"scalar-virtual-text relative overflow-auto\"\n :class=\"containerClass\"\n tabindex=\"0\"\n @keydown=\"handleKeydown\"\n @scroll=\"handleScroll\">\n <!-- Zero-height wrapper so the search bar does not contribute to scrollHeight -->\n <div\n v-if=\"searchable && searchOpen\"\n class=\"sticky top-0 z-10 flex h-0 w-full justify-end overflow-visible\">\n <ScalarVirtualTextSearch\n ref=\"searchRef\"\n v-model:query=\"searchQuery\"\n :activeMatchIndex=\"activeMatchIndex\"\n :matchCount=\"searchMatches.length\"\n @close=\"closeSearch\"\n @next=\"nextMatch\"\n @prev=\"prevMatch\" />\n </div>\n\n <!-- Invisible spacer that defines the full scrollable height -->\n <div :style=\"{ height: `${totalHeight}px` }\" />\n <code\n class=\"scalar-virtual-text-content absolute left-0 right-0 top-0\"\n :class=\"contentClass\"\n :style=\"{ transform: `translateY(${contentOffset}px)` }\">\n <div\n v-for=\"(line, index) in visibleLines\"\n :key=\"renderStartIndex + index\"\n class=\"scalar-virtual-text-line\"\n :class=\"lineClass\"\n :style=\"{\n height: `${props.lineHeight}px`,\n lineHeight: `${props.lineHeight}px`,\n }\">\n <template\n v-if=\"searchQuery && matchLineSet.has(toAbsoluteIndex(index))\">\n <template\n v-for=\"(segment, sIdx) in getLineSegments(toAbsoluteIndex(index))\"\n :key=\"sIdx\">\n <mark\n v-if=\"segment.highlight\"\n class=\"scalar-virtual-text-highlight\"\n :class=\"\n segment.active ? 'scalar-virtual-text-highlight-active' : ''\n \">\n {{ segment.text }}\n </mark>\n <template v-else>{{ segment.text }}</template>\n </template>\n </template>\n <template v-else>{{ line }}</template>\n </div>\n </code>\n </div>\n</template>\n\n<style scoped>\n.scalar-virtual-text:focus {\n outline: none;\n}\n\n.scalar-virtual-text-highlight {\n background: var(--scalar-background-accent);\n color: inherit;\n border-radius: 2px;\n padding: 1px 0;\n}\n\n.scalar-virtual-text-highlight-active {\n background: var(--scalar-color-accent);\n color: var(--scalar-background-1);\n border-radius: 2px;\n}\n</style>\n"],"mappings":""}
@@ -1,4 +1,10 @@
1
- import { Fragment, computed, createElementBlock, createElementVNode, defineComponent, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, toDisplayString, watchEffect } from "vue";
1
+ import ScalarVirtualTextSearch_default from "./ScalarVirtualTextSearch.vue.js";
2
+ import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, toDisplayString, watch } from "vue";
3
+ //#region src/components/ScalarVirtualText/ScalarVirtualText.vue?vue&type=script&setup=true&lang.ts
4
+ var _hoisted_1 = {
5
+ key: 0,
6
+ class: "sticky top-0 z-10 flex h-0 w-full justify-end overflow-visible"
7
+ };
2
8
  var ScalarVirtualText_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
3
9
  __name: "ScalarVirtualText",
4
10
  props: {
@@ -6,38 +12,123 @@ var ScalarVirtualText_vue_vue_type_script_setup_true_lang_default = /* @__PURE__
6
12
  lineHeight: { default: 20 },
7
13
  containerClass: { default: "" },
8
14
  contentClass: { default: "" },
9
- lineClass: { default: "" }
15
+ lineClass: { default: "" },
16
+ searchable: {
17
+ type: Boolean,
18
+ default: false
19
+ }
10
20
  },
11
21
  setup(__props) {
12
22
  const props = __props;
13
23
  const containerRef = ref(null);
14
- const contentRef = ref(null);
24
+ const searchRef = ref(null);
15
25
  const scrollPosition = ref(0);
16
26
  const containerHeight = ref(0);
27
+ const searchQuery = ref("");
28
+ const searchOpen = ref(false);
29
+ const activeMatchIndex = ref(0);
17
30
  /** Array of text broken into lines */
18
31
  const lines = computed(() => props.text.split("\n"));
19
- /**
20
- * Total height of all lines combined
21
- *
22
- * TODO: there is a problem with this calculation which screws up the scrollbar a bit
23
- */
32
+ /** Total height of all lines combined */
24
33
  const totalHeight = computed(() => lines.value.length * props.lineHeight);
25
34
  /** Index of the first visible line */
26
35
  const visibleStartIndex = computed(() => Math.floor(scrollPosition.value / props.lineHeight));
27
36
  /** Index of the last visible line */
28
37
  const visibleEndIndex = computed(() => Math.min(Math.ceil((scrollPosition.value + containerHeight.value) / props.lineHeight), lines.value.length));
38
+ const BUFFER = 10;
39
+ /** Index of the first rendered line (clamped, shared between slice and offset) */
40
+ const renderStartIndex = computed(() => Math.max(0, visibleStartIndex.value - BUFFER));
29
41
  /** Array of visible lines, including buffer for smooth scrolling */
30
42
  const visibleLines = computed(() => {
31
- const buffer = 10;
32
- const start = Math.max(0, visibleStartIndex.value - buffer);
33
- const end = Math.min(lines.value.length, visibleEndIndex.value + buffer);
34
- return lines.value.slice(start, end);
43
+ const end = Math.min(lines.value.length, visibleEndIndex.value + BUFFER);
44
+ return lines.value.slice(renderStartIndex.value, end);
45
+ });
46
+ /** Pixel offset for the visible content slice */
47
+ const contentOffset = computed(() => renderStartIndex.value * props.lineHeight);
48
+ const searchMatches = computed(() => {
49
+ const query = searchQuery.value;
50
+ if (!query) return [];
51
+ const lowerQuery = query.toLowerCase();
52
+ const matches = [];
53
+ for (let i = 0; i < lines.value.length; i++) {
54
+ const line = lines.value[i];
55
+ if (!line) continue;
56
+ const lowerLine = line.toLowerCase();
57
+ let pos = 0;
58
+ while (pos < lowerLine.length) {
59
+ const idx = lowerLine.indexOf(lowerQuery, pos);
60
+ if (idx === -1) break;
61
+ matches.push({
62
+ line: i,
63
+ start: idx,
64
+ end: idx + query.length
65
+ });
66
+ pos = idx + lowerQuery.length;
67
+ }
68
+ }
69
+ return matches;
70
+ });
71
+ /** Set of line indices that contain at least one match, for quick lookup */
72
+ const matchLineSet = computed(() => {
73
+ const set = /* @__PURE__ */ new Set();
74
+ for (const m of searchMatches.value) set.add(m.line);
75
+ return set;
76
+ });
77
+ /** Clamp active index when match count changes */
78
+ watch(() => searchMatches.value.length, (count) => {
79
+ if (count === 0) activeMatchIndex.value = 0;
80
+ else if (activeMatchIndex.value >= count) activeMatchIndex.value = count - 1;
81
+ });
82
+ /** Scroll the container so the active match's line is visible */
83
+ const scrollToMatch = (match) => {
84
+ if (!containerRef.value) return;
85
+ const targetScroll = match.line * props.lineHeight;
86
+ const viewTop = containerRef.value.scrollTop;
87
+ const viewBottom = viewTop + containerRef.value.clientHeight;
88
+ if (targetScroll < viewTop || targetScroll + props.lineHeight > viewBottom) containerRef.value.scrollTop = targetScroll - containerRef.value.clientHeight / 2;
89
+ };
90
+ const goToMatch = (index) => {
91
+ const match = searchMatches.value[index];
92
+ if (!match) return;
93
+ activeMatchIndex.value = index;
94
+ scrollToMatch(match);
95
+ };
96
+ const nextMatch = () => {
97
+ if (searchMatches.value.length === 0) return;
98
+ goToMatch((activeMatchIndex.value + 1) % searchMatches.value.length);
99
+ };
100
+ const prevMatch = () => {
101
+ if (searchMatches.value.length === 0) return;
102
+ goToMatch((activeMatchIndex.value - 1 + searchMatches.value.length) % searchMatches.value.length);
103
+ };
104
+ const openSearch = () => {
105
+ searchOpen.value = true;
106
+ nextTick(() => searchRef.value?.focus());
107
+ };
108
+ const closeSearch = () => {
109
+ searchOpen.value = false;
110
+ searchQuery.value = "";
111
+ activeMatchIndex.value = 0;
112
+ };
113
+ /** Intercepts Cmd/Ctrl+F when searchable and the container has focus */
114
+ const handleKeydown = (e) => {
115
+ if (!props.searchable) return;
116
+ if ((e.metaKey || e.ctrlKey) && e.key === "f") {
117
+ e.preventDefault();
118
+ e.stopPropagation();
119
+ openSearch();
120
+ }
121
+ if (e.key === "Escape" && searchOpen.value) {
122
+ e.preventDefault();
123
+ closeSearch();
124
+ }
125
+ };
126
+ /** Jump to the first match whenever the query changes */
127
+ watch(searchQuery, () => {
128
+ activeMatchIndex.value = 0;
129
+ const first = searchMatches.value[0];
130
+ if (first) scrollToMatch(first);
35
131
  });
36
- /** Style object for the content container, controls "scrolling" */
37
- const contentStyle = computed(() => ({
38
- height: `${totalHeight.value}px`,
39
- transform: `translateY(${Math.max(0, visibleStartIndex.value - 10) * props.lineHeight}px)`
40
- }));
41
132
  /** Updates the scroll position when the container is scrolled */
42
133
  const handleScroll = () => containerRef.value && (scrollPosition.value = containerRef.value.scrollTop);
43
134
  /** Updates the container height when the window is resized */
@@ -49,31 +140,86 @@ var ScalarVirtualText_vue_vue_type_script_setup_true_lang_default = /* @__PURE__
49
140
  onBeforeUnmount(() => {
50
141
  window.removeEventListener("resize", updateContainerHeight);
51
142
  });
52
- watchEffect(() => {
53
- if (!contentRef.value) return;
54
- contentRef.value.style.transform = `translateY(${Math.max(0, visibleStartIndex.value - 10) * props.lineHeight}px)`;
55
- });
143
+ const getLineSegments = (absoluteIndex) => {
144
+ const lineText = lines.value[absoluteIndex] ?? "";
145
+ if (!searchQuery.value || !matchLineSet.value.has(absoluteIndex)) return [{
146
+ text: lineText,
147
+ highlight: false,
148
+ active: false
149
+ }];
150
+ const lineMatches = searchMatches.value.filter((m) => m.line === absoluteIndex);
151
+ const segments = [];
152
+ let cursor = 0;
153
+ for (const match of lineMatches) {
154
+ if (match.start > cursor) segments.push({
155
+ text: lineText.slice(cursor, match.start),
156
+ highlight: false,
157
+ active: false
158
+ });
159
+ const isActive = searchMatches.value.indexOf(match) === activeMatchIndex.value;
160
+ segments.push({
161
+ text: lineText.slice(match.start, match.end),
162
+ highlight: true,
163
+ active: isActive
164
+ });
165
+ cursor = match.end;
166
+ }
167
+ if (cursor < lineText.length) segments.push({
168
+ text: lineText.slice(cursor),
169
+ highlight: false,
170
+ active: false
171
+ });
172
+ return segments;
173
+ };
174
+ /**
175
+ * Converts a relative visible-line index to the absolute line index
176
+ * in the full text.
177
+ */
178
+ const toAbsoluteIndex = (relativeIndex) => renderStartIndex.value + relativeIndex;
56
179
  return (_ctx, _cache) => {
57
180
  return openBlock(), createElementBlock("div", {
58
181
  ref_key: "containerRef",
59
182
  ref: containerRef,
60
- class: normalizeClass(["scalar-virtual-text overflow-auto", __props.containerClass]),
183
+ class: normalizeClass(["scalar-virtual-text relative overflow-auto", __props.containerClass]),
184
+ tabindex: "0",
185
+ onKeydown: handleKeydown,
61
186
  onScroll: handleScroll
62
- }, [createElementVNode("code", {
63
- ref_key: "contentRef",
64
- ref: contentRef,
65
- class: normalizeClass(["scalar-virtual-text-content", __props.contentClass]),
66
- style: normalizeStyle(contentStyle.value)
67
- }, [(openBlock(true), createElementBlock(Fragment, null, renderList(visibleLines.value, (line, index) => {
68
- return openBlock(), createElementBlock("div", {
69
- key: visibleStartIndex.value + index,
70
- class: normalizeClass(["scalar-virtual-text-line", __props.lineClass]),
71
- style: normalizeStyle({
72
- height: `${props.lineHeight}px`,
73
- lineHeight: `${props.lineHeight}px`
74
- })
75
- }, toDisplayString(line), 7);
76
- }), 128))], 6)], 34);
187
+ }, [
188
+ __props.searchable && searchOpen.value ? (openBlock(), createElementBlock("div", _hoisted_1, [createVNode(ScalarVirtualTextSearch_default, {
189
+ ref_key: "searchRef",
190
+ ref: searchRef,
191
+ query: searchQuery.value,
192
+ "onUpdate:query": _cache[0] || (_cache[0] = ($event) => searchQuery.value = $event),
193
+ activeMatchIndex: activeMatchIndex.value,
194
+ matchCount: searchMatches.value.length,
195
+ onClose: closeSearch,
196
+ onNext: nextMatch,
197
+ onPrev: prevMatch
198
+ }, null, 8, [
199
+ "query",
200
+ "activeMatchIndex",
201
+ "matchCount"
202
+ ])])) : createCommentVNode("", true),
203
+ createElementVNode("div", { style: normalizeStyle({ height: `${totalHeight.value}px` }) }, null, 4),
204
+ createElementVNode("code", {
205
+ class: normalizeClass(["scalar-virtual-text-content absolute left-0 right-0 top-0", __props.contentClass]),
206
+ style: normalizeStyle({ transform: `translateY(${contentOffset.value}px)` })
207
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(visibleLines.value, (line, index) => {
208
+ return openBlock(), createElementBlock("div", {
209
+ key: renderStartIndex.value + index,
210
+ class: normalizeClass(["scalar-virtual-text-line", __props.lineClass]),
211
+ style: normalizeStyle({
212
+ height: `${props.lineHeight}px`,
213
+ lineHeight: `${props.lineHeight}px`
214
+ })
215
+ }, [searchQuery.value && matchLineSet.value.has(toAbsoluteIndex(index)) ? (openBlock(true), createElementBlock(Fragment, { key: 0 }, renderList(getLineSegments(toAbsoluteIndex(index)), (segment, sIdx) => {
216
+ return openBlock(), createElementBlock(Fragment, { key: sIdx }, [segment.highlight ? (openBlock(), createElementBlock("mark", {
217
+ key: 0,
218
+ class: normalizeClass(["scalar-virtual-text-highlight", segment.active ? "scalar-virtual-text-highlight-active" : ""])
219
+ }, toDisplayString(segment.text), 3)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(segment.text), 1)], 64))], 64);
220
+ }), 128)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(line), 1)], 64))], 6);
221
+ }), 128))], 6)
222
+ ], 34);
77
223
  };
78
224
  }
79
225
  });
@@ -1 +1 @@
1
- {"version":3,"file":"ScalarVirtualText.vue.script.js","names":[],"sources":["../../../src/components/ScalarVirtualText/ScalarVirtualText.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar Virtual Text component\n *\n * Renders large blocks of text efficiently using virtualized scrolling.\n * Only the visible lines are rendered to the DOM.\n *\n * @example\n * <ScalarVirtualText text=\"line1\\nline2\\nline3\" :lineHeight=\"20\" />\n */\nexport default {}\n</script>\n<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watchEffect } from 'vue'\n\nconst props = withDefaults(\n defineProps<{\n /** Text to display */\n text: string\n /**\n * Height of each line\n * @default 20\n */\n lineHeight?: number\n containerClass?: string\n contentClass?: string\n lineClass?: string\n }>(),\n {\n lineHeight: 20,\n containerClass: '',\n contentClass: '',\n lineClass: '',\n },\n)\n\nconst containerRef = ref<HTMLElement | null>(null)\nconst contentRef = ref<HTMLElement | null>(null)\nconst scrollPosition = ref(0)\nconst containerHeight = ref(0)\n\n/** Array of text broken into lines */\nconst lines = computed(() => props.text.split('\\n'))\n/**\n * Total height of all lines combined\n *\n * TODO: there is a problem with this calculation which screws up the scrollbar a bit\n */\nconst totalHeight = computed(() => lines.value.length * props.lineHeight)\n\n/** Index of the first visible line */\nconst visibleStartIndex = computed(() =>\n Math.floor(scrollPosition.value / props.lineHeight),\n)\n\n/** Index of the last visible line */\nconst visibleEndIndex = computed(() =>\n Math.min(\n Math.ceil(\n (scrollPosition.value + containerHeight.value) / props.lineHeight,\n ),\n lines.value.length,\n ),\n)\n\n/** Array of visible lines, including buffer for smooth scrolling */\nconst visibleLines = computed(() => {\n const buffer = 10\n const start = Math.max(0, visibleStartIndex.value - buffer)\n const end = Math.min(lines.value.length, visibleEndIndex.value + buffer)\n\n return lines.value.slice(start, end)\n})\n\n/** Style object for the content container, controls \"scrolling\" */\nconst contentStyle = computed(() => ({\n height: `${totalHeight.value}px`,\n transform: `translateY(${Math.max(0, visibleStartIndex.value - 10) * props.lineHeight}px)`,\n}))\n\n/** Updates the scroll position when the container is scrolled */\nconst handleScroll = () =>\n containerRef.value && (scrollPosition.value = containerRef.value.scrollTop)\n\n/** Updates the container height when the window is resized */\nconst updateContainerHeight = () =>\n containerRef.value &&\n (containerHeight.value = containerRef.value.clientHeight)\n\nonMounted(() => {\n updateContainerHeight()\n window.addEventListener('resize', updateContainerHeight)\n})\n\nonBeforeUnmount(() => {\n window.removeEventListener('resize', updateContainerHeight)\n})\n\nwatchEffect(() => {\n if (!contentRef.value) {\n return\n }\n\n contentRef.value.style.transform = `translateY(${Math.max(0, visibleStartIndex.value - 10) * props.lineHeight}px)`\n})\n</script>\n\n<template>\n <div\n ref=\"containerRef\"\n class=\"scalar-virtual-text overflow-auto\"\n :class=\"containerClass\"\n @scroll=\"handleScroll\">\n <code\n ref=\"contentRef\"\n class=\"scalar-virtual-text-content\"\n :class=\"contentClass\"\n :style=\"contentStyle\">\n <div\n v-for=\"(line, index) in visibleLines\"\n :key=\"visibleStartIndex + index\"\n class=\"scalar-virtual-text-line\"\n :class=\"lineClass\"\n :style=\"{\n height: `${props.lineHeight}px`,\n lineHeight: `${props.lineHeight}px`,\n }\">\n {{ line }}\n </div>\n </code>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;EAeA,MAAM,QAAQ;EAqBd,MAAM,eAAe,IAAwB,KAAI;EACjD,MAAM,aAAa,IAAwB,KAAI;EAC/C,MAAM,iBAAiB,IAAI,EAAC;EAC5B,MAAM,kBAAkB,IAAI,EAAC;;EAG7B,MAAM,QAAQ,eAAe,MAAM,KAAK,MAAM,KAAK,CAAA;;;;;;EAMnD,MAAM,cAAc,eAAe,MAAM,MAAM,SAAS,MAAM,WAAU;;EAGxE,MAAM,oBAAoB,eACxB,KAAK,MAAM,eAAe,QAAQ,MAAM,WAAW,CACrD;;EAGA,MAAM,kBAAkB,eACtB,KAAK,IACH,KAAK,MACF,eAAe,QAAQ,gBAAgB,SAAS,MAAM,WACxD,EACD,MAAM,MAAM,OACb,CACH;;EAGA,MAAM,eAAe,eAAe;GAClC,MAAM,SAAS;GACf,MAAM,QAAQ,KAAK,IAAI,GAAG,kBAAkB,QAAQ,OAAM;GAC1D,MAAM,MAAM,KAAK,IAAI,MAAM,MAAM,QAAQ,gBAAgB,QAAQ,OAAM;AAEvE,UAAO,MAAM,MAAM,MAAM,OAAO,IAAG;IACpC;;EAGD,MAAM,eAAe,gBAAgB;GACnC,QAAQ,GAAG,YAAY,MAAM;GAC7B,WAAW,cAAc,KAAK,IAAI,GAAG,kBAAkB,QAAQ,GAAG,GAAG,MAAM,WAAW;GACvF,EAAC;;EAGF,MAAM,qBACJ,aAAa,UAAU,eAAe,QAAQ,aAAa,MAAM;;EAGnE,MAAM,8BACJ,aAAa,UACZ,gBAAgB,QAAQ,aAAa,MAAM;AAE9C,kBAAgB;AACd,0BAAsB;AACtB,UAAO,iBAAiB,UAAU,sBAAqB;IACxD;AAED,wBAAsB;AACpB,UAAO,oBAAoB,UAAU,sBAAqB;IAC3D;AAED,oBAAkB;AAChB,OAAI,CAAC,WAAW,MACd;AAGF,cAAW,MAAM,MAAM,YAAY,cAAc,KAAK,IAAI,GAAG,kBAAkB,QAAQ,GAAG,GAAG,MAAM,WAAW;IAC/G;;uBAIC,mBAsBM,OAAA;aArBA;IAAJ,KAAI;IACJ,OAAK,eAAA,CAAC,qCACE,QAAA,eAAc,CAAA;IACrB,UAAQ;OACT,mBAgBO,QAAA;aAfD;IAAJ,KAAI;IACJ,OAAK,eAAA,CAAC,+BACE,QAAA,aAAY,CAAA;IACnB,OAAK,eAAE,aAAA,MAAY;yBACpB,mBAUM,UAAA,MAAA,WAToB,aAAA,QAAhB,MAAM,UAAK;wBADrB,mBAUM,OAAA;KARH,KAAK,kBAAA,QAAoB;KAC1B,OAAK,eAAA,CAAC,4BACE,QAAA,UAAS,CAAA;KAChB,OAAK,eAAA;iBAAyB,MAAM,WAAU;qBAA+B,MAAM,WAAU;;uBAI3F,KAAI,EAAA,EAAA"}
1
+ {"version":3,"file":"ScalarVirtualText.vue.script.js","names":[],"sources":["../../../src/components/ScalarVirtualText/ScalarVirtualText.vue"],"sourcesContent":["<script lang=\"ts\">\n/**\n * Scalar Virtual Text component\n *\n * Renders large blocks of text efficiently using virtualized scrolling.\n * Only the visible lines are rendered to the DOM. Supports in-content\n * search with match highlighting and navigation (Cmd/Ctrl+F).\n *\n * @example\n * <ScalarVirtualText text=\"line1\\nline2\\nline3\" :lineHeight=\"20\" />\n *\n * @example\n * <ScalarVirtualText text=\"lots of text\" searchable />\n */\nexport default {}\n</script>\n<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport ScalarVirtualTextSearch from './ScalarVirtualTextSearch.vue'\n\nconst props = withDefaults(\n defineProps<{\n /** Text to display */\n text: string\n /**\n * Height of each line\n * @default 20\n */\n lineHeight?: number\n containerClass?: string\n contentClass?: string\n lineClass?: string\n /** Enable built-in search with Cmd/Ctrl+F */\n searchable?: boolean\n }>(),\n {\n lineHeight: 20,\n containerClass: '',\n contentClass: '',\n lineClass: '',\n searchable: false,\n },\n)\n\nconst containerRef = ref<HTMLElement | null>(null)\nconst searchRef = ref<InstanceType<typeof ScalarVirtualTextSearch> | null>(null)\nconst scrollPosition = ref(0)\nconst containerHeight = ref(0)\n\nconst searchQuery = ref('')\nconst searchOpen = ref(false)\nconst activeMatchIndex = ref(0)\n\n/** Array of text broken into lines */\nconst lines = computed(() => props.text.split('\\n'))\n/** Total height of all lines combined */\nconst totalHeight = computed(() => lines.value.length * props.lineHeight)\n\n/** Index of the first visible line */\nconst visibleStartIndex = computed(() =>\n Math.floor(scrollPosition.value / props.lineHeight),\n)\n\n/** Index of the last visible line */\nconst visibleEndIndex = computed(() =>\n Math.min(\n Math.ceil(\n (scrollPosition.value + containerHeight.value) / props.lineHeight,\n ),\n lines.value.length,\n ),\n)\n\nconst BUFFER = 10\n\n/** Index of the first rendered line (clamped, shared between slice and offset) */\nconst renderStartIndex = computed(() =>\n Math.max(0, visibleStartIndex.value - BUFFER),\n)\n\n/** Array of visible lines, including buffer for smooth scrolling */\nconst visibleLines = computed(() => {\n const end = Math.min(lines.value.length, visibleEndIndex.value + BUFFER)\n return lines.value.slice(renderStartIndex.value, end)\n})\n\n/** Pixel offset for the visible content slice */\nconst contentOffset = computed(() => renderStartIndex.value * props.lineHeight)\n\n// ── Search ────────────────────────────────────────────────────────────\n\ntype SearchMatch = { line: number; start: number; end: number }\n\n/** All matches across the full text, computed only when the query changes */\nconst searchMatches = computed<SearchMatch[]>(() => {\n const query = searchQuery.value\n if (!query) return []\n\n const lowerQuery = query.toLowerCase()\n const matches: SearchMatch[] = []\n\n for (let i = 0; i < lines.value.length; i++) {\n const line = lines.value[i]\n if (!line) continue\n const lowerLine = line.toLowerCase()\n let pos = 0\n while (pos < lowerLine.length) {\n const idx = lowerLine.indexOf(lowerQuery, pos)\n if (idx === -1) break\n matches.push({ line: i, start: idx, end: idx + query.length })\n pos = idx + lowerQuery.length\n }\n }\n return matches\n})\n\n/** Set of line indices that contain at least one match, for quick lookup */\nconst matchLineSet = computed(() => {\n const set = new Set<number>()\n for (const m of searchMatches.value) set.add(m.line)\n return set\n})\n\n/** Clamp active index when match count changes */\nwatch(\n () => searchMatches.value.length,\n (count) => {\n if (count === 0) {\n activeMatchIndex.value = 0\n } else if (activeMatchIndex.value >= count) {\n activeMatchIndex.value = count - 1\n }\n },\n)\n\n/** Scroll the container so the active match's line is visible */\nconst scrollToMatch = (match: SearchMatch) => {\n if (!containerRef.value) return\n\n const targetScroll = match.line * props.lineHeight\n const viewTop = containerRef.value.scrollTop\n const viewBottom = viewTop + containerRef.value.clientHeight\n\n if (targetScroll < viewTop || targetScroll + props.lineHeight > viewBottom) {\n containerRef.value.scrollTop =\n targetScroll - containerRef.value.clientHeight / 2\n }\n}\n\nconst goToMatch = (index: number) => {\n const match = searchMatches.value[index]\n if (!match) return\n activeMatchIndex.value = index\n scrollToMatch(match)\n}\n\nconst nextMatch = () => {\n if (searchMatches.value.length === 0) return\n goToMatch((activeMatchIndex.value + 1) % searchMatches.value.length)\n}\n\nconst prevMatch = () => {\n if (searchMatches.value.length === 0) return\n goToMatch(\n (activeMatchIndex.value - 1 + searchMatches.value.length) %\n searchMatches.value.length,\n )\n}\n\nconst openSearch = () => {\n searchOpen.value = true\n void nextTick(() => searchRef.value?.focus())\n}\n\nconst closeSearch = () => {\n searchOpen.value = false\n searchQuery.value = ''\n activeMatchIndex.value = 0\n}\n\n/** Intercepts Cmd/Ctrl+F when searchable and the container has focus */\nconst handleKeydown = (e: KeyboardEvent) => {\n if (!props.searchable) return\n\n const modifier = e.metaKey || e.ctrlKey\n if (modifier && e.key === 'f') {\n e.preventDefault()\n e.stopPropagation()\n openSearch()\n }\n if (e.key === 'Escape' && searchOpen.value) {\n e.preventDefault()\n closeSearch()\n }\n}\n\n/** Jump to the first match whenever the query changes */\nwatch(searchQuery, () => {\n activeMatchIndex.value = 0\n const first = searchMatches.value[0]\n if (first) {\n scrollToMatch(first)\n }\n})\n\n// ── Scroll / resize ───────────────────────────────────────────────────\n\n/** Updates the scroll position when the container is scrolled */\nconst handleScroll = () =>\n containerRef.value && (scrollPosition.value = containerRef.value.scrollTop)\n\n/** Updates the container height when the window is resized */\nconst updateContainerHeight = () =>\n containerRef.value &&\n (containerHeight.value = containerRef.value.clientHeight)\n\nonMounted(() => {\n updateContainerHeight()\n window.addEventListener('resize', updateContainerHeight)\n})\n\nonBeforeUnmount(() => {\n window.removeEventListener('resize', updateContainerHeight)\n})\n\n// ── Line rendering with highlights ────────────────────────────────────\n\ntype LineSegment = { text: string; highlight: boolean; active: boolean }\n\n/**\n * For a given absolute line index, splits the line text into segments\n * with match highlighting metadata.\n */\nconst getLineSegments = (absoluteIndex: number): LineSegment[] => {\n const lineText = lines.value[absoluteIndex] ?? ''\n if (!searchQuery.value || !matchLineSet.value.has(absoluteIndex)) {\n return [{ text: lineText, highlight: false, active: false }]\n }\n\n const lineMatches = searchMatches.value.filter(\n (m) => m.line === absoluteIndex,\n )\n const segments: LineSegment[] = []\n let cursor = 0\n\n for (const match of lineMatches) {\n if (match.start > cursor) {\n segments.push({\n text: lineText.slice(cursor, match.start),\n highlight: false,\n active: false,\n })\n }\n\n const isActive =\n searchMatches.value.indexOf(match) === activeMatchIndex.value\n\n segments.push({\n text: lineText.slice(match.start, match.end),\n highlight: true,\n active: isActive,\n })\n cursor = match.end\n }\n\n if (cursor < lineText.length) {\n segments.push({\n text: lineText.slice(cursor),\n highlight: false,\n active: false,\n })\n }\n\n return segments\n}\n\n/**\n * Converts a relative visible-line index to the absolute line index\n * in the full text.\n */\nconst toAbsoluteIndex = (relativeIndex: number): number =>\n renderStartIndex.value + relativeIndex\n</script>\n\n<template>\n <div\n ref=\"containerRef\"\n class=\"scalar-virtual-text relative overflow-auto\"\n :class=\"containerClass\"\n tabindex=\"0\"\n @keydown=\"handleKeydown\"\n @scroll=\"handleScroll\">\n <!-- Zero-height wrapper so the search bar does not contribute to scrollHeight -->\n <div\n v-if=\"searchable && searchOpen\"\n class=\"sticky top-0 z-10 flex h-0 w-full justify-end overflow-visible\">\n <ScalarVirtualTextSearch\n ref=\"searchRef\"\n v-model:query=\"searchQuery\"\n :activeMatchIndex=\"activeMatchIndex\"\n :matchCount=\"searchMatches.length\"\n @close=\"closeSearch\"\n @next=\"nextMatch\"\n @prev=\"prevMatch\" />\n </div>\n\n <!-- Invisible spacer that defines the full scrollable height -->\n <div :style=\"{ height: `${totalHeight}px` }\" />\n <code\n class=\"scalar-virtual-text-content absolute left-0 right-0 top-0\"\n :class=\"contentClass\"\n :style=\"{ transform: `translateY(${contentOffset}px)` }\">\n <div\n v-for=\"(line, index) in visibleLines\"\n :key=\"renderStartIndex + index\"\n class=\"scalar-virtual-text-line\"\n :class=\"lineClass\"\n :style=\"{\n height: `${props.lineHeight}px`,\n lineHeight: `${props.lineHeight}px`,\n }\">\n <template\n v-if=\"searchQuery && matchLineSet.has(toAbsoluteIndex(index))\">\n <template\n v-for=\"(segment, sIdx) in getLineSegments(toAbsoluteIndex(index))\"\n :key=\"sIdx\">\n <mark\n v-if=\"segment.highlight\"\n class=\"scalar-virtual-text-highlight\"\n :class=\"\n segment.active ? 'scalar-virtual-text-highlight-active' : ''\n \">\n {{ segment.text }}\n </mark>\n <template v-else>{{ segment.text }}</template>\n </template>\n </template>\n <template v-else>{{ line }}</template>\n </div>\n </code>\n </div>\n</template>\n\n<style scoped>\n.scalar-virtual-text:focus {\n outline: none;\n}\n\n.scalar-virtual-text-highlight {\n background: var(--scalar-background-accent);\n color: inherit;\n border-radius: 2px;\n padding: 1px 0;\n}\n\n.scalar-virtual-text-highlight-active {\n background: var(--scalar-color-accent);\n color: var(--scalar-background-1);\n border-radius: 2px;\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;EAqBA,MAAM,QAAQ;EAwBd,MAAM,eAAe,IAAwB,KAAI;EACjD,MAAM,YAAY,IAAyD,KAAI;EAC/E,MAAM,iBAAiB,IAAI,EAAC;EAC5B,MAAM,kBAAkB,IAAI,EAAC;EAE7B,MAAM,cAAc,IAAI,GAAE;EAC1B,MAAM,aAAa,IAAI,MAAK;EAC5B,MAAM,mBAAmB,IAAI,EAAC;;EAG9B,MAAM,QAAQ,eAAe,MAAM,KAAK,MAAM,KAAK,CAAA;;EAEnD,MAAM,cAAc,eAAe,MAAM,MAAM,SAAS,MAAM,WAAU;;EAGxE,MAAM,oBAAoB,eACxB,KAAK,MAAM,eAAe,QAAQ,MAAM,WAAW,CACrD;;EAGA,MAAM,kBAAkB,eACtB,KAAK,IACH,KAAK,MACF,eAAe,QAAQ,gBAAgB,SAAS,MAAM,WACxD,EACD,MAAM,MAAM,OACb,CACH;EAEA,MAAM,SAAS;;EAGf,MAAM,mBAAmB,eACvB,KAAK,IAAI,GAAG,kBAAkB,QAAQ,OAAO,CAC/C;;EAGA,MAAM,eAAe,eAAe;GAClC,MAAM,MAAM,KAAK,IAAI,MAAM,MAAM,QAAQ,gBAAgB,QAAQ,OAAM;AACvE,UAAO,MAAM,MAAM,MAAM,iBAAiB,OAAO,IAAG;IACrD;;EAGD,MAAM,gBAAgB,eAAe,iBAAiB,QAAQ,MAAM,WAAU;EAO9E,MAAM,gBAAgB,eAA8B;GAClD,MAAM,QAAQ,YAAY;AAC1B,OAAI,CAAC,MAAO,QAAO,EAAC;GAEpB,MAAM,aAAa,MAAM,aAAY;GACrC,MAAM,UAAyB,EAAC;AAEhC,QAAK,IAAI,IAAI,GAAG,IAAI,MAAM,MAAM,QAAQ,KAAK;IAC3C,MAAM,OAAO,MAAM,MAAM;AACzB,QAAI,CAAC,KAAM;IACX,MAAM,YAAY,KAAK,aAAY;IACnC,IAAI,MAAM;AACV,WAAO,MAAM,UAAU,QAAQ;KAC7B,MAAM,MAAM,UAAU,QAAQ,YAAY,IAAG;AAC7C,SAAI,QAAQ,GAAI;AAChB,aAAQ,KAAK;MAAE,MAAM;MAAG,OAAO;MAAK,KAAK,MAAM,MAAM;MAAQ,CAAA;AAC7D,WAAM,MAAM,WAAW;;;AAG3B,UAAO;IACR;;EAGD,MAAM,eAAe,eAAe;GAClC,MAAM,sBAAM,IAAI,KAAY;AAC5B,QAAK,MAAM,KAAK,cAAc,MAAO,KAAI,IAAI,EAAE,KAAI;AACnD,UAAO;IACR;;AAGD,cACQ,cAAc,MAAM,SACzB,UAAU;AACT,OAAI,UAAU,EACZ,kBAAiB,QAAQ;YAChB,iBAAiB,SAAS,MACnC,kBAAiB,QAAQ,QAAQ;IAGvC;;EAGA,MAAM,iBAAiB,UAAuB;AAC5C,OAAI,CAAC,aAAa,MAAO;GAEzB,MAAM,eAAe,MAAM,OAAO,MAAM;GACxC,MAAM,UAAU,aAAa,MAAM;GACnC,MAAM,aAAa,UAAU,aAAa,MAAM;AAEhD,OAAI,eAAe,WAAW,eAAe,MAAM,aAAa,WAC9D,cAAa,MAAM,YACjB,eAAe,aAAa,MAAM,eAAe;;EAIvD,MAAM,aAAa,UAAkB;GACnC,MAAM,QAAQ,cAAc,MAAM;AAClC,OAAI,CAAC,MAAO;AACZ,oBAAiB,QAAQ;AACzB,iBAAc,MAAK;;EAGrB,MAAM,kBAAkB;AACtB,OAAI,cAAc,MAAM,WAAW,EAAG;AACtC,cAAW,iBAAiB,QAAQ,KAAK,cAAc,MAAM,OAAM;;EAGrE,MAAM,kBAAkB;AACtB,OAAI,cAAc,MAAM,WAAW,EAAG;AACtC,cACG,iBAAiB,QAAQ,IAAI,cAAc,MAAM,UAChD,cAAc,MAAM,OACxB;;EAGF,MAAM,mBAAmB;AACvB,cAAW,QAAQ;AACd,kBAAe,UAAU,OAAO,OAAO,CAAA;;EAG9C,MAAM,oBAAoB;AACxB,cAAW,QAAQ;AACnB,eAAY,QAAQ;AACpB,oBAAiB,QAAQ;;;EAI3B,MAAM,iBAAiB,MAAqB;AAC1C,OAAI,CAAC,MAAM,WAAY;AAGvB,QADiB,EAAE,WAAW,EAAE,YAChB,EAAE,QAAQ,KAAK;AAC7B,MAAE,gBAAe;AACjB,MAAE,iBAAgB;AAClB,gBAAW;;AAEb,OAAI,EAAE,QAAQ,YAAY,WAAW,OAAO;AAC1C,MAAE,gBAAe;AACjB,iBAAY;;;;AAKhB,QAAM,mBAAmB;AACvB,oBAAiB,QAAQ;GACzB,MAAM,QAAQ,cAAc,MAAM;AAClC,OAAI,MACF,eAAc,MAAK;IAEtB;;EAKD,MAAM,qBACJ,aAAa,UAAU,eAAe,QAAQ,aAAa,MAAM;;EAGnE,MAAM,8BACJ,aAAa,UACZ,gBAAgB,QAAQ,aAAa,MAAM;AAE9C,kBAAgB;AACd,0BAAsB;AACtB,UAAO,iBAAiB,UAAU,sBAAqB;IACxD;AAED,wBAAsB;AACpB,UAAO,oBAAoB,UAAU,sBAAqB;IAC3D;EAUD,MAAM,mBAAmB,kBAAyC;GAChE,MAAM,WAAW,MAAM,MAAM,kBAAkB;AAC/C,OAAI,CAAC,YAAY,SAAS,CAAC,aAAa,MAAM,IAAI,cAAc,CAC9D,QAAO,CAAC;IAAE,MAAM;IAAU,WAAW;IAAO,QAAQ;IAAO,CAAA;GAG7D,MAAM,cAAc,cAAc,MAAM,QACrC,MAAM,EAAE,SAAS,cACpB;GACA,MAAM,WAA0B,EAAC;GACjC,IAAI,SAAS;AAEb,QAAK,MAAM,SAAS,aAAa;AAC/B,QAAI,MAAM,QAAQ,OAChB,UAAS,KAAK;KACZ,MAAM,SAAS,MAAM,QAAQ,MAAM,MAAM;KACzC,WAAW;KACX,QAAQ;KACT,CAAA;IAGH,MAAM,WACJ,cAAc,MAAM,QAAQ,MAAM,KAAK,iBAAiB;AAE1D,aAAS,KAAK;KACZ,MAAM,SAAS,MAAM,MAAM,OAAO,MAAM,IAAI;KAC5C,WAAW;KACX,QAAQ;KACT,CAAA;AACD,aAAS,MAAM;;AAGjB,OAAI,SAAS,SAAS,OACpB,UAAS,KAAK;IACZ,MAAM,SAAS,MAAM,OAAO;IAC5B,WAAW;IACX,QAAQ;IACT,CAAA;AAGH,UAAO;;;;;;EAOT,MAAM,mBAAmB,kBACvB,iBAAiB,QAAQ;;uBAIzB,mBAuDM,OAAA;aAtDA;IAAJ,KAAI;IACJ,OAAK,eAAA,CAAC,8CACE,QAAA,eAAc,CAAA;IACtB,UAAS;IACR,WAAS;IACT,UAAQ;;IAGD,QAAA,cAAc,WAAA,SAAA,WAAA,EADtB,mBAWM,OAXN,YAWM,CARJ,YAOsB,iCAAA;cANhB;KAAJ,KAAI;KACI,OAAO,YAAA;yEAAW,QAAA;KACzB,kBAAkB,iBAAA;KAClB,YAAY,cAAA,MAAc;KAC1B,SAAO;KACP,QAAM;KACN,QAAM;;;;;;IAIX,mBAA+C,OAAA,EAAzC,OAAK,eAAA,EAAA,QAAA,GAAe,YAAA,MAAW,KAAA,CAAA,EAAA,EAAA,MAAA,EAAA;IACrC,mBA+BO,QAAA;KA9BL,OAAK,eAAA,CAAC,6DACE,QAAA,aAAY,CAAA;KACnB,OAAK,eAAA,EAAA,WAAA,cAA6B,cAAA,MAAa,MAAA,CAAA;0BAChD,mBA0BM,UAAA,MAAA,WAzBoB,aAAA,QAAhB,MAAM,UAAK;yBADrB,mBA0BM,OAAA;MAxBH,KAAK,iBAAA,QAAmB;MACzB,OAAK,eAAA,CAAC,4BACE,QAAA,UAAS,CAAA;MAChB,OAAK,eAAA;kBAAyB,MAAM,WAAU;sBAA+B,MAAM,WAAU;;SAKtF,YAAA,SAAe,aAAA,MAAa,IAAI,gBAAgB,MAAK,CAAA,IAAA,UAAA,KAAA,EAC3D,mBAYW,UAAA,EAAA,KAAA,GAAA,EAAA,WAXiB,gBAAgB,gBAAgB,MAAK,CAAA,GAAvD,SAAS,SAAI;8DACf,MAAI,EAAA,CAEF,QAAQ,aAAA,WAAA,EADhB,mBAOO,QAAA;;OALL,OAAK,eAAA,CAAC,iCACmB,QAAQ,SAAM,yCAAA,GAAA,CAAA;yBAGpC,QAAQ,KAAI,EAAA,EAAA,KAAA,WAAA,EAEjB,mBAA8C,UAAA,EAAA,KAAA,GAAA,EAAA,CAAA,gBAAA,gBAA1B,QAAQ,KAAI,EAAA,EAAA,CAAA,EAAA,GAAA,EAAA,EAAA,GAAA;+BAGpC,mBAAsC,UAAA,EAAA,KAAA,GAAA,EAAA,CAAA,gBAAA,gBAAlB,KAAI,EAAA,EAAA,CAAA,EAAA,GAAA,EAAA,EAAA,EAAA"}