@feugene/granularity 0.43.0 → 0.44.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 (47) hide show
  1. package/README.md +3 -3
  2. package/dist/chunks/config-BkQ2jvXv.js +49 -0
  3. package/dist/chunks/config-BkQ2jvXv.js.map +1 -0
  4. package/dist/chunks/{en-C_QkUTRL.js → en-DiHzFact.js} +3 -1
  5. package/dist/chunks/en-DiHzFact.js.map +1 -0
  6. package/dist/chunks/{es-DemSEhZE.js → es-1ZL6NtkB.js} +3 -1
  7. package/dist/chunks/es-1ZL6NtkB.js.map +1 -0
  8. package/dist/chunks/{es-B4t_3Wnh.js → es-CeKkMxZ_.js} +3 -3
  9. package/dist/chunks/{es-B4t_3Wnh.js.map → es-CeKkMxZ_.js.map} +1 -1
  10. package/dist/chunks/{generated-VcxJTo_q.js → generated-Hxu_sWPs.js} +28 -0
  11. package/dist/chunks/{generated-VcxJTo_q.js.map → generated-Hxu_sWPs.js.map} +1 -1
  12. package/dist/chunks/{granular-provider-BohXQcNt.js → granular-provider-CCNbQa5U.js} +2 -0
  13. package/dist/chunks/granular-provider-CCNbQa5U.js.map +1 -0
  14. package/dist/chunks/{ru-CLUErjhY.js → ru-Dr2RYn0P.js} +3 -1
  15. package/dist/chunks/ru-Dr2RYn0P.js.map +1 -0
  16. package/dist/chunks/useScrollSpy-CIJpw0RI.js +360 -0
  17. package/dist/chunks/useScrollSpy-CIJpw0RI.js.map +1 -0
  18. package/dist/components/GrScrollSpy/chunks/GrScrollSpy-CQCH3zLC.js +207 -0
  19. package/dist/components/GrScrollSpy/chunks/GrScrollSpy-CQCH3zLC.js.map +1 -0
  20. package/dist/components/GrScrollSpy/index.js +3 -0
  21. package/dist/composables/useScrollSpy.js +2 -0
  22. package/dist/granular-provider-node.js +1 -1
  23. package/dist/granular-provider.js +1 -1
  24. package/dist/i18n/all.js +1 -1
  25. package/dist/i18n/index.js +1 -1
  26. package/dist/index.js +4 -1
  27. package/dist/theme.js +1 -1
  28. package/dist/tokens.js +1 -1
  29. package/dist/types/components/GrScrollSpy/GrScrollSpy.vue.d.ts +105 -0
  30. package/dist/types/components/GrScrollSpy/config.d.ts +1 -0
  31. package/dist/types/components/GrScrollSpy/grScrollSpyStyles.d.ts +26 -0
  32. package/dist/types/components/GrScrollSpy/index.d.ts +9 -0
  33. package/dist/types/components/GrScrollSpy/safelist.d.ts +1 -0
  34. package/dist/types/components/GrScrollSpy/scrollSpyItems.d.ts +44 -0
  35. package/dist/types/composables/internal/scrollSpyGeometry.d.ts +67 -0
  36. package/dist/types/composables/useScrollSpy.d.ts +59 -0
  37. package/dist/types/granular-provider/shared.d.ts +1 -0
  38. package/dist/types/i18n/messages/en.d.ts +6 -0
  39. package/dist/types/i18n/messages/es.d.ts +6 -0
  40. package/dist/types/i18n/messages/ru.d.ts +6 -0
  41. package/dist/types/index.d.ts +3 -0
  42. package/dist/web-types.json +96 -1
  43. package/package.json +9 -1
  44. package/dist/chunks/en-C_QkUTRL.js.map +0 -1
  45. package/dist/chunks/es-DemSEhZE.js.map +0 -1
  46. package/dist/chunks/granular-provider-BohXQcNt.js.map +0 -1
  47. package/dist/chunks/ru-CLUErjhY.js.map +0 -1
@@ -57,6 +57,7 @@ import { t as grPromptDialogConfig } from "./config-C0Ea9ePP.js";
57
57
  import { t as grRadioConfig } from "./config-BUNxHnEz.js";
58
58
  import { t as grRadioGroupConfig } from "./config-QaIc_gRa.js";
59
59
  import { t as grRatingConfig } from "./config-DRbojEzO.js";
60
+ import { t as grScrollSpyConfig } from "./config-BkQ2jvXv.js";
60
61
  import { t as grSegmentedConfig } from "./config-rLkc9Zn2.js";
61
62
  import { t as grSelectConfig } from "./config--t2dxj0t.js";
62
63
  import { t as grSidebarConfig } from "./config-DNDgp9bB.js";
@@ -172,6 +173,7 @@ var granularityComponentConfigs = {
172
173
  GrRadioGroup: grRadioGroupConfig,
173
174
  GrRating: grRatingConfig,
174
175
  GrResponseErrorBanner: grResponseErrorBannerConfig,
176
+ GrScrollSpy: grScrollSpyConfig,
175
177
  GrSegmented: grSegmentedConfig,
176
178
  GrSelect: grSelectConfig,
177
179
  GrSidebar: grSidebarConfig,
@@ -0,0 +1 @@
1
+ {"version":3,"file":"granular-provider-CCNbQa5U.js","names":[],"sources":["../../src/components/GrDialog/config.ts","../../src/components/GrResponseErrorBanner/config.ts","../../src/granular-provider/shared.ts","../../src/granular-provider/index.ts"],"sourcesContent":["import { defineGranularComponent } from '@feugene/unocss-preset-granular/contract'\n\nimport { grDialogSafelist } from './safelist'\n\nexport const grDialogConfig = defineGranularComponent(import.meta.url, {\n name: 'GrDialog',\n dependencies: ['GrButton', 'GrModal'],\n safelist: grDialogSafelist,\n})\n","import { defineGranularComponent } from '@feugene/unocss-preset-granular/contract'\n\nexport const grResponseErrorBannerConfig = defineGranularComponent(import.meta.url, {\n name: 'GrResponseErrorBanner',\n dependencies: ['GrAlert', 'GrButton'],\n})\n","// `id`, `theme.*`, `packageBaseUrl` и реестр компонентов провайдера.\n//\n// Все `new URL('../styles/...', import.meta.url)` обязаны лежать именно здесь:\n// `shared.ts` в одной директории с `index.ts` / `node.ts`, поэтому\n// относительные пути совпадают. Для браузера безопасно — бандлеры\n// транслируют такие литералы в статические asset-URL.\n//\n// `apps/showcase/scripts/generate-component-api.mjs` читает список компонентов\n// из `Object.keys(granularityComponentConfigs)` через vite SSR.\nimport {\n defineGranularProvider,\n type GranularComponentDescriptor,\n type GranularProvider,\n resolvePackageBaseUrl,\n} from '@feugene/unocss-preset-granular/contract'\n// <granularity:components:imports> — блок генерируется `yarn generate:registry`\nimport { grAffixConfig } from '../components/GrAffix/config'\nimport { grAlertConfig } from '../components/GrAlert/config'\nimport { grAutocompleteConfig } from '../components/GrAutocomplete/config'\nimport { grAvatarConfig } from '../components/GrAvatar/config'\nimport { grBadgeConfig } from '../components/GrBadge/config'\nimport { grBadgeWrapConfig } from '../components/GrBadgeWrap/config'\nimport { grBottomNavConfig } from '../components/GrBottomNav/config'\nimport { grBreadcrumbsConfig } from '../components/GrBreadcrumbs/config'\nimport { grButtonConfig } from '../components/GrButton/config'\nimport { grButtonGroupConfig } from '../components/GrButtonGroup/config'\nimport { grCardConfig } from '../components/GrCard/config'\nimport { grCarouselConfig } from '../components/GrCarousel/config'\nimport { grCheckboxConfig } from '../components/GrCheckbox/config'\nimport { grCheckboxGroupConfig } from '../components/GrCheckboxGroup/config'\nimport { grChipConfig } from '../components/GrChip/config'\nimport { grChipGroupConfig } from '../components/GrChipGroup/config'\nimport { grCollapseConfig } from '../components/GrCollapse/config'\nimport { grColorPickerConfig } from '../components/GrColorPicker/config'\nimport { grCommandPaletteConfig } from '../components/GrCommandPalette/config'\nimport { grConfigProviderConfig } from '../components/GrConfigProvider/config'\nimport { grConfirmDialogConfig } from '../components/GrConfirmDialog/config'\nimport { grContextMenuConfig } from '../components/GrContextMenu/config'\nimport { grDataTableConfig } from '../components/GrDataTable/config'\nimport { grDeltaConfig } from '../components/GrDelta/config'\nimport { grDescriptionListConfig } from '../components/GrDescriptionList/config'\nimport { grDialogConfig } from '../components/GrDialog/config'\nimport { grDialogServiceConfig } from '../components/GrDialogService/config'\nimport { grDividerConfig } from '../components/GrDivider/config'\nimport { grDrawerConfig } from '../components/GrDrawer/config'\nimport { grDropdownConfig } from '../components/GrDropdown/config'\nimport { grDropdownMenuConfig } from '../components/GrDropdownMenu/config'\nimport { grEmptyStateConfig } from '../components/GrEmptyState/config'\nimport { grFilePreviewConfig } from '../components/GrFilePreview/config'\nimport { grFileUploadConfig } from '../components/GrFileUpload/config'\nimport { grFormConfig } from '../components/GrForm/config'\nimport { grFormFieldConfig } from '../components/GrFormField/config'\nimport { grFormFileConfig } from '../components/GrFormFile/config'\nimport { grFormSectionConfig } from '../components/GrFormSection/config'\nimport { grIconConfig } from '../components/GrIcon/config'\nimport { grImageViewerConfig } from '../components/GrImageViewer/config'\nimport { grInputConfig } from '../components/GrInput/config'\nimport { grInputTagConfig } from '../components/GrInputTag/config'\nimport { grJsonViewerConfig } from '../components/GrJsonViewer/config'\nimport { grKbdConfig } from '../components/GrKbd/config'\nimport { grLinkConfig } from '../components/GrLink/config'\nimport { grListConfig } from '../components/GrList/config'\nimport { grLoadingConfig } from '../components/GrLoading/config'\nimport { grModalConfig } from '../components/GrModal/config'\nimport { grNavbarConfig } from '../components/GrNavbar/config'\nimport { grNumberInputConfig } from '../components/GrNumberInput/config'\nimport { grPaginationConfig } from '../components/GrPagination/config'\nimport { grPopoverConfig } from '../components/GrPopover/config'\nimport { grProgressBarConfig } from '../components/GrProgressBar/config'\nimport { grProgressCircleConfig } from '../components/GrProgressCircle/config'\nimport { grPromptDialogConfig } from '../components/GrPromptDialog/config'\nimport { grRadioConfig } from '../components/GrRadio/config'\nimport { grRadioGroupConfig } from '../components/GrRadioGroup/config'\nimport { grRatingConfig } from '../components/GrRating/config'\nimport { grResponseErrorBannerConfig } from '../components/GrResponseErrorBanner/config'\nimport { grScrollSpyConfig } from '../components/GrScrollSpy/config'\nimport { grSegmentedConfig } from '../components/GrSegmented/config'\nimport { grSelectConfig } from '../components/GrSelect/config'\nimport { grSidebarConfig } from '../components/GrSidebar/config'\nimport { grSkeletonConfig } from '../components/GrSkeleton/config'\nimport { grSliderConfig } from '../components/GrSlider/config'\nimport { grSortableListConfig } from '../components/GrSortableList/config'\nimport { grSplitterConfig } from '../components/GrSplitter/config'\nimport { grStatisticConfig } from '../components/GrStatistic/config'\nimport { grStepsConfig } from '../components/GrSteps/config'\nimport { grSwitchConfig } from '../components/GrSwitch/config'\nimport { grTableConfig } from '../components/GrTable/config'\nimport { grTabPanelsConfig } from '../components/GrTabPanels/config'\nimport { grTabsConfig } from '../components/GrTabs/config'\nimport { grTextareaConfig } from '../components/GrTextarea/config'\nimport { grTimelineConfig } from '../components/GrTimeline/config'\nimport { grToasterConfig } from '../components/GrToaster/config'\nimport { grTooltipConfig } from '../components/GrTooltip/config'\nimport { grTransferConfig } from '../components/GrTransfer/config'\nimport { grTreeConfig } from '../components/GrTree/config'\nimport { grTreeSelectConfig } from '../components/GrTreeSelect/config'\nimport { grValueConfig } from '../components/GrValue/config'\n// </granularity:components:imports>\n\n/** Идентификатор провайдера — совпадает с именем пакета. */\nexport const GRANULARITY_PROVIDER_ID = '@feugene/granularity'\n\n// Не заменять на `new URL('..', import.meta.url)`: Vite и rolldown распознают\n// этот литерал и подставляют `data:`-URL, после чего scan-директории пустеют.\nconst packageBaseUrl = resolvePackageBaseUrl(import.meta.url)\n\n/** Встроенные темы пакета. Единственный источник правды о списке тем. */\nexport const granularityThemeNames = ['light', 'dark'] as const\nexport type GranularityThemeName = (typeof granularityThemeNames)[number]\n\n/** Темы, активные по умолчанию, если окружение не переопределяет выбор. */\nexport const granularityDefaultThemes: readonly GranularityThemeName[] = ['light']\n\nconst theme = {\n baseCssUrl: new URL('../styles/base.css', import.meta.url).href,\n tokensCssUrl: new URL('../styles/tokens.css', import.meta.url).href,\n themes: {\n light: new URL('../styles/themes/light.css', import.meta.url).href,\n dark: new URL('../styles/themes/dark.css', import.meta.url).href,\n },\n defaultThemes: granularityDefaultThemes,\n} as const\n\n/**\n * Реестр публичных компонентов. Ключи совпадают с subpath-экспортом\n * `@feugene/granularity/components/<Name>`.\n */\nexport const granularityComponentConfigs = {\n // <granularity:components:registry> — блок генерируется `yarn generate:registry`\n GrAffix: grAffixConfig,\n GrAlert: grAlertConfig,\n GrAutocomplete: grAutocompleteConfig,\n GrAvatar: grAvatarConfig,\n GrBadge: grBadgeConfig,\n GrBadgeWrap: grBadgeWrapConfig,\n GrBottomNav: grBottomNavConfig,\n GrBreadcrumbs: grBreadcrumbsConfig,\n GrButton: grButtonConfig,\n GrButtonGroup: grButtonGroupConfig,\n GrCard: grCardConfig,\n GrCarousel: grCarouselConfig,\n GrCheckbox: grCheckboxConfig,\n GrCheckboxGroup: grCheckboxGroupConfig,\n GrChip: grChipConfig,\n GrChipGroup: grChipGroupConfig,\n GrCollapse: grCollapseConfig,\n GrColorPicker: grColorPickerConfig,\n GrCommandPalette: grCommandPaletteConfig,\n GrConfigProvider: grConfigProviderConfig,\n GrConfirmDialog: grConfirmDialogConfig,\n GrContextMenu: grContextMenuConfig,\n GrDataTable: grDataTableConfig,\n GrDelta: grDeltaConfig,\n GrDescriptionList: grDescriptionListConfig,\n GrDialog: grDialogConfig,\n GrDialogService: grDialogServiceConfig,\n GrDivider: grDividerConfig,\n GrDrawer: grDrawerConfig,\n GrDropdown: grDropdownConfig,\n GrDropdownMenu: grDropdownMenuConfig,\n GrEmptyState: grEmptyStateConfig,\n GrFilePreview: grFilePreviewConfig,\n GrFileUpload: grFileUploadConfig,\n GrForm: grFormConfig,\n GrFormField: grFormFieldConfig,\n GrFormFile: grFormFileConfig,\n GrFormSection: grFormSectionConfig,\n GrIcon: grIconConfig,\n GrImageViewer: grImageViewerConfig,\n GrInput: grInputConfig,\n GrInputTag: grInputTagConfig,\n GrJsonViewer: grJsonViewerConfig,\n GrKbd: grKbdConfig,\n GrLink: grLinkConfig,\n GrList: grListConfig,\n GrLoading: grLoadingConfig,\n GrModal: grModalConfig,\n GrNavbar: grNavbarConfig,\n GrNumberInput: grNumberInputConfig,\n GrPagination: grPaginationConfig,\n GrPopover: grPopoverConfig,\n GrProgressBar: grProgressBarConfig,\n GrProgressCircle: grProgressCircleConfig,\n GrPromptDialog: grPromptDialogConfig,\n GrRadio: grRadioConfig,\n GrRadioGroup: grRadioGroupConfig,\n GrRating: grRatingConfig,\n GrResponseErrorBanner: grResponseErrorBannerConfig,\n GrScrollSpy: grScrollSpyConfig,\n GrSegmented: grSegmentedConfig,\n GrSelect: grSelectConfig,\n GrSidebar: grSidebarConfig,\n GrSkeleton: grSkeletonConfig,\n GrSlider: grSliderConfig,\n GrSortableList: grSortableListConfig,\n GrSplitter: grSplitterConfig,\n GrStatistic: grStatisticConfig,\n GrSteps: grStepsConfig,\n GrSwitch: grSwitchConfig,\n GrTable: grTableConfig,\n GrTabPanels: grTabPanelsConfig,\n GrTabs: grTabsConfig,\n GrTextarea: grTextareaConfig,\n GrTimeline: grTimelineConfig,\n GrToaster: grToasterConfig,\n GrTooltip: grTooltipConfig,\n GrTransfer: grTransferConfig,\n GrTree: grTreeConfig,\n GrTreeSelect: grTreeSelectConfig,\n GrValue: grValueConfig,\n // </granularity:components:registry>\n}\n\nexport type GranularityComponentName = keyof typeof granularityComponentConfigs\n\n/** Базовый набор компонентов в порядке реестра. */\nconst baseComponents: readonly GranularComponentDescriptor[] = Object.values(\n granularityComponentConfigs,\n)\n\n/**\n * Собирает granular-provider пакета.\n *\n * `overrides` — точка расширения для потребителя: дескриптор с именем из\n * базового реестра заменяет его, остальные дописываются в конец.\n */\nexport function createGranularityProvider(\n overrides: readonly GranularComponentDescriptor[] = [],\n): GranularProvider {\n const overrideByName = new Map(\n overrides.map(component => [component.name, component]),\n )\n const components: GranularComponentDescriptor[] = [\n ...baseComponents.map(component => overrideByName.get(component.name) ?? component),\n ...overrides.filter(component => !baseComponents.some(base => base.name === component.name)),\n ]\n\n return defineGranularProvider({\n id: GRANULARITY_PROVIDER_ID,\n contractVersion: 1,\n packageBaseUrl,\n components,\n theme,\n })\n}\n","import { createGranularityProvider } from './shared'\n\nexport * from './shared'\n\nexport const granularityProvider = createGranularityProvider()\n\nexport default granularityProvider\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIA,IAAa,iBAAiB,wBAAwB,YAAY,KAAK;CACrE,MAAM;CACN,cAAc,CAAC,YAAY,SAAS;CACpC,UAAU;AACZ,CAAC;;;ACND,IAAa,8BAA8B,wBAAwB,YAAY,KAAK;CAClF,MAAM;CACN,cAAc,CAAC,WAAW,UAAU;AACtC,CAAC;;;;AC+FD,IAAa,0BAA0B;AAIvC,IAAM,iBAAiB,sBAAsB,YAAY,GAAG;;AAG5D,IAAa,wBAAwB,CAAC,SAAS,MAAM;;AAIrD,IAAa,2BAA4D,CAAC,OAAO;AAEjF,IAAM,QAAQ;CACZ,YAAY,IAAA,IAAA,q5KAAA,KAAA,YAAA,GAAA,CAA8C,CAAC;CAC3D,cAAc,IAAA,IAAA,65YAAA,KAAA,YAAA,GAAA,CAAgD,CAAC;CAC/D,QAAQ;EACN,OAAO,IAAA,IAAA,iyYAAA,KAAA,YAAA,GAAA,CAAsD,CAAC;EAC9D,MAAM,IAAA,IAAA,y/SAAA,KAAA,YAAA,GAAA,CAAqD,CAAC;CAC9D;CACA,eAAe;AACjB;;;;;AAMA,IAAa,8BAA8B;CAEzC,SAAS;CACT,SAAS;CACT,gBAAgB;CAChB,UAAU;CACV,SAAS;CACT,aAAa;CACb,aAAa;CACb,eAAe;CACf,UAAU;CACV,eAAe;CACf,QAAQ;CACR,YAAY;CACZ,YAAY;CACZ,iBAAiB;CACjB,QAAQ;CACR,aAAa;CACb,YAAY;CACZ,eAAe;CACf,kBAAkB;CAClB,kBAAkB;CAClB,iBAAiB;CACjB,eAAe;CACf,aAAa;CACb,SAAS;CACT,mBAAmB;CACnB,UAAU;CACV,iBAAiB;CACjB,WAAW;CACX,UAAU;CACV,YAAY;CACZ,gBAAgB;CAChB,cAAc;CACd,eAAe;CACf,cAAc;CACd,QAAQ;CACR,aAAa;CACb,YAAY;CACZ,eAAe;CACf,QAAQ;CACR,eAAe;CACf,SAAS;CACT,YAAY;CACZ,cAAc;CACd,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,WAAW;CACX,SAAS;CACT,UAAU;CACV,eAAe;CACf,cAAc;CACd,WAAW;CACX,eAAe;CACf,kBAAkB;CAClB,gBAAgB;CAChB,SAAS;CACT,cAAc;CACd,UAAU;CACV,uBAAuB;CACvB,aAAa;CACb,aAAa;CACb,UAAU;CACV,WAAW;CACX,YAAY;CACZ,UAAU;CACV,gBAAgB;CAChB,YAAY;CACZ,aAAa;CACb,SAAS;CACT,UAAU;CACV,SAAS;CACT,aAAa;CACb,QAAQ;CACR,YAAY;CACZ,YAAY;CACZ,WAAW;CACX,WAAW;CACX,YAAY;CACZ,QAAQ;CACR,cAAc;CACd,SAAS;AAEX;;AAKA,IAAM,iBAAyD,OAAO,OACpE,2BACF;;;;;;;AAQA,SAAgB,0BACd,YAAoD,CAAC,GACnC;CAClB,MAAM,iBAAiB,IAAI,IACzB,UAAU,KAAI,cAAa,CAAC,UAAU,MAAM,SAAS,CAAC,CACxD;CACA,MAAM,aAA4C,CAChD,GAAG,eAAe,KAAI,cAAa,eAAe,IAAI,UAAU,IAAI,KAAK,SAAS,GAClF,GAAG,UAAU,QAAO,cAAa,CAAC,eAAe,MAAK,SAAQ,KAAK,SAAS,UAAU,IAAI,CAAC,CAC7F;CAEA,OAAO,uBAAuB;EAC5B,IAAI;EACJ,iBAAiB;EACjB;EACA;EACA;CACF,CAAC;AACH;;;AChPA,IAAa,sBAAsB,0BAA0B"}
@@ -155,6 +155,7 @@ var pagination = {
155
155
  "status": "Страница {page} из {count}"
156
156
  };
157
157
  var navbar = { "openMenu": "Открыть меню" };
158
+ var scrollSpy = { "label": "На этой странице" };
158
159
  var responseError = {
159
160
  "networkTitle": "Нет связи с сервером",
160
161
  "networkMessage": "Проверьте подключение к интернету и попробуйте ещё раз.",
@@ -377,6 +378,7 @@ var ru_default = {
377
378
  statistic,
378
379
  pagination,
379
380
  navbar,
381
+ scrollSpy,
380
382
  responseError,
381
383
  colors,
382
384
  drawer,
@@ -412,4 +414,4 @@ var ru_default = {
412
414
  contextMenu
413
415
  };
414
416
  //#endregion
415
- export { autocomplete, avatar, badgeWrap, bottomNav, breadcrumbs, button, carousel, chip, collapse, colorPicker, colors, commandPalette, common, contextMenu, dataTable, ru_default as default, delta, dialog, drawer, emptyState, filePreview, fileUpload, fileValidation, form, formErrorBanner, formFile, imageViewer, input, inputTag, jsonViewer, kbd, link, list, loading, modal, navbar, numberInput, pagination, rating, responseError, select, sidebar, slider, sortable, splitter, statistic, steps, table, tabs, textarea, timeline, toaster, transfer, tree, treeSelect, uploadErrorBanner };
417
+ export { autocomplete, avatar, badgeWrap, bottomNav, breadcrumbs, button, carousel, chip, collapse, colorPicker, colors, commandPalette, common, contextMenu, dataTable, ru_default as default, delta, dialog, drawer, emptyState, filePreview, fileUpload, fileValidation, form, formErrorBanner, formFile, imageViewer, input, inputTag, jsonViewer, kbd, link, list, loading, modal, navbar, numberInput, pagination, rating, responseError, scrollSpy, select, sidebar, slider, sortable, splitter, statistic, steps, table, tabs, textarea, timeline, toaster, transfer, tree, treeSelect, uploadErrorBanner };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ru-Dr2RYn0P.js","names":[],"sources":["../../src/i18n/locales/ru.json"],"sourcesContent":["{\n \"transfer\": {\n \"sourceTitle\": \"Доступны\",\n \"targetTitle\": \"Выбраны\",\n \"search\": \"Поиск\",\n \"toTarget\": \"Перенести к выбранным\",\n \"toSource\": \"Вернуть к доступным\",\n \"selectAll\": \"Выбрать всё показанное\",\n \"moveUp\": \"Раньше\",\n \"moveDown\": \"Позже\",\n \"selected\": \"{count} из {total}\",\n \"shown\": \"Показано {shown} из {total}\",\n \"empty\": \"Здесь пока пусто\",\n \"emptyFiltered\": \"Ничего не найдено\",\n \"moved\": \"{count} перенесено в «{list}»\",\n \"reordered\": \"Позиция {position} из {count}\",\n \"reorderFiltered\": \"Пока включён поиск, порядок не меняется\",\n \"cancelled\": \"Перенос отменён\"\n },\n \"carousel\": {\n \"roledescription\": \"карусель\",\n \"slideRoledescription\": \"слайд\",\n \"slidePosition\": \"{index} из {total}\",\n \"previous\": \"Предыдущий слайд\",\n \"next\": \"Следующий слайд\",\n \"play\": \"Запустить автопоказ\",\n \"pause\": \"Остановить автопоказ\",\n \"indicators\": \"Слайды\",\n \"autoplayStopped\": \"Автопоказ остановлен\"\n },\n \"chip\": {\n \"remove\": \"Убрать\",\n \"removeNamed\": \"Убрать {label}\"\n },\n \"common\": {\n \"confirm\": \"Подтвердить\",\n \"cancel\": \"Отмена\",\n \"add\": \"Добавить\",\n \"clear\": \"Очистить\",\n \"close\": \"Закрыть\"\n },\n \"loading\": {\n \"defaultText\": \"Загрузка...\"\n },\n \"steps\": {\n \"label\": \"Ход выполнения\",\n \"status\": \"Шаг {step} из {count}\",\n \"completed\": \"выполнен\",\n \"error\": \"есть ошибки\"\n },\n \"treeSelect\": {\n \"filterPlaceholder\": \"Поиск…\",\n \"empty\": \"Нет данных\",\n \"loading\": \"Загрузка…\"\n },\n \"badgeWrap\": {\n \"count\": \"{count} новых\"\n },\n \"avatar\": {\n \"group\": \"Участники\",\n \"groupOverflow\": \"и ещё {count}\",\n \"status\": {\n \"online\": \"в сети\",\n \"offline\": \"не в сети\",\n \"busy\": \"занят\",\n \"away\": \"отошёл\"\n }\n },\n \"tree\": {\n \"drag\": \"Перетащить\",\n \"expand\": \"Развернуть\",\n \"collapse\": \"Свернуть\",\n \"moved\": \"{label} перемещён\"\n },\n \"select\": {\n \"customValuePlaceholder\": \"Добавить значение…\",\n \"addOption\": \"Добавить «{value}»\",\n \"clearOption\": \"Не выбрано\",\n \"searchPlaceholder\": \"Поиск…\",\n \"loading\": \"Загрузка…\",\n \"noResults\": \"Ничего не найдено\",\n \"removeTag\": \"Удалить {label}\"\n },\n \"dataTable\": {\n \"loading\": \"Загрузка…\",\n \"empty\": \"Нет данных\",\n \"selectAll\": \"Выбрать все строки\",\n \"selectRow\": \"Выбрать строку\",\n \"sortAsc\": \"Нажмите для сортировки по возрастанию\",\n \"sortDesc\": \"Нажмите для сортировки по убыванию\",\n \"sortNone\": \"Нажмите, чтобы снять сортировку\",\n \"moveColumn\": \"Переставить колонку {label}\",\n \"columnMoved\": \"Колонка {label} перемещена на позицию {position} из {count}\",\n \"resizeColumn\": \"Ширина колонки {label}\",\n \"columnResized\": \"Ширина колонки {label} — {width} пикселей\"\n },\n \"breadcrumbs\": {\n \"label\": \"Навигационная цепочка\",\n \"expand\": \"Показать скрытые уровни\"\n },\n \"autocomplete\": {\n \"loading\": \"Загрузка…\",\n \"noResults\": \"Ничего не найдено\",\n \"addOption\": \"Добавить «{value}»\",\n \"removeValue\": \"Убрать {label}\",\n \"typeMore\": {\n \"one\": \"Введите минимум {n} символ\",\n \"few\": \"Введите минимум {n} символа\",\n \"many\": \"Введите минимум {n} символов\",\n \"other\": \"Введите минимум {n} символа\"\n }\n },\n \"form\": {\n \"required\": \"Обязательное поле\",\n \"min\": \"Минимум {min}\",\n \"max\": \"Максимум {max}\",\n \"len\": \"Длина должна быть {len}\",\n \"pattern\": \"Неверный формат\",\n \"email\": \"Введите корректный e-mail\",\n \"url\": \"Введите корректный URL\",\n \"file\": \"Некорректный файл\",\n \"invalid\": \"Некорректное значение\",\n \"readonly\": \"Только для чтения\",\n \"validating\": \"Проверяем…\"\n },\n \"dialog\": {\n \"confirm\": {\n \"title\": \"Подтверждение\"\n },\n \"prompt\": {\n \"title\": \"Введите значение\",\n \"label\": \"Значение\",\n \"required\": \"Введите значение.\"\n },\n \"ok\": \"ОК\"\n },\n \"switch\": {\n \"loading\": \"Сохраняем…\"\n },\n \"slider\": {\n \"min\": \"минимум\",\n \"max\": \"максимум\"\n },\n \"sidebar\": {\n \"expand\": \"Развернуть панель\",\n \"collapse\": \"Свернуть панель\"\n },\n \"splitter\": {\n \"label\": \"Изменить размер панелей\"\n },\n \"statistic\": {\n \"trendUp\": \"Рост\",\n \"trendDown\": \"Падение\",\n \"trendFlat\": \"Без изменений\"\n },\n \"pagination\": {\n \"label\": \"Пагинация\",\n \"pageSize\": \"Размер страницы\",\n \"prev\": \"Назад\",\n \"next\": \"Вперёд\",\n \"page\": \"Страница {n}\",\n \"first\": \"Первая страница\",\n \"last\": \"Последняя страница\",\n \"jumpTo\": \"Перейти к\",\n \"total\": \"{from}–{to} из {total}\",\n \"status\": \"Страница {page} из {count}\"\n },\n \"navbar\": {\n \"openMenu\": \"Открыть меню\"\n },\n \"scrollSpy\": {\n \"label\": \"На этой странице\"\n },\n \"responseError\": {\n \"networkTitle\": \"Нет связи с сервером\",\n \"networkMessage\": \"Проверьте подключение к интернету и попробуйте ещё раз.\",\n \"abortedTitle\": \"Запрос отменён\",\n \"abortedMessage\": \"Запрос был отменён.\",\n \"validationTitle\": \"Проверьте данные\",\n \"validationMessage\": \"Сервер вернул ошибку валидации. Исправьте поля и попробуйте снова.\",\n \"clientTitle\": \"Ошибка запроса\",\n \"clientMessage\": \"Сервер отклонил запрос.\",\n \"serverTitle\": \"Ошибка сервера\",\n \"serverMessage\": \"Произошла ошибка на сервере. Попробуйте ещё раз.\",\n \"unknownTitle\": \"Что-то пошло не так\",\n \"unknownMessage\": \"Не удалось выполнить операцию. Попробуйте ещё раз.\",\n \"statusLabel\": \"HTTP {status}\",\n \"retryLabel\": \"Повторить\",\n \"dismissLabel\": \"Скрыть\"\n },\n \"colors\": {\n \"neutral\": \"Нейтральный\",\n \"success\": \"Успех\",\n \"warning\": \"Предупреждение\",\n \"danger\": \"Опасность\",\n \"info\": \"Инфо\",\n \"primary\": \"Primary\",\n \"slate\": \"Сланцевый\",\n \"azure\": \"Лазурный\"\n },\n \"drawer\": {\n \"title\": \"Панель\"\n },\n \"modal\": {\n \"title\": \"Диалог\"\n },\n \"numberInput\": {\n \"increase\": \"Увеличить\",\n \"decrease\": \"Уменьшить\"\n },\n \"toaster\": {\n \"dismiss\": \"Закрыть\",\n \"region\": \"Уведомления\"\n },\n \"inputTag\": {\n \"removeTag\": \"Удалить тег\",\n \"removeTagNamed\": \"Удалить тег {tag}\",\n \"clearAll\": \"Удалить все теги\",\n \"added\": \"Добавлен тег: {tag}\",\n \"addedMany\": \"Добавлено тегов: {count}\",\n \"removed\": \"Удалён тег: {tag}\",\n \"cleared\": \"Все теги удалены\",\n \"limitReached\": \"Достигнут предел тегов\"\n },\n \"fileUpload\": {\n \"placeholder\": \"Перетащите файлы сюда или нажмите для выбора\",\n \"progress\": \"Прогресс загрузки\",\n \"uploading\": \"Загрузка…\",\n \"success\": \"Загрузка завершена\",\n \"error\": \"Не удалось загрузить\",\n \"remove\": \"Убрать {fileName}\",\n \"retry\": \"Повторить загрузку\",\n \"retryFile\": \"Повторить {fileName}\",\n \"abortFile\": \"Отменить загрузку {fileName}\",\n \"statusPending\": \"В очереди\",\n \"statusUploading\": \"Загружается\",\n \"statusSuccess\": \"Загружен\",\n \"statusError\": \"Ошибка\"\n },\n \"formFile\": {\n \"upload\": \"Загрузить файл\",\n \"change\": \"Заменить файл\",\n \"remove\": \"Удалить\",\n \"clearAll\": \"Очистить всё\",\n \"placeholder\": \"Файлы не выбраны\",\n \"validateError\": \"Не удалось проверить перетащенные файлы\",\n \"removeFile\": \"Убрать {fileName}\"\n },\n \"imageViewer\": {\n \"close\": \"Закрыть просмотрщик\",\n \"prev\": \"Предыдущее изображение\",\n \"next\": \"Следующее изображение\",\n \"zoomIn\": \"Увеличить\",\n \"zoomOut\": \"Уменьшить\",\n \"resetZoom\": \"Сбросить масштаб\",\n \"zoomToNatural\": \"Натуральный размер (1:1)\",\n \"rotateLeft\": \"Повернуть влево\",\n \"rotateRight\": \"Повернуть вправо\",\n \"download\": \"Скачать изображение\",\n \"empty\": \"Нет изображения\",\n \"label\": \"Просмотр изображений\",\n \"position\": \"Изображение {index} из {total}\"\n },\n \"formErrorBanner\": {\n \"validationTitle\": \"Проверьте поля формы\",\n \"validationMessage\": \"Сервер отклонил данные. Исправьте выделенные поля и попробуйте снова.\",\n \"clientTitle\": \"Не удалось сохранить\",\n \"serverTitle\": \"Ошибка сохранения\",\n \"serverMessage\": \"Не удалось сохранить данные. Попробуйте позже.\"\n },\n \"uploadErrorBanner\": {\n \"networkTitle\": \"Не удалось загрузить файлы\",\n \"networkMessage\": \"Проверьте подключение и попробуйте снова.\",\n \"validationTitle\": \"Файлы не прошли проверку\",\n \"validationMessage\": \"Некоторые файлы не соответствуют требованиям.\",\n \"serverTitle\": \"Ошибка загрузки на сервере\",\n \"serverMessage\": \"Сервер не смог обработать загрузку. Попробуйте снова.\"\n },\n \"input\": {\n \"clear\": \"Очистить\",\n \"showPassword\": \"Показать пароль\",\n \"hidePassword\": \"Скрыть пароль\",\n \"limitReached\": \"Достигнут предел символов\"\n },\n \"textarea\": {\n \"lines\": {\n \"one\": \"{n} строка\",\n \"few\": \"{n} строки\",\n \"many\": \"{n} строк\",\n \"other\": \"{n} строки\"\n }\n },\n \"rating\": {\n \"label\": \"Оценка\",\n \"valueText\": \"{value} из {max}\",\n \"valueTextWithLabel\": \"{value} из {max}, {label}\"\n },\n \"commandPalette\": {\n \"label\": \"Палитра команд\",\n \"placeholder\": \"Введите команду или запрос…\",\n \"empty\": \"Ничего не найдено\",\n \"recent\": \"Недавние\",\n \"loading\": \"Загрузка…\"\n },\n \"delta\": {\n \"increase\": \"рост\"\n },\n \"filePreview\": {\n \"label\": \"Файл\"\n },\n \"fileValidation\": {\n \"accept\": \"Файл «{fileName}» не подходит под accept=\\\"{accept}\\\"\",\n \"extension\": \"У файла «{fileName}» недопустимое расширение «.{extension}»\",\n \"mimeType\": \"У файла «{fileName}» недопустимый тип «{mimeType}»\",\n \"mimeTypeFallback\": \"У файла «{fileName}» неопределённый тип «{mimeType}»\",\n \"maxFileSize\": \"Файл «{fileName}» слишком большой ({size} байт), максимум {maxBytes}\",\n \"maxCount\": \"Выбрано слишком много файлов ({count}), максимум — {maxCount}\",\n \"maxTotalSize\": \"Суммарный размер файлов слишком большой ({total} байт), максимум {maxBytes}\"\n },\n \"kbd\": {\n \"command\": \"Command\",\n \"option\": \"Option\",\n \"shift\": \"Shift\",\n \"control\": \"Control\",\n \"enter\": \"Enter\",\n \"escape\": \"Escape\",\n \"tab\": \"Tab\",\n \"backspace\": \"Backspace\",\n \"delete\": \"Delete\",\n \"pageUp\": \"Page Up\",\n \"pageDown\": \"Page Down\",\n \"arrowUp\": \"стрелка вверх\",\n \"arrowDown\": \"стрелка вниз\",\n \"arrowLeft\": \"стрелка влево\",\n \"arrowRight\": \"стрелка вправо\",\n \"then\": \"затем\"\n },\n \"link\": {\n \"opensInNewTab\": \"откроется в новой вкладке\"\n },\n \"colorPicker\": {\n \"panelLabel\": \"Выбор цвета\",\n \"hue\": \"Оттенок\",\n \"saturation\": \"Насыщенность\",\n \"lightness\": \"Светлота\",\n \"opacity\": \"Непрозрачность\",\n \"hexLabel\": \"Значение hex\",\n \"presetsLabel\": \"Палитра\",\n \"swatchLabel\": \"Цвет {color}\"\n },\n \"collapse\": {\n \"empty\": \"Пока пусто\"\n },\n \"list\": {\n \"empty\": \"Пока пусто\"\n },\n \"button\": {\n \"loading\": \"Загрузка…\"\n },\n \"table\": {\n \"empty\": \"Пока пусто\"\n },\n \"timeline\": {\n \"empty\": \"Пока пусто\"\n },\n \"tabs\": {\n \"empty\": \"Пока пусто\",\n \"close\": \"Закрыть вкладку\"\n },\n \"bottomNav\": {\n \"label\": \"Нижняя навигация\",\n \"badge\": \"{count} новых\"\n },\n \"emptyState\": {\n \"title\": \"Пока пусто\"\n },\n \"sortable\": {\n \"label\": \"Сортируемый список\",\n \"handle\": \"Переставить\",\n \"empty\": \"Пока нечего сортировать\",\n \"grabbed\": \"Взято. Позиция {position} из {count}\",\n \"moved\": \"Позиция {position} из {count}\",\n \"dropped\": \"Отпущено на позиции {position} из {count}\",\n \"cancelled\": \"Перенос отменён\"\n },\n \"jsonViewer\": {\n \"search\": \"Поиск по ключу или значению\",\n \"expandAll\": \"Раскрыть всё\",\n \"collapseAll\": \"Свернуть всё\",\n \"copy\": \"Скопировать значение\",\n \"copied\": \"Значение скопировано\",\n \"copyFailed\": \"Не удалось скопировать значение\",\n \"moreItems\": \"ещё\"\n },\n \"contextMenu\": {\n \"label\": \"Контекстное меню\"\n }\n}\n"],"mappings":""}
@@ -0,0 +1,360 @@
1
+ import { onMounted, onScopeDispose, readonly, ref, watch } from "vue";
2
+ //#region src/composables/internal/scrollSpyGeometry.ts
3
+ /**
4
+ * Допуск линии активации.
5
+ *
6
+ * `GrAffix` от эпсилона отказался осознанно — там рядом стоит CSS, у которого
7
+ * его нет, и расхождение с ним было бы дороже дребезга. Здесь параллельного
8
+ * источника истины нет: единственный потребитель числа — этот модуль. А повод
9
+ * конкретный: прокрутка ставится так, чтобы верх раздела совпал с линией, но
10
+ * браузеры округляют смещение, и приземление на `line + 0.5` дало бы «кликнул
11
+ * третий раздел — подсветился второй».
12
+ */
13
+ var SNAP_EPSILON = 2;
14
+ /**
15
+ * Какой раздел читают сейчас.
16
+ *
17
+ * Правило — «последний, чей верх выше линии», а не «самый видимый». Максимум
18
+ * доли пересечения ошибается двумя способами: длинный раздел выигрывает у
19
+ * короткого независимо от того, что читают, и подсветка отскакивает назад,
20
+ * когда следующий раздел только показался снизу. Здесь ответ — заголовок,
21
+ * ближайший сверху к линии чтения.
22
+ *
23
+ * Порядок массива на ответ не влияет: выбор идёт по геометрии. Это снимает
24
+ * класс дефектов, где объявленный список разошёлся с порядком разделов на
25
+ * странице — подсветку в таком случае невозможно отладить.
26
+ *
27
+ * `atEnd` побеждает правило линии, и без него последний раздел недостижим:
28
+ * раздел короче остатка скроллпорта никогда не доводит свой верх до линии,
29
+ * потому что прокрутка кончается раньше.
30
+ */
31
+ function activeSectionId(rects, line, atEnd) {
32
+ if (rects.length === 0) return null;
33
+ let best = null;
34
+ for (const rect of rects) {
35
+ if (!atEnd && rect.top - line > SNAP_EPSILON) continue;
36
+ if (best === null || rect.top > best.top) best = rect;
37
+ }
38
+ return best?.id ?? null;
39
+ }
40
+ /**
41
+ * `rootMargin` наблюдателя: верх корня поджимается на отступ, и тогда его край
42
+ * совпадает с линией активации.
43
+ *
44
+ * Единицы только `px`: `rootMargin` произвольных длин не принимает, поэтому
45
+ * отступ приходит сюда уже замеренным.
46
+ */
47
+ function scrollSpyRootMargin(offsetPx) {
48
+ const inset = `${-(Number.isFinite(offsetPx) ? Math.max(0, Math.round(offsetPx)) : 0)}px`;
49
+ const zero = "0px";
50
+ return [
51
+ inset,
52
+ zero,
53
+ zero,
54
+ zero
55
+ ].join(" ");
56
+ }
57
+ /**
58
+ * Куда прокрутить скроллпорт, чтобы верх раздела встал на линию активации.
59
+ *
60
+ * Своя арифметика вместо `scrollIntoView` — по трём независимым причинам:
61
+ * тот двигает всех предков (у карусели это стоило прыжка страницы на каждом
62
+ * шаге автопрокрутки), не умеет наш отступ, и в jsdom отсутствует вовсе, то
63
+ * есть клик стал бы непроверяемым.
64
+ */
65
+ function scrollSpyTargetTop(scrollTop, sectionTop, portTop, offset, maxScrollTop) {
66
+ const upper = Math.max(0, Number.isFinite(maxScrollTop) ? maxScrollTop : 0);
67
+ const target = scrollTop + (sectionTop - portTop) - Math.max(0, offset);
68
+ return Math.min(upper, Math.max(0, target));
69
+ }
70
+ /**
71
+ * Прокручивается ли предок.
72
+ *
73
+ * Читается вычисленный `overflow-y`, а не объявленный: при
74
+ * `overflow-x: hidden; overflow-y: visible` спецификация превращает вычисленный
75
+ * `overflow-y` в `auto`, то есть предок действительно становится скроллпортом.
76
+ */
77
+ function isScrollableOverflow(overflowY) {
78
+ return overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay";
79
+ }
80
+ /**
81
+ * Доехали ли до дна.
82
+ *
83
+ * `maxScrollTop > 1` — обязательное условие, а не перестраховка: содержимое
84
+ * короче скроллпорта даёт «дно» на нулевой прокрутке, и правило `atEnd`
85
+ * подсветило бы последний раздел на странице, где позиции чтения нет вовсе.
86
+ */
87
+ function isScrolledToEnd(scrollTop, maxScrollTop) {
88
+ return maxScrollTop > 1 && scrollTop >= maxScrollTop - SNAP_EPSILON;
89
+ }
90
+ //#endregion
91
+ //#region src/composables/useScrollSpy.ts
92
+ /**
93
+ * Порог простоя прокрутки, после которого она считается осевшей.
94
+ *
95
+ * Не догадка о длительности анимации — длину плавной прокрутки браузер
96
+ * выбирает сам от расстояния, и любой фиксированный таймаут либо короток, либо
97
+ * длинен. Это стандартный полифилл `scrollend`: порог обязан превышать
98
+ * межкадровый интервал потока событий с запасом на джанк и оставаться короче
99
+ * осмысленной паузы пользователя.
100
+ */
101
+ var SETTLE_IDLE_MS = 120;
102
+ /** Насколько близко к цели считается «доехали». */
103
+ var SETTLE_EPSILON = 2;
104
+ function useScrollSpy(options) {
105
+ const active = ref(null);
106
+ let observer = null;
107
+ /** Идёт ли наблюдение: без него выводить активный раздел не из чего. */
108
+ let observing = false;
109
+ /** Элемент, чей `scrollTop` двигаем. Для окна — `document.scrollingElement`. */
110
+ let portEl = null;
111
+ /** Корень наблюдателя: `null` для вьюпорта. */
112
+ let portRoot = null;
113
+ /** Куда вешать `scroll`: на элемент или на документ. */
114
+ let portEvents = null;
115
+ let isDocumentPort = true;
116
+ let appliedOffsetPx = 0;
117
+ let lastAtEnd = false;
118
+ let pinnedId = null;
119
+ let pendingTop = null;
120
+ let settled = true;
121
+ let idleTimer = null;
122
+ const warnedMissing = /* @__PURE__ */ new Set();
123
+ function element(id) {
124
+ const custom = options.elementFor?.(id);
125
+ if (custom !== void 0) return custom ?? null;
126
+ return typeof document === "undefined" ? null : document.getElementById(id);
127
+ }
128
+ function portTop() {
129
+ if (!portEl) return 0;
130
+ return isDocumentPort ? 0 : portEl.getBoundingClientRect().top;
131
+ }
132
+ function portMaxScroll() {
133
+ return portEl ? portEl.scrollHeight - portEl.clientHeight : 0;
134
+ }
135
+ function measureOffset() {
136
+ const value = options.offset?.() ?? 0;
137
+ return Number.isFinite(value) ? Math.max(0, value) : 0;
138
+ }
139
+ /**
140
+ * Скроллпорт ищется от **первого раздела**, а не от места вызова: оглавление
141
+ * обычно стоит рядом с содержимым, отдельной колонкой, и обход предков от
142
+ * него привёл бы не туда. Скроллпорт определяют разделы, за ними и следим.
143
+ *
144
+ * Документ представляем через `document.scrollingElement`, а не через
145
+ * `window`: так остаётся один кодовый путь для окна и для коробки, и клик
146
+ * становится проверяемым — `window.scrollTo` в jsdom заглушка, печатающая
147
+ * «Not implemented», а `Element.scrollTop` там обычное хранимое свойство.
148
+ */
149
+ function resolvePort() {
150
+ const explicit = options.scroller?.();
151
+ if (explicit) {
152
+ portEl = explicit;
153
+ portRoot = explicit;
154
+ portEvents = explicit;
155
+ isDocumentPort = false;
156
+ return;
157
+ }
158
+ if (typeof document !== "undefined" && typeof getComputedStyle === "function") {
159
+ const ids = options.sections();
160
+ let current = (ids.length > 0 ? element(ids[0]) : null)?.parentElement ?? null;
161
+ while (current && current !== document.body) {
162
+ if (isScrollableOverflow(getComputedStyle(current).overflowY)) {
163
+ portEl = current;
164
+ portRoot = current;
165
+ portEvents = current;
166
+ isDocumentPort = false;
167
+ return;
168
+ }
169
+ current = current.parentElement;
170
+ }
171
+ }
172
+ portEl = typeof document === "undefined" ? null : document.scrollingElement ?? document.documentElement;
173
+ portRoot = null;
174
+ portEvents = typeof document === "undefined" ? null : document;
175
+ isDocumentPort = true;
176
+ }
177
+ function setActive(next) {
178
+ if (next === active.value) return;
179
+ const previous = active.value;
180
+ active.value = next;
181
+ options.onChange?.(next, previous);
182
+ }
183
+ function warnMissing(id) {
184
+ if (!(process.env.NODE_ENV !== "production") || warnedMissing.has(id)) return;
185
+ warnedMissing.add(id);
186
+ console.warn(`[granularity] useScrollSpy: раздела #${id} нет в документе — пункт оглавления останется неактивным, а клик по нему ничего не сделает. Обычно это опечатка в идентификаторе; если раздел дорисовывается позже, предупреждение можно не читать.`);
187
+ }
188
+ function recompute() {
189
+ if (pinnedId !== null) return;
190
+ if (!observing) {
191
+ setActive(null);
192
+ return;
193
+ }
194
+ const rects = [];
195
+ for (const id of options.sections()) {
196
+ const el = element(id);
197
+ if (!el) {
198
+ warnMissing(id);
199
+ continue;
200
+ }
201
+ rects.push({
202
+ id,
203
+ top: el.getBoundingClientRect().top
204
+ });
205
+ }
206
+ setActive(activeSectionId(rects, portTop() + appliedOffsetPx, lastAtEnd));
207
+ }
208
+ function clearIdleTimer() {
209
+ if (idleTimer !== null) {
210
+ clearTimeout(idleTimer);
211
+ idleTimer = null;
212
+ }
213
+ }
214
+ function restartIdleTimer() {
215
+ clearIdleTimer();
216
+ idleTimer = setTimeout(() => {
217
+ idleTimer = null;
218
+ settled = true;
219
+ }, SETTLE_IDLE_MS);
220
+ }
221
+ function unpin() {
222
+ if (pinnedId === null) return;
223
+ pinnedId = null;
224
+ pendingTop = null;
225
+ settled = true;
226
+ clearIdleTimer();
227
+ recompute();
228
+ }
229
+ function onScroll() {
230
+ const top = portEl?.scrollTop ?? 0;
231
+ const atEnd = isScrolledToEnd(top, portMaxScroll());
232
+ if (atEnd !== lastAtEnd) {
233
+ lastAtEnd = atEnd;
234
+ recompute();
235
+ }
236
+ if (pinnedId === null) return;
237
+ if (settled) {
238
+ unpin();
239
+ return;
240
+ }
241
+ if (pendingTop !== null && Math.abs(top - pendingTop) <= SETTLE_EPSILON) {
242
+ settled = true;
243
+ return;
244
+ }
245
+ if (!hasScrollEnd()) restartIdleTimer();
246
+ }
247
+ function onScrollEnd() {
248
+ settled = true;
249
+ }
250
+ /**
251
+ * Пользователь перехватил прокрутку. Закреп снимается немедленно: браузер
252
+ * плавную прокрутку в этот момент отменяет, и цели мы не достигнем никогда.
253
+ */
254
+ function onUserScrollIntent() {
255
+ unpin();
256
+ }
257
+ function hasScrollEnd() {
258
+ return typeof window !== "undefined" && "onscrollend" in window;
259
+ }
260
+ function prefersReducedMotion() {
261
+ return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
262
+ }
263
+ function detach() {
264
+ observer?.disconnect();
265
+ observer = null;
266
+ observing = false;
267
+ clearIdleTimer();
268
+ if (portEvents) {
269
+ portEvents.removeEventListener("scroll", onScroll);
270
+ portEvents.removeEventListener("scrollend", onScrollEnd);
271
+ portEvents.removeEventListener("wheel", onUserScrollIntent);
272
+ portEvents.removeEventListener("touchstart", onUserScrollIntent);
273
+ portEvents.removeEventListener("keydown", onUserScrollIntent);
274
+ }
275
+ }
276
+ function attach() {
277
+ detach();
278
+ if (options.disabled?.() === true) {
279
+ pinnedId = null;
280
+ pendingTop = null;
281
+ setActive(null);
282
+ return;
283
+ }
284
+ resolvePort();
285
+ appliedOffsetPx = measureOffset();
286
+ lastAtEnd = isScrolledToEnd(portEl?.scrollTop ?? 0, portMaxScroll());
287
+ if (portEvents) {
288
+ portEvents.addEventListener("scroll", onScroll, { passive: true });
289
+ portEvents.addEventListener("scrollend", onScrollEnd);
290
+ portEvents.addEventListener("wheel", onUserScrollIntent, { passive: true });
291
+ portEvents.addEventListener("touchstart", onUserScrollIntent, { passive: true });
292
+ portEvents.addEventListener("keydown", onUserScrollIntent);
293
+ }
294
+ if (typeof IntersectionObserver === "undefined") return;
295
+ observing = true;
296
+ observer = new IntersectionObserver(handle, {
297
+ root: portRoot,
298
+ rootMargin: scrollSpyRootMargin(appliedOffsetPx),
299
+ threshold: 0
300
+ });
301
+ for (const id of options.sections()) {
302
+ const el = element(id);
303
+ if (el) observer.observe(el);
304
+ else warnMissing(id);
305
+ }
306
+ }
307
+ function handle() {
308
+ recompute();
309
+ if (measureOffset() !== appliedOffsetPx) attach();
310
+ }
311
+ function scrollTo(id, scrollOptions) {
312
+ const el = element(id);
313
+ if (!el || !portEl) {
314
+ warnMissing(id);
315
+ return;
316
+ }
317
+ appliedOffsetPx = measureOffset();
318
+ const top = scrollSpyTargetTop(portEl.scrollTop, el.getBoundingClientRect().top, portTop(), appliedOffsetPx, portMaxScroll());
319
+ pin(id);
320
+ pendingTop = top;
321
+ settled = false;
322
+ const behavior = scrollOptions?.behavior ?? (prefersReducedMotion() ? "auto" : "smooth");
323
+ if (typeof portEl.scrollTo === "function") portEl.scrollTo({
324
+ top,
325
+ behavior
326
+ });
327
+ else portEl.scrollTop = top;
328
+ if (!hasScrollEnd()) restartIdleTimer();
329
+ }
330
+ function pin(id) {
331
+ if (id === null) {
332
+ unpin();
333
+ return;
334
+ }
335
+ pinnedId = id;
336
+ setActive(id);
337
+ }
338
+ function refresh() {
339
+ attach();
340
+ recompute();
341
+ }
342
+ onMounted(refresh);
343
+ onScopeDispose(detach);
344
+ watch(() => [
345
+ options.sections(),
346
+ options.disabled?.(),
347
+ options.scroller?.()
348
+ ], () => {
349
+ if (pinnedId !== null && !options.sections().includes(pinnedId)) unpin();
350
+ refresh();
351
+ }, { flush: "post" });
352
+ return {
353
+ active: readonly(active),
354
+ scrollTo,
355
+ refresh,
356
+ pin
357
+ };
358
+ }
359
+ //#endregion
360
+ export { useScrollSpy as t };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useScrollSpy-CIJpw0RI.js","names":[],"sources":["../../src/composables/internal/scrollSpyGeometry.ts","../../src/composables/useScrollSpy.ts"],"sourcesContent":["/**\n * Арифметика положения чтения: чистые функции без DOM и без реактивности.\n *\n * Вынесена отдельно по той же причине, что и геометрия перестановки в\n * `dragSortGeometry.ts`: ошибка здесь проявляется не исключением, а\n * «подсветился не тот раздел», и поймать это можно только тестом на числах. В\n * jsdom проверить нечем вовсе — там нет ни прокрутки, ни раскладки.\n *\n * Композабл `useScrollSpy` сводит `DOMRect` к числам и зовёт эти функции; что\n * значит активный раздел для разметки, решает уже компонент.\n */\n\n/** Верх раздела в координатах вьюпорта. */\nexport interface ScrollSpyRect {\n id: string\n top: number\n}\n\n/**\n * Допуск линии активации.\n *\n * `GrAffix` от эпсилона отказался осознанно — там рядом стоит CSS, у которого\n * его нет, и расхождение с ним было бы дороже дребезга. Здесь параллельного\n * источника истины нет: единственный потребитель числа — этот модуль. А повод\n * конкретный: прокрутка ставится так, чтобы верх раздела совпал с линией, но\n * браузеры округляют смещение, и приземление на `line + 0.5` дало бы «кликнул\n * третий раздел — подсветился второй».\n */\nconst SNAP_EPSILON = 2\n\n/**\n * Какой раздел читают сейчас.\n *\n * Правило — «последний, чей верх выше линии», а не «самый видимый». Максимум\n * доли пересечения ошибается двумя способами: длинный раздел выигрывает у\n * короткого независимо от того, что читают, и подсветка отскакивает назад,\n * когда следующий раздел только показался снизу. Здесь ответ — заголовок,\n * ближайший сверху к линии чтения.\n *\n * Порядок массива на ответ не влияет: выбор идёт по геометрии. Это снимает\n * класс дефектов, где объявленный список разошёлся с порядком разделов на\n * странице — подсветку в таком случае невозможно отладить.\n *\n * `atEnd` побеждает правило линии, и без него последний раздел недостижим:\n * раздел короче остатка скроллпорта никогда не доводит свой верх до линии,\n * потому что прокрутка кончается раньше.\n */\nexport function activeSectionId(\n rects: readonly ScrollSpyRect[],\n line: number,\n atEnd: boolean,\n): string | null {\n // Пустой вход — это сразу три случая: пустой список, ни одного элемента в\n // DOM, наблюдение выключено. Все три означают «сказать нечего».\n if (rects.length === 0)\n return null\n\n let best: ScrollSpyRect | null = null\n\n for (const rect of rects) {\n if (!atEnd && rect.top - line > SNAP_EPSILON)\n continue\n\n if (best === null || rect.top > best.top)\n best = rect\n }\n\n return best?.id ?? null\n}\n\n/**\n * `rootMargin` наблюдателя: верх корня поджимается на отступ, и тогда его край\n * совпадает с линией активации.\n *\n * Единицы только `px`: `rootMargin` произвольных длин не принимает, поэтому\n * отступ приходит сюда уже замеренным.\n */\nexport function scrollSpyRootMargin(offsetPx: number): string {\n const px = Number.isFinite(offsetPx) ? Math.max(0, Math.round(offsetPx)) : 0\n const negative = -px\n // Число выносится в переменную, чтобы литерал остался одним токеном: гейт\n // safelist разбирает шаблонные строки по пробелам, а обломок вида `-${px}px`\n // принимает за утилиту UnoCSS — которой он, по совпадению, и оказывается.\n const inset = `${negative}px`\n const zero = '0px'\n\n return [inset, zero, zero, zero].join(' ')\n}\n\n/**\n * Куда прокрутить скроллпорт, чтобы верх раздела встал на линию активации.\n *\n * Своя арифметика вместо `scrollIntoView` — по трём независимым причинам:\n * тот двигает всех предков (у карусели это стоило прыжка страницы на каждом\n * шаге автопрокрутки), не умеет наш отступ, и в jsdom отсутствует вовсе, то\n * есть клик стал бы непроверяемым.\n */\nexport function scrollSpyTargetTop(\n scrollTop: number,\n sectionTop: number,\n portTop: number,\n offset: number,\n maxScrollTop: number,\n): number {\n const upper = Math.max(0, Number.isFinite(maxScrollTop) ? maxScrollTop : 0)\n const target = scrollTop + (sectionTop - portTop) - Math.max(0, offset)\n\n return Math.min(upper, Math.max(0, target))\n}\n\n/**\n * Прокручивается ли предок.\n *\n * Читается вычисленный `overflow-y`, а не объявленный: при\n * `overflow-x: hidden; overflow-y: visible` спецификация превращает вычисленный\n * `overflow-y` в `auto`, то есть предок действительно становится скроллпортом.\n */\nexport function isScrollableOverflow(overflowY: string): boolean {\n return overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay'\n}\n\n/**\n * Доехали ли до дна.\n *\n * `maxScrollTop > 1` — обязательное условие, а не перестраховка: содержимое\n * короче скроллпорта даёт «дно» на нулевой прокрутке, и правило `atEnd`\n * подсветило бы последний раздел на странице, где позиции чтения нет вовсе.\n */\nexport function isScrolledToEnd(scrollTop: number, maxScrollTop: number): boolean {\n return maxScrollTop > 1 && scrollTop >= maxScrollTop - SNAP_EPSILON\n}\n","import type { Ref } from 'vue'\nimport { onMounted, onScopeDispose, readonly, ref, watch } from 'vue'\n\nimport {\n activeSectionId,\n isScrollableOverflow,\n isScrolledToEnd,\n scrollSpyRootMargin,\n scrollSpyTargetTop,\n} from './internal/scrollSpyGeometry'\n\n/**\n * Отслеживание того, до какого раздела дочитали.\n *\n * Композабл владеет всем, что следует из **положения прокрутки**: линией\n * активации, выбором раздела, прокруткой к нему и закрепом на время этой\n * прокрутки. Всё, что следует из того, что пункт оглавления является\n * **ссылкой** — разметка, `aria-current`, модификаторы клика, — остаётся у\n * компонента `GrScrollSpy`.\n *\n * ```ts\n * const spy = useScrollSpy({\n * sections: () => ids.value,\n * offset: () => headerHeight.value,\n * onChange: id => emit('activeChange', id),\n * })\n * ```\n */\n\nexport interface UseScrollSpyOptions {\n /**\n * Идентификаторы разделов. Порядок задаёт порядок пунктов у потребителя, но\n * на ответ не влияет: активный выбирается по геометрии.\n */\n sections: () => readonly string[]\n /**\n * Элемент раздела. По умолчанию — `document.getElementById`.\n *\n * Дефолт не догадка: идентификаторы разделов и есть цели ссылок `#id`, то\n * есть DOM-id по определению жанра.\n */\n elementFor?: (id: string) => HTMLElement | null | undefined\n /** Скроллпорт. Не задан — ищется от первого раздела, не найден — документ. */\n scroller?: () => HTMLElement | null | undefined\n /**\n * Отступ линии активации от верха скроллпорта, **в пикселях**.\n *\n * Именно число, а не CSS-длина: резолв `4rem` или `var(...)` требует\n * `getComputedStyle`, то есть DOM, — а композабл обязан оставаться\n * проверяемым без него. Длину резолвит потребитель.\n */\n offset?: () => number\n /** Наблюдение выключено: `active` сбрасывается в `null`. */\n disabled?: () => boolean\n /** Активный сменился. Зовётся на смене, а не на каждой записи наблюдателя. */\n onChange?: (id: string | null, previous: string | null) => void\n}\n\nexport interface UseScrollSpyReturn {\n /** Активный раздел. `null` — линия ещё не дошла до первого. */\n active: Readonly<Ref<string | null>>\n /** Прокрутить к разделу и закрепить его активным до следующей прокрутки. */\n scrollTo: (id: string, options?: { behavior?: ScrollBehavior }) => void\n /** Перечитать отступ, скроллпорт и геометрию. */\n refresh: () => void\n /** Закрепить активным вручную: страницу прокручивает не оглавление. */\n pin: (id: string | null) => void\n}\n\n/**\n * Порог простоя прокрутки, после которого она считается осевшей.\n *\n * Не догадка о длительности анимации — длину плавной прокрутки браузер\n * выбирает сам от расстояния, и любой фиксированный таймаут либо короток, либо\n * длинен. Это стандартный полифилл `scrollend`: порог обязан превышать\n * межкадровый интервал потока событий с запасом на джанк и оставаться короче\n * осмысленной паузы пользователя.\n */\nconst SETTLE_IDLE_MS = 120\n\n/** Насколько близко к цели считается «доехали». */\nconst SETTLE_EPSILON = 2\n\nexport function useScrollSpy(options: UseScrollSpyOptions): UseScrollSpyReturn {\n const active = ref<string | null>(null)\n\n let observer: IntersectionObserver | null = null\n /** Идёт ли наблюдение: без него выводить активный раздел не из чего. */\n let observing = false\n /** Элемент, чей `scrollTop` двигаем. Для окна — `document.scrollingElement`. */\n let portEl: HTMLElement | null = null\n /** Корень наблюдателя: `null` для вьюпорта. */\n let portRoot: Element | null = null\n /** Куда вешать `scroll`: на элемент или на документ. */\n let portEvents: EventTarget | null = null\n let isDocumentPort = true\n\n let appliedOffsetPx = 0\n let lastAtEnd = false\n\n let pinnedId: string | null = null\n let pendingTop: number | null = null\n let settled = true\n let idleTimer: ReturnType<typeof setTimeout> | null = null\n\n const warnedMissing = new Set<string>()\n\n function element(id: string): HTMLElement | null {\n const custom = options.elementFor?.(id)\n if (custom !== undefined)\n return custom ?? null\n\n return typeof document === 'undefined' ? null : document.getElementById(id)\n }\n\n function portTop(): number {\n if (!portEl)\n return 0\n\n return isDocumentPort ? 0 : portEl.getBoundingClientRect().top\n }\n\n function portMaxScroll(): number {\n return portEl ? portEl.scrollHeight - portEl.clientHeight : 0\n }\n\n function measureOffset(): number {\n const value = options.offset?.() ?? 0\n\n return Number.isFinite(value) ? Math.max(0, value) : 0\n }\n\n /**\n * Скроллпорт ищется от **первого раздела**, а не от места вызова: оглавление\n * обычно стоит рядом с содержимым, отдельной колонкой, и обход предков от\n * него привёл бы не туда. Скроллпорт определяют разделы, за ними и следим.\n *\n * Документ представляем через `document.scrollingElement`, а не через\n * `window`: так остаётся один кодовый путь для окна и для коробки, и клик\n * становится проверяемым — `window.scrollTo` в jsdom заглушка, печатающая\n * «Not implemented», а `Element.scrollTop` там обычное хранимое свойство.\n */\n function resolvePort(): void {\n const explicit = options.scroller?.()\n\n if (explicit) {\n portEl = explicit\n portRoot = explicit\n portEvents = explicit\n isDocumentPort = false\n\n return\n }\n\n if (typeof document !== 'undefined' && typeof getComputedStyle === 'function') {\n const ids = options.sections()\n const first = ids.length > 0 ? element(ids[0]) : null\n let current = first?.parentElement ?? null\n\n // До `<body>`, не включая его: `useScrollLock` вешает на него\n // `overflow: hidden` на время открытой модалки.\n while (current && current !== document.body) {\n if (isScrollableOverflow(getComputedStyle(current).overflowY)) {\n portEl = current\n portRoot = current\n portEvents = current\n isDocumentPort = false\n\n return\n }\n\n current = current.parentElement\n }\n }\n\n // `scrollingElement` предпочтителен, но существует не везде — в jsdom его нет\n // вовсе, и без фолбэка прокрутка молча не выполнялась бы, а тест этого не\n // увидел бы: ранний выход исключения не бросает.\n portEl = (typeof document === 'undefined'\n ? null\n : document.scrollingElement ?? document.documentElement) as HTMLElement | null\n // `null`, а не `documentElement`: по спецификации это разные корни, и Safari\n // исторически расходился на втором.\n portRoot = null\n portEvents = typeof document === 'undefined' ? null : document\n isDocumentPort = true\n }\n\n function setActive(next: string | null): void {\n if (next === active.value)\n return\n\n const previous = active.value\n active.value = next\n options.onChange?.(next, previous)\n }\n\n function warnMissing(id: string): void {\n if (!__GR_DEV__ || warnedMissing.has(id))\n return\n\n warnedMissing.add(id)\n console.warn(\n `[granularity] useScrollSpy: раздела #${id} нет в документе — пункт оглавления `\n + 'останется неактивным, а клик по нему ничего не сделает. Обычно это опечатка в '\n + 'идентификаторе; если раздел дорисовывается позже, предупреждение можно не читать.',\n )\n }\n\n function recompute(): void {\n if (pinnedId !== null)\n return\n\n // Не наблюдаем — значит не знаем. Один замер на монтировании был бы верен\n // ровно до первой прокрутки, а дальше подсветка врала бы молча.\n if (!observing) {\n setActive(null)\n\n return\n }\n\n const rects: { id: string, top: number }[] = []\n\n for (const id of options.sections()) {\n const el = element(id)\n\n if (!el) {\n warnMissing(id)\n continue\n }\n\n rects.push({ id, top: el.getBoundingClientRect().top })\n }\n\n setActive(activeSectionId(rects, portTop() + appliedOffsetPx, lastAtEnd))\n }\n\n function clearIdleTimer(): void {\n if (idleTimer !== null) {\n clearTimeout(idleTimer)\n idleTimer = null\n }\n }\n\n function restartIdleTimer(): void {\n clearIdleTimer()\n idleTimer = setTimeout(() => {\n idleTimer = null\n settled = true\n }, SETTLE_IDLE_MS)\n }\n\n function unpin(): void {\n if (pinnedId === null)\n return\n\n pinnedId = null\n pendingTop = null\n settled = true\n clearIdleTimer()\n recompute()\n }\n\n function onScroll(): void {\n const top = portEl?.scrollTop ?? 0\n const atEnd = isScrolledToEnd(top, portMaxScroll())\n\n if (atEnd !== lastAtEnd) {\n lastAtEnd = atEnd\n recompute()\n }\n\n if (pinnedId === null)\n return\n\n // Отпускаем на первом событии прокрутки **после** того, как она осела. Это\n // и есть ответ на перетаскивание полосы прокрутки: ни `wheel`, ни\n // `touchstart` там не приходят, а `scroll` есть всегда.\n if (settled) {\n unpin()\n\n return\n }\n\n if (pendingTop !== null && Math.abs(top - pendingTop) <= SETTLE_EPSILON) {\n settled = true\n\n return\n }\n\n if (!hasScrollEnd())\n restartIdleTimer()\n }\n\n function onScrollEnd(): void {\n settled = true\n }\n\n /**\n * Пользователь перехватил прокрутку. Закреп снимается немедленно: браузер\n * плавную прокрутку в этот момент отменяет, и цели мы не достигнем никогда.\n */\n function onUserScrollIntent(): void {\n unpin()\n }\n\n function hasScrollEnd(): boolean {\n return typeof window !== 'undefined' && 'onscrollend' in window\n }\n\n function prefersReducedMotion(): boolean {\n return typeof window !== 'undefined'\n && window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true\n }\n\n function detach(): void {\n observer?.disconnect()\n observer = null\n observing = false\n clearIdleTimer()\n\n if (portEvents) {\n portEvents.removeEventListener('scroll', onScroll)\n portEvents.removeEventListener('scrollend', onScrollEnd)\n portEvents.removeEventListener('wheel', onUserScrollIntent)\n portEvents.removeEventListener('touchstart', onUserScrollIntent)\n portEvents.removeEventListener('keydown', onUserScrollIntent)\n }\n }\n\n function attach(): void {\n detach()\n\n if (options.disabled?.() === true) {\n pinnedId = null\n pendingTop = null\n setActive(null)\n\n return\n }\n\n resolvePort()\n appliedOffsetPx = measureOffset()\n lastAtEnd = isScrolledToEnd(portEl?.scrollTop ?? 0, portMaxScroll())\n\n if (portEvents) {\n portEvents.addEventListener('scroll', onScroll, { passive: true })\n portEvents.addEventListener('scrollend', onScrollEnd)\n portEvents.addEventListener('wheel', onUserScrollIntent, { passive: true })\n portEvents.addEventListener('touchstart', onUserScrollIntent, { passive: true })\n portEvents.addEventListener('keydown', onUserScrollIntent)\n }\n\n // Наблюдателя может не быть вовсе (сервер, jsdom, старый движок). Тогда\n // подсветки нет, и это правильная деградация: подсветить первый раздел\n // значило бы поставить `aria-current` на пункт, который активным не\n // является, — то есть солгать диктору. Прокрутка по клику при этом работает.\n if (typeof IntersectionObserver === 'undefined')\n return\n\n observing = true\n observer = new IntersectionObserver(handle, {\n root: portRoot,\n rootMargin: scrollSpyRootMargin(appliedOffsetPx),\n // Второй порог удвоил бы вызовы, не создав ни одной новой развилки:\n // ответ считается по прямоугольникам, а не по доле пересечения.\n threshold: 0,\n })\n\n for (const id of options.sections()) {\n const el = element(id)\n\n if (el)\n observer.observe(el)\n else\n warnMissing(id)\n }\n }\n\n function handle(): void {\n recompute()\n\n // Самоизлечение: dev-сервер UnoCSS отдаёт правила по требованию, и в момент\n // монтирования CSS-длина отступа могла ещё не разрешиться — тогда линия\n // собрана из нуля. Пересобираем, как только замер разошёлся с применённым.\n if (measureOffset() !== appliedOffsetPx)\n attach()\n }\n\n function scrollTo(id: string, scrollOptions?: { behavior?: ScrollBehavior }): void {\n const el = element(id)\n\n if (!el || !portEl) {\n warnMissing(id)\n\n return\n }\n\n appliedOffsetPx = measureOffset()\n\n const top = scrollSpyTargetTop(\n portEl.scrollTop,\n el.getBoundingClientRect().top,\n portTop(),\n appliedOffsetPx,\n portMaxScroll(),\n )\n\n // Подсветка переезжает сразу, до всякой прокрутки: иначе она добежала бы по\n // промежуточным пунктам, пока плавная прокрутка едет к цели.\n pin(id)\n pendingTop = top\n settled = false\n\n const behavior = scrollOptions?.behavior ?? (prefersReducedMotion() ? 'auto' : 'smooth')\n\n // Не `portEl.scrollTo?.(…) ?? (portEl.scrollTop = top)`: метод возвращает\n // `undefined`, и правая часть выполнилась бы всегда.\n if (typeof portEl.scrollTo === 'function')\n portEl.scrollTo({ top, behavior })\n else\n portEl.scrollTop = top\n\n if (!hasScrollEnd())\n restartIdleTimer()\n }\n\n function pin(id: string | null): void {\n if (id === null) {\n unpin()\n\n return\n }\n\n pinnedId = id\n setActive(id)\n }\n\n function refresh(): void {\n attach()\n recompute()\n }\n\n onMounted(refresh)\n onScopeDispose(detach)\n\n // `flush: 'post'` обязателен: до обновления DOM новых разделов ещё нет, и\n // наблюдатель повесился бы на пустоту.\n watch(\n () => [options.sections(), options.disabled?.(), options.scroller?.()] as const,\n () => {\n if (pinnedId !== null && !options.sections().includes(pinnedId))\n unpin()\n\n refresh()\n },\n { flush: 'post' },\n )\n\n return { active: readonly(active), scrollTo, refresh, pin }\n}\n"],"mappings":"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"}