@feugene/granularity 0.27.2 → 0.28.1

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 (38) hide show
  1. package/dist/chunks/{config-DcfWZOi5.js → config-C0Eu8jpG.js} +14 -1
  2. package/dist/chunks/config-C0Eu8jpG.js.map +1 -0
  3. package/dist/chunks/{config-CWthWcWY.js → config-_jbgsFpw.js} +2 -1
  4. package/dist/chunks/config-_jbgsFpw.js.map +1 -0
  5. package/dist/chunks/{grDropdownMenuStyles-DG3aYHkT.js → grDropdownMenuStyles-Cf8OdR8S.js} +20 -3
  6. package/dist/chunks/grDropdownMenuStyles-Cf8OdR8S.js.map +1 -0
  7. package/dist/chunks/{granular-provider-DvvRayEh.js → granular-provider-Dds_519x.js} +2 -2
  8. package/dist/chunks/{granular-provider-DvvRayEh.js.map → granular-provider-Dds_519x.js.map} +1 -1
  9. package/dist/components/GrBreadcrumbs/chunks/{GrBreadcrumbs-jtSnMEtg.js → GrBreadcrumbs-BKOvl8W1.js} +32 -13
  10. package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-BKOvl8W1.js.map +1 -0
  11. package/dist/components/GrBreadcrumbs/index.js +2 -2
  12. package/dist/components/GrContextMenu/chunks/{GrContextMenu-DDAUvrDK.js → GrContextMenu-BNQDbAsY.js} +1 -1
  13. package/dist/components/GrContextMenu/chunks/{GrContextMenu-DDAUvrDK.js.map → GrContextMenu-BNQDbAsY.js.map} +1 -1
  14. package/dist/components/GrContextMenu/index.js +1 -1
  15. package/dist/components/GrDelta/chunks/{GrDelta-04j9WmEY.js → GrDelta-BZwkgQW-.js} +0 -0
  16. package/dist/components/GrDelta/chunks/GrDelta-BZwkgQW-.js.map +1 -0
  17. package/dist/components/GrDelta/index.js +1 -1
  18. package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-5zoOL8Oj.js → GrDropdownMenu-Cg7qHV9J.js} +2 -2
  19. package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-5zoOL8Oj.js.map → GrDropdownMenu-Cg7qHV9J.js.map} +1 -1
  20. package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenuList-CT_1TQJo.js → GrDropdownMenuList-RW5IBvmu.js} +1 -1
  21. package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenuList-CT_1TQJo.js.map → GrDropdownMenuList-RW5IBvmu.js.map} +1 -1
  22. package/dist/components/GrDropdownMenu/index.js +3 -3
  23. package/dist/directives/clickOutside.js +83 -21
  24. package/dist/directives/clickOutside.js.map +1 -1
  25. package/dist/granular-provider-node.js +1 -1
  26. package/dist/granular-provider.js +1 -1
  27. package/dist/index.js +7 -7
  28. package/dist/types/components/GrBreadcrumbs/GrBreadcrumbs.vue.d.ts +11 -0
  29. package/dist/types/components/GrBreadcrumbs/grBreadcrumbsStyles.d.ts +21 -0
  30. package/dist/types/components/GrDropdownMenu/grDropdownMenuStyles.d.ts +18 -2
  31. package/dist/types/internal/granularityFormat.d.ts +2 -2
  32. package/dist/web-types.json +10 -1
  33. package/package.json +2 -1
  34. package/dist/chunks/config-CWthWcWY.js.map +0 -1
  35. package/dist/chunks/config-DcfWZOi5.js.map +0 -1
  36. package/dist/chunks/grDropdownMenuStyles-DG3aYHkT.js.map +0 -1
  37. package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-jtSnMEtg.js.map +0 -1
  38. package/dist/components/GrDelta/chunks/GrDelta-04j9WmEY.js.map +0 -1
@@ -47,35 +47,98 @@ function resolveExclude(doc, exclude) {
47
47
  return resolved;
48
48
  }
49
49
  var states = /* @__PURE__ */ new WeakMap();
50
+ /**
51
+ * Слушатель — один на документ и вид события, а не на элемент.
52
+ *
53
+ * Каждый элемент со своим слушателем — это десяток подписок на документе у
54
+ * страницы с десятком оверлеев, и полное переподключение на каждом обновлении
55
+ * хозяина: хук `updated` срабатывает на любой ререндер, а набор событий при
56
+ * этом обычно тот же. Здесь подписка живёт по счётчику потребителей, а элементы
57
+ * обходятся общим обработчиком.
58
+ */
59
+ var documentElements = /* @__PURE__ */ new WeakMap();
60
+ var documentSubscriptions = /* @__PURE__ */ new WeakMap();
61
+ function subscriptionKey(type, capture) {
62
+ return `${type}|${capture ? "capture" : "bubble"}`;
63
+ }
64
+ function keysOf(options) {
65
+ if (!options.enabled || !options.handler) return [];
66
+ return options.events.map((type) => subscriptionKey(type, options.capture));
67
+ }
68
+ function dispatch(doc, event, capture) {
69
+ const elements = documentElements.get(doc);
70
+ if (!elements) return;
71
+ for (const el of [...elements]) {
72
+ const state = states.get(el);
73
+ if (!state) continue;
74
+ const { enabled, handler, exclude, events } = state.options;
75
+ if (!enabled || !handler) continue;
76
+ if (state.options.capture !== capture) continue;
77
+ if (!events.includes(event.type)) continue;
78
+ if ("button" in event && typeof event.button === "number" && event.button !== 0) continue;
79
+ if (!el.isConnected) continue;
80
+ if (isEventInside(el, event)) continue;
81
+ if (resolveExclude(state.doc, exclude).some((item) => isEventInside(item, event))) continue;
82
+ handler(event);
83
+ }
84
+ }
85
+ function subscribe(doc, key) {
86
+ let subscriptions = documentSubscriptions.get(doc);
87
+ if (!subscriptions) {
88
+ subscriptions = /* @__PURE__ */ new Map();
89
+ documentSubscriptions.set(doc, subscriptions);
90
+ }
91
+ const existing = subscriptions.get(key);
92
+ if (existing) {
93
+ existing.count += 1;
94
+ return;
95
+ }
96
+ const [type, mode] = key.split("|");
97
+ const capture = mode === "capture";
98
+ const listener = (event) => dispatch(doc, event, capture);
99
+ subscriptions.set(key, {
100
+ listener,
101
+ count: 1
102
+ });
103
+ doc.addEventListener(type, listener, capture);
104
+ }
105
+ function unsubscribe(doc, key) {
106
+ const subscriptions = documentSubscriptions.get(doc);
107
+ const existing = subscriptions?.get(key);
108
+ if (!subscriptions || !existing) return;
109
+ existing.count -= 1;
110
+ if (existing.count > 0) return;
111
+ const [type, mode] = key.split("|");
112
+ doc.removeEventListener(type, existing.listener, mode === "capture");
113
+ subscriptions.delete(key);
114
+ }
50
115
  function unbind(el) {
51
116
  const state = states.get(el);
52
117
  if (!state) return;
53
- for (const event of state.options.events) state.doc.removeEventListener(event, state.listener, state.options.capture);
118
+ for (const key of state.keys) unsubscribe(state.doc, key);
119
+ documentElements.get(state.doc)?.delete(el);
54
120
  states.delete(el);
55
121
  }
56
122
  function bind(el, value) {
57
- const options = normalize(value);
58
123
  const doc = el.ownerDocument;
59
- const listener = (event) => {
60
- const current = states.get(el);
61
- if (!current) return;
62
- const { enabled, handler, exclude } = current.options;
63
- if (!enabled || !handler) return;
64
- if ("button" in event && typeof event.button === "number" && event.button !== 0) return;
65
- if (!el.isConnected) return;
66
- if (isEventInside(el, event)) return;
67
- const excluded = resolveExclude(current.doc, exclude);
68
- for (const ex of excluded) if (isEventInside(ex, event)) return;
69
- handler(event);
70
- };
71
- const nextState = {
124
+ const previous = states.get(el);
125
+ if (previous && previous.doc !== doc) unbind(el);
126
+ const options = normalize(value);
127
+ const nextKeys = keysOf(options);
128
+ const prevKeys = states.get(el)?.keys ?? [];
129
+ states.set(el, {
72
130
  options,
73
131
  doc,
74
- listener
75
- };
76
- states.set(el, nextState);
77
- if (!options.enabled || !options.handler) return;
78
- for (const event of options.events) doc.addEventListener(event, listener, options.capture);
132
+ keys: nextKeys
133
+ });
134
+ let elements = documentElements.get(doc);
135
+ if (!elements) {
136
+ elements = /* @__PURE__ */ new Set();
137
+ documentElements.set(doc, elements);
138
+ }
139
+ elements.add(el);
140
+ for (const key of nextKeys) if (!prevKeys.includes(key)) subscribe(doc, key);
141
+ for (const key of prevKeys) if (!nextKeys.includes(key)) unsubscribe(doc, key);
79
142
  }
80
143
  /**
81
144
  * `v-click-outside` — обработчик клика вне элемента (например, закрытие дропдауна/модалки).
@@ -85,7 +148,6 @@ var vClickOutside = {
85
148
  bind(el, binding.value);
86
149
  },
87
150
  updated(el, binding) {
88
- unbind(el);
89
151
  bind(el, binding.value);
90
152
  },
91
153
  unmounted(el) {
@@ -1 +1 @@
1
- {"version":3,"file":"clickOutside.js","names":[],"sources":["../../src/directives/clickOutside.ts"],"sourcesContent":["import type { Directive } from 'vue'\n\nexport type ClickOutsideHandler = (event: MouseEvent | TouchEvent | PointerEvent) => void\n\nexport type ClickOutsideExclude = HTMLElement | string | (() => HTMLElement | null | undefined)\n\nexport type ClickOutsideBindingValue =\n | ClickOutsideHandler\n | {\n handler: ClickOutsideHandler\n /** По умолчанию `true`. */\n enabled?: boolean\n /** По умолчанию `true` (слушаем в capture, чтобы stopPropagation внутри не ломал закрытие). */\n capture?: boolean\n /** Список событий. По умолчанию `['click']`. */\n events?: Array<'click' | 'mousedown' | 'touchstart' | 'pointerdown'>\n /** Элементы/селекторы, клики по которым считаются \"внутри\". */\n exclude?: ClickOutsideExclude[]\n }\n\ntype NormalizedOptions = {\n enabled: boolean\n capture: boolean\n events: Array<'click' | 'mousedown' | 'touchstart' | 'pointerdown'>\n handler: ClickOutsideHandler | undefined\n exclude: ClickOutsideExclude[]\n}\n\nfunction normalize(value: ClickOutsideBindingValue | undefined): NormalizedOptions {\n if (typeof value === 'function') {\n return { enabled: true, capture: true, events: ['click'], handler: value, exclude: [] }\n }\n\n if (value && typeof value === 'object') {\n return {\n enabled: value.enabled ?? true,\n capture: value.capture ?? true,\n events: value.events?.length ? value.events : ['click'],\n handler: value.handler,\n exclude: value.exclude ?? [],\n }\n }\n\n return { enabled: false, capture: true, events: ['click'], handler: undefined, exclude: [] }\n}\n\nfunction isEventInside(el: HTMLElement, event: Event): boolean {\n const target = event.target as Node | null\n if (!target) return false\n\n const composedPath = (event as any).composedPath?.() as EventTarget[] | undefined\n if (Array.isArray(composedPath)) {\n return composedPath.includes(el)\n }\n\n return el.contains(target)\n}\n\nfunction resolveExclude(doc: Document, exclude: ClickOutsideExclude[]): HTMLElement[] {\n const resolved: HTMLElement[] = []\n\n for (const item of exclude) {\n if (typeof item === 'string') {\n const found = doc.querySelector(item)\n if (found instanceof HTMLElement) resolved.push(found)\n continue\n }\n\n if (typeof item === 'function') {\n const found = item()\n if (found instanceof HTMLElement) resolved.push(found)\n continue\n }\n\n if (item instanceof HTMLElement) {\n resolved.push(item)\n }\n }\n\n return resolved\n}\n\ntype InternalState = {\n options: NormalizedOptions\n doc: Document\n listener: (event: Event) => void\n}\n\nconst states = new WeakMap<HTMLElement, InternalState>()\n\nfunction unbind(el: HTMLElement) {\n const state = states.get(el)\n if (!state) return\n\n for (const event of state.options.events) {\n state.doc.removeEventListener(event, state.listener, state.options.capture)\n }\n\n states.delete(el)\n}\n\nfunction bind(el: HTMLElement, value: ClickOutsideBindingValue | undefined) {\n const options = normalize(value)\n const doc = el.ownerDocument\n\n const listener = (event: Event) => {\n const current = states.get(el)\n if (!current) return\n\n const { enabled, handler, exclude } = current.options\n if (!enabled || !handler) return\n\n if ('button' in (event as any) && typeof (event as any).button === 'number' && (event as any).button !== 0) {\n return\n }\n\n if (!el.isConnected) return\n\n if (isEventInside(el, event)) return\n\n const excluded = resolveExclude(current.doc, exclude)\n for (const ex of excluded) {\n if (isEventInside(ex, event)) return\n }\n\n handler(event as any)\n }\n\n const nextState: InternalState = { options, doc, listener }\n states.set(el, nextState)\n\n if (!options.enabled || !options.handler) return\n\n for (const event of options.events) {\n doc.addEventListener(event, listener, options.capture)\n }\n}\n\n/**\n * `v-click-outside` — обработчик клика вне элемента (например, закрытие дропдауна/модалки).\n */\nexport const vClickOutside: Directive<HTMLElement, ClickOutsideBindingValue> = {\n mounted(el, binding) {\n bind(el, binding.value)\n },\n updated(el, binding) {\n unbind(el)\n bind(el, binding.value)\n },\n unmounted(el) {\n unbind(el)\n },\n}\n"],"mappings":";AA4BA,SAAS,UAAU,OAAgE;CACjF,IAAI,OAAO,UAAU,YACnB,OAAO;EAAE,SAAS;EAAM,SAAS;EAAM,QAAQ,CAAC,OAAO;EAAG,SAAS;EAAO,SAAS,CAAC;CAAE;CAGxF,IAAI,SAAS,OAAO,UAAU,UAC5B,OAAO;EACL,SAAS,MAAM,WAAW;EAC1B,SAAS,MAAM,WAAW;EAC1B,QAAQ,MAAM,QAAQ,SAAS,MAAM,SAAS,CAAC,OAAO;EACtD,SAAS,MAAM;EACf,SAAS,MAAM,WAAW,CAAC;CAC7B;CAGF,OAAO;EAAE,SAAS;EAAO,SAAS;EAAM,QAAQ,CAAC,OAAO;EAAG,SAAS,KAAA;EAAW,SAAS,CAAC;CAAE;AAC7F;AAEA,SAAS,cAAc,IAAiB,OAAuB;CAC7D,MAAM,SAAS,MAAM;CACrB,IAAI,CAAC,QAAQ,OAAO;CAEpB,MAAM,eAAgB,MAAc,eAAe;CACnD,IAAI,MAAM,QAAQ,YAAY,GAC5B,OAAO,aAAa,SAAS,EAAE;CAGjC,OAAO,GAAG,SAAS,MAAM;AAC3B;AAEA,SAAS,eAAe,KAAe,SAA+C;CACpF,MAAM,WAA0B,CAAC;CAEjC,KAAK,MAAM,QAAQ,SAAS;EAC1B,IAAI,OAAO,SAAS,UAAU;GAC5B,MAAM,QAAQ,IAAI,cAAc,IAAI;GACpC,IAAI,iBAAiB,aAAa,SAAS,KAAK,KAAK;GACrD;EACF;EAEA,IAAI,OAAO,SAAS,YAAY;GAC9B,MAAM,QAAQ,KAAK;GACnB,IAAI,iBAAiB,aAAa,SAAS,KAAK,KAAK;GACrD;EACF;EAEA,IAAI,gBAAgB,aAClB,SAAS,KAAK,IAAI;CAEtB;CAEA,OAAO;AACT;AAQA,IAAM,yBAAS,IAAI,QAAoC;AAEvD,SAAS,OAAO,IAAiB;CAC/B,MAAM,QAAQ,OAAO,IAAI,EAAE;CAC3B,IAAI,CAAC,OAAO;CAEZ,KAAK,MAAM,SAAS,MAAM,QAAQ,QAChC,MAAM,IAAI,oBAAoB,OAAO,MAAM,UAAU,MAAM,QAAQ,OAAO;CAG5E,OAAO,OAAO,EAAE;AAClB;AAEA,SAAS,KAAK,IAAiB,OAA6C;CAC1E,MAAM,UAAU,UAAU,KAAK;CAC/B,MAAM,MAAM,GAAG;CAEf,MAAM,YAAY,UAAiB;EACjC,MAAM,UAAU,OAAO,IAAI,EAAE;EAC7B,IAAI,CAAC,SAAS;EAEd,MAAM,EAAE,SAAS,SAAS,YAAY,QAAQ;EAC9C,IAAI,CAAC,WAAW,CAAC,SAAS;EAE1B,IAAI,YAAa,SAAiB,OAAQ,MAAc,WAAW,YAAa,MAAc,WAAW,GACvG;EAGF,IAAI,CAAC,GAAG,aAAa;EAErB,IAAI,cAAc,IAAI,KAAK,GAAG;EAE9B,MAAM,WAAW,eAAe,QAAQ,KAAK,OAAO;EACpD,KAAK,MAAM,MAAM,UACf,IAAI,cAAc,IAAI,KAAK,GAAG;EAGhC,QAAQ,KAAY;CACtB;CAEA,MAAM,YAA2B;EAAE;EAAS;EAAK;CAAS;CAC1D,OAAO,IAAI,IAAI,SAAS;CAExB,IAAI,CAAC,QAAQ,WAAW,CAAC,QAAQ,SAAS;CAE1C,KAAK,MAAM,SAAS,QAAQ,QAC1B,IAAI,iBAAiB,OAAO,UAAU,QAAQ,OAAO;AAEzD;;;;AAKA,IAAa,gBAAkE;CAC7E,QAAQ,IAAI,SAAS;EACnB,KAAK,IAAI,QAAQ,KAAK;CACxB;CACA,QAAQ,IAAI,SAAS;EACnB,OAAO,EAAE;EACT,KAAK,IAAI,QAAQ,KAAK;CACxB;CACA,UAAU,IAAI;EACZ,OAAO,EAAE;CACX;AACF"}
1
+ {"version":3,"file":"clickOutside.js","names":[],"sources":["../../src/directives/clickOutside.ts"],"sourcesContent":["import type { Directive } from 'vue'\n\nexport type ClickOutsideHandler = (event: MouseEvent | TouchEvent | PointerEvent) => void\n\nexport type ClickOutsideExclude = HTMLElement | string | (() => HTMLElement | null | undefined)\n\nexport type ClickOutsideBindingValue =\n | ClickOutsideHandler\n | {\n handler: ClickOutsideHandler\n /** По умолчанию `true`. */\n enabled?: boolean\n /** По умолчанию `true` (слушаем в capture, чтобы stopPropagation внутри не ломал закрытие). */\n capture?: boolean\n /** Список событий. По умолчанию `['click']`. */\n events?: Array<'click' | 'mousedown' | 'touchstart' | 'pointerdown'>\n /** Элементы/селекторы, клики по которым считаются \"внутри\". */\n exclude?: ClickOutsideExclude[]\n }\n\ntype NormalizedOptions = {\n enabled: boolean\n capture: boolean\n events: Array<'click' | 'mousedown' | 'touchstart' | 'pointerdown'>\n handler: ClickOutsideHandler | undefined\n exclude: ClickOutsideExclude[]\n}\n\nfunction normalize(value: ClickOutsideBindingValue | undefined): NormalizedOptions {\n if (typeof value === 'function') {\n return { enabled: true, capture: true, events: ['click'], handler: value, exclude: [] }\n }\n\n if (value && typeof value === 'object') {\n return {\n enabled: value.enabled ?? true,\n capture: value.capture ?? true,\n events: value.events?.length ? value.events : ['click'],\n handler: value.handler,\n exclude: value.exclude ?? [],\n }\n }\n\n return { enabled: false, capture: true, events: ['click'], handler: undefined, exclude: [] }\n}\n\nfunction isEventInside(el: HTMLElement, event: Event): boolean {\n const target = event.target as Node | null\n if (!target) return false\n\n const composedPath = (event as any).composedPath?.() as EventTarget[] | undefined\n if (Array.isArray(composedPath)) {\n return composedPath.includes(el)\n }\n\n return el.contains(target)\n}\n\nfunction resolveExclude(doc: Document, exclude: ClickOutsideExclude[]): HTMLElement[] {\n const resolved: HTMLElement[] = []\n\n for (const item of exclude) {\n if (typeof item === 'string') {\n const found = doc.querySelector(item)\n if (found instanceof HTMLElement) resolved.push(found)\n continue\n }\n\n if (typeof item === 'function') {\n const found = item()\n if (found instanceof HTMLElement) resolved.push(found)\n continue\n }\n\n if (item instanceof HTMLElement) {\n resolved.push(item)\n }\n }\n\n return resolved\n}\n\ntype ElementState = {\n options: NormalizedOptions\n doc: Document\n /** Ключи подписок, которые этот элемент держит на своём документе. */\n keys: string[]\n}\n\nconst states = new WeakMap<HTMLElement, ElementState>()\n\n/**\n * Слушатель — один на документ и вид события, а не на элемент.\n *\n * Каждый элемент со своим слушателем — это десяток подписок на документе у\n * страницы с десятком оверлеев, и полное переподключение на каждом обновлении\n * хозяина: хук `updated` срабатывает на любой ререндер, а набор событий при\n * этом обычно тот же. Здесь подписка живёт по счётчику потребителей, а элементы\n * обходятся общим обработчиком.\n */\nconst documentElements = new WeakMap<Document, Set<HTMLElement>>()\nconst documentSubscriptions = new WeakMap<Document, Map<string, { listener: (event: Event) => void, count: number }>>()\n\nfunction subscriptionKey(type: string, capture: boolean): string {\n return `${type}|${capture ? 'capture' : 'bubble'}`\n}\n\nfunction keysOf(options: NormalizedOptions): string[] {\n if (!options.enabled || !options.handler) return []\n\n return options.events.map(type => subscriptionKey(type, options.capture))\n}\n\nfunction dispatch(doc: Document, event: Event, capture: boolean): void {\n const elements = documentElements.get(doc)\n if (!elements) return\n\n // Копия: обработчик закрывает оверлей, а тот на размонтировании правит набор.\n for (const el of [...elements]) {\n const state = states.get(el)\n if (!state) continue\n\n const { enabled, handler, exclude, events } = state.options\n if (!enabled || !handler) continue\n if (state.options.capture !== capture) continue\n if (!events.includes(event.type as NormalizedOptions['events'][number])) continue\n\n if ('button' in (event as any) && typeof (event as any).button === 'number' && (event as any).button !== 0) {\n continue\n }\n\n if (!el.isConnected) continue\n if (isEventInside(el, event)) continue\n\n const excluded = resolveExclude(state.doc, exclude)\n if (excluded.some(item => isEventInside(item, event))) continue\n\n handler(event as any)\n }\n}\n\nfunction subscribe(doc: Document, key: string): void {\n let subscriptions = documentSubscriptions.get(doc)\n if (!subscriptions) {\n subscriptions = new Map()\n documentSubscriptions.set(doc, subscriptions)\n }\n\n const existing = subscriptions.get(key)\n if (existing) {\n existing.count += 1\n return\n }\n\n const [type, mode] = key.split('|')\n const capture = mode === 'capture'\n const listener = (event: Event) => dispatch(doc, event, capture)\n\n subscriptions.set(key, { listener, count: 1 })\n doc.addEventListener(type, listener, capture)\n}\n\nfunction unsubscribe(doc: Document, key: string): void {\n const subscriptions = documentSubscriptions.get(doc)\n const existing = subscriptions?.get(key)\n if (!subscriptions || !existing) return\n\n existing.count -= 1\n if (existing.count > 0) return\n\n const [type, mode] = key.split('|')\n doc.removeEventListener(type, existing.listener, mode === 'capture')\n subscriptions.delete(key)\n}\n\nfunction unbind(el: HTMLElement) {\n const state = states.get(el)\n if (!state) return\n\n for (const key of state.keys) unsubscribe(state.doc, key)\n\n documentElements.get(state.doc)?.delete(el)\n states.delete(el)\n}\n\nfunction bind(el: HTMLElement, value: ClickOutsideBindingValue | undefined) {\n const doc = el.ownerDocument\n const previous = states.get(el)\n\n // Элемент, переехавший в другой документ (портал в отдельное окно), обязан\n // отпустить подписки прежнего: там его больше нет.\n if (previous && previous.doc !== doc) unbind(el)\n\n const options = normalize(value)\n const nextKeys = keysOf(options)\n const prevKeys = states.get(el)?.keys ?? []\n\n states.set(el, { options, doc, keys: nextKeys })\n\n let elements = documentElements.get(doc)\n if (!elements) {\n elements = new Set()\n documentElements.set(doc, elements)\n }\n elements.add(el)\n\n for (const key of nextKeys) if (!prevKeys.includes(key)) subscribe(doc, key)\n for (const key of prevKeys) if (!nextKeys.includes(key)) unsubscribe(doc, key)\n}\n\n/**\n * `v-click-outside` — обработчик клика вне элемента (например, закрытие дропдауна/модалки).\n */\nexport const vClickOutside: Directive<HTMLElement, ClickOutsideBindingValue> = {\n mounted(el, binding) {\n bind(el, binding.value)\n },\n updated(el, binding) {\n // Не `unbind` + `bind`: разницу подписок считает сам `bind`, а снятие с\n // последующей постановкой того же слушателя — лишняя работа на каждый\n // ререндер хозяина.\n bind(el, binding.value)\n },\n unmounted(el) {\n unbind(el)\n },\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { a as granularityDefaultThemes, i as granularityComponentConfigs, n as GRANULARITY_PROVIDER_ID, o as granularityThemeNames, r as createGranularityProvider, t as granularityProvider } from "./chunks/granular-provider-DvvRayEh.js";
1
+ import { a as granularityDefaultThemes, i as granularityComponentConfigs, n as GRANULARITY_PROVIDER_ID, o as granularityThemeNames, r as createGranularityProvider, t as granularityProvider } from "./chunks/granular-provider-Dds_519x.js";
2
2
  export { GRANULARITY_PROVIDER_ID, createGranularityProvider, granularityProvider as default, granularityProvider, granularityComponentConfigs, granularityDefaultThemes, granularityThemeNames };
@@ -1,2 +1,2 @@
1
- import { a as granularityDefaultThemes, i as granularityComponentConfigs, n as GRANULARITY_PROVIDER_ID, o as granularityThemeNames, r as createGranularityProvider, t as granularityProvider } from "./chunks/granular-provider-DvvRayEh.js";
1
+ import { a as granularityDefaultThemes, i as granularityComponentConfigs, n as GRANULARITY_PROVIDER_ID, o as granularityThemeNames, r as createGranularityProvider, t as granularityProvider } from "./chunks/granular-provider-Dds_519x.js";
2
2
  export { GRANULARITY_PROVIDER_ID, createGranularityProvider, granularityProvider as default, granularityProvider, granularityComponentConfigs, granularityDefaultThemes, granularityThemeNames };
package/dist/index.js CHANGED
@@ -35,8 +35,8 @@ import { t as GrBadgeWrap_default } from "./components/GrBadgeWrap/chunks/GrBadg
35
35
  import { n as grBottomNavSafelist, t as grBottomNavConfig } from "./chunks/config-DM0lsduD.js";
36
36
  import { t as GrBottomNav_default } from "./components/GrBottomNav/chunks/GrBottomNav-wtFatLjB.js";
37
37
  import { t as GrLink_default } from "./components/GrLink/chunks/GrLink-DAGCFYO7.js";
38
- import { m as resolveBreadcrumbsLayout, n as grBreadcrumbsSafelist, p as resolveBreadcrumbsFit, t as grBreadcrumbsConfig } from "./chunks/config-DcfWZOi5.js";
39
- import { t as GrBreadcrumbs_default } from "./components/GrBreadcrumbs/chunks/GrBreadcrumbs-jtSnMEtg.js";
38
+ import { g as resolveBreadcrumbsLayout, h as resolveBreadcrumbsFit, n as grBreadcrumbsSafelist, t as grBreadcrumbsConfig } from "./chunks/config-C0Eu8jpG.js";
39
+ import { t as GrBreadcrumbs_default } from "./components/GrBreadcrumbs/chunks/GrBreadcrumbs-BKOvl8W1.js";
40
40
  import { n as useGrButtonGroup } from "./chunks/context-BfIQv7m1.js";
41
41
  import { t as GrButton_default } from "./components/GrButton/chunks/GrButton-Bw2AH4uX.js";
42
42
  import { t as grButtonSafelist } from "./chunks/safelist-BOq5Li7p.js";
@@ -79,14 +79,14 @@ import { a as extendDefaultParsers, n as coreResponseErrorParsers, r as createRe
79
79
  import { t as GrConfirmDialog_default } from "./components/GrConfirmDialog/chunks/GrConfirmDialog-B9mIU78E.js";
80
80
  import { t as grConfirmDialogConfig } from "./chunks/config-A-bBiotF.js";
81
81
  import "./components/GrConfirmDialog/index.js";
82
- import { a as GrDropdownMenuItem_default, c as GrDropdownMenuDivider_default, o as GrDropdownMenuGroup_default, s as GrDropdownMenuHeader_default, t as GrDropdownMenuList_default } from "./components/GrDropdownMenu/chunks/GrDropdownMenuList-CT_1TQJo.js";
83
- import { t as GrContextMenu_default } from "./components/GrContextMenu/chunks/GrContextMenu-DDAUvrDK.js";
82
+ import { a as GrDropdownMenuItem_default, c as GrDropdownMenuDivider_default, o as GrDropdownMenuGroup_default, s as GrDropdownMenuHeader_default, t as GrDropdownMenuList_default } from "./components/GrDropdownMenu/chunks/GrDropdownMenuList-RW5IBvmu.js";
83
+ import { t as GrContextMenu_default } from "./components/GrContextMenu/chunks/GrContextMenu-BNQDbAsY.js";
84
84
  import { t as grContextMenuConfig } from "./chunks/config-CPSxM7XE.js";
85
85
  import { t as GrTable_default } from "./components/GrTable/chunks/GrTable-Zcdz2St_.js";
86
86
  import { t as useDragSort } from "./chunks/useDragSort-0l47h4yb.js";
87
87
  import { t as GrDataTable_default } from "./components/GrDataTable/chunks/GrDataTable-DU00V44y.js";
88
88
  import { n as grDataTableSafelist, t as grDataTableConfig } from "./chunks/config-DeyT9dJl.js";
89
- import { t as GrDelta_default } from "./components/GrDelta/chunks/GrDelta-04j9WmEY.js";
89
+ import { t as GrDelta_default } from "./components/GrDelta/chunks/GrDelta-BZwkgQW-.js";
90
90
  import { n as grValueSafelist, t as grValueConfig } from "./chunks/config-Z5LeUXys.js";
91
91
  import { t as GrValue_default } from "./components/GrValue/chunks/GrValue-ezkQM05Q.js";
92
92
  import { n as deltaTone, t as deltaDirection } from "./chunks/deltaTone-B5OoS1vo.js";
@@ -109,8 +109,8 @@ import { t as GrDrawer_default } from "./components/GrDrawer/chunks/GrDrawer-BCr
109
109
  import { t as GrDropdown_default } from "./components/GrDropdown/chunks/GrDropdown-13Wt9eB_.js";
110
110
  import { n as grDropdownSafelist, t as grDropdownConfig } from "./chunks/config-uzUuosv7.js";
111
111
  import "./components/GrDropdown/index.js";
112
- import { n as GrDropdownMenuColumn_default, r as GrDropdownMenu_default, t as GrDropdownMenuColumns_default } from "./components/GrDropdownMenu/chunks/GrDropdownMenu-5zoOL8Oj.js";
113
- import { n as grDropdownMenuSafelist, t as grDropdownMenuConfig } from "./chunks/config-CWthWcWY.js";
112
+ import { n as GrDropdownMenuColumn_default, r as GrDropdownMenu_default, t as GrDropdownMenuColumns_default } from "./components/GrDropdownMenu/chunks/GrDropdownMenu-Cg7qHV9J.js";
113
+ import { n as grDropdownMenuSafelist, t as grDropdownMenuConfig } from "./chunks/config-_jbgsFpw.js";
114
114
  import { t as GrEmptyState_default } from "./components/GrEmptyState/chunks/GrEmptyState-D4T5pzXA.js";
115
115
  import { n as grEmptyStateSafelist, t as grEmptyStateConfig } from "./chunks/config-CWtfxkM9.js";
116
116
  import { n as fileKindOf, r as isPreviewableKind, t as GrFilePreview_default } from "./components/GrFilePreview/chunks/GrFilePreview-C9Z7hMK8.js";
@@ -28,6 +28,14 @@ export interface GrBreadcrumbsProps {
28
28
  ariaLabel?: string;
29
29
  /** Последний пункт остаётся ссылкой (текущая страница кликабельна). */
30
30
  linkCurrent?: boolean;
31
+ /**
32
+ * Какой пункт — текущая страница. Не задан — последний.
33
+ *
34
+ * `-1` означает, что текущей страницы в пути нет: её имя стоит в `h1`, а
35
+ * путь показывает только родителей. Тогда `aria-current` не получает никто,
36
+ * и последний пункт остаётся обычной ссылкой.
37
+ */
38
+ currentIndex?: number;
31
39
  /** i18n-метка кнопки раскрытия схлопнутого пути. */
32
40
  expandLabel?: string;
33
41
  /**
@@ -64,6 +72,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
64
72
  itemsAfterCollapse: number;
65
73
  ariaLabel: undefined;
66
74
  linkCurrent: boolean;
75
+ currentIndex: undefined;
67
76
  expandLabel: undefined;
68
77
  autoCollapse: boolean;
69
78
  }>>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<__VLS_WithDefaults<__VLS_TypePropsToOption<GrBreadcrumbsProps>, {
@@ -75,6 +84,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
75
84
  itemsAfterCollapse: number;
76
85
  ariaLabel: undefined;
77
86
  linkCurrent: boolean;
87
+ currentIndex: undefined;
78
88
  expandLabel: undefined;
79
89
  autoCollapse: boolean;
80
90
  }>>> & Readonly<{}>, {
@@ -86,6 +96,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
86
96
  maxItems: number;
87
97
  itemsAfterCollapse: number;
88
98
  linkCurrent: boolean;
99
+ currentIndex: number;
89
100
  expandLabel: string;
90
101
  autoCollapse: boolean;
91
102
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -12,6 +12,16 @@ export type GrBreadcrumbItem = {
12
12
  * UnoCSS-сборки (`i-lucide-*` требует вашего `presetIcons`).
13
13
  */
14
14
  icon?: string | Component;
15
+ /**
16
+ * Показывать только иконку: подпись прячется визуально, но остаётся в
17
+ * разметке и в дереве доступности. Классический «домик» в начале пути.
18
+ *
19
+ * Подпись не выбрасывается и не подменяется на `aria-label` намеренно: путь
20
+ * читают и поиском, и диктором, и пункт без имени для незрячего пустой.
21
+ * Без `icon` поле игнорируется — прятать подпись, ничего не показав взамен,
22
+ * значит стереть пункт.
23
+ */
24
+ iconOnly?: boolean;
15
25
  disabled?: boolean;
16
26
  /** Доступное имя пункта, когда подписи недостаточно («Настройки» → «Настройки проекта»). */
17
27
  ariaLabel?: string;
@@ -35,8 +45,19 @@ export declare const breadcrumbsSizeClassBySize: Record<GrBreadcrumbsSize, strin
35
45
  export declare const breadcrumbsSeparatorClass = "select-none text-[var(--gr-muted-fg)]";
36
46
  /** Текущая страница: не ссылка, но и не «выключенный» текст — обычный акцент подписи. */
37
47
  export declare const breadcrumbsCurrentClass = "inline-flex items-center gap-1 min-w-0 font-600 text-[var(--gr-fg)]";
48
+ /**
49
+ * Обёртка пункта — flex, а не блок с линейным боксом.
50
+ *
51
+ * Базовая линия `inline-flex` берётся от первого flex-элемента: у пункта
52
+ * с иконкой это блочная иконка, и линейный бокс раздувается на высоту
53
+ * подстрочных выносных. Пункт с иконкой выходил на 3px выше соседей, а при
54
+ * `align-items: center` списка это смещало его содержимое на полтора пикселя.
55
+ */
56
+ export declare const breadcrumbsItemWrapClass = "flex items-center min-w-0";
38
57
  export declare const breadcrumbsItemIconClass = "inline-block h-4 w-4 shrink-0";
39
58
  export declare const breadcrumbsLabelClass = "truncate";
59
+ /** Подпись пункта-иконки: не видна, но читается диктором и поиском. */
60
+ export declare const breadcrumbsLabelHiddenClass = "sr-only";
40
61
  /**
41
62
  * Кнопка «…»: раскрывает схлопнутую середину пути на месте. Оформлена как
42
63
  * приглушённая ссылка, чтобы не выбиваться из ряда.
@@ -24,8 +24,24 @@ export declare const itemShortcutClass = "ml-auto pl-4 text-[length:var(--gr-tex
24
24
  export declare const headerClass = "px-4 py-2 text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)] tracking-wide text-[var(--gr-muted-fg)]";
25
25
  export declare const listBaseClass = "w-full";
26
26
  export declare const dividersClass = "divide-y divide-[var(--gr-brd)]";
27
- export declare const borderTopClass = "border-t border-[var(--gr-brd)]";
28
- export declare const borderBottomClass = "border-b border-[var(--gr-brd)]";
27
+ /**
28
+ * Линия у края панели рисуется псевдоэлементом с инсетом, а не рамкой бокса.
29
+ *
30
+ * Список лежит в поле панели — 1 px рамки плюс 4 px `p-1`, — а угол скруглён на
31
+ * `--gr-radius-xl` (16 px). На глубине 5 px дуга ещё идёт: штрих рамки занимает
32
+ * там `x ∈ [4.4, 5.8]`, и правило во всю ширину, начинаясь на `x = 5`, упирается
33
+ * не в вертикальный край, а в дугу. Вместо двух самостоятельных линий у каждого
34
+ * угла виден клин, в который они сходятся.
35
+ *
36
+ * Инсет тот же, что у `GrDropdownMenuDivider :inset` (8 px), и уводит концы из
37
+ * полосы радиуса. Пункты при этом остаются во всю ширину: их ширина — часть
38
+ * попадания подсветки в поле панели, и сжимать список целиком нельзя.
39
+ *
40
+ * Та же арифметика уже учтена у `itemBaseClass`; здесь она доведена до линий.
41
+ */
42
+ export declare const borderEdgeClass = "relative";
43
+ export declare const borderTopClass = "before:absolute before:inset-x-2 before:top-0 before:h-px before:bg-[var(--gr-brd)] before:content-empty";
44
+ export declare const borderBottomClass = "after:absolute after:inset-x-2 after:bottom-0 after:h-px after:bg-[var(--gr-brd)] after:content-empty";
29
45
  export declare const columnsBaseClass = "w-full grid divide-x divide-[var(--gr-brd)]";
30
46
  export declare const columnBaseClass = "px-3 py-2 flex items-center";
31
47
  export declare const dividerClass = "border-t border-[var(--gr-brd)]";
@@ -7,8 +7,8 @@
7
7
  * приложением с i18n и без него не возникает.
8
8
  *
9
9
  * Инстансы `Intl` кэшируются: их построение примерно на порядок дороже самого
10
- * форматирования. Кэш ограничен и сбрасывается целиком иначе сгенерированные
11
- * на лету опции растили бы его без предела.
10
+ * форматирования. Кэш ограничен иначе сгенерированные на лету опции растили
11
+ * бы его без предела, и вытесняет по давности чтения.
12
12
  */
13
13
  /**
14
14
  * Локаль приходит из настроек приложения, а `Intl` бросает на некорректном
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
3
3
  "name": "@feugene/granularity",
4
- "version": "0.27.2",
4
+ "version": "0.28.1",
5
5
  "framework": "vue",
6
6
  "js-types-syntax": "typescript",
7
7
  "description-markup": "markdown",
@@ -863,6 +863,15 @@
863
863
  "type": "boolean | undefined"
864
864
  }
865
865
  },
866
+ {
867
+ "name": "currentIndex",
868
+ "description": "Какой пункт — текущая страница. Не задан — последний. `-1` означает, что текущей страницы в пути нет: её имя стоит в `h1`, а путь показывает только родителей. Тогда `aria-current` не получает никто, и последний пункт остаётся обычной ссылкой.",
869
+ "default": "undefined",
870
+ "value": {
871
+ "kind": "expression",
872
+ "type": "number | undefined"
873
+ }
874
+ },
866
875
  {
867
876
  "name": "expandLabel",
868
877
  "description": "i18n-метка кнопки раскрытия схлопнутого пути.",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@feugene/granularity",
3
3
  "description": "Granularity design system package with Vue 3 components, consumed via the `@feugene/unocss-preset-granular` preset.",
4
- "version": "0.27.2",
4
+ "version": "0.28.1",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": {
7
7
  "name": "Evgeniy Fureev",
@@ -553,6 +553,7 @@
553
553
  "generate:tokens": "node --experimental-strip-types scripts/generate-tokens.mjs",
554
554
  "lint": "eslint . --cache",
555
555
  "lint:fix": "eslint . --cache --fix",
556
+ "sizes": "node scripts/report-entry-sizes.mjs",
556
557
  "test": "vitest --config vitest.config.ts",
557
558
  "test:coverage": "vitest run --config vitest.config.ts --coverage",
558
559
  "test:run": "vitest run --config vitest.config.ts",
@@ -1 +0,0 @@
1
- {"version":3,"file":"config-CWthWcWY.js","names":[],"sources":["../../src/components/GrDropdownMenu/safelist.ts","../../src/components/GrDropdownMenu/config.ts"],"sourcesContent":["import { splitClassTokens } from '../shared/classTokens'\n\nimport {\n alignClass,\n borderBottomClass,\n borderTopClass,\n colsClass,\n columnBaseClass,\n columnsBaseClass,\n dividerClass,\n dividerInsetClass,\n dividersClass,\n headerClass,\n itemBaseClass,\n itemDisabledClass,\n itemIndicatorClass,\n itemInteractiveClass,\n itemShortcutClass,\n itemVariantClass,\n listBaseClass,\n textAlignClass,\n} from './grDropdownMenuStyles'\n\n/**\n * Классы каталога живут в `.ts`-хелпере, а его бандлер волен вынести в общий\n * `dist/chunks/` — вне области скана компонента. Симптом промаха тут особенно\n * незаметный: размеры пунктов на месте, а выравнивание, колонки и цвета\n * пропали. Гейт — `src/__tests__/safelist.test.ts`.\n */\nexport const grDropdownMenuSafelist = [...new Set([\n ...Object.values(alignClass).flatMap(splitClassTokens),\n ...Object.values(textAlignClass).flatMap(splitClassTokens),\n ...Object.values(colsClass).flatMap(splitClassTokens),\n ...Object.values(itemVariantClass).flatMap(splitClassTokens),\n ...splitClassTokens(itemBaseClass),\n ...splitClassTokens(itemInteractiveClass),\n ...splitClassTokens(itemDisabledClass),\n ...splitClassTokens(itemIndicatorClass),\n ...splitClassTokens(itemShortcutClass),\n ...splitClassTokens(headerClass),\n ...splitClassTokens(listBaseClass),\n ...splitClassTokens(dividersClass),\n ...splitClassTokens(borderTopClass),\n ...splitClassTokens(borderBottomClass),\n ...splitClassTokens(columnsBaseClass),\n ...splitClassTokens(columnBaseClass),\n ...splitClassTokens(dividerClass),\n ...splitClassTokens(dividerInsetClass),\n])]\n","import { defineGranularComponent } from '@feugene/unocss-preset-granular/contract'\n\nimport { grDropdownMenuSafelist } from './safelist'\n\nexport const grDropdownMenuConfig = defineGranularComponent(import.meta.url, {\n name: 'GrDropdownMenu',\n dependencies: ['GrDropdown'],\n safelist: grDropdownMenuSafelist,\n})\n"],"mappings":";;;;;;;;;;AA6BA,IAAa,yBAAyB,CAAC,mBAAG,IAAI,IAAI;CAC9C,GAAG,OAAO,OAAO,UAAU,CAAC,CAAC,QAAQ,gBAAgB;CACrD,GAAG,OAAO,OAAO,cAAc,CAAC,CAAC,QAAQ,gBAAgB;CACzD,GAAG,OAAO,OAAO,SAAS,CAAC,CAAC,QAAQ,gBAAgB;CACpD,GAAG,OAAO,OAAO,gBAAgB,CAAC,CAAC,QAAQ,gBAAgB;CAC3D,GAAG,iBAAiB,aAAa;CACjC,GAAG,iBAAiB,oBAAoB;CACxC,GAAG,iBAAiB,iBAAiB;CACrC,GAAG,iBAAiB,kBAAkB;CACtC,GAAG,iBAAiB,iBAAiB;CACrC,GAAG,iBAAiB,WAAW;CAC/B,GAAG,iBAAiB,aAAa;CACjC,GAAG,iBAAiB,aAAa;CACjC,GAAG,iBAAiB,cAAc;CAClC,GAAG,iBAAiB,iBAAiB;CACrC,GAAG,iBAAiB,gBAAgB;CACpC,GAAG,iBAAiB,eAAe;CACnC,GAAG,iBAAiB,YAAY;CAChC,GAAG,iBAAiB,iBAAiB;AACzC,CAAC,CAAC;;;AC5CF,IAAa,uBAAuB,wBAAwB,OAAO,KAAK,KAAK;CAC3E,MAAM;CACN,cAAc,CAAC,YAAY;CAC3B,UAAU;AACZ,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"config-DcfWZOi5.js","names":[],"sources":["../../src/components/GrBreadcrumbs/grBreadcrumbsStyles.ts","../../src/components/GrBreadcrumbs/safelist.ts","../../src/components/GrBreadcrumbs/config.ts"],"sourcesContent":["import type { Component } from 'vue'\n\nimport type { GrComponentSize } from '../shared/sizes'\n\nexport type GrBreadcrumbsSize = GrComponentSize\n\n/** Пункт пути. Кроме `label` всё опционально: последний пункт обычно без ссылки. */\nexport type GrBreadcrumbItem = {\n label: string\n href?: string\n /** Цель роутерной ссылки: уезжает в `GrLink` через `attrs` вместе с `as`. */\n to?: unknown\n /**\n * Иконка перед подписью — декоративна. Vue-компонент либо класс иконки вашей\n * UnoCSS-сборки (`i-lucide-*` требует вашего `presetIcons`).\n */\n icon?: string | Component\n disabled?: boolean\n /** Доступное имя пункта, когда подписи недостаточно («Настройки» → «Настройки проекта»). */\n ariaLabel?: string\n}\n\nexport const breadcrumbsRootClass = 'w-full min-w-0'\nexport const breadcrumbsListClass = 'flex items-center gap-1 m-0 p-0 [list-style:none]'\n\n/**\n * Перенос — умолчание: длинный путь уезжает на вторую строку и остаётся читаемым.\n * В режиме `autoCollapse` строка одна, иначе переполнения не случится никогда и\n * мерить будет нечего: список просто перенесётся.\n *\n * Исключение — раскрытый вручную путь: в одну строку он бы не влез (её нехватка и\n * вызвала схлопывание), `overflow: hidden` срезал бы хвост вместе с текущей\n * страницей, а фокус на раскрытом пункте молча уводил бы скролл контейнера.\n * Поэтому раскрытие возвращает перенос.\n */\nexport const breadcrumbsListWrapClass = 'flex-wrap'\nexport const breadcrumbsListNowrapClass = 'flex-nowrap overflow-hidden'\n\n/** Кегль строки пути. Шкала общая с `GrLink`: путь — это ряд ссылок. */\nexport const breadcrumbsSizeClassBySize: Record<GrBreadcrumbsSize, string> = {\n xs: 'text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)]',\n sm: 'text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)]',\n md: 'text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)]',\n lg: 'text-[length:var(--gr-text-base)] leading-[var(--gr-leading-base)]',\n}\n\nexport const breadcrumbsSeparatorClass = 'select-none text-[var(--gr-muted-fg)]'\n\n/** Текущая страница: не ссылка, но и не «выключенный» текст — обычный акцент подписи. */\nexport const breadcrumbsCurrentClass = 'inline-flex items-center gap-1 min-w-0 font-600 text-[var(--gr-fg)]'\n\nexport const breadcrumbsItemIconClass = 'inline-block h-4 w-4 shrink-0'\n\nexport const breadcrumbsLabelClass = 'truncate'\n\n/**\n * Кнопка «…»: раскрывает схлопнутую середину пути на месте. Оформлена как\n * приглушённая ссылка, чтобы не выбиваться из ряда.\n */\nexport const breadcrumbsEllipsisClass = 'inline-flex items-center rounded-[var(--gr-radius-sm)] px-1 text-[var(--gr-muted-fg)] hover:text-[var(--gr-fg)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]'\n\nexport type GrBreadcrumbsLinkComponent = string | Component\n\n/**\n * Раскладка пути: какие пункты видны и где стоит кнопка раскрытия.\n *\n * Чистая функция, потому что вся её сложность — арифметика границ, и проверять\n * её монтированием компонента дороже, чем таблицей входов.\n */\nexport type GrBreadcrumbsLayoutEntry =\n | { kind: 'item', item: GrBreadcrumbItem, index: number }\n | { kind: 'ellipsis', hiddenCount: number }\n\n/**\n * Сколько пунктов хвоста влезает в доступную ширину.\n *\n * Порог по числу пунктов (`maxItems`) на узком экране предсказывает плохо: три\n * коротких пункта влезают, два длинных — нет. Здесь решение принимается по\n * измеренным ширинам.\n *\n * Голова не ужимается: `itemsBeforeCollapse` — обычно один корневой пункт, и он\n * дёшев. Хвост ужимается до упора, но **не меньше одного**: последний пункт\n * отвечает на вопрос «где я сейчас», и прятать его бессмысленно — путь\n * превратился бы в «Главная / …».\n *\n * Возврат — число пунктов хвоста; `items.length` означает «влезло всё».\n */\nexport function resolveBreadcrumbsFit(options: {\n itemWidths: number[]\n separatorWidth: number\n ellipsisWidth: number\n available: number\n itemsBeforeCollapse: number\n /** Зазор списка (`gap`). Стоит между каждой парой соседей, включая разделители. */\n gapWidth?: number\n}): number {\n const { itemWidths, separatorWidth, ellipsisWidth, available, itemsBeforeCollapse, gapWidth = 0 } = options\n const total = itemWidths.length\n\n if (total === 0) return 0\n\n const sum = (from: number, to: number): number =>\n itemWidths.slice(from, to).reduce((acc, width) => acc + width, 0)\n\n /**\n * Ширина строки из `parts` видимых блоков: сами блоки, разделители между ними\n * и зазоры. Элементов в списке `2 * parts - 1` (блоки и разделители вперемешку),\n * значит зазоров на один меньше — без них расчёт систематически оптимистичен и\n * путь подрезается уже после схлопывания.\n */\n const rowWidth = (content: number, parts: number): number =>\n content\n + Math.max(0, parts - 1) * separatorWidth\n + Math.max(0, 2 * parts - 2) * gapWidth\n\n if (rowWidth(sum(0, total), total) <= available) return total\n\n const before = Math.max(0, Math.min(itemsBeforeCollapse, total))\n\n for (let after = total - before - 1; after > 1; after--) {\n const content = sum(0, before) + ellipsisWidth + sum(total - after, total)\n\n if (rowWidth(content, before + 1 + after) <= available) return after\n }\n\n return 1\n}\n\nexport function resolveBreadcrumbsLayout(options: {\n items: GrBreadcrumbItem[]\n maxItems?: number\n itemsBeforeCollapse: number\n itemsAfterCollapse: number\n expanded: boolean\n}): GrBreadcrumbsLayoutEntry[] {\n const { items, maxItems, itemsBeforeCollapse, itemsAfterCollapse, expanded } = options\n const all: GrBreadcrumbsLayoutEntry[] = items.map((item, index) => ({ kind: 'item', item, index }))\n\n if (expanded || !maxItems || maxItems < 1 || items.length <= maxItems)\n return all\n\n // Хвост важнее головы: «где я сейчас» читается справа, поэтому при нехватке\n // места сначала жертвуем серединой, а не концом.\n const before = Math.max(0, Math.min(itemsBeforeCollapse, items.length))\n const after = Math.max(0, Math.min(itemsAfterCollapse, items.length - before))\n const hiddenCount = items.length - before - after\n\n if (hiddenCount <= 1)\n return all\n\n return [\n ...all.slice(0, before),\n { kind: 'ellipsis', hiddenCount },\n ...all.slice(items.length - after),\n ]\n}\n","import { splitClassTokens } from '../shared/classTokens'\nimport {\n breadcrumbsCurrentClass,\n breadcrumbsEllipsisClass,\n breadcrumbsItemIconClass,\n breadcrumbsLabelClass,\n breadcrumbsListClass,\n breadcrumbsListNowrapClass,\n breadcrumbsListWrapClass,\n breadcrumbsRootClass,\n breadcrumbsSeparatorClass,\n breadcrumbsSizeClassBySize,\n} from './grBreadcrumbsStyles'\n\n// Всё, что живёт в `grBreadcrumbsStyles.ts`: и мапа размеров, и строковые\n// литералы. Хелпер уезжает в общий `dist/chunks/`, вне области скана\n// компонента — гейт `src/__tests__/safelist.test.ts`.\nexport const grBreadcrumbsSafelist = [...new Set([\n ...Object.values(breadcrumbsSizeClassBySize).flatMap(splitClassTokens),\n ...splitClassTokens(breadcrumbsRootClass),\n ...splitClassTokens(breadcrumbsListClass),\n ...splitClassTokens(breadcrumbsListWrapClass),\n ...splitClassTokens(breadcrumbsListNowrapClass),\n ...splitClassTokens(breadcrumbsSeparatorClass),\n ...splitClassTokens(breadcrumbsCurrentClass),\n ...splitClassTokens(breadcrumbsItemIconClass),\n ...splitClassTokens(breadcrumbsLabelClass),\n ...splitClassTokens(breadcrumbsEllipsisClass),\n])]\n","import { defineGranularComponent } from '@feugene/unocss-preset-granular/contract'\n\nimport { grBreadcrumbsSafelist } from './safelist'\n\nexport const grBreadcrumbsConfig = defineGranularComponent(import.meta.url, {\n name: 'GrBreadcrumbs',\n safelist: grBreadcrumbsSafelist,\n dependencies: ['GrLink'],\n})\n"],"mappings":";;;AAsBA,IAAa,uBAAuB;AACpC,IAAa,uBAAuB;;;;;;;;;;;AAYpC,IAAa,2BAA2B;AACxC,IAAa,6BAA6B;;AAG1C,IAAa,6BAAgE;CAC3E,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;AACN;AAEA,IAAa,4BAA4B;;AAGzC,IAAa,0BAA0B;AAEvC,IAAa,2BAA2B;AAExC,IAAa,wBAAwB;;;;;AAMrC,IAAa,2BAA2B;;;;;;;;;;;;;;;AA4BxC,SAAgB,sBAAsB,SAQ3B;CACT,MAAM,EAAE,YAAY,gBAAgB,eAAe,WAAW,qBAAqB,WAAW,MAAM;CACpG,MAAM,QAAQ,WAAW;CAEzB,IAAI,UAAU,GAAG,OAAO;CAExB,MAAM,OAAO,MAAc,OACzB,WAAW,MAAM,MAAM,EAAE,CAAC,CAAC,QAAQ,KAAK,UAAU,MAAM,OAAO,CAAC;;;;;;;CAQlE,MAAM,YAAY,SAAiB,UACjC,UACE,KAAK,IAAI,GAAG,QAAQ,CAAC,IAAI,iBACzB,KAAK,IAAI,GAAG,IAAI,QAAQ,CAAC,IAAI;CAEjC,IAAI,SAAS,IAAI,GAAG,KAAK,GAAG,KAAK,KAAK,WAAW,OAAO;CAExD,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,IAAI,qBAAqB,KAAK,CAAC;CAE/D,KAAK,IAAI,QAAQ,QAAQ,SAAS,GAAG,QAAQ,GAAG,SAG9C,IAAI,SAFY,IAAI,GAAG,MAAM,IAAI,gBAAgB,IAAI,QAAQ,OAAO,KAAK,GAEnD,SAAS,IAAI,KAAK,KAAK,WAAW,OAAO;CAGjE,OAAO;AACT;AAEA,SAAgB,yBAAyB,SAMV;CAC7B,MAAM,EAAE,OAAO,UAAU,qBAAqB,oBAAoB,aAAa;CAC/E,MAAM,MAAkC,MAAM,KAAK,MAAM,WAAW;EAAE,MAAM;EAAQ;EAAM;CAAM,EAAE;CAElG,IAAI,YAAY,CAAC,YAAY,WAAW,KAAK,MAAM,UAAU,UAC3D,OAAO;CAIT,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,IAAI,qBAAqB,MAAM,MAAM,CAAC;CACtE,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,oBAAoB,MAAM,SAAS,MAAM,CAAC;CAC7E,MAAM,cAAc,MAAM,SAAS,SAAS;CAE5C,IAAI,eAAe,GACjB,OAAO;CAET,OAAO;EACL,GAAG,IAAI,MAAM,GAAG,MAAM;EACtB;GAAE,MAAM;GAAY;EAAY;EAChC,GAAG,IAAI,MAAM,MAAM,SAAS,KAAK;CACnC;AACF;;;AC1IA,IAAa,wBAAwB,CAAC,mBAAG,IAAI,IAAI;CAC/C,GAAG,OAAO,OAAO,0BAA0B,CAAC,CAAC,QAAQ,gBAAgB;CACrE,GAAG,iBAAiB,oBAAoB;CACxC,GAAG,iBAAiB,oBAAoB;CACxC,GAAG,iBAAiB,wBAAwB;CAC5C,GAAG,iBAAiB,0BAA0B;CAC9C,GAAG,iBAAiB,yBAAyB;CAC7C,GAAG,iBAAiB,uBAAuB;CAC3C,GAAG,iBAAiB,wBAAwB;CAC5C,GAAG,iBAAiB,qBAAqB;CACzC,GAAG,iBAAiB,wBAAwB;AAC9C,CAAC,CAAC;;;ACxBF,IAAa,sBAAsB,wBAAwB,OAAO,KAAK,KAAK;CAC1E,MAAM;CACN,UAAU;CACV,cAAc,CAAC,QAAQ;AACzB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"grDropdownMenuStyles-DG3aYHkT.js","names":[],"sources":["../../src/components/GrDropdownMenu/grDropdownMenuStyles.ts"],"sourcesContent":["export type GrDropdownMenuItemAlign = 'left' | 'center' | 'right'\nexport type GrDropdownMenuItemVariant = 'default' | 'danger'\nexport type GrDropdownMenuHeaderAlign = GrDropdownMenuItemAlign\nexport type GrDropdownMenuColumnAlign = GrDropdownMenuItemAlign\nexport type GrDropdownMenuColumnsCount = 2 | 3 | 4\n\nexport const alignClass: Record<GrDropdownMenuItemAlign, string> = {\n left: 'justify-start text-left',\n center: 'justify-center text-center',\n right: 'justify-end text-right',\n}\n\nexport const textAlignClass: Record<GrDropdownMenuHeaderAlign, string> = {\n left: 'text-left',\n center: 'text-center',\n right: 'text-right',\n}\n\nexport const colsClass: Record<GrDropdownMenuColumnsCount, string> = {\n 2: 'grid-cols-2',\n 3: 'grid-cols-3',\n 4: 'grid-cols-4',\n}\n\n/**\n * Радиус здесь не украшение: фон подсветки лежит на самом пункте, а панель\n * `GrDropdown` скруглена на 16 px и содержимое не обрезает. Прямоугольник во всю\n * ширину заливал бы угловые сегменты, вырезанные этим радиусом. Восемь\n * пикселей — та же ступень, что у опций `GrSelect` и `GrAutocomplete`; внутренний\n * угол панели после рамки и поля ≈ 11 px, так что подсветка до скругления не\n * достаёт. Радиус в базовом классе, а не отдельной константой: hover, disabled и\n * кольцо фокуса висят на одном элементе.\n */\nexport const itemBaseClass = 'w-full min-h-10 px-4 py-2.5 rounded-[var(--gr-radius-md)] flex items-center gap-2 text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] transition-colors duration-[var(--gr-duration-fast)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]'\n\n// Насыщенный тон как цвет текста запрещён — для текста есть `-text`-роль.\nexport const itemVariantClass: Record<GrDropdownMenuItemVariant, string> = {\n default: 'text-[var(--gr-fg)]',\n danger: 'text-[var(--gr-danger-text)]',\n}\n\nexport const itemInteractiveClass = 'cursor-pointer hover:bg-[var(--gr-accent)] hover:text-[var(--gr-accent-fg)]'\n\n// Disabled гасится фоном, а не `opacity`: прозрачность разбавляет выверенные на\n// AA токены текста. `pointer-events-none` оставлен — он же гасит hover.\nexport const itemDisabledClass = 'cursor-not-allowed pointer-events-none bg-[var(--gr-muted)] text-[var(--gr-muted-fg)]'\n\nexport const itemIndicatorClass = 'h-3.5 w-3.5 shrink-0'\nexport const itemShortcutClass = 'ml-auto pl-4 text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)] text-[var(--gr-muted-fg)]'\n\nexport const headerClass = 'px-4 py-2 text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-xs)] tracking-wide text-[var(--gr-muted-fg)]'\n\nexport const listBaseClass = 'w-full'\nexport const dividersClass = 'divide-y divide-[var(--gr-brd)]'\nexport const borderTopClass = 'border-t border-[var(--gr-brd)]'\nexport const borderBottomClass = 'border-b border-[var(--gr-brd)]'\n\nexport const columnsBaseClass = 'w-full grid divide-x divide-[var(--gr-brd)]'\nexport const columnBaseClass = 'px-3 py-2 flex items-center'\n\nexport const dividerClass = 'border-t border-[var(--gr-brd)]'\nexport const dividerInsetClass = 'mx-2'\n\nexport function grDropdownMenuItemClass(options: {\n align: GrDropdownMenuItemAlign\n variant: GrDropdownMenuItemVariant\n disabled: boolean\n}): string {\n return [\n itemBaseClass,\n alignClass[options.align],\n options.disabled ? itemDisabledClass : itemVariantClass[options.variant],\n options.disabled ? '' : itemInteractiveClass,\n ].filter(Boolean).join(' ')\n}\n\nexport function grDropdownMenuListClass(options: {\n dividers: boolean\n borderTop: boolean\n borderBottom: boolean\n}): string {\n return [\n listBaseClass,\n options.dividers ? dividersClass : '',\n options.borderTop ? borderTopClass : '',\n options.borderBottom ? borderBottomClass : '',\n ].filter(Boolean).join(' ')\n}\n"],"mappings":";AAMA,IAAa,aAAsD;CACjE,MAAM;CACN,QAAQ;CACR,OAAO;AACT;AAEA,IAAa,iBAA4D;CACvE,MAAM;CACN,QAAQ;CACR,OAAO;AACT;AAEA,IAAa,YAAwD;CACnE,GAAG;CACH,GAAG;CACH,GAAG;AACL;;;;;;;;;;AAWA,IAAa,gBAAgB;AAG7B,IAAa,mBAA8D;CACzE,SAAS;CACT,QAAQ;AACV;AAEA,IAAa,uBAAuB;AAIpC,IAAa,oBAAoB;AAEjC,IAAa,qBAAqB;AAClC,IAAa,oBAAoB;AAEjC,IAAa,cAAc;AAE3B,IAAa,gBAAgB;AAC7B,IAAa,gBAAgB;AAC7B,IAAa,iBAAiB;AAC9B,IAAa,oBAAoB;AAEjC,IAAa,mBAAmB;AAChC,IAAa,kBAAkB;AAE/B,IAAa,eAAe;AAC5B,IAAa,oBAAoB;AAEjC,SAAgB,wBAAwB,SAI7B;CACT,OAAO;EACL;EACA,WAAW,QAAQ;EACnB,QAAQ,WAAW,oBAAoB,iBAAiB,QAAQ;EAChE,QAAQ,WAAW,KAAK;CAC1B,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;AAC5B;AAEA,SAAgB,wBAAwB,SAI7B;CACT,OAAO;EACL;EACA,QAAQ,WAAW,gBAAgB;EACnC,QAAQ,YAAY,iBAAiB;EACrC,QAAQ,eAAe,oBAAoB;CAC7C,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;AAC5B"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"GrBreadcrumbs-jtSnMEtg.js","names":[],"sources":["../../../../src/components/GrBreadcrumbs/GrBreadcrumbs.vue","../../../../src/components/GrBreadcrumbs/GrBreadcrumbs.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport GrLink from '../GrLink/GrLink.vue'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { iconClass, iconTag } from '../shared/icon'\n\nimport {\n breadcrumbsCurrentClass,\n breadcrumbsEllipsisClass,\n breadcrumbsItemIconClass,\n breadcrumbsLabelClass,\n breadcrumbsListClass,\n breadcrumbsListNowrapClass,\n breadcrumbsListWrapClass,\n breadcrumbsRootClass,\n breadcrumbsSeparatorClass,\n breadcrumbsSizeClassBySize,\n type GrBreadcrumbItem,\n type GrBreadcrumbsLinkComponent,\n type GrBreadcrumbsSize,\n resolveBreadcrumbsFit,\n resolveBreadcrumbsLayout,\n} from './grBreadcrumbsStyles'\n\nexport type { GrBreadcrumbItem, GrBreadcrumbsSize } from './grBreadcrumbsStyles'\n\n/**\n * Путь до текущей страницы (WAI-ARIA breadcrumb): `<nav>` с именем, внутри\n * упорядоченный список ссылок, последний пункт — текущая страница.\n *\n * Ссылки рисует `GrLink`, поэтому роутер подключается тем же способом, что и\n * везде в пакете: `as` + `to`.\n */\nexport interface GrBreadcrumbsProps {\n /** Путь от корня к текущей странице. Последний пункт — текущая страница. */\n items: GrBreadcrumbItem[]\n /** Компонент ссылки для всех пунктов: `RouterLink`, `NuxtLink`, `Link` Inertia. */\n as?: GrBreadcrumbsLinkComponent\n /** Разделитель между пунктами. Декоративен: диктору не читается. */\n separator?: string\n size?: GrBreadcrumbsSize\n /**\n * С какого числа пунктов схлопывать середину в «…». `0`/не задано — показывать\n * путь целиком.\n */\n maxItems?: number\n /** Сколько пунктов оставить в начале при схлопывании. */\n itemsBeforeCollapse?: number\n /** Сколько пунктов оставить в конце при схлопывании. */\n itemsAfterCollapse?: number\n /** Имя лендмарка. Не задано — берётся из локали. */\n ariaLabel?: string\n /** Последний пункт остаётся ссылкой (текущая страница кликабельна). */\n linkCurrent?: boolean\n /** i18n-метка кнопки раскрытия схлопнутого пути. */\n expandLabel?: string\n /**\n * Схлопывать по доступной ширине, а не по числу пунктов. Путь становится\n * однострочным, середина уходит под «…» ровно настолько, насколько не влезает.\n *\n * Без пропа поведение прежнее: список переносится на вторую строку, а порог\n * задаёт `maxItems`. Вместе пропы совместимы — `maxItems` остаётся жёстким\n * потолком, ширина ужимает дальше.\n */\n autoCollapse?: boolean\n}\n\nconst props = withDefaults(defineProps<GrBreadcrumbsProps>(), {\n as: undefined,\n separator: undefined,\n size: undefined,\n maxItems: undefined,\n itemsBeforeCollapse: 1,\n itemsAfterCollapse: 1,\n ariaLabel: undefined,\n linkCurrent: false,\n expandLabel: undefined,\n autoCollapse: false,\n})\n\ndefineSlots<{\n /** Содержимое пункта целиком (иконка и подпись). */\n item?: (props: { item: GrBreadcrumbItem, index: number, isCurrent: boolean }) => unknown\n /** Разделитель между пунктами. */\n separator?: () => unknown\n /** Кнопка раскрытия схлопнутой середины. */\n ellipsis?: (props: { hiddenCount: number, expand: () => void }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrBreadcrumbs' })\nconst resolvedSeparator = useGrComponentProp('GrBreadcrumbs', 'separator', () => props.separator, '/')\n\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.breadcrumbs.label', 'Breadcrumb'))\nconst resolvedExpandLabel = computed(() => props.expandLabel ?? t('gr.breadcrumbs.expand', 'Show hidden path'))\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst listEl = ref<HTMLElement | null>(null)\nconst expanded = ref(false)\n\n/**\n * Схлопывание по ширине: сколько пунктов хвоста влезает.\n *\n * `undefined` — ещё не мерили, показываем путь целиком. Первый кадр в режиме\n * `autoCollapse` намеренно полный: ширины снимаются с него, а посчитать их до\n * рендера неоткуда.\n */\nconst fitAfter = ref<number | undefined>(undefined)\n\n/** Ширины не зависят от контейнера, поэтому живут до смены самого пути. */\nlet measured: { items: number[], separator: number } | null = null\n/**\n * Ширина кнопки «…» узнаётся только когда она отрисована, то есть уже после\n * первого схлопывания. До этого считаем её нулевой: поправка может лишь\n * ужать хвост ещё на пункт, а не вернуть спрятанное, поэтому качелей не будет.\n */\nlet ellipsisWidth = 0\n/** Ширина, на которой считали в прошлый раз: по её смене раскрытый путь сворачивается. */\nlet lastAvailable = 0\n\n// Новый путь — новая страница: раскрытая середина прошлого пути к ней отношения\n// не имеет, а измеренные ширины относятся к прошлым подписям.\nwatch(() => props.items, () => {\n expanded.value = false\n measured = null\n ellipsisWidth = 0\n fitAfter.value = undefined\n void nextTick(measure)\n})\n\nconst lastIndex = computed(() => props.items.length - 1)\n\n/**\n * Схлопывание по ширине выражается через тот же `maxItems`: раскладку считает\n * одна функция, а режимы отличаются только тем, откуда взялось число.\n */\nconst autoMaxItems = computed(() => {\n if (!props.autoCollapse || fitAfter.value === undefined) return undefined\n if (fitAfter.value >= props.items.length) return undefined\n\n return props.items.length - 1\n})\n\nconst effectiveMaxItems = computed(() => {\n const limits = [props.maxItems, autoMaxItems.value].filter((value): value is number => typeof value === 'number' && value > 0)\n\n return limits.length > 0 ? Math.min(...limits) : undefined\n})\n\nconst effectiveAfter = computed(() => (\n props.autoCollapse && fitAfter.value !== undefined ? fitAfter.value : props.itemsAfterCollapse\n))\n\nconst entries = computed(() => resolveBreadcrumbsLayout({\n items: props.items,\n maxItems: effectiveMaxItems.value,\n itemsBeforeCollapse: props.itemsBeforeCollapse,\n itemsAfterCollapse: effectiveAfter.value,\n expanded: expanded.value,\n}))\n\nfunction isCurrent(index: number): boolean {\n return index === lastIndex.value\n}\n\n/** Ссылкой пункт становится, только если ведёт куда-то и не выключен. */\nfunction isLink(item: GrBreadcrumbItem, index: number): boolean {\n if (item.disabled) return false\n if (isCurrent(index) && !props.linkCurrent) return false\n return item.href !== undefined || item.to !== undefined || props.as !== undefined\n}\n\nfunction widthOf(el: Element | null | undefined): number {\n // `scrollWidth`, а не прямоугольник: у подписи стоит `truncate`, и в узком\n // контейнере прямоугольник вернул бы уже ужатую ширину, а не собственную.\n return el ? (el as HTMLElement).scrollWidth : 0\n}\n\n/**\n * Снять ширины с полного пути и пересчитать, сколько пунктов хвоста влезает.\n *\n * Замер делается один раз на путь: подписи от ширины контейнера не зависят,\n * поэтому `resize` пересчитывает только арифметику.\n */\nfunction measure(): void {\n if (!props.autoCollapse) return\n\n const list = listEl.value\n if (!list) return\n\n const ellipsis = list.querySelector('[data-gr-breadcrumbs-ellipsis-item]')\n if (ellipsis) ellipsisWidth = widthOf(ellipsis)\n\n if (!measured) {\n // Ширины снимаем только с полного пути: спрятанных пунктов в DOM нет.\n if (fitAfter.value !== undefined) return\n\n const wraps = [...list.querySelectorAll('[data-gr-breadcrumbs-item-wrap]')]\n if (wraps.length !== props.items.length) return\n\n measured = {\n items: wraps.map(widthOf),\n separator: widthOf(list.querySelector('[data-gr-breadcrumbs-separator]')),\n }\n }\n\n const available = list.clientWidth\n // jsdom и скрытый контейнер отдают 0 — решать по такой ширине нечего.\n if (available <= 0) return\n\n lastAvailable = available\n\n fitAfter.value = resolveBreadcrumbsFit({\n itemWidths: measured.items,\n separatorWidth: measured.separator,\n ellipsisWidth,\n available,\n itemsBeforeCollapse: props.itemsBeforeCollapse,\n gapWidth: listGap(list),\n })\n}\n\n/** Зазор списка в пикселях: он задан классом, а не пропом, поэтому спрашиваем стиль. */\nfunction listGap(list: HTMLElement): number {\n if (typeof getComputedStyle !== 'function') return 0\n\n return Number.parseFloat(getComputedStyle(list).columnGap) || 0\n}\n\n/**\n * Ширина кнопки «…» известна только после того, как она отрисована, то есть\n * после первого схлопывания. Поэтому решение пересчитывается ещё раз: поправка\n * либо ничего не меняет, либо ужимает хвост на пункт. Цикла нет — второй проход\n * оставляет `fitAfter` прежним, и наблюдатель больше не срабатывает.\n */\nwatch(fitAfter, () => void nextTick(measure))\n\nlet observer: ResizeObserver | null = null\n\nwatch(() => props.autoCollapse, (enabled) => {\n if (enabled) {\n void nextTick(measure)\n return\n }\n\n measured = null\n ellipsisWidth = 0\n fitAfter.value = undefined\n})\n\nonMounted(() => {\n void nextTick(measure)\n\n // На сервере и в jsdom `ResizeObserver` отсутствует — измерять там нечего.\n if (typeof ResizeObserver === 'undefined') return\n\n observer = new ResizeObserver(() => {\n // Смена ширины — новая раскладка, и раскрытая руками середина относилась к\n // прежней. Свернуть её иначе нечем: в раскрытом пути кнопки «…» нет, то есть\n // поворот телефона оставлял бы путь развёрнутым навсегда. Высота меняется от\n // самого раскрытия (путь переносится на вторую строку) — на неё не реагируем.\n if (expanded.value && listEl.value && listEl.value.clientWidth !== lastAvailable)\n expanded.value = false\n\n measure()\n })\n if (listEl.value) observer.observe(listEl.value)\n})\n\nonBeforeUnmount(() => observer?.disconnect())\n\nasync function expand(): Promise<void> {\n expanded.value = true\n\n // Кнопка исчезает вместе со схлопыванием, и фокус ушёл бы на `<body>`.\n // Переводим его на первый раскрытый пункт — там, где кнопка и стояла. Пункты\n // без ссылки держат `tabindex=\"-1\"` именно ради этого случая.\n await nextTick()\n const before = Math.max(0, Math.min(props.itemsBeforeCollapse, props.items.length))\n const revealed = rootEl.value?.querySelectorAll<HTMLElement>('[data-gr-breadcrumbs-item]')\n revealed?.[before]?.focus()\n}\n</script>\n\n<template>\n <nav\n ref=\"rootEl\"\n data-gr-breadcrumbs\n :class=\"[breadcrumbsRootClass, breadcrumbsSizeClassBySize[resolvedSize]]\"\n :aria-label=\"resolvedAriaLabel\"\n >\n <ol\n ref=\"listEl\"\n data-gr-breadcrumbs-list\n :class=\"[breadcrumbsListClass, autoCollapse && !expanded ? breadcrumbsListNowrapClass : breadcrumbsListWrapClass]\"\n >\n <template v-for=\"(entry, position) in entries\" :key=\"entry.kind === 'item' ? `item-${entry.index}` : 'ellipsis'\">\n <!--\n Разделитель — отдельный элемент списка и помечен `aria-hidden`:\n структуру пути диктору сообщает сам список, а «/» он бы читал вслух.\n -->\n <li\n v-if=\"position > 0\"\n data-gr-breadcrumbs-separator\n :class=\"breadcrumbsSeparatorClass\"\n aria-hidden=\"true\"\n >\n <slot name=\"separator\">\n {{ resolvedSeparator }}\n </slot>\n </li>\n\n <li v-if=\"entry.kind === 'ellipsis'\" data-gr-breadcrumbs-ellipsis-item class=\"min-w-0\">\n <slot name=\"ellipsis\" :hidden-count=\"entry.hiddenCount\" :expand=\"expand\">\n <button\n type=\"button\"\n data-gr-breadcrumbs-ellipsis\n data-testid=\"gr-breadcrumbs-ellipsis\"\n :class=\"breadcrumbsEllipsisClass\"\n :aria-label=\"resolvedExpandLabel\"\n @click=\"expand\"\n >\n …\n </button>\n </slot>\n </li>\n\n <li v-else data-gr-breadcrumbs-item-wrap class=\"min-w-0\">\n <GrLink\n v-if=\"isLink(entry.item, entry.index)\"\n :key=\"`link-${entry.index}`\"\n data-gr-breadcrumbs-item\n :as=\"as\"\n :href=\"entry.item.href\"\n :to=\"entry.item.to\"\n :size=\"resolvedSize\"\n :variant=\"isCurrent(entry.index) ? 'default' : 'muted'\"\n :aria-label=\"entry.item.ariaLabel\"\n :aria-current=\"isCurrent(entry.index) ? 'page' : undefined\"\n >\n <slot name=\"item\" :item=\"entry.item\" :index=\"entry.index\" :is-current=\"isCurrent(entry.index)\">\n <component\n :is=\"iconTag(entry.item.icon)\"\n v-if=\"entry.item.icon\"\n :class=\"[breadcrumbsItemIconClass, iconClass(entry.item.icon)]\"\n aria-hidden=\"true\"\n />\n <span :class=\"breadcrumbsLabelClass\">{{ entry.item.label }}</span>\n </slot>\n </GrLink>\n\n <!--\n Текущая страница и выключенный пункт — не ссылки. `aria-current`\n остаётся на текущем: именно он отвечает на вопрос «где я».\n -->\n <span\n v-else\n data-gr-breadcrumbs-item\n :class=\"breadcrumbsCurrentClass\"\n :aria-label=\"entry.item.ariaLabel\"\n :aria-current=\"isCurrent(entry.index) ? 'page' : undefined\"\n :aria-disabled=\"entry.item.disabled ? 'true' : undefined\"\n tabindex=\"-1\"\n >\n <slot name=\"item\" :item=\"entry.item\" :index=\"entry.index\" :is-current=\"isCurrent(entry.index)\">\n <component\n :is=\"iconTag(entry.item.icon)\"\n v-if=\"entry.item.icon\"\n :class=\"[breadcrumbsItemIconClass, iconClass(entry.item.icon)]\"\n aria-hidden=\"true\"\n />\n <span :class=\"breadcrumbsLabelClass\">{{ entry.item.label }}</span>\n </slot>\n </span>\n </li>\n </template>\n </ol>\n </nav>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport GrLink from '../GrLink/GrLink.vue'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { iconClass, iconTag } from '../shared/icon'\n\nimport {\n breadcrumbsCurrentClass,\n breadcrumbsEllipsisClass,\n breadcrumbsItemIconClass,\n breadcrumbsLabelClass,\n breadcrumbsListClass,\n breadcrumbsListNowrapClass,\n breadcrumbsListWrapClass,\n breadcrumbsRootClass,\n breadcrumbsSeparatorClass,\n breadcrumbsSizeClassBySize,\n type GrBreadcrumbItem,\n type GrBreadcrumbsLinkComponent,\n type GrBreadcrumbsSize,\n resolveBreadcrumbsFit,\n resolveBreadcrumbsLayout,\n} from './grBreadcrumbsStyles'\n\nexport type { GrBreadcrumbItem, GrBreadcrumbsSize } from './grBreadcrumbsStyles'\n\n/**\n * Путь до текущей страницы (WAI-ARIA breadcrumb): `<nav>` с именем, внутри\n * упорядоченный список ссылок, последний пункт — текущая страница.\n *\n * Ссылки рисует `GrLink`, поэтому роутер подключается тем же способом, что и\n * везде в пакете: `as` + `to`.\n */\nexport interface GrBreadcrumbsProps {\n /** Путь от корня к текущей странице. Последний пункт — текущая страница. */\n items: GrBreadcrumbItem[]\n /** Компонент ссылки для всех пунктов: `RouterLink`, `NuxtLink`, `Link` Inertia. */\n as?: GrBreadcrumbsLinkComponent\n /** Разделитель между пунктами. Декоративен: диктору не читается. */\n separator?: string\n size?: GrBreadcrumbsSize\n /**\n * С какого числа пунктов схлопывать середину в «…». `0`/не задано — показывать\n * путь целиком.\n */\n maxItems?: number\n /** Сколько пунктов оставить в начале при схлопывании. */\n itemsBeforeCollapse?: number\n /** Сколько пунктов оставить в конце при схлопывании. */\n itemsAfterCollapse?: number\n /** Имя лендмарка. Не задано — берётся из локали. */\n ariaLabel?: string\n /** Последний пункт остаётся ссылкой (текущая страница кликабельна). */\n linkCurrent?: boolean\n /** i18n-метка кнопки раскрытия схлопнутого пути. */\n expandLabel?: string\n /**\n * Схлопывать по доступной ширине, а не по числу пунктов. Путь становится\n * однострочным, середина уходит под «…» ровно настолько, насколько не влезает.\n *\n * Без пропа поведение прежнее: список переносится на вторую строку, а порог\n * задаёт `maxItems`. Вместе пропы совместимы — `maxItems` остаётся жёстким\n * потолком, ширина ужимает дальше.\n */\n autoCollapse?: boolean\n}\n\nconst props = withDefaults(defineProps<GrBreadcrumbsProps>(), {\n as: undefined,\n separator: undefined,\n size: undefined,\n maxItems: undefined,\n itemsBeforeCollapse: 1,\n itemsAfterCollapse: 1,\n ariaLabel: undefined,\n linkCurrent: false,\n expandLabel: undefined,\n autoCollapse: false,\n})\n\ndefineSlots<{\n /** Содержимое пункта целиком (иконка и подпись). */\n item?: (props: { item: GrBreadcrumbItem, index: number, isCurrent: boolean }) => unknown\n /** Разделитель между пунктами. */\n separator?: () => unknown\n /** Кнопка раскрытия схлопнутой середины. */\n ellipsis?: (props: { hiddenCount: number, expand: () => void }) => unknown\n}>()\n\nconst { t } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrBreadcrumbs' })\nconst resolvedSeparator = useGrComponentProp('GrBreadcrumbs', 'separator', () => props.separator, '/')\n\nconst resolvedAriaLabel = computed(() => props.ariaLabel ?? t('gr.breadcrumbs.label', 'Breadcrumb'))\nconst resolvedExpandLabel = computed(() => props.expandLabel ?? t('gr.breadcrumbs.expand', 'Show hidden path'))\n\nconst rootEl = ref<HTMLElement | null>(null)\nconst listEl = ref<HTMLElement | null>(null)\nconst expanded = ref(false)\n\n/**\n * Схлопывание по ширине: сколько пунктов хвоста влезает.\n *\n * `undefined` — ещё не мерили, показываем путь целиком. Первый кадр в режиме\n * `autoCollapse` намеренно полный: ширины снимаются с него, а посчитать их до\n * рендера неоткуда.\n */\nconst fitAfter = ref<number | undefined>(undefined)\n\n/** Ширины не зависят от контейнера, поэтому живут до смены самого пути. */\nlet measured: { items: number[], separator: number } | null = null\n/**\n * Ширина кнопки «…» узнаётся только когда она отрисована, то есть уже после\n * первого схлопывания. До этого считаем её нулевой: поправка может лишь\n * ужать хвост ещё на пункт, а не вернуть спрятанное, поэтому качелей не будет.\n */\nlet ellipsisWidth = 0\n/** Ширина, на которой считали в прошлый раз: по её смене раскрытый путь сворачивается. */\nlet lastAvailable = 0\n\n// Новый путь — новая страница: раскрытая середина прошлого пути к ней отношения\n// не имеет, а измеренные ширины относятся к прошлым подписям.\nwatch(() => props.items, () => {\n expanded.value = false\n measured = null\n ellipsisWidth = 0\n fitAfter.value = undefined\n void nextTick(measure)\n})\n\nconst lastIndex = computed(() => props.items.length - 1)\n\n/**\n * Схлопывание по ширине выражается через тот же `maxItems`: раскладку считает\n * одна функция, а режимы отличаются только тем, откуда взялось число.\n */\nconst autoMaxItems = computed(() => {\n if (!props.autoCollapse || fitAfter.value === undefined) return undefined\n if (fitAfter.value >= props.items.length) return undefined\n\n return props.items.length - 1\n})\n\nconst effectiveMaxItems = computed(() => {\n const limits = [props.maxItems, autoMaxItems.value].filter((value): value is number => typeof value === 'number' && value > 0)\n\n return limits.length > 0 ? Math.min(...limits) : undefined\n})\n\nconst effectiveAfter = computed(() => (\n props.autoCollapse && fitAfter.value !== undefined ? fitAfter.value : props.itemsAfterCollapse\n))\n\nconst entries = computed(() => resolveBreadcrumbsLayout({\n items: props.items,\n maxItems: effectiveMaxItems.value,\n itemsBeforeCollapse: props.itemsBeforeCollapse,\n itemsAfterCollapse: effectiveAfter.value,\n expanded: expanded.value,\n}))\n\nfunction isCurrent(index: number): boolean {\n return index === lastIndex.value\n}\n\n/** Ссылкой пункт становится, только если ведёт куда-то и не выключен. */\nfunction isLink(item: GrBreadcrumbItem, index: number): boolean {\n if (item.disabled) return false\n if (isCurrent(index) && !props.linkCurrent) return false\n return item.href !== undefined || item.to !== undefined || props.as !== undefined\n}\n\nfunction widthOf(el: Element | null | undefined): number {\n // `scrollWidth`, а не прямоугольник: у подписи стоит `truncate`, и в узком\n // контейнере прямоугольник вернул бы уже ужатую ширину, а не собственную.\n return el ? (el as HTMLElement).scrollWidth : 0\n}\n\n/**\n * Снять ширины с полного пути и пересчитать, сколько пунктов хвоста влезает.\n *\n * Замер делается один раз на путь: подписи от ширины контейнера не зависят,\n * поэтому `resize` пересчитывает только арифметику.\n */\nfunction measure(): void {\n if (!props.autoCollapse) return\n\n const list = listEl.value\n if (!list) return\n\n const ellipsis = list.querySelector('[data-gr-breadcrumbs-ellipsis-item]')\n if (ellipsis) ellipsisWidth = widthOf(ellipsis)\n\n if (!measured) {\n // Ширины снимаем только с полного пути: спрятанных пунктов в DOM нет.\n if (fitAfter.value !== undefined) return\n\n const wraps = [...list.querySelectorAll('[data-gr-breadcrumbs-item-wrap]')]\n if (wraps.length !== props.items.length) return\n\n measured = {\n items: wraps.map(widthOf),\n separator: widthOf(list.querySelector('[data-gr-breadcrumbs-separator]')),\n }\n }\n\n const available = list.clientWidth\n // jsdom и скрытый контейнер отдают 0 — решать по такой ширине нечего.\n if (available <= 0) return\n\n lastAvailable = available\n\n fitAfter.value = resolveBreadcrumbsFit({\n itemWidths: measured.items,\n separatorWidth: measured.separator,\n ellipsisWidth,\n available,\n itemsBeforeCollapse: props.itemsBeforeCollapse,\n gapWidth: listGap(list),\n })\n}\n\n/** Зазор списка в пикселях: он задан классом, а не пропом, поэтому спрашиваем стиль. */\nfunction listGap(list: HTMLElement): number {\n if (typeof getComputedStyle !== 'function') return 0\n\n return Number.parseFloat(getComputedStyle(list).columnGap) || 0\n}\n\n/**\n * Ширина кнопки «…» известна только после того, как она отрисована, то есть\n * после первого схлопывания. Поэтому решение пересчитывается ещё раз: поправка\n * либо ничего не меняет, либо ужимает хвост на пункт. Цикла нет — второй проход\n * оставляет `fitAfter` прежним, и наблюдатель больше не срабатывает.\n */\nwatch(fitAfter, () => void nextTick(measure))\n\nlet observer: ResizeObserver | null = null\n\nwatch(() => props.autoCollapse, (enabled) => {\n if (enabled) {\n void nextTick(measure)\n return\n }\n\n measured = null\n ellipsisWidth = 0\n fitAfter.value = undefined\n})\n\nonMounted(() => {\n void nextTick(measure)\n\n // На сервере и в jsdom `ResizeObserver` отсутствует — измерять там нечего.\n if (typeof ResizeObserver === 'undefined') return\n\n observer = new ResizeObserver(() => {\n // Смена ширины — новая раскладка, и раскрытая руками середина относилась к\n // прежней. Свернуть её иначе нечем: в раскрытом пути кнопки «…» нет, то есть\n // поворот телефона оставлял бы путь развёрнутым навсегда. Высота меняется от\n // самого раскрытия (путь переносится на вторую строку) — на неё не реагируем.\n if (expanded.value && listEl.value && listEl.value.clientWidth !== lastAvailable)\n expanded.value = false\n\n measure()\n })\n if (listEl.value) observer.observe(listEl.value)\n})\n\nonBeforeUnmount(() => observer?.disconnect())\n\nasync function expand(): Promise<void> {\n expanded.value = true\n\n // Кнопка исчезает вместе со схлопыванием, и фокус ушёл бы на `<body>`.\n // Переводим его на первый раскрытый пункт — там, где кнопка и стояла. Пункты\n // без ссылки держат `tabindex=\"-1\"` именно ради этого случая.\n await nextTick()\n const before = Math.max(0, Math.min(props.itemsBeforeCollapse, props.items.length))\n const revealed = rootEl.value?.querySelectorAll<HTMLElement>('[data-gr-breadcrumbs-item]')\n revealed?.[before]?.focus()\n}\n</script>\n\n<template>\n <nav\n ref=\"rootEl\"\n data-gr-breadcrumbs\n :class=\"[breadcrumbsRootClass, breadcrumbsSizeClassBySize[resolvedSize]]\"\n :aria-label=\"resolvedAriaLabel\"\n >\n <ol\n ref=\"listEl\"\n data-gr-breadcrumbs-list\n :class=\"[breadcrumbsListClass, autoCollapse && !expanded ? breadcrumbsListNowrapClass : breadcrumbsListWrapClass]\"\n >\n <template v-for=\"(entry, position) in entries\" :key=\"entry.kind === 'item' ? `item-${entry.index}` : 'ellipsis'\">\n <!--\n Разделитель — отдельный элемент списка и помечен `aria-hidden`:\n структуру пути диктору сообщает сам список, а «/» он бы читал вслух.\n -->\n <li\n v-if=\"position > 0\"\n data-gr-breadcrumbs-separator\n :class=\"breadcrumbsSeparatorClass\"\n aria-hidden=\"true\"\n >\n <slot name=\"separator\">\n {{ resolvedSeparator }}\n </slot>\n </li>\n\n <li v-if=\"entry.kind === 'ellipsis'\" data-gr-breadcrumbs-ellipsis-item class=\"min-w-0\">\n <slot name=\"ellipsis\" :hidden-count=\"entry.hiddenCount\" :expand=\"expand\">\n <button\n type=\"button\"\n data-gr-breadcrumbs-ellipsis\n data-testid=\"gr-breadcrumbs-ellipsis\"\n :class=\"breadcrumbsEllipsisClass\"\n :aria-label=\"resolvedExpandLabel\"\n @click=\"expand\"\n >\n …\n </button>\n </slot>\n </li>\n\n <li v-else data-gr-breadcrumbs-item-wrap class=\"min-w-0\">\n <GrLink\n v-if=\"isLink(entry.item, entry.index)\"\n :key=\"`link-${entry.index}`\"\n data-gr-breadcrumbs-item\n :as=\"as\"\n :href=\"entry.item.href\"\n :to=\"entry.item.to\"\n :size=\"resolvedSize\"\n :variant=\"isCurrent(entry.index) ? 'default' : 'muted'\"\n :aria-label=\"entry.item.ariaLabel\"\n :aria-current=\"isCurrent(entry.index) ? 'page' : undefined\"\n >\n <slot name=\"item\" :item=\"entry.item\" :index=\"entry.index\" :is-current=\"isCurrent(entry.index)\">\n <component\n :is=\"iconTag(entry.item.icon)\"\n v-if=\"entry.item.icon\"\n :class=\"[breadcrumbsItemIconClass, iconClass(entry.item.icon)]\"\n aria-hidden=\"true\"\n />\n <span :class=\"breadcrumbsLabelClass\">{{ entry.item.label }}</span>\n </slot>\n </GrLink>\n\n <!--\n Текущая страница и выключенный пункт — не ссылки. `aria-current`\n остаётся на текущем: именно он отвечает на вопрос «где я».\n -->\n <span\n v-else\n data-gr-breadcrumbs-item\n :class=\"breadcrumbsCurrentClass\"\n :aria-label=\"entry.item.ariaLabel\"\n :aria-current=\"isCurrent(entry.index) ? 'page' : undefined\"\n :aria-disabled=\"entry.item.disabled ? 'true' : undefined\"\n tabindex=\"-1\"\n >\n <slot name=\"item\" :item=\"entry.item\" :index=\"entry.index\" :is-current=\"isCurrent(entry.index)\">\n <component\n :is=\"iconTag(entry.item.icon)\"\n v-if=\"entry.item.icon\"\n :class=\"[breadcrumbsItemIconClass, iconClass(entry.item.icon)]\"\n aria-hidden=\"true\"\n />\n <span :class=\"breadcrumbsLabelClass\">{{ entry.item.label }}</span>\n </slot>\n </span>\n </li>\n </template>\n </ol>\n </nav>\n</template>\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"GrDelta-04j9WmEY.js","names":[],"sources":["../../../../src/internal/granularityFormat.ts","../../../../src/components/GrDelta/GrDelta.vue","../../../../src/components/GrDelta/GrDelta.vue"],"sourcesContent":["/**\n * Форматирование чисел и дат по локали.\n *\n * Своё, а не из `@feugene/fint-i18n`: тот объявлен **опциональной** peer-зависимостью,\n * и пакет обязан работать без него. Под капотом всё равно `Intl`, то есть\n * результат совпадает с `useI18nFormat()` библиотеки — расхождения между\n * приложением с i18n и без него не возникает.\n *\n * Инстансы `Intl` кэшируются: их построение примерно на порядок дороже самого\n * форматирования. Кэш ограничен и сбрасывается целиком — иначе сгенерированные\n * на лету опции растили бы его без предела.\n */\n\nconst CACHE_LIMIT = 64\n\nconst numberFormats = new Map<string, Intl.NumberFormat>()\nconst dateFormats = new Map<string, Intl.DateTimeFormat>()\n\nfunction cacheKey(locale: string, options?: object): string {\n return options ? `${locale}\u0000${JSON.stringify(options)}` : locale\n}\n\nfunction take<T>(cache: Map<string, T>, key: string, create: () => T): T {\n const cached = cache.get(key)\n if (cached) return cached\n\n const created = create()\n if (cache.size >= CACHE_LIMIT) cache.clear()\n cache.set(key, created)\n\n return created\n}\n\n/**\n * Локаль приходит из настроек приложения, а `Intl` бросает на некорректном\n * теге. Форматирование не имеет права уронить рендер, поэтому неизвестный тег\n * сводим к `en`.\n */\nexport function formatNumber(\n locale: string,\n value: number | bigint,\n options?: Intl.NumberFormatOptions,\n): string {\n try {\n return take(numberFormats, cacheKey(locale, options), () => new Intl.NumberFormat(locale, options)).format(value)\n }\n catch {\n return take(numberFormats, cacheKey('en', options), () => new Intl.NumberFormat('en', options)).format(value)\n }\n}\n\n/**\n * Те же правила, что у `formatNumber`, но по частям: вызывающему нужно знать,\n * где в записи стоит знак, чтобы вынести его отдельным узлом.\n *\n * Инстанс тот же, что у `formatNumber`: `Intl.NumberFormat` умеет оба метода, а\n * второй кэш под теми же ключами разъехался бы с первым на первом вытеснении.\n */\nexport function formatNumberToParts(\n locale: string,\n value: number | bigint,\n options?: Intl.NumberFormatOptions,\n): Intl.NumberFormatPart[] {\n try {\n return take(numberFormats, cacheKey(locale, options), () => new Intl.NumberFormat(locale, options)).formatToParts(value)\n }\n catch {\n return take(numberFormats, cacheKey('en', options), () => new Intl.NumberFormat('en', options)).formatToParts(value)\n }\n}\n\n/**\n * Ведущий знак и остаток записи.\n *\n * Нужно там, где знаку полагается свой узел разметки: валюта обязана стоять\n * после знака (`+$0,03`, а не `$+0,03`). Резать готовую строку по первому\n * символу нельзя — в RTL-локалях знаку предшествует невидимая метка\n * направления (`he-IL` — U+200E, `ar-EG` — U+061C), которой и разворачивается\n * минус относительно цифр. Метка уходит вместе со знаком: она управляет им.\n *\n * `sign` пуст, когда знака нет вовсе (ноль, положительное при `signDisplay:\n * 'auto'`, `NaN`) либо когда перед ним стоит содержательная часть. Во втором\n * случае запись не делится совсем: вернуть половину по догадке хуже, чем\n * вернуть целое.\n */\nexport function splitLeadingSign(\n parts: readonly Intl.NumberFormatPart[],\n): { sign: string, rest: string } {\n const join = (from: readonly Intl.NumberFormatPart[]): string => from.map(part => part.value).join('')\n const at = parts.findIndex(part => part.type === 'minusSign' || part.type === 'plusSign')\n\n if (at === -1 || parts.slice(0, at).some(part => part.type !== 'literal'))\n return { sign: '', rest: join(parts) }\n\n return { sign: join(parts.slice(0, at + 1)), rest: join(parts.slice(at + 1)) }\n}\n\nexport function formatDate(\n locale: string,\n value: Date | number | string,\n options?: Intl.DateTimeFormatOptions,\n): string {\n const date = value instanceof Date ? value : new Date(value)\n\n // Невалидную дату возвращаем как есть: пустая или «Invalid Date» в интерфейсе\n // хуже, чем исходное значение, но лучше, чем исключение на рендере.\n if (Number.isNaN(date.getTime())) return String(value)\n\n try {\n return take(dateFormats, cacheKey(locale, options), () => new Intl.DateTimeFormat(locale, options)).format(date)\n }\n catch {\n return take(dateFormats, cacheKey('en', options), () => new Intl.DateTimeFormat('en', options)).format(date)\n }\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport { formatNumberToParts, splitLeadingSign } from '../../internal/granularityFormat'\nimport GrValue from '../GrValue'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\n\nimport IconMinus from '~icons/lucide/minus'\nimport IconTrendingDown from '~icons/lucide/trending-down'\nimport IconTrendingUp from '~icons/lucide/trending-up'\n\nimport { deltaDirection, deltaTone, type GrDeltaPolarity, type GrDeltaTone } from './deltaTone'\nimport {\n deltaArrowClass,\n deltaEmptyClass,\n deltaRootClass,\n deltaSizeClass,\n deltaValueStyle,\n deltaToneClass,\n} from './grDeltaStyles'\n\nimport type { GrComponentSize } from '../shared/sizes'\n\nexport type { GrDeltaPolarity, GrDeltaTone } from './deltaTone'\n\n/**\n * Величина со знаком и тоном **внутри строки текста**: «Дельта: +$0.004».\n *\n * Компонент презентационный: дельту не считает и периоды не сравнивает —\n * «к чему сравнивать» знает приложение. Блочная плитка показателя — `GrStatistic`.\n */\nexport interface GrDeltaProps {\n /** Величина. `null` — её нет; это не ноль. */\n value: number | null\n /** Знаков после запятой. Не задано — как отдаст локаль. */\n precision?: number\n /** Приписка перед числом: валюта, единица. */\n prefix?: string\n /** Приписка после числа: `%`, `мс`. */\n suffix?: string\n /**\n * Что считать хорошим. Для выручки рост — успех, для себестоимости и времени\n * отклика — наоборот; без этого компонент врал бы в половине случаев.\n */\n polarity?: GrDeltaPolarity\n /** Ставить `+` у положительных. Минус подставляет `Intl` сам. */\n showSign?: boolean\n /** Стрелка направления. Декоративна: направление уже в знаке. */\n showArrow?: boolean\n /** Чем печатать отсутствующую величину. */\n emptyText?: string\n /** Локаль форматирования. Не задана — локаль i18n-адаптера. */\n locale?: string\n size?: GrComponentSize\n}\n\nconst props = withDefaults(defineProps<GrDeltaProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n precision: undefined,\n prefix: undefined,\n suffix: undefined,\n polarity: undefined,\n showSign: undefined,\n showArrow: undefined,\n emptyText: undefined,\n locale: undefined,\n size: undefined,\n})\n\nconst { locale } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrDelta' })\nconst resolvedLocale = computed(() => props.locale ?? locale.value ?? 'en')\nconst resolvedPolarity = useGrComponentProp('GrDelta', 'polarity', () => props.polarity, 'positive-good')\nconst resolvedShowSign = useGrComponentProp('GrDelta', 'showSign', () => props.showSign, true)\nconst resolvedShowArrow = useGrComponentProp('GrDelta', 'showArrow', () => props.showArrow, false)\nconst resolvedEmptyText = useGrComponentProp('GrDelta', 'emptyText', () => props.emptyText, '—')\n\nconst isEmpty = computed(() => props.value === null || props.value === undefined)\n\nconst tone = computed<GrDeltaTone>(() => (\n isEmpty.value ? 'neutral' : deltaTone(props.value as number, resolvedPolarity.value)\n))\n\nconst direction = computed(() => (isEmpty.value ? 'flat' : deltaDirection(props.value as number)))\n\nconst formatOptions = computed<Intl.NumberFormatOptions>(() => ({\n signDisplay: resolvedShowSign.value ? 'exceptZero' : 'auto',\n ...(props.precision === undefined\n ? {}\n : { minimumFractionDigits: props.precision, maximumFractionDigits: props.precision }),\n}))\n\n/**\n * Знак и число по отдельности.\n *\n * Знак — префикс форматирования, а не склейка строки. `'+' + value` уже ломало\n * `GrStatistic`: строка `'+10'` разбирается как число, и разряды теряются.\n * Поэтому знак не дописывают, а **вынимают** из готовых частей `Intl`.\n *\n * Делится величина только вместе с `prefix`: валюта обязана стоять после знака\n * (`+$0,0280`, а не `$+0,0280`). Без приписки делить нечего — знак остаётся\n * внутри величины, и разметка не меняется ни на байт.\n */\nconst valueNodes = computed<{ sign: string, value: string }>(() => {\n if (isEmpty.value) return { sign: '', value: resolvedEmptyText.value }\n\n const parts = formatNumberToParts(resolvedLocale.value, props.value as number, formatOptions.value)\n\n if (!props.prefix) return { sign: '', value: parts.map(part => part.value).join('') }\n\n const { sign, rest } = splitLeadingSign(parts)\n\n return { sign, value: rest }\n})\n\nconst sign = computed(() => valueNodes.value.sign)\nconst formatted = computed(() => valueNodes.value.value)\n\nconst arrowIcon = computed(() => {\n if (direction.value === 'up') return IconTrendingUp\n if (direction.value === 'down') return IconTrendingDown\n return IconMinus\n})\n\nconst rootClass = computed(() => [\n deltaRootClass,\n deltaSizeClass[resolvedSize.value],\n isEmpty.value ? deltaEmptyClass : deltaToneClass[tone.value],\n].filter(Boolean).join(' '))\n</script>\n\n<template>\n <span data-gr-delta :data-tone=\"isEmpty ? undefined : tone\" :class=\"rootClass\" :style=\"deltaValueStyle\">\n <component\n :is=\"arrowIcon\"\n v-if=\"resolvedShowArrow && !isEmpty\"\n data-gr-delta-arrow\n :class=\"deltaArrowClass\"\n aria-hidden=\"true\"\n />\n\n <!-- Запись величины рисует `GrValue`: приписки оформляются там же, где у\n остальных величин пакета, и настраиваются теми же токенами. Знак идёт\n в `#lead` — он обязан встать перед валютой, а не после неё. -->\n <GrValue\n :value=\"formatted\"\n :prefix=\"isEmpty ? undefined : prefix\"\n :suffix=\"isEmpty ? undefined : suffix\"\n >\n <template v-if=\"sign\" #lead>\n <span data-gr-delta-sign>{{ sign }}</span>\n </template>\n </GrValue>\n </span>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport { formatNumberToParts, splitLeadingSign } from '../../internal/granularityFormat'\nimport GrValue from '../GrValue'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\n\nimport IconMinus from '~icons/lucide/minus'\nimport IconTrendingDown from '~icons/lucide/trending-down'\nimport IconTrendingUp from '~icons/lucide/trending-up'\n\nimport { deltaDirection, deltaTone, type GrDeltaPolarity, type GrDeltaTone } from './deltaTone'\nimport {\n deltaArrowClass,\n deltaEmptyClass,\n deltaRootClass,\n deltaSizeClass,\n deltaValueStyle,\n deltaToneClass,\n} from './grDeltaStyles'\n\nimport type { GrComponentSize } from '../shared/sizes'\n\nexport type { GrDeltaPolarity, GrDeltaTone } from './deltaTone'\n\n/**\n * Величина со знаком и тоном **внутри строки текста**: «Дельта: +$0.004».\n *\n * Компонент презентационный: дельту не считает и периоды не сравнивает —\n * «к чему сравнивать» знает приложение. Блочная плитка показателя — `GrStatistic`.\n */\nexport interface GrDeltaProps {\n /** Величина. `null` — её нет; это не ноль. */\n value: number | null\n /** Знаков после запятой. Не задано — как отдаст локаль. */\n precision?: number\n /** Приписка перед числом: валюта, единица. */\n prefix?: string\n /** Приписка после числа: `%`, `мс`. */\n suffix?: string\n /**\n * Что считать хорошим. Для выручки рост — успех, для себестоимости и времени\n * отклика — наоборот; без этого компонент врал бы в половине случаев.\n */\n polarity?: GrDeltaPolarity\n /** Ставить `+` у положительных. Минус подставляет `Intl` сам. */\n showSign?: boolean\n /** Стрелка направления. Декоративна: направление уже в знаке. */\n showArrow?: boolean\n /** Чем печатать отсутствующую величину. */\n emptyText?: string\n /** Локаль форматирования. Не задана — локаль i18n-адаптера. */\n locale?: string\n size?: GrComponentSize\n}\n\nconst props = withDefaults(defineProps<GrDeltaProps>(), {\n // Дефолты живут в резолверах ниже: Vue подставил бы свой раньше, чем\n // компонент заглянет в `GrConfigProvider`.\n precision: undefined,\n prefix: undefined,\n suffix: undefined,\n polarity: undefined,\n showSign: undefined,\n showArrow: undefined,\n emptyText: undefined,\n locale: undefined,\n size: undefined,\n})\n\nconst { locale } = useGranularityTranslations()\n\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrDelta' })\nconst resolvedLocale = computed(() => props.locale ?? locale.value ?? 'en')\nconst resolvedPolarity = useGrComponentProp('GrDelta', 'polarity', () => props.polarity, 'positive-good')\nconst resolvedShowSign = useGrComponentProp('GrDelta', 'showSign', () => props.showSign, true)\nconst resolvedShowArrow = useGrComponentProp('GrDelta', 'showArrow', () => props.showArrow, false)\nconst resolvedEmptyText = useGrComponentProp('GrDelta', 'emptyText', () => props.emptyText, '—')\n\nconst isEmpty = computed(() => props.value === null || props.value === undefined)\n\nconst tone = computed<GrDeltaTone>(() => (\n isEmpty.value ? 'neutral' : deltaTone(props.value as number, resolvedPolarity.value)\n))\n\nconst direction = computed(() => (isEmpty.value ? 'flat' : deltaDirection(props.value as number)))\n\nconst formatOptions = computed<Intl.NumberFormatOptions>(() => ({\n signDisplay: resolvedShowSign.value ? 'exceptZero' : 'auto',\n ...(props.precision === undefined\n ? {}\n : { minimumFractionDigits: props.precision, maximumFractionDigits: props.precision }),\n}))\n\n/**\n * Знак и число по отдельности.\n *\n * Знак — префикс форматирования, а не склейка строки. `'+' + value` уже ломало\n * `GrStatistic`: строка `'+10'` разбирается как число, и разряды теряются.\n * Поэтому знак не дописывают, а **вынимают** из готовых частей `Intl`.\n *\n * Делится величина только вместе с `prefix`: валюта обязана стоять после знака\n * (`+$0,0280`, а не `$+0,0280`). Без приписки делить нечего — знак остаётся\n * внутри величины, и разметка не меняется ни на байт.\n */\nconst valueNodes = computed<{ sign: string, value: string }>(() => {\n if (isEmpty.value) return { sign: '', value: resolvedEmptyText.value }\n\n const parts = formatNumberToParts(resolvedLocale.value, props.value as number, formatOptions.value)\n\n if (!props.prefix) return { sign: '', value: parts.map(part => part.value).join('') }\n\n const { sign, rest } = splitLeadingSign(parts)\n\n return { sign, value: rest }\n})\n\nconst sign = computed(() => valueNodes.value.sign)\nconst formatted = computed(() => valueNodes.value.value)\n\nconst arrowIcon = computed(() => {\n if (direction.value === 'up') return IconTrendingUp\n if (direction.value === 'down') return IconTrendingDown\n return IconMinus\n})\n\nconst rootClass = computed(() => [\n deltaRootClass,\n deltaSizeClass[resolvedSize.value],\n isEmpty.value ? deltaEmptyClass : deltaToneClass[tone.value],\n].filter(Boolean).join(' '))\n</script>\n\n<template>\n <span data-gr-delta :data-tone=\"isEmpty ? undefined : tone\" :class=\"rootClass\" :style=\"deltaValueStyle\">\n <component\n :is=\"arrowIcon\"\n v-if=\"resolvedShowArrow && !isEmpty\"\n data-gr-delta-arrow\n :class=\"deltaArrowClass\"\n aria-hidden=\"true\"\n />\n\n <!-- Запись величины рисует `GrValue`: приписки оформляются там же, где у\n остальных величин пакета, и настраиваются теми же токенами. Знак идёт\n в `#lead` — он обязан встать перед валютой, а не после неё. -->\n <GrValue\n :value=\"formatted\"\n :prefix=\"isEmpty ? undefined : prefix\"\n :suffix=\"isEmpty ? undefined : suffix\"\n >\n <template v-if=\"sign\" #lead>\n <span data-gr-delta-sign>{{ sign }}</span>\n </template>\n </GrValue>\n </span>\n</template>\n"],"mappings":"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"}