@feugene/granularity 0.27.0 → 0.27.2

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 (37) hide show
  1. package/dist/components/GrColorPicker/chunks/{GrColorPicker-0n_KGeUy.js → GrColorPicker-CE-xtNP-.js} +1 -1
  2. package/dist/components/GrColorPicker/chunks/{GrColorPicker-0n_KGeUy.js.map → GrColorPicker-CE-xtNP-.js.map} +1 -1
  3. package/dist/components/GrColorPicker/index.js +1 -1
  4. package/dist/components/GrDialogService/chunks/{GrDialogService-B--br5so.js → GrDialogService-k4SYIv64.js} +1 -1
  5. package/dist/components/GrDialogService/chunks/{GrDialogService-B--br5so.js.map → GrDialogService-k4SYIv64.js.map} +1 -1
  6. package/dist/components/GrDialogService/index.js +1 -1
  7. package/dist/components/GrInput/chunks/{GrInput-c53iltz0.js → GrInput-BqqoEl1b.js} +1 -1
  8. package/dist/components/GrInput/chunks/GrInput-BqqoEl1b.js.map +1 -0
  9. package/dist/components/GrInput/index.js +1 -1
  10. package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-ClTvjlFx.js → GrJsonViewer-BoldivFc.js} +1 -1
  11. package/dist/components/GrJsonViewer/chunks/{GrJsonViewer-ClTvjlFx.js.map → GrJsonViewer-BoldivFc.js.map} +1 -1
  12. package/dist/components/GrJsonViewer/index.js +1 -1
  13. package/dist/components/GrPagination/chunks/{GrPagination-CaNTZc-v.js → GrPagination-kQhWlfs-.js} +1 -1
  14. package/dist/components/GrPagination/chunks/{GrPagination-CaNTZc-v.js.map → GrPagination-kQhWlfs-.js.map} +1 -1
  15. package/dist/components/GrPagination/index.js +1 -1
  16. package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-DMhALhSS.js → GrPromptDialog-D1ADRFxx.js} +1 -1
  17. package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-DMhALhSS.js.map → GrPromptDialog-D1ADRFxx.js.map} +1 -1
  18. package/dist/components/GrPromptDialog/index.js +1 -1
  19. package/dist/components/GrSelect/chunks/{GrSelect-BE1VNTiY.js → GrSelect-D-KU9Lx_.js} +1 -1
  20. package/dist/components/GrSelect/chunks/{GrSelect-BE1VNTiY.js.map → GrSelect-D-KU9Lx_.js.map} +1 -1
  21. package/dist/components/GrSelect/index.js +1 -1
  22. package/dist/components/GrSwitch/chunks/{GrSwitch-KRlRMwgp.js → GrSwitch-g_OOSkh7.js} +4 -1
  23. package/dist/components/GrSwitch/chunks/GrSwitch-g_OOSkh7.js.map +1 -0
  24. package/dist/components/GrSwitch/index.js +1 -1
  25. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-Db_uKdNa.js → GrTreeSelect-CS1WQWsT.js} +1 -1
  26. package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-Db_uKdNa.js.map → GrTreeSelect-CS1WQWsT.js.map} +1 -1
  27. package/dist/components/GrTreeSelect/index.js +1 -1
  28. package/dist/index.js +9 -9
  29. package/dist/types/components/GrInput/GrInput.vue.d.ts +9 -1
  30. package/dist/types/components/GrRadioGroup/index.d.ts +1 -1
  31. package/dist/types/components/GrSwitch/GrSwitch.vue.d.ts +9 -1
  32. package/dist/vue/index.js +2 -0
  33. package/dist/vue/index.js.map +1 -1
  34. package/dist/web-types.json +19 -17
  35. package/package.json +3 -3
  36. package/dist/components/GrInput/chunks/GrInput-c53iltz0.js.map +0 -1
  37. package/dist/components/GrSwitch/chunks/GrSwitch-KRlRMwgp.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/vue/plugin.ts"],"sourcesContent":["import type { App, Component, Directive, Plugin } from 'vue'\n\n/**\n * Лёгкий контейнер, который может быть зарегистрирован в Vue:\n * - SFC-компонент (Vue сам компилирует `__name` из имени файла) — достаточно просто передать сам компонент,\n * - SFC-компонент с опциональным `install` (по аналогии с Element Plus / Naive UI),\n * - объект с собственным `install`,\n * - либо объект `{ name, component }`, если имя нужно задать явно.\n */\nexport type GranularityInstallableComponent =\n | (Component & { name?: string; __name?: string; install?: (app: App) => void })\n | { install: (app: App) => void }\n | { name: string; component: Component }\n\n/**\n * Директива — либо пара `{ name, directive }`, либо объект с `install`.\n */\nexport type GranularityInstallableDirective =\n | { name: string; directive: Directive }\n | { install: (app: App) => void }\n\nexport interface CreateGranularityOptions {\n /**\n * Список компонентов для глобальной регистрации.\n * Пакет сам по себе ничего не импортирует — пользователь передаёт сюда только те компоненты,\n * которые ему действительно нужны. За счёт этого tree-shaking остаётся полным.\n */\n components?: GranularityInstallableComponent[]\n /**\n * Список директив. Передавайте ровно те, что нужны, по аналогии с компонентами.\n */\n directives?: GranularityInstallableDirective[]\n /**\n * Произвольные `provide`-значения. Ключи могут быть `symbol` или строкой.\n * Пакет не навязывает конкретные ключи (например, i18n) — выбор за пользователем.\n */\n provides?: Array<{ key: string | symbol; value: unknown }>\n /**\n * Произвольные `app.config.globalProperties` — по умолчанию ничего не ставим.\n */\n globalProperties?: Record<string, unknown>\n}\n\n/**\n * Вспомогательная обёртка: превращает произвольный компонент в `GranularityInstallableComponent`\n * с явным именем регистрации.\n *\n * ```ts\n * import { GrButton } from '@feugene/granularity/components/GrButton'\n * import { defineInstallable } from '@feugene/granularity/vue'\n *\n * const installable = defineInstallable(GrButton, 'GrButton')\n * ```\n */\nexport function defineInstallable<T extends Component>(component: T, name: string): GranularityInstallableComponent {\n return {\n install(app: App) {\n app.component(name, component)\n },\n }\n}\n\nfunction installComponent(app: App, c: GranularityInstallableComponent): void {\n if (typeof (c as any).install === 'function') {\n ;(c as any).install(app)\n return\n }\n\n // Форма `{ name, component }` — явное указание имени для произвольного компонента.\n // Допускаем как object-компоненты (SFC / options), так и functional-компоненты.\n if (\n typeof (c as any).name === 'string'\n && (c as any).component\n && (typeof (c as any).component === 'object' || typeof (c as any).component === 'function')\n ) {\n const {name, component} = c as { name: string; component: Component }\n app.component(name, component)\n return\n }\n\n // SFC: Vue 3 компилирует имя компонента из имени файла в поле `__name`\n // (например, `GrButton.vue` → `__name: 'GrButton'`). Также поддерживаем явное `name`.\n const resolvedName\n = (c as Component & { name?: string }).name\n ?? (c as { __name?: string }).__name\n if (resolvedName) {\n app.component(resolvedName, c as Component)\n return\n }\n\n // Сознательно без throw — просто сообщаем в dev-режиме.\n \n console.warn('[granularity] component passed to createGranularity has no `install`, `name`, or `__name` — skipped.')\n}\n\nfunction installDirective(app: App, d: GranularityInstallableDirective): void {\n if ('install' in d && typeof d.install === 'function') {\n d.install(app)\n return\n }\n\n if ('name' in d && 'directive' in d) {\n app.directive(d.name, d.directive)\n return\n }\n\n // Сознательно без throw — согласовано с `installComponent`.\n \n console.warn('[granularity] directive passed to createGranularity has no `install` or `{ name, directive }` — skipped.')\n}\n\n/**\n * Применяет опции к переданному приложению. Экспортируется отдельно от фабрики —\n * удобно, когда плагин не нужен (например, в тестах).\n */\nexport function installGranularity(app: App, options: CreateGranularityOptions = {}): void {\n for (const c of options.components ?? [])\n installComponent(app, c)\n\n for (const d of options.directives ?? [])\n installDirective(app, d)\n\n for (const {key, value} of options.provides ?? [])\n app.provide(key as any, value)\n\n if (options.globalProperties) {\n for (const [key, value] of Object.entries(options.globalProperties))\n app.config.globalProperties[key] = value\n }\n}\n\n/**\n * Создаёт Vue-плагин `{ install }`, который при `app.use(...)` зарегистрирует переданные\n * компоненты, директивы и provide-значения. Сам модуль НЕ импортирует ни компонентов,\n * ни директив из пакета — всё приходит снаружи. Это сохраняет гранулярность:\n * бандлер увидит только те символы, которые вы реально передали.\n *\n * ```ts\n * import { createApp } from 'vue'\n * import { createGranularity } from '@feugene/granularity/vue'\n * import { GrButton } from '@feugene/granularity/components/GrButton'\n * import { GrInput } from '@feugene/granularity/components/GrInput'\n * import { vHotkey } from '@feugene/granularity/directives/hotkey'\n *\n * createApp(App)\n * .use(createGranularity({\n * // Достаточно передать сами компоненты — имя будет взято из `__name` (Vue SFC)\n * // или из `name`; можно также указать явно через `{ name, component }`.\n * components: [GrButton, GrInput],\n * directives: [{ name: 'hotkey', directive: vHotkey }],\n * }))\n * .mount('#app')\n * ```\n */\nexport function createGranularity(options: CreateGranularityOptions = {}): Plugin {\n return {\n install(app: App) {\n installGranularity(app, options)\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;AAsDA,SAAgB,kBAAuC,WAAc,MAA+C;CAChH,OAAO,EACH,QAAQ,KAAU;EACd,IAAI,UAAU,MAAM,SAAS;CACjC,EACJ;AACJ;AAEA,SAAS,iBAAiB,KAAU,GAA0C;CAC1E,IAAI,OAAQ,EAAU,YAAY,YAAY;EACzC,EAAW,QAAQ,GAAG;EACvB;CACJ;CAIA,IACI,OAAQ,EAAU,SAAS,YACvB,EAAU,cACV,OAAQ,EAAU,cAAc,YAAY,OAAQ,EAAU,cAAc,aAClF;EACE,MAAM,EAAC,MAAM,cAAa;EAC1B,IAAI,UAAU,MAAM,SAAS;EAC7B;CACJ;CAIA,MAAM,eACC,EAAoC,QAC/B,EAA0B;CACtC,IAAI,cAAc;EACd,IAAI,UAAU,cAAc,CAAc;EAC1C;CACJ;CAIA,QAAQ,KAAK,sGAAsG;AACvH;AAEA,SAAS,iBAAiB,KAAU,GAA0C;CAC1E,IAAI,aAAa,KAAK,OAAO,EAAE,YAAY,YAAY;EACnD,EAAE,QAAQ,GAAG;EACb;CACJ;CAEA,IAAI,UAAU,KAAK,eAAe,GAAG;EACjC,IAAI,UAAU,EAAE,MAAM,EAAE,SAAS;EACjC;CACJ;CAIA,QAAQ,KAAK,0GAA0G;AAC3H;;;;;AAMA,SAAgB,mBAAmB,KAAU,UAAoC,CAAC,GAAS;CACvF,KAAK,MAAM,KAAK,QAAQ,cAAc,CAAC,GACnC,iBAAiB,KAAK,CAAC;CAE3B,KAAK,MAAM,KAAK,QAAQ,cAAc,CAAC,GACnC,iBAAiB,KAAK,CAAC;CAE3B,KAAK,MAAM,EAAC,KAAK,WAAU,QAAQ,YAAY,CAAC,GAC5C,IAAI,QAAQ,KAAY,KAAK;CAEjC,IAAI,QAAQ,kBACR,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,gBAAgB,GAC9D,IAAI,OAAO,iBAAiB,OAAO;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,kBAAkB,UAAoC,CAAC,GAAW;CAC9E,OAAO,EACH,QAAQ,KAAU;EACd,mBAAmB,KAAK,OAAO;CACnC,EACJ;AACJ"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/vue/plugin.ts"],"sourcesContent":["import type { App, Component, Directive, Plugin } from 'vue'\n\n/**\n * Лёгкий контейнер, который может быть зарегистрирован в Vue:\n * - SFC-компонент (Vue сам компилирует `__name` из имени файла) — достаточно просто передать сам компонент,\n * - SFC-компонент с опциональным `install` (по аналогии с Element Plus / Naive UI),\n * - объект с собственным `install`,\n * - либо объект `{ name, component }`, если имя нужно задать явно.\n */\nexport type GranularityInstallableComponent =\n | (Component & { name?: string; __name?: string; install?: (app: App) => void })\n | { install: (app: App) => void }\n | { name: string; component: Component }\n\n/**\n * Директива — либо пара `{ name, directive }`, либо объект с `install`.\n */\nexport type GranularityInstallableDirective =\n | { name: string; directive: Directive }\n | { install: (app: App) => void }\n\nexport interface CreateGranularityOptions {\n /**\n * Список компонентов для глобальной регистрации.\n * Пакет сам по себе ничего не импортирует — пользователь передаёт сюда только те компоненты,\n * которые ему действительно нужны. За счёт этого tree-shaking остаётся полным.\n */\n components?: GranularityInstallableComponent[]\n /**\n * Список директив. Передавайте ровно те, что нужны, по аналогии с компонентами.\n */\n directives?: GranularityInstallableDirective[]\n /**\n * Произвольные `provide`-значения. Ключи могут быть `symbol` или строкой.\n * Пакет не навязывает конкретные ключи (например, i18n) — выбор за пользователем.\n */\n provides?: Array<{ key: string | symbol; value: unknown }>\n /**\n * Произвольные `app.config.globalProperties` — по умолчанию ничего не ставим.\n */\n globalProperties?: Record<string, unknown>\n}\n\n/**\n * Вспомогательная обёртка: превращает произвольный компонент в `GranularityInstallableComponent`\n * с явным именем регистрации.\n *\n * ```ts\n * import { GrButton } from '@feugene/granularity/components/GrButton'\n * import { defineInstallable } from '@feugene/granularity/vue'\n *\n * const installable = defineInstallable(GrButton, 'GrButton')\n * ```\n */\nexport function defineInstallable<T extends Component>(component: T, name: string): GranularityInstallableComponent {\n return {\n install(app: App) {\n app.component(name, component)\n },\n }\n}\n\nfunction installComponent(app: App, c: GranularityInstallableComponent): void {\n if (typeof (c as any).install === 'function') {\n ;(c as any).install(app)\n return\n }\n\n // Форма `{ name, component }` — явное указание имени для произвольного компонента.\n // Допускаем как object-компоненты (SFC / options), так и functional-компоненты.\n if (\n typeof (c as any).name === 'string'\n && (c as any).component\n && (typeof (c as any).component === 'object' || typeof (c as any).component === 'function')\n ) {\n const {name, component} = c as { name: string; component: Component }\n app.component(name, component)\n return\n }\n\n // SFC: Vue 3 компилирует имя компонента из имени файла в поле `__name`\n // (например, `GrButton.vue` → `__name: 'GrButton'`). Также поддерживаем явное `name`.\n const resolvedName\n = (c as Component & { name?: string }).name\n ?? (c as { __name?: string }).__name\n if (resolvedName) {\n app.component(resolvedName, c as Component)\n return\n }\n\n // Сознательно без throw — просто сообщаем в dev-режиме.\n if (!__GR_DEV__) return\n\n console.warn('[granularity] component passed to createGranularity has no `install`, `name`, or `__name` — skipped.')\n}\n\nfunction installDirective(app: App, d: GranularityInstallableDirective): void {\n if ('install' in d && typeof d.install === 'function') {\n d.install(app)\n return\n }\n\n if ('name' in d && 'directive' in d) {\n app.directive(d.name, d.directive)\n return\n }\n\n // Сознательно без throw — согласовано с `installComponent`.\n if (!__GR_DEV__) return\n\n console.warn('[granularity] directive passed to createGranularity has no `install` or `{ name, directive }` — skipped.')\n}\n\n/**\n * Применяет опции к переданному приложению. Экспортируется отдельно от фабрики —\n * удобно, когда плагин не нужен (например, в тестах).\n */\nexport function installGranularity(app: App, options: CreateGranularityOptions = {}): void {\n for (const c of options.components ?? [])\n installComponent(app, c)\n\n for (const d of options.directives ?? [])\n installDirective(app, d)\n\n for (const {key, value} of options.provides ?? [])\n app.provide(key as any, value)\n\n if (options.globalProperties) {\n for (const [key, value] of Object.entries(options.globalProperties))\n app.config.globalProperties[key] = value\n }\n}\n\n/**\n * Создаёт Vue-плагин `{ install }`, который при `app.use(...)` зарегистрирует переданные\n * компоненты, директивы и provide-значения. Сам модуль НЕ импортирует ни компонентов,\n * ни директив из пакета — всё приходит снаружи. Это сохраняет гранулярность:\n * бандлер увидит только те символы, которые вы реально передали.\n *\n * ```ts\n * import { createApp } from 'vue'\n * import { createGranularity } from '@feugene/granularity/vue'\n * import { GrButton } from '@feugene/granularity/components/GrButton'\n * import { GrInput } from '@feugene/granularity/components/GrInput'\n * import { vHotkey } from '@feugene/granularity/directives/hotkey'\n *\n * createApp(App)\n * .use(createGranularity({\n * // Достаточно передать сами компоненты — имя будет взято из `__name` (Vue SFC)\n * // или из `name`; можно также указать явно через `{ name, component }`.\n * components: [GrButton, GrInput],\n * directives: [{ name: 'hotkey', directive: vHotkey }],\n * }))\n * .mount('#app')\n * ```\n */\nexport function createGranularity(options: CreateGranularityOptions = {}): Plugin {\n return {\n install(app: App) {\n installGranularity(app, options)\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;AAsDA,SAAgB,kBAAuC,WAAc,MAA+C;CAChH,OAAO,EACH,QAAQ,KAAU;EACd,IAAI,UAAU,MAAM,SAAS;CACjC,EACJ;AACJ;AAEA,SAAS,iBAAiB,KAAU,GAA0C;CAC1E,IAAI,OAAQ,EAAU,YAAY,YAAY;EACzC,EAAW,QAAQ,GAAG;EACvB;CACJ;CAIA,IACI,OAAQ,EAAU,SAAS,YACvB,EAAU,cACV,OAAQ,EAAU,cAAc,YAAY,OAAQ,EAAU,cAAc,aAClF;EACE,MAAM,EAAC,MAAM,cAAa;EAC1B,IAAI,UAAU,MAAM,SAAS;EAC7B;CACJ;CAIA,MAAM,eACC,EAAoC,QAC/B,EAA0B;CACtC,IAAI,cAAc;EACd,IAAI,UAAU,cAAc,CAAc;EAC1C;CACJ;CAGA,IAAI,EAAA,OAAA,YAAA,eAAA,QAAA,IAAA,aAAA,eAAa;CAEjB,QAAQ,KAAK,sGAAsG;AACvH;AAEA,SAAS,iBAAiB,KAAU,GAA0C;CAC1E,IAAI,aAAa,KAAK,OAAO,EAAE,YAAY,YAAY;EACnD,EAAE,QAAQ,GAAG;EACb;CACJ;CAEA,IAAI,UAAU,KAAK,eAAe,GAAG;EACjC,IAAI,UAAU,EAAE,MAAM,EAAE,SAAS;EACjC;CACJ;CAGA,IAAI,EAAA,OAAA,YAAA,eAAA,QAAA,IAAA,aAAA,eAAa;CAEjB,QAAQ,KAAK,0GAA0G;AAC3H;;;;;AAMA,SAAgB,mBAAmB,KAAU,UAAoC,CAAC,GAAS;CACvF,KAAK,MAAM,KAAK,QAAQ,cAAc,CAAC,GACnC,iBAAiB,KAAK,CAAC;CAE3B,KAAK,MAAM,KAAK,QAAQ,cAAc,CAAC,GACnC,iBAAiB,KAAK,CAAC;CAE3B,KAAK,MAAM,EAAC,KAAK,WAAU,QAAQ,YAAY,CAAC,GAC5C,IAAI,QAAQ,KAAY,KAAK;CAEjC,IAAI,QAAQ,kBACR,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,gBAAgB,GAC9D,IAAI,OAAO,iBAAiB,OAAO;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,kBAAkB,UAAoC,CAAC,GAAW;CAC9E,OAAO,EACH,QAAQ,KAAU;EACd,mBAAmB,KAAK,OAAO;CACnC,EACJ;AACJ"}
@@ -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.0",
4
+ "version": "0.27.2",
5
5
  "framework": "vue",
6
6
  "js-types-syntax": "typescript",
7
7
  "description-markup": "markdown",
@@ -5297,6 +5297,15 @@
5297
5297
  "type": "\"number\" | \"search\" | \"text\" | \"email\" | \"password\" | \"tel\" | \"url\" | undefined"
5298
5298
  }
5299
5299
  },
5300
+ {
5301
+ "name": "modelValue",
5302
+ "description": "Значение поля. Необязательное: без `v-model` поле рисуется пустым. Дефолт — пустая строка, а не `undefined`: длина значения читается напрямую (`showClear`), и `undefined` уронил бы рендер.",
5303
+ "default": "\"\"",
5304
+ "value": {
5305
+ "kind": "expression",
5306
+ "type": "string | undefined"
5307
+ }
5308
+ },
5300
5309
  {
5301
5310
  "name": "disabled",
5302
5311
  "default": "false",
@@ -5523,14 +5532,6 @@
5523
5532
  "kind": "expression",
5524
5533
  "type": "GrInputTextAlign | undefined"
5525
5534
  }
5526
- },
5527
- {
5528
- "name": "modelValue",
5529
- "required": true,
5530
- "value": {
5531
- "kind": "expression",
5532
- "type": "string"
5533
- }
5534
5535
  }
5535
5536
  ],
5536
5537
  "slots": [
@@ -9899,6 +9900,15 @@
9899
9900
  "symbol": "GrSwitch"
9900
9901
  },
9901
9902
  "props": [
9903
+ {
9904
+ "name": "modelValue",
9905
+ "description": "Значение переключателя. Необязательное: без `v-model` контрол рисуется выключенным — так же, как `GrCheckbox`. Обязательный проп заставлял бы заводить `ref` даже там, где переключатель presentational.",
9906
+ "default": "false",
9907
+ "value": {
9908
+ "kind": "expression",
9909
+ "type": "boolean | undefined"
9910
+ }
9911
+ },
9902
9912
  {
9903
9913
  "name": "disabled",
9904
9914
  "default": "false",
@@ -10021,14 +10031,6 @@
10021
10031
  "kind": "expression",
10022
10032
  "type": "string | undefined"
10023
10033
  }
10024
- },
10025
- {
10026
- "name": "modelValue",
10027
- "required": true,
10028
- "value": {
10029
- "kind": "expression",
10030
- "type": "boolean"
10031
- }
10032
10034
  }
10033
10035
  ],
10034
10036
  "slots": [
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.0",
4
+ "version": "0.27.2",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": {
7
7
  "name": "Evgeniy Fureev",
@@ -523,7 +523,7 @@
523
523
  "devDependencies": {
524
524
  "@antfu/eslint-config": "^9.1.0",
525
525
  "@feugene/fint-i18n": "^0.6.0",
526
- "@feugene/granularity-test-kit": "^0.2.0",
526
+ "@feugene/granularity-test-kit": "^0.4.0",
527
527
  "@feugene/unocss-preset-granular": "^0.9.1",
528
528
  "@floating-ui/dom": "^1.8.0",
529
529
  "@iconify-json/lucide": "^1.2.117",
@@ -545,7 +545,7 @@
545
545
  "vue-tsc": "^3.3.7"
546
546
  },
547
547
  "scripts": {
548
- "build": "vite build && node scripts/check-dist-dev-guard.mjs && node scripts/generate-web-types.mjs && vue-tsc -p tsconfig.build.json",
548
+ "build": "vite build && gr-check-dist-dev-guard && node scripts/check-dist-no-tests.mjs && node scripts/generate-web-types.mjs && vue-tsc -p tsconfig.build.json",
549
549
  "check:messages": "fint-i18n-check-messages src/i18n/locales",
550
550
  "dev": "vite build --watch",
551
551
  "doctor": "granular doctor ./granular.options.mjs --strict",
@@ -1 +0,0 @@
1
- {"version":3,"file":"GrInput-c53iltz0.js","names":["$slots","$attrs"],"sources":["../../../../src/components/GrInput/GrInput.vue","../../../../src/components/GrInput/GrInput.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type { GrComponentSize } from '../shared/sizes'\nimport { computed, ref, useId } from 'vue'\n\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport type { InputHTMLAttributes } from 'vue'\n\nimport { grInputFieldClass, grInputShellClass, paddingX } from './grInputStyles'\n\nimport IconLoader from '~icons/lucide/loader-2'\nimport IconX from '~icons/lucide/x'\nimport IconEye from '~icons/lucide/eye'\nimport IconEyeOff from '~icons/lucide/eye-off'\n\nexport interface GrInputProps {\n modelValue: string\n type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url'\n placeholder?: string\n autocomplete?: string\n inputmode?: InputHTMLAttributes['inputmode']\n disabled?: boolean\n /** Только для чтения: значение видно и выделяемо, но не редактируется. */\n readonly?: boolean\n invalid?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n state?: 'default' | 'success' | 'warning' | 'danger'\n name?: string\n id?: string\n size?: GrInputSize\n\n /** Показывать кнопку очистки, когда есть значение (и не disabled/readonly). */\n clearable?: boolean\n /** i18n aria-label кнопки очистки. */\n clearLabel?: string\n /** Ограничение длины + основа для счётчика символов. */\n maxlength?: number\n /** Показывать счётчик символов (`len` или `len/maxlength`). */\n showCount?: boolean\n /**\n * Фоновая работа по полю (проверка занятости логина, автосохранение):\n * спиннер в trailing-области + `aria-busy`. Ввод не блокируется —\n * для этого есть `disabled`/`readonly`.\n */\n loading?: boolean\n /** Кнопка показать/скрыть пароль (только при `type=\"password\"`). */\n passwordToggle?: boolean\n /** i18n aria-label кнопки показать/скрыть пароль. */\n passwordShowLabel?: string\n passwordHideLabel?: string\n\n textAlign?: GrInputTextAlign\n\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n /**\n * Фиксированная ширина у prefix/suffix: аддон получает жёсткую ширину\n * (из `*MaxWidth` → `*MinWidth` → дефолт), а контент обрезается по краю\n * (prefix — справа, suffix — слева). По умолчанию аддоны «растягиваются»\n * под контент (в пределах min/max), а излишек клипается оболочкой.\n */\n prefixFixed?: boolean\n suffixFixed?: boolean\n}\n\nexport interface GrInputEmits {\n (e: 'update:modelValue', value: string): void\n /** Значение зафиксировано нативным `change` — по `blur` или `Enter`. */\n (e: 'change', value: string): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n /**\n * Значение стёрто кнопкой очистки. Отдельное событие потому, что по\n * `update:modelValue` программную очистку от ручного стирания не отличить.\n */\n (e: 'clear'): void\n}\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nexport type GrInputSize = GrComponentSize\nexport type GrInputTextAlign = 'left' | 'center' | 'right'\n\nconst props = withDefaults(\n defineProps<GrInputProps>(),\n {\n type: 'text',\n placeholder: undefined,\n autocomplete: undefined,\n inputmode: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n state: 'default',\n name: undefined,\n id: undefined,\n size: undefined,\n\n // Настраивается через `GrConfigProvider`; дефолт — в резолвере ниже.\n clearable: undefined,\n clearLabel: undefined,\n maxlength: undefined,\n showCount: false,\n loading: false,\n passwordToggle: false,\n passwordShowLabel: undefined,\n passwordHideLabel: undefined,\n\n textAlign: 'left',\n\n prefixMinWidth: undefined,\n prefixMaxWidth: undefined,\n suffixMinWidth: undefined,\n suffixMaxWidth: undefined,\n prefixFixed: false,\n suffixFixed: false,\n },\n)\n\nconst emit = defineEmits<GrInputEmits>()\ndefineSlots<{\n /** Аддон слева от поля: иконка, код валюты, метка. */\n prefix?: () => any\n /** Аддон справа от поля: единица измерения, подсказка. */\n suffix?: () => any\n}>()\n\n\n// Контекст `GrFormField` (если инпут внутри него): даёт id/aria-describedby/\n// invalid/required как fallback, чтобы не прокидывать `forId` вручную.\nconst field = useGrFormFieldContext()\n\n// Эффективные значения: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrInput' })\nconst resolvedClearable = useGrComponentProp('GrInput', 'clearable', () => props.clearable, false)\n\nconst resolvedId = computed(() => props.id ?? field?.id.value)\n\n// Счётчик обязан быть частью описания поля: иначе «12 / 60» видно глазами, но\n// не слышно — при том, что ограничение длины и есть смысл счётчика.\nconst countId = useId()\nconst describedBy = computed(() =>\n [field?.describedById.value, props.showCount ? countId : undefined]\n .filter(Boolean)\n .join(' ') || undefined,\n)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nconst inputEl = ref<HTMLInputElement | null>(null)\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\n/** Выделить содержимое — спутник `focus()` для «подставили значение, перезапишите». */\nfunction select(): void {\n inputEl.value?.select()\n}\n\ndefineExpose({\n focus,\n blur,\n select,\n})\n\nconst ADDON_MIN_WIDTH_BY_SIZE: Record<GrInputSize, string> = {\n xs: '2rem', // w-8\n sm: '2.25rem', // w-9\n md: '2.5rem', // w-10\n lg: '3rem', // w-12\n}\n\nconst passwordVisible = ref(false)\n// Тип поля с учётом переключателя пароля.\nconst resolvedType = computed(() => (props.type === 'password' && passwordVisible.value ? 'text' : props.type))\n\nconst showPasswordToggle = computed(() => props.passwordToggle && props.type === 'password' && !isDisabled.value)\nconst showClear = computed(() => resolvedClearable.value && props.modelValue.length > 0 && !isDisabled.value && !isReadonly.value)\n\nconst trailingCount = computed(() => (showClear.value ? 1 : 0) + (showPasswordToggle.value ? 1 : 0) + (props.loading ? 1 : 0))\nconst trailingReserve = computed(() => (trailingCount.value > 0 ? `${trailingCount.value * 28}px` : '0px'))\n\nconst {\n prefixEl,\n suffixEl,\n prefixStyle,\n suffixStyle,\n fieldPadding: inputStyle,\n} = useControlAddons(() => props, {\n defaultMinWidth: () => ADDON_MIN_WIDTH_BY_SIZE[resolvedSize.value],\n paddingX: () => paddingX[resolvedSize.value],\n trailingReserve: () => trailingReserve.value,\n})\n\n// Border/ring/disabled — на оболочке (`focus-within`), размеры/выравнивание — на инпуте.\nconst shellClass = computed(() => grInputShellClass({\n state: props.state,\n invalid: isInvalid.value,\n disabled: isDisabled.value,\n}))\n\nconst className = computed(() => grInputFieldClass({\n size: resolvedSize.value,\n align: props.textAlign,\n}))\n\nfunction onInput(e: Event): void {\n emit('update:modelValue', (e.target as HTMLInputElement).value)\n}\n\n// Объявленный emit уходит из `$attrs`, поэтому нативные события переизлучаем\n// руками — иначе `@change`/`@focus`/`@blur` у потребителя перестали бы работать.\nfunction onChange(e: Event): void {\n emit('change', (e.target as HTMLInputElement).value)\n}\n\nfunction onFocus(e: FocusEvent): void {\n emit('focus', e)\n}\n\nfunction onBlur(e: FocusEvent): void {\n emit('blur', e)\n}\n\n// ————— Trailing-контролы: очистка, переключатель пароля, счётчик символов.\nconst { t } = useGranularityTranslations()\nconst resolvedClearLabel = computed(() => props.clearLabel ?? t('gr.input.clear', 'Clear'))\nconst resolvedPasswordShowLabel = computed(() => props.passwordShowLabel ?? t('gr.input.showPassword', 'Show password'))\nconst resolvedPasswordHideLabel = computed(() => props.passwordHideLabel ?? t('gr.input.hidePassword', 'Hide password'))\n\n// Счётчик символов: `len` или `len / maxlength`.\nconst countText = computed(() =>\n props.maxlength !== undefined ? `${props.modelValue.length} / ${props.maxlength}` : String(props.modelValue.length),\n)\n\n/**\n * Живой регион объявляет только исчерпание лимита. Читать вслух каждый символ\n * нельзя — диктор захлебнётся, а сам счётчик и так связан с полем через\n * `aria-describedby` и читается при фокусе.\n */\nconst limitReached = computed(() =>\n props.maxlength !== undefined && props.modelValue.length >= props.maxlength,\n)\nconst liveMessage = computed(() =>\n limitReached.value ? t('gr.input.limitReached', 'Character limit reached') : '',\n)\n\nfunction clear(): void {\n emit('update:modelValue', '')\n emit('clear')\n inputEl.value?.focus()\n}\n\nfunction togglePassword(): void {\n passwordVisible.value = !passwordVisible.value\n inputEl.value?.focus()\n}\n</script>\n\n<template>\n <div data-gr-input class=\"w-full\">\n <div :class=\"shellClass\">\n <div\n v-if=\"$slots.prefix\"\n ref=\"prefixEl\"\n data-testid=\"gr-input-prefix\"\n class=\"absolute inset-y-0 left-0 flex items-center justify-center border-r border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :style=\"prefixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"prefix\" />\n </div>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n v-bind=\"$attrs\"\n :name=\"props.name\"\n :type=\"resolvedType\"\n :inputmode=\"props.inputmode\"\n :autocomplete=\"props.autocomplete\"\n :placeholder=\"props.placeholder\"\n :disabled=\"isDisabled\"\n :readonly=\"isReadonly\"\n :maxlength=\"props.maxlength\"\n :value=\"props.modelValue\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :aria-busy=\"props.loading ? 'true' : undefined\"\n class=\"w-full bg-transparent text-[var(--gr-fg)] placeholder:text-[var(--gr-muted-fg)] focus:placeholder:text-transparent focus:outline-none disabled:cursor-not-allowed disabled:text-[var(--gr-muted-fg)]\"\n :class=\"className\"\n :style=\"inputStyle\"\n @input=\"onInput\"\n @change=\"onChange\"\n @focus=\"onFocus\"\n @blur=\"onBlur\"\n >\n\n <div\n v-if=\"trailingCount > 0\"\n data-gr-input-trailing\n class=\"absolute inset-y-0 right-1 flex items-center gap-0.5\"\n >\n <span\n v-if=\"loading\"\n data-gr-input-spinner\n class=\"flex h-6 w-6 items-center justify-center text-[var(--gr-muted-fg)]\"\n aria-hidden=\"true\"\n >\n <IconLoader class=\"h-4 w-4 animate-spin\" />\n </span>\n\n <button\n v-if=\"showClear\"\n type=\"button\"\n data-gr-input-clear\n :aria-label=\"resolvedClearLabel\"\n class=\"flex h-6 w-6 items-center justify-center rounded-[var(--gr-radius-sm)] text-[var(--gr-muted-fg)] transition-colors hover:bg-[var(--gr-muted)] hover:text-[var(--gr-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n @click=\"clear\"\n >\n <IconX class=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n\n <button\n v-if=\"showPasswordToggle\"\n type=\"button\"\n data-gr-input-password-toggle\n :aria-label=\"passwordVisible ? resolvedPasswordHideLabel : resolvedPasswordShowLabel\"\n :aria-pressed=\"passwordVisible ? 'true' : 'false'\"\n class=\"flex h-6 w-6 items-center justify-center rounded-[var(--gr-radius-sm)] text-[var(--gr-muted-fg)] transition-colors hover:bg-[var(--gr-muted)] hover:text-[var(--gr-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n @click=\"togglePassword\"\n >\n <IconEyeOff v-if=\"passwordVisible\" class=\"h-4 w-4\" aria-hidden=\"true\" />\n <IconEye v-else class=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n\n <div\n v-if=\"$slots.suffix\"\n ref=\"suffixEl\"\n data-testid=\"gr-input-suffix\"\n class=\"absolute inset-y-0 right-0 flex items-center justify-center border-l border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"suffixFixed ? '[direction:rtl]' : ''\"\n :style=\"suffixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"suffix\" />\n </div>\n </div>\n\n <div\n v-if=\"showCount\"\n :id=\"countId\"\n data-gr-input-count\n class=\"mt-1 text-right text-[length:var(--gr-control-text-xs)] leading-[var(--gr-leading-xs)] text-[var(--gr-muted-fg)] tabular-nums\"\n >\n {{ countText }}\n </div>\n\n <span\n v-if=\"showCount\"\n data-gr-input-live\n class=\"sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n >{{ liveMessage }}</span>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport type { GrComponentSize } from '../shared/sizes'\nimport { computed, ref, useId } from 'vue'\n\nimport { useControlAddons } from '../../composables/internal/useControlAddons'\nimport { useGrComponentProp, useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\nimport type { InputHTMLAttributes } from 'vue'\n\nimport { grInputFieldClass, grInputShellClass, paddingX } from './grInputStyles'\n\nimport IconLoader from '~icons/lucide/loader-2'\nimport IconX from '~icons/lucide/x'\nimport IconEye from '~icons/lucide/eye'\nimport IconEyeOff from '~icons/lucide/eye-off'\n\nexport interface GrInputProps {\n modelValue: string\n type?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url'\n placeholder?: string\n autocomplete?: string\n inputmode?: InputHTMLAttributes['inputmode']\n disabled?: boolean\n /** Только для чтения: значение видно и выделяемо, но не редактируется. */\n readonly?: boolean\n invalid?: boolean\n /** Обязательное поле (`aria-required`). Складывается с `required` у `GrFormField`. */\n required?: boolean\n /** Доступное имя вне `GrFormField`. */\n ariaLabel?: string\n state?: 'default' | 'success' | 'warning' | 'danger'\n name?: string\n id?: string\n size?: GrInputSize\n\n /** Показывать кнопку очистки, когда есть значение (и не disabled/readonly). */\n clearable?: boolean\n /** i18n aria-label кнопки очистки. */\n clearLabel?: string\n /** Ограничение длины + основа для счётчика символов. */\n maxlength?: number\n /** Показывать счётчик символов (`len` или `len/maxlength`). */\n showCount?: boolean\n /**\n * Фоновая работа по полю (проверка занятости логина, автосохранение):\n * спиннер в trailing-области + `aria-busy`. Ввод не блокируется —\n * для этого есть `disabled`/`readonly`.\n */\n loading?: boolean\n /** Кнопка показать/скрыть пароль (только при `type=\"password\"`). */\n passwordToggle?: boolean\n /** i18n aria-label кнопки показать/скрыть пароль. */\n passwordShowLabel?: string\n passwordHideLabel?: string\n\n textAlign?: GrInputTextAlign\n\n prefixMinWidth?: string\n prefixMaxWidth?: string\n suffixMinWidth?: string\n suffixMaxWidth?: string\n /**\n * Фиксированная ширина у prefix/suffix: аддон получает жёсткую ширину\n * (из `*MaxWidth` → `*MinWidth` → дефолт), а контент обрезается по краю\n * (prefix — справа, suffix — слева). По умолчанию аддоны «растягиваются»\n * под контент (в пределах min/max), а излишек клипается оболочкой.\n */\n prefixFixed?: boolean\n suffixFixed?: boolean\n}\n\nexport interface GrInputEmits {\n (e: 'update:modelValue', value: string): void\n /** Значение зафиксировано нативным `change` — по `blur` или `Enter`. */\n (e: 'change', value: string): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n /**\n * Значение стёрто кнопкой очистки. Отдельное событие потому, что по\n * `update:modelValue` программную очистку от ручного стирания не отличить.\n */\n (e: 'clear'): void\n}\n\ndefineOptions({\n inheritAttrs: false,\n})\n\nexport type GrInputSize = GrComponentSize\nexport type GrInputTextAlign = 'left' | 'center' | 'right'\n\nconst props = withDefaults(\n defineProps<GrInputProps>(),\n {\n type: 'text',\n placeholder: undefined,\n autocomplete: undefined,\n inputmode: undefined,\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n state: 'default',\n name: undefined,\n id: undefined,\n size: undefined,\n\n // Настраивается через `GrConfigProvider`; дефолт — в резолвере ниже.\n clearable: undefined,\n clearLabel: undefined,\n maxlength: undefined,\n showCount: false,\n loading: false,\n passwordToggle: false,\n passwordShowLabel: undefined,\n passwordHideLabel: undefined,\n\n textAlign: 'left',\n\n prefixMinWidth: undefined,\n prefixMaxWidth: undefined,\n suffixMinWidth: undefined,\n suffixMaxWidth: undefined,\n prefixFixed: false,\n suffixFixed: false,\n },\n)\n\nconst emit = defineEmits<GrInputEmits>()\ndefineSlots<{\n /** Аддон слева от поля: иконка, код валюты, метка. */\n prefix?: () => any\n /** Аддон справа от поля: единица измерения, подсказка. */\n suffix?: () => any\n}>()\n\n\n// Контекст `GrFormField` (если инпут внутри него): даёт id/aria-describedby/\n// invalid/required как fallback, чтобы не прокидывать `forId` вручную.\nconst field = useGrFormFieldContext()\n\n// Эффективные значения: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, { component: 'GrInput' })\nconst resolvedClearable = useGrComponentProp('GrInput', 'clearable', () => props.clearable, false)\n\nconst resolvedId = computed(() => props.id ?? field?.id.value)\n\n// Счётчик обязан быть частью описания поля: иначе «12 / 60» видно глазами, но\n// не слышно — при том, что ограничение длины и есть смысл счётчика.\nconst countId = useId()\nconst describedBy = computed(() =>\n [field?.describedById.value, props.showCount ? countId : undefined]\n .filter(Boolean)\n .join(' ') || undefined,\n)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nconst inputEl = ref<HTMLInputElement | null>(null)\n\nfunction focus(): void {\n inputEl.value?.focus()\n}\n\nfunction blur(): void {\n inputEl.value?.blur()\n}\n\n/** Выделить содержимое — спутник `focus()` для «подставили значение, перезапишите». */\nfunction select(): void {\n inputEl.value?.select()\n}\n\ndefineExpose({\n focus,\n blur,\n select,\n})\n\nconst ADDON_MIN_WIDTH_BY_SIZE: Record<GrInputSize, string> = {\n xs: '2rem', // w-8\n sm: '2.25rem', // w-9\n md: '2.5rem', // w-10\n lg: '3rem', // w-12\n}\n\nconst passwordVisible = ref(false)\n// Тип поля с учётом переключателя пароля.\nconst resolvedType = computed(() => (props.type === 'password' && passwordVisible.value ? 'text' : props.type))\n\nconst showPasswordToggle = computed(() => props.passwordToggle && props.type === 'password' && !isDisabled.value)\nconst showClear = computed(() => resolvedClearable.value && props.modelValue.length > 0 && !isDisabled.value && !isReadonly.value)\n\nconst trailingCount = computed(() => (showClear.value ? 1 : 0) + (showPasswordToggle.value ? 1 : 0) + (props.loading ? 1 : 0))\nconst trailingReserve = computed(() => (trailingCount.value > 0 ? `${trailingCount.value * 28}px` : '0px'))\n\nconst {\n prefixEl,\n suffixEl,\n prefixStyle,\n suffixStyle,\n fieldPadding: inputStyle,\n} = useControlAddons(() => props, {\n defaultMinWidth: () => ADDON_MIN_WIDTH_BY_SIZE[resolvedSize.value],\n paddingX: () => paddingX[resolvedSize.value],\n trailingReserve: () => trailingReserve.value,\n})\n\n// Border/ring/disabled — на оболочке (`focus-within`), размеры/выравнивание — на инпуте.\nconst shellClass = computed(() => grInputShellClass({\n state: props.state,\n invalid: isInvalid.value,\n disabled: isDisabled.value,\n}))\n\nconst className = computed(() => grInputFieldClass({\n size: resolvedSize.value,\n align: props.textAlign,\n}))\n\nfunction onInput(e: Event): void {\n emit('update:modelValue', (e.target as HTMLInputElement).value)\n}\n\n// Объявленный emit уходит из `$attrs`, поэтому нативные события переизлучаем\n// руками — иначе `@change`/`@focus`/`@blur` у потребителя перестали бы работать.\nfunction onChange(e: Event): void {\n emit('change', (e.target as HTMLInputElement).value)\n}\n\nfunction onFocus(e: FocusEvent): void {\n emit('focus', e)\n}\n\nfunction onBlur(e: FocusEvent): void {\n emit('blur', e)\n}\n\n// ————— Trailing-контролы: очистка, переключатель пароля, счётчик символов.\nconst { t } = useGranularityTranslations()\nconst resolvedClearLabel = computed(() => props.clearLabel ?? t('gr.input.clear', 'Clear'))\nconst resolvedPasswordShowLabel = computed(() => props.passwordShowLabel ?? t('gr.input.showPassword', 'Show password'))\nconst resolvedPasswordHideLabel = computed(() => props.passwordHideLabel ?? t('gr.input.hidePassword', 'Hide password'))\n\n// Счётчик символов: `len` или `len / maxlength`.\nconst countText = computed(() =>\n props.maxlength !== undefined ? `${props.modelValue.length} / ${props.maxlength}` : String(props.modelValue.length),\n)\n\n/**\n * Живой регион объявляет только исчерпание лимита. Читать вслух каждый символ\n * нельзя — диктор захлебнётся, а сам счётчик и так связан с полем через\n * `aria-describedby` и читается при фокусе.\n */\nconst limitReached = computed(() =>\n props.maxlength !== undefined && props.modelValue.length >= props.maxlength,\n)\nconst liveMessage = computed(() =>\n limitReached.value ? t('gr.input.limitReached', 'Character limit reached') : '',\n)\n\nfunction clear(): void {\n emit('update:modelValue', '')\n emit('clear')\n inputEl.value?.focus()\n}\n\nfunction togglePassword(): void {\n passwordVisible.value = !passwordVisible.value\n inputEl.value?.focus()\n}\n</script>\n\n<template>\n <div data-gr-input class=\"w-full\">\n <div :class=\"shellClass\">\n <div\n v-if=\"$slots.prefix\"\n ref=\"prefixEl\"\n data-testid=\"gr-input-prefix\"\n class=\"absolute inset-y-0 left-0 flex items-center justify-center border-r border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :style=\"prefixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"prefix\" />\n </div>\n\n <input\n :id=\"resolvedId\"\n ref=\"inputEl\"\n v-bind=\"$attrs\"\n :name=\"props.name\"\n :type=\"resolvedType\"\n :inputmode=\"props.inputmode\"\n :autocomplete=\"props.autocomplete\"\n :placeholder=\"props.placeholder\"\n :disabled=\"isDisabled\"\n :readonly=\"isReadonly\"\n :maxlength=\"props.maxlength\"\n :value=\"props.modelValue\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-describedby=\"describedBy\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-label=\"ariaLabel\"\n :aria-busy=\"props.loading ? 'true' : undefined\"\n class=\"w-full bg-transparent text-[var(--gr-fg)] placeholder:text-[var(--gr-muted-fg)] focus:placeholder:text-transparent focus:outline-none disabled:cursor-not-allowed disabled:text-[var(--gr-muted-fg)]\"\n :class=\"className\"\n :style=\"inputStyle\"\n @input=\"onInput\"\n @change=\"onChange\"\n @focus=\"onFocus\"\n @blur=\"onBlur\"\n >\n\n <div\n v-if=\"trailingCount > 0\"\n data-gr-input-trailing\n class=\"absolute inset-y-0 right-1 flex items-center gap-0.5\"\n >\n <span\n v-if=\"loading\"\n data-gr-input-spinner\n class=\"flex h-6 w-6 items-center justify-center text-[var(--gr-muted-fg)]\"\n aria-hidden=\"true\"\n >\n <IconLoader class=\"h-4 w-4 animate-spin\" />\n </span>\n\n <button\n v-if=\"showClear\"\n type=\"button\"\n data-gr-input-clear\n :aria-label=\"resolvedClearLabel\"\n class=\"flex h-6 w-6 items-center justify-center rounded-[var(--gr-radius-sm)] text-[var(--gr-muted-fg)] transition-colors hover:bg-[var(--gr-muted)] hover:text-[var(--gr-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n @click=\"clear\"\n >\n <IconX class=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n\n <button\n v-if=\"showPasswordToggle\"\n type=\"button\"\n data-gr-input-password-toggle\n :aria-label=\"passwordVisible ? resolvedPasswordHideLabel : resolvedPasswordShowLabel\"\n :aria-pressed=\"passwordVisible ? 'true' : 'false'\"\n class=\"flex h-6 w-6 items-center justify-center rounded-[var(--gr-radius-sm)] text-[var(--gr-muted-fg)] transition-colors hover:bg-[var(--gr-muted)] hover:text-[var(--gr-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]\"\n @click=\"togglePassword\"\n >\n <IconEyeOff v-if=\"passwordVisible\" class=\"h-4 w-4\" aria-hidden=\"true\" />\n <IconEye v-else class=\"h-4 w-4\" aria-hidden=\"true\" />\n </button>\n </div>\n\n <div\n v-if=\"$slots.suffix\"\n ref=\"suffixEl\"\n data-testid=\"gr-input-suffix\"\n class=\"absolute inset-y-0 right-0 flex items-center justify-center border-l border-[var(--gr-brd)] px-2 text-[var(--gr-muted-fg)] pointer-events-none select-none truncate\"\n :class=\"suffixFixed ? '[direction:rtl]' : ''\"\n :style=\"suffixStyle\"\n aria-hidden=\"true\"\n >\n <slot name=\"suffix\" />\n </div>\n </div>\n\n <div\n v-if=\"showCount\"\n :id=\"countId\"\n data-gr-input-count\n class=\"mt-1 text-right text-[length:var(--gr-control-text-xs)] leading-[var(--gr-leading-xs)] text-[var(--gr-muted-fg)] tabular-nums\"\n >\n {{ countText }}\n </div>\n\n <span\n v-if=\"showCount\"\n data-gr-input-live\n class=\"sr-only\"\n role=\"status\"\n aria-live=\"polite\"\n >{{ liveMessage }}</span>\n </div>\n</template>\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"GrSwitch-KRlRMwgp.js","names":["$attrs","$slots"],"sources":["../../../../src/components/GrSwitch/GrSwitch.vue","../../../../src/components/GrSwitch/GrSwitch.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport IconLoader from '~icons/lucide/loader-circle'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport {\n grSwitchLabelClass,\n grSwitchRootClass,\n grSwitchSpinnerClass,\n grSwitchThumbClass,\n grSwitchTrackClass,\n type GrSwitchLabelPosition,\n type GrSwitchSize,\n} from './grSwitchStyles'\n\nexport type { GrSwitchLabelPosition, GrSwitchSize } from './grSwitchStyles'\n\n/**\n * Пропсы публичного GR-примитива «Switch».\n */\nexport interface GrSwitchProps {\n modelValue: boolean\n disabled?: boolean\n /** Только для чтения: состояние видно, но не переключается. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n ariaLabel?: string\n size?: GrSwitchSize\n /** Сторона подписи относительно дорожки. */\n labelPosition?: GrSwitchLabelPosition\n /** Идёт сохранение: бегунок показывает спиннер, переключение заблокировано. */\n loading?: boolean\n /** i18n: что именно грузится. `aria-busy` сам по себе часть AT не объявляет. */\n loadingText?: string\n /** Имя поля для нативной отправки формы. Без него скрытое поле не рендерится. */\n name?: string\n /** Значение, уходящее в форму во включённом состоянии. */\n value?: string\n /** `id` формы, если переключатель лежит вне неё. */\n form?: string\n /** Кастомный цвет фона в активном состоянии. Если не задан — `var(--gr-primary)`. */\n activeBackgroundColor?: string\n /** Кастомный цвет фона в неактивном состоянии. Если не задан — `var(--gr-muted)`. */\n inactiveBackgroundColor?: string\n}\n\nexport interface GrSwitchEmits {\n (e: 'update:modelValue', value: boolean): void\n (e: 'change', value: boolean): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst getCustomColor = (value?: string) => value?.trim() || undefined\n\n// Скрытое поле — сосед кнопки, а не её потомок: интерактивный контент внутри\n// `<button>` невалиден. Из-за этого корень компонента — фрагмент, и атрибуты\n// потребителя надо донести до кнопки руками.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(\n defineProps<GrSwitchProps>(),\n {\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n size: undefined,\n labelPosition: 'end',\n loading: false,\n loadingText: undefined,\n name: undefined,\n value: 'on',\n form: undefined,\n activeBackgroundColor: undefined,\n inactiveBackgroundColor: undefined,\n },\n)\n\n// Контекст `GrFormField`: id для `<label for>`, описание ошибкой, невалидность.\n// `<button>` — labelable-элемент, поэтому клик по подписи фокусирует переключатель.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nconst { t } = useGranularityTranslations()\n\nconst rootEl = ref<HTMLButtonElement | null>(null)\n\nfunction focus(): void {\n rootEl.value?.focus()\n}\n\nfunction blur(): void {\n rootEl.value?.blur()\n}\n\ndefineExpose({ focus, blur })\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, {\n component: 'GrSwitch',\n supported: ['xs', 'sm', 'md', 'lg'],\n})\n\nconst emit = defineEmits<GrSwitchEmits>()\ndefineSlots<{\n /** Подпись переключателя. */\n default?: () => any\n}>()\n\n\nconst rootClass = computed(() => grSwitchRootClass(props.labelPosition))\n\nconst trackClass = computed(() => grSwitchTrackClass(resolvedSize.value))\n\nconst trackStyle = computed(() => {\n const isChecked = props.modelValue\n const defaultBackgroundColor = isChecked ? 'var(--gr-primary)' : 'var(--gr-muted)'\n const customBackgroundColor = getCustomColor(\n isChecked ? props.activeBackgroundColor : props.inactiveBackgroundColor,\n )\n // Недоступность гасится токеном и перебивает кастомный цвет: иначе выключенный\n // переключатель остался бы таким же ярким, как рабочий.\n const backgroundColor = isDisabled.value\n ? 'var(--gr-disabled-bg)'\n : customBackgroundColor ?? defaultBackgroundColor\n\n return {\n '--gr-switch-track-bg': backgroundColor,\n '--gr-switch-track-brd': isDisabled.value\n ? 'var(--gr-disabled-brd)'\n : customBackgroundColor\n ? backgroundColor\n : isChecked\n ? 'var(--gr-primary)'\n : 'var(--gr-brd)',\n backgroundColor: 'var(--gr-switch-track-bg)',\n }\n})\n\nconst thumbClass = computed(() => grSwitchThumbClass({size: resolvedSize.value, checked: props.modelValue}))\n\nconst spinnerClass = computed(() => grSwitchSpinnerClass(resolvedSize.value))\n\nconst labelClass = computed(() => grSwitchLabelClass(resolvedSize.value, isDisabled.value))\n\nconst resolvedLoadingText = computed(() => props.loadingText ?? t('gr.switch.loading', 'Saving…'))\n\n// Выключенный переключатель не отправляется вовсе — так устроен чекбокс в HTML,\n// и сервер отличает «выкл» по отсутствию ключа.\nconst submitsValue = computed(\n () => props.modelValue && !isDisabled.value && Boolean(props.name),\n)\n\nfunction toggle(): void {\n if (isDisabled.value || isReadonly.value || props.loading) {\n return\n }\n\n const next = !props.modelValue\n emit('update:modelValue', next)\n emit('change', next)\n}\n</script>\n\n<template>\n <input\n v-if=\"submitsValue\"\n type=\"hidden\"\n :name=\"name\"\n :value=\"value\"\n :form=\"form\"\n >\n\n <button\n :id=\"fieldId\"\n ref=\"rootEl\"\n v-bind=\"$attrs\"\n type=\"button\"\n role=\"switch\"\n data-gr-switch\n :aria-checked=\"modelValue ? 'true' : 'false'\"\n :aria-label=\"ariaLabel\"\n :aria-describedby=\"describedBy\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-busy=\"loading ? 'true' : undefined\"\n :disabled=\"isDisabled\"\n :class=\"rootClass\"\n @click=\"toggle\"\n @focus=\"emit('focus', $event)\"\n @blur=\"emit('blur', $event)\"\n >\n <span\n data-testid=\"gr-switch-track\"\n data-gr-switch-track\n :class=\"trackClass\"\n :style=\"trackStyle\"\n >\n <span\n data-testid=\"gr-switch-thumb\"\n data-gr-switch-thumb\n :class=\"thumbClass\"\n aria-hidden=\"true\"\n >\n <IconLoader v-if=\"loading\" data-gr-switch-spinner :class=\"spinnerClass\" />\n </span>\n </span>\n <span\n v-if=\"$slots.default\"\n data-gr-switch-label\n :class=\"labelClass\"\n >\n <slot />\n </span>\n <span v-if=\"loading\" data-gr-switch-loading-text class=\"sr-only\">{{ resolvedLoadingText }}</span>\n </button>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport IconLoader from '~icons/lucide/loader-circle'\n\nimport { useGrComponentSize } from '../GrConfigProvider/context'\nimport { useGrFormFieldContext } from '../GrFormField/context'\nimport { useGrFormControl } from '../../composables/useGrFormControl'\nimport { useGranularityTranslations } from '../../internal/granularityI18n'\n\nimport {\n grSwitchLabelClass,\n grSwitchRootClass,\n grSwitchSpinnerClass,\n grSwitchThumbClass,\n grSwitchTrackClass,\n type GrSwitchLabelPosition,\n type GrSwitchSize,\n} from './grSwitchStyles'\n\nexport type { GrSwitchLabelPosition, GrSwitchSize } from './grSwitchStyles'\n\n/**\n * Пропсы публичного GR-примитива «Switch».\n */\nexport interface GrSwitchProps {\n modelValue: boolean\n disabled?: boolean\n /** Только для чтения: состояние видно, но не переключается. */\n readonly?: boolean\n /** Визуальное и ARIA-состояние ошибки. */\n invalid?: boolean\n /** Обязательное поле (`aria-required`). */\n required?: boolean\n ariaLabel?: string\n size?: GrSwitchSize\n /** Сторона подписи относительно дорожки. */\n labelPosition?: GrSwitchLabelPosition\n /** Идёт сохранение: бегунок показывает спиннер, переключение заблокировано. */\n loading?: boolean\n /** i18n: что именно грузится. `aria-busy` сам по себе часть AT не объявляет. */\n loadingText?: string\n /** Имя поля для нативной отправки формы. Без него скрытое поле не рендерится. */\n name?: string\n /** Значение, уходящее в форму во включённом состоянии. */\n value?: string\n /** `id` формы, если переключатель лежит вне неё. */\n form?: string\n /** Кастомный цвет фона в активном состоянии. Если не задан — `var(--gr-primary)`. */\n activeBackgroundColor?: string\n /** Кастомный цвет фона в неактивном состоянии. Если не задан — `var(--gr-muted)`. */\n inactiveBackgroundColor?: string\n}\n\nexport interface GrSwitchEmits {\n (e: 'update:modelValue', value: boolean): void\n (e: 'change', value: boolean): void\n (e: 'focus', event: FocusEvent): void\n (e: 'blur', event: FocusEvent): void\n}\n\nconst getCustomColor = (value?: string) => value?.trim() || undefined\n\n// Скрытое поле — сосед кнопки, а не её потомок: интерактивный контент внутри\n// `<button>` невалиден. Из-за этого корень компонента — фрагмент, и атрибуты\n// потребителя надо донести до кнопки руками.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(\n defineProps<GrSwitchProps>(),\n {\n disabled: false,\n readonly: false,\n invalid: false,\n required: false,\n ariaLabel: undefined,\n size: undefined,\n labelPosition: 'end',\n loading: false,\n loadingText: undefined,\n name: undefined,\n value: 'on',\n form: undefined,\n activeBackgroundColor: undefined,\n inactiveBackgroundColor: undefined,\n },\n)\n\n// Контекст `GrFormField`: id для `<label for>`, описание ошибкой, невалидность.\n// `<button>` — labelable-элемент, поэтому клик по подписи фокусирует переключатель.\nconst field = useGrFormFieldContext()\nconst fieldId = computed(() => field?.id.value)\nconst describedBy = computed(() => field?.describedById.value)\nconst {\n disabled: isDisabled,\n invalid: isInvalid,\n required: isRequired,\n readonly: isReadonly,\n} = useGrFormControl(() => props)\n\nconst { t } = useGranularityTranslations()\n\nconst rootEl = ref<HTMLButtonElement | null>(null)\n\nfunction focus(): void {\n rootEl.value?.focus()\n}\n\nfunction blur(): void {\n rootEl.value?.blur()\n}\n\ndefineExpose({ focus, blur })\n\n// Эффективный размер: локальный проп → `GrConfigProvider` → дефолт компонента.\nconst resolvedSize = useGrComponentSize(() => props.size, {\n component: 'GrSwitch',\n supported: ['xs', 'sm', 'md', 'lg'],\n})\n\nconst emit = defineEmits<GrSwitchEmits>()\ndefineSlots<{\n /** Подпись переключателя. */\n default?: () => any\n}>()\n\n\nconst rootClass = computed(() => grSwitchRootClass(props.labelPosition))\n\nconst trackClass = computed(() => grSwitchTrackClass(resolvedSize.value))\n\nconst trackStyle = computed(() => {\n const isChecked = props.modelValue\n const defaultBackgroundColor = isChecked ? 'var(--gr-primary)' : 'var(--gr-muted)'\n const customBackgroundColor = getCustomColor(\n isChecked ? props.activeBackgroundColor : props.inactiveBackgroundColor,\n )\n // Недоступность гасится токеном и перебивает кастомный цвет: иначе выключенный\n // переключатель остался бы таким же ярким, как рабочий.\n const backgroundColor = isDisabled.value\n ? 'var(--gr-disabled-bg)'\n : customBackgroundColor ?? defaultBackgroundColor\n\n return {\n '--gr-switch-track-bg': backgroundColor,\n '--gr-switch-track-brd': isDisabled.value\n ? 'var(--gr-disabled-brd)'\n : customBackgroundColor\n ? backgroundColor\n : isChecked\n ? 'var(--gr-primary)'\n : 'var(--gr-brd)',\n backgroundColor: 'var(--gr-switch-track-bg)',\n }\n})\n\nconst thumbClass = computed(() => grSwitchThumbClass({size: resolvedSize.value, checked: props.modelValue}))\n\nconst spinnerClass = computed(() => grSwitchSpinnerClass(resolvedSize.value))\n\nconst labelClass = computed(() => grSwitchLabelClass(resolvedSize.value, isDisabled.value))\n\nconst resolvedLoadingText = computed(() => props.loadingText ?? t('gr.switch.loading', 'Saving…'))\n\n// Выключенный переключатель не отправляется вовсе — так устроен чекбокс в HTML,\n// и сервер отличает «выкл» по отсутствию ключа.\nconst submitsValue = computed(\n () => props.modelValue && !isDisabled.value && Boolean(props.name),\n)\n\nfunction toggle(): void {\n if (isDisabled.value || isReadonly.value || props.loading) {\n return\n }\n\n const next = !props.modelValue\n emit('update:modelValue', next)\n emit('change', next)\n}\n</script>\n\n<template>\n <input\n v-if=\"submitsValue\"\n type=\"hidden\"\n :name=\"name\"\n :value=\"value\"\n :form=\"form\"\n >\n\n <button\n :id=\"fieldId\"\n ref=\"rootEl\"\n v-bind=\"$attrs\"\n type=\"button\"\n role=\"switch\"\n data-gr-switch\n :aria-checked=\"modelValue ? 'true' : 'false'\"\n :aria-label=\"ariaLabel\"\n :aria-describedby=\"describedBy\"\n :aria-invalid=\"isInvalid ? 'true' : undefined\"\n :aria-required=\"isRequired ? 'true' : undefined\"\n :aria-readonly=\"isReadonly ? 'true' : undefined\"\n :aria-busy=\"loading ? 'true' : undefined\"\n :disabled=\"isDisabled\"\n :class=\"rootClass\"\n @click=\"toggle\"\n @focus=\"emit('focus', $event)\"\n @blur=\"emit('blur', $event)\"\n >\n <span\n data-testid=\"gr-switch-track\"\n data-gr-switch-track\n :class=\"trackClass\"\n :style=\"trackStyle\"\n >\n <span\n data-testid=\"gr-switch-thumb\"\n data-gr-switch-thumb\n :class=\"thumbClass\"\n aria-hidden=\"true\"\n >\n <IconLoader v-if=\"loading\" data-gr-switch-spinner :class=\"spinnerClass\" />\n </span>\n </span>\n <span\n v-if=\"$slots.default\"\n data-gr-switch-label\n :class=\"labelClass\"\n >\n <slot />\n </span>\n <span v-if=\"loading\" data-gr-switch-loading-text class=\"sr-only\">{{ resolvedLoadingText }}</span>\n </button>\n</template>\n"],"mappings":"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"}