@feugene/granularity-media 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +80 -0
  2. package/dist/chunks/config-CrV_FDnM.js +38 -0
  3. package/dist/chunks/en-BrjpU7lq.js +13 -0
  4. package/dist/chunks/es-BdTkuOLs.js +13 -0
  5. package/dist/chunks/rolldown-runtime-Dy4uBu1J.js +11 -0
  6. package/dist/chunks/ru-ByR24Aqk.js +13 -0
  7. package/dist/chunks/ru-C4IsSzR5.js +4 -0
  8. package/dist/components/GrImageCrop/chunks/GrImageCrop-DYM2vZHV.js +271 -0
  9. package/dist/components/GrImageCrop/index.js +3 -0
  10. package/dist/granular-provider-node.js +2 -0
  11. package/dist/granular-provider.js +17 -0
  12. package/dist/i18n/all.js +9 -0
  13. package/dist/i18n/index.js +12 -0
  14. package/dist/index.js +3 -0
  15. package/dist/resolver.js +12 -0
  16. package/dist/types/__tests__/componentDefaults.test.d.ts +1 -0
  17. package/dist/types/__tests__/componentDocs.test.d.ts +1 -0
  18. package/dist/types/__tests__/componentTokens.test.d.ts +1 -0
  19. package/dist/types/__tests__/emitNaming.test.d.ts +1 -0
  20. package/dist/types/__tests__/envGuard.test.d.ts +1 -0
  21. package/dist/types/__tests__/gateCoverage.test.d.ts +1 -0
  22. package/dist/types/__tests__/localeCompleteness.test.d.ts +1 -0
  23. package/dist/types/__tests__/registry.generated.test.d.ts +1 -0
  24. package/dist/types/__tests__/styleTokens.test.d.ts +1 -0
  25. package/dist/types/componentNames.d.ts +12 -0
  26. package/dist/types/components/GrImageCrop/GrImageCrop.vue.d.ts +130 -0
  27. package/dist/types/components/GrImageCrop/__tests__/GrImageCrop.test.d.ts +1 -0
  28. package/dist/types/components/GrImageCrop/__tests__/cropGeometry.test.d.ts +1 -0
  29. package/dist/types/components/GrImageCrop/config.d.ts +9 -0
  30. package/dist/types/components/GrImageCrop/cropGeometry.d.ts +62 -0
  31. package/dist/types/components/GrImageCrop/defaults.d.ts +16 -0
  32. package/dist/types/components/GrImageCrop/grImageCropStyles.d.ts +42 -0
  33. package/dist/types/components/GrImageCrop/index.d.ts +9 -0
  34. package/dist/types/granular-provider/index.d.ts +3 -0
  35. package/dist/types/granular-provider/node.d.ts +2 -0
  36. package/dist/types/granular-provider/shared.d.ts +27 -0
  37. package/dist/types/i18n/all.d.ts +11 -0
  38. package/dist/types/i18n/index.d.ts +28 -0
  39. package/dist/types/i18n/messages/const.d.ts +8 -0
  40. package/dist/types/i18n/messages/en.d.ts +21 -0
  41. package/dist/types/i18n/messages/es.d.ts +21 -0
  42. package/dist/types/i18n/messages/index.d.ts +22 -0
  43. package/dist/types/i18n/messages/ru.d.ts +21 -0
  44. package/dist/types/index.d.ts +1 -0
  45. package/dist/types/internal/classTokens.d.ts +7 -0
  46. package/dist/types/resolver.d.ts +38 -0
  47. package/package.json +129 -0
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # `@feugene/granularity-media`
2
+
3
+ Работа с изображением и камерой для `@feugene/granularity`. Первый компонент —
4
+ `GrImageCrop`: пользователь выбирает кадр, приложение получает готовый `Blob`.
5
+
6
+ ```bash
7
+ yarn add @feugene/granularity-media
8
+ ```
9
+
10
+ Своих зависимостей у пакета нет: кадрирование держится на Canvas и браузерных
11
+ API, а не на библиотеке. Наружу объявлены только peer — ядро, пресет и Vue.
12
+
13
+ ## Быстрый старт
14
+
15
+ ```vue
16
+ <script setup lang="ts">
17
+ import { ref, useTemplateRef } from 'vue'
18
+ import { GrImageCrop } from '@feugene/granularity-media'
19
+
20
+ const file = ref<File | null>(null)
21
+ const cropper = useTemplateRef('cropper')
22
+
23
+ async function save() {
24
+ const blob = await cropper.value?.crop()
25
+ if (blob)
26
+ await fetch('/api/avatar', { method: 'POST', body: blob })
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <GrImageCrop ref="cropper" :src="file" shape="circle" :output="{ width: 256, height: 256 }" />
32
+ <button @click="save">Сохранить</button>
33
+ </template>
34
+ ```
35
+
36
+ ## Рамка неподвижна, двигается картинка
37
+
38
+ Обратная модель — рамка ездит по изображению — требует двух жестов вместо
39
+ одного, а её угловые ручки на телефоне меньше пальца. Здесь жест один: тянуть
40
+ картинку, а увеличение живёт отдельным контролом (по умолчанию `GrSlider` ядра,
41
+ заменяется слотом `#controls`).
42
+
43
+ Соотношение сторон кадра задаёт приложение (`aspectRatio`), а не пользователь:
44
+ место, куда картинка потом встанет, известно заранее.
45
+
46
+ ## Кадр считается в пикселях исходника
47
+
48
+ Без `output` результат получает размер захваченной области **исходного файла**, а
49
+ не окна на экране: окно почти всегда меньше картинки, и вывод по нему молча
50
+ ополовинил бы разрешение. Нужен точный размер — он задаётся явно.
51
+
52
+ ## Что внутри
53
+
54
+ | | |
55
+ | --- | --- |
56
+ | **Жест** | перетаскивание на `useDragGesture` ядра: оборванный жест откатывается, а не коммитит |
57
+ | **Клавиатура** | стрелки двигают кадр, `+`/`-` меняют увеличение, `Home` сбрасывает |
58
+ | **Экспорт** | `crop()` отдаёт `Blob`; тип и качество — `output.type`, `output.quality` |
59
+ | **Формы кадра** | прямоугольник и круг (`shape="circle"` — маска показа, результат остаётся прямоугольным) |
60
+ | **Серверный рендер** | браузерного API в `setup` нет, оборачивать в `<ClientOnly>` не нужно |
61
+
62
+ ## Подключение к сборке
63
+
64
+ Провайдер — рядом с ядром, резолвер — **перед** жадным `GranularityResolver()`:
65
+
66
+ ```ts
67
+ presetGranularNode({
68
+ providers: [granularityProvider, granularityMediaProvider],
69
+ components: ['@feugene/granularity-media:GrImageCrop'],
70
+ })
71
+ ```
72
+
73
+ Локали подключаются тем же `createFintI18n`, что и словари ядра — блок
74
+ `GR_MEDIA_I18N_BLOCK`.
75
+
76
+ ## Документация
77
+
78
+ - [`docs/components.md`](./docs/components.md) — состав пакета;
79
+ - [`docs/components/GrImageCrop.md`](./docs/components/GrImageCrop.md) — страница компонента;
80
+ - [`docs/keyboard.md`](./docs/keyboard.md), [`docs/ssr.md`](./docs/ssr.md) — сквозные правила пакета.
@@ -0,0 +1,38 @@
1
+ import { defineGranularComponent as e } from "@feugene/unocss-preset-granular/contract";
2
+ //#region src/internal/classTokens.ts
3
+ function t(e) {
4
+ return e.split(/\s+/).filter(Boolean);
5
+ }
6
+ //#endregion
7
+ //#region src/components/GrImageCrop/grImageCropStyles.ts
8
+ var n = "grid gap-2", r = "relative w-full select-none overflow-hidden rounded-[var(--gr-radius-md)] border border-[var(--gr-brd)] bg-[var(--gr-muted)] touch-none focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]", i = "pointer-events-none absolute left-1/2 top-1/2 max-w-none origin-center", a = "pointer-events-none absolute inset-0 m-auto aspect-square rounded-[var(--gr-radius-full)] border border-[var(--gr-image-crop-guide,var(--gr-brd))] shadow-[0_0_0_9999px_var(--gr-image-crop-mask,rgb(0_0_0/0.55))]", o = "pointer-events-none absolute inset-0 border border-[var(--gr-image-crop-guide,var(--gr-brd))]", s = "transition-transform duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-out)]", c = "flex items-center gap-3", l = "flex items-center justify-center text-[var(--gr-muted-fg)]", u = {
9
+ xs: "text-[length:var(--gr-control-text-xs)] leading-[var(--gr-control-leading-xs)]",
10
+ sm: "text-[length:var(--gr-control-text-sm)] leading-[var(--gr-control-leading-sm)]",
11
+ md: "text-[length:var(--gr-control-text-md)] leading-[var(--gr-control-leading-md)]",
12
+ lg: "text-[length:var(--gr-control-text-lg)] leading-[var(--gr-control-leading-lg)]"
13
+ }, d = {
14
+ xs: "min-h-32",
15
+ sm: "min-h-40",
16
+ md: "min-h-48",
17
+ lg: "min-h-56"
18
+ }, f = [
19
+ ...t(n),
20
+ ...t(r),
21
+ ...t(i),
22
+ ...t(s),
23
+ ...t(a),
24
+ ...t(o),
25
+ ...t(c),
26
+ ...t(l),
27
+ ...Object.values(u).flatMap(t),
28
+ ...Object.values(d).flatMap(t)
29
+ ], p = e(import.meta.url, {
30
+ name: "GrImageCrop",
31
+ safelist: f,
32
+ dependencies: [{
33
+ provider: "@feugene/granularity",
34
+ components: ["GrSlider"]
35
+ }]
36
+ });
37
+ //#endregion
38
+ export { f as a, o as c, u as d, r as f, l as i, n as l, a as n, i as o, c as r, s, p as t, d as u };
@@ -0,0 +1,13 @@
1
+ import { t as e } from "./rolldown-runtime-Dy4uBu1J.js";
2
+ //#region src/i18n/locales/en.json
3
+ var t = /* @__PURE__ */ e({
4
+ default: () => r,
5
+ imageCrop: () => n
6
+ }), n = {
7
+ label: "Image crop",
8
+ frame: "Frame: arrow keys move the image, plus and minus zoom",
9
+ empty: "No image selected",
10
+ zoom: "Zoom"
11
+ }, r = { imageCrop: n };
12
+ //#endregion
13
+ export { t as n, r as t };
@@ -0,0 +1,13 @@
1
+ import { t as e } from "./rolldown-runtime-Dy4uBu1J.js";
2
+ //#region src/i18n/locales/es.json
3
+ var t = /* @__PURE__ */ e({
4
+ default: () => r,
5
+ imageCrop: () => n
6
+ }), n = {
7
+ label: "Recorte de imagen",
8
+ frame: "Marco: las flechas mueven la imagen, más y menos ajustan el zoom",
9
+ empty: "Ninguna imagen seleccionada",
10
+ zoom: "Zoom"
11
+ }, r = { imageCrop: n };
12
+ //#endregion
13
+ export { t as n, r as t };
@@ -0,0 +1,11 @@
1
+ //#region \0rolldown/runtime.js
2
+ var e = Object.defineProperty, t = (t, n) => {
3
+ let r = {};
4
+ for (var i in t) e(r, i, {
5
+ get: t[i],
6
+ enumerable: !0
7
+ });
8
+ return n || e(r, Symbol.toStringTag, { value: "Module" }), r;
9
+ };
10
+ //#endregion
11
+ export { t };
@@ -0,0 +1,13 @@
1
+ import { t as e } from "./rolldown-runtime-Dy4uBu1J.js";
2
+ //#region src/i18n/locales/ru.json
3
+ var t = /* @__PURE__ */ e({
4
+ default: () => r,
5
+ imageCrop: () => n
6
+ }), n = {
7
+ label: "Кадрирование изображения",
8
+ frame: "Кадр: стрелки двигают изображение, плюс и минус меняют увеличение",
9
+ empty: "Изображение не выбрано",
10
+ zoom: "Увеличение"
11
+ }, r = { imageCrop: n };
12
+ //#endregion
13
+ export { t as n, r as t };
@@ -0,0 +1,4 @@
1
+ //#region src/i18n/messages/const.ts
2
+ var e = "grMedia", t = { en: { [e]: async () => import("./en-BrjpU7lq.js").then((e) => e.n) } }, n = { es: { [e]: async () => import("./es-BdTkuOLs.js").then((e) => e.n) } }, r = { ru: { [e]: async () => import("./ru-ByR24Aqk.js").then((e) => e.n) } };
3
+ //#endregion
4
+ export { e as i, n, t as r, r as t };
@@ -0,0 +1,271 @@
1
+ import { c as e, d as t, f as n, i as r, l as i, n as a, o, r as s, s as c, u as l } from "../../../chunks/config-CrV_FDnM.js";
2
+ import { Fragment as u, computed as d, createCommentVNode as ee, createElementBlock as f, createElementVNode as p, createVNode as te, defineComponent as m, normalizeClass as h, normalizeStyle as g, onBeforeUnmount as _, onMounted as ne, openBlock as v, ref as y, renderSlot as b, shallowRef as x, toDisplayString as re, unref as S, watch as ie } from "vue";
3
+ import ae from "@feugene/granularity/components/GrSlider";
4
+ import { useDragGesture as oe } from "@feugene/granularity/composables/useDragGesture";
5
+ import { useGrComponentSize as se } from "@feugene/granularity/composables/useGrComponentConfig";
6
+ import { useGranularityTranslations as ce } from "@feugene/granularity/composables/useGranularityTranslations";
7
+ //#region src/components/GrImageCrop/cropGeometry.ts
8
+ function C(e, t) {
9
+ return e.width <= 0 || e.height <= 0 ? 1 : Math.max(t.width / e.width, t.height / e.height);
10
+ }
11
+ function w(e, t, n) {
12
+ let r = C(e, t) * n;
13
+ return {
14
+ width: e.width * r,
15
+ height: e.height * r
16
+ };
17
+ }
18
+ function T(e, t, n) {
19
+ let r = w(e, t, n);
20
+ return {
21
+ x: Math.max(0, (r.width - t.width) / 2),
22
+ y: Math.max(0, (r.height - t.height) / 2)
23
+ };
24
+ }
25
+ function E(e, t, n, r) {
26
+ let i = T(t, n, r);
27
+ return {
28
+ x: Math.min(i.x, Math.max(-i.x, e.x)),
29
+ y: Math.min(i.y, Math.max(-i.y, e.y))
30
+ };
31
+ }
32
+ function D(e, t, n, r) {
33
+ let i = C(e, t) * n;
34
+ if (i <= 0) return {
35
+ sx: 0,
36
+ sy: 0,
37
+ sw: 0,
38
+ sh: 0
39
+ };
40
+ let a = E(r, e, t, n), o = w(e, t, n);
41
+ return {
42
+ sx: (o.width - t.width) / 2 / i - a.x / i,
43
+ sy: (o.height - t.height) / 2 / i - a.y / i,
44
+ sw: t.width / i,
45
+ sh: t.height / i
46
+ };
47
+ }
48
+ function O(e, t) {
49
+ return {
50
+ width: e,
51
+ height: e / (t > 0 ? t : 1)
52
+ };
53
+ }
54
+ //#endregion
55
+ //#region src/components/GrImageCrop/GrImageCrop.vue?vue&type=script&setup=true&lang.ts
56
+ var le = ["aria-label"], ue = [
57
+ "tabindex",
58
+ "aria-label",
59
+ "aria-disabled"
60
+ ], de = ["src"], k = 12, A = .15, j = /* @__PURE__ */ m({
61
+ __name: "GrImageCrop",
62
+ props: {
63
+ src: { default: null },
64
+ aspectRatio: { default: 1 },
65
+ shape: { default: "rect" },
66
+ zoom: { default: 1 },
67
+ maxZoom: { default: 4 },
68
+ output: { default: void 0 },
69
+ size: { default: void 0 },
70
+ disabled: {
71
+ type: Boolean,
72
+ default: !1
73
+ },
74
+ ariaLabel: { default: void 0 }
75
+ },
76
+ emits: [
77
+ "update:zoom",
78
+ "change",
79
+ "load",
80
+ "error"
81
+ ],
82
+ setup(m, { expose: C, emit: w }) {
83
+ let T = m, j = w, { t: M } = ce(), N = se(() => T.size, { component: "GrImageCrop" }), P = y(null), F = x(null), I = y({
84
+ width: 0,
85
+ height: 0
86
+ }), L = y(0), R = y({
87
+ x: 0,
88
+ y: 0
89
+ }), z = y(null), B = d(() => O(L.value, T.aspectRatio)), V = d(() => F.value !== null && I.value.width > 0), H = d(() => Math.min(T.maxZoom, Math.max(1, T.zoom))), fe = d(() => {
90
+ let e = B.value.width > 0 && I.value.width > 0 ? Math.max(B.value.width / I.value.width, B.value.height / I.value.height) * H.value : 1;
91
+ return {
92
+ width: `${I.value.width}px`,
93
+ height: `${I.value.height}px`,
94
+ transform: `translate(-50%, -50%) translate(${R.value.x}px, ${R.value.y}px) scale(${e})`
95
+ };
96
+ });
97
+ function U() {
98
+ return D(I.value, B.value, H.value, R.value);
99
+ }
100
+ function W() {
101
+ V.value && j("change", U());
102
+ }
103
+ function G(e, t) {
104
+ T.disabled || !V.value || (R.value = E({
105
+ x: R.value.x + e,
106
+ y: R.value.y + t
107
+ }, I.value, B.value, H.value), W());
108
+ }
109
+ function K(e) {
110
+ if (T.disabled) return;
111
+ let t = Math.min(T.maxZoom, Math.max(1, e));
112
+ j("update:zoom", t), R.value = E(R.value, I.value, B.value, t), W();
113
+ }
114
+ function q() {
115
+ R.value = {
116
+ x: 0,
117
+ y: 0
118
+ }, j("update:zoom", 1), W();
119
+ }
120
+ let J = y({
121
+ x: 0,
122
+ y: 0
123
+ }), Y = y({
124
+ x: 0,
125
+ y: 0
126
+ }), X = oe({
127
+ disabled: () => T.disabled || !V.value,
128
+ onStart: (e) => {
129
+ J.value = {
130
+ x: e.clientX,
131
+ y: e.clientY
132
+ }, Y.value = { ...R.value };
133
+ },
134
+ onMove: (e) => {
135
+ R.value = E({
136
+ x: Y.value.x + (e.clientX - J.value.x),
137
+ y: Y.value.y + (e.clientY - J.value.y)
138
+ }, I.value, B.value, H.value);
139
+ },
140
+ onEnd: () => W(),
141
+ onCancel: () => {
142
+ R.value = Y.value;
143
+ }
144
+ });
145
+ function pe(e) {
146
+ T.disabled || !V.value || (e.preventDefault(), K(H.value + (e.deltaY < 0 ? A : -.15)));
147
+ }
148
+ function me(e) {
149
+ let t = {
150
+ ArrowLeft: () => G(k, 0),
151
+ ArrowRight: () => G(-12, 0),
152
+ ArrowUp: () => G(0, k),
153
+ ArrowDown: () => G(0, -12),
154
+ "+": () => K(H.value + A),
155
+ "=": () => K(H.value + A),
156
+ "-": () => K(H.value - A),
157
+ Home: () => q()
158
+ }[e.key];
159
+ t && (e.preventDefault(), t());
160
+ }
161
+ function Z() {
162
+ z.value &&= (URL.revokeObjectURL(z.value), null);
163
+ }
164
+ function he(e) {
165
+ if (!e) return null;
166
+ if (typeof e == "string") return e;
167
+ let t = URL.createObjectURL(e);
168
+ return z.value = t, t;
169
+ }
170
+ function Q(e) {
171
+ Z();
172
+ let t = he(e);
173
+ if (!t) {
174
+ F.value = null, I.value = {
175
+ width: 0,
176
+ height: 0
177
+ };
178
+ return;
179
+ }
180
+ let n = new Image();
181
+ n.crossOrigin = "anonymous", n.addEventListener("load", () => {
182
+ F.value = n, I.value = {
183
+ width: n.naturalWidth,
184
+ height: n.naturalHeight
185
+ }, R.value = {
186
+ x: 0,
187
+ y: 0
188
+ }, j("load", { ...I.value }), W();
189
+ }), n.addEventListener("error", (e) => j("error", e)), n.src = t;
190
+ }
191
+ let $ = null;
192
+ ne(() => {
193
+ P.value && (L.value = P.value.clientWidth, $ = new ResizeObserver(([e]) => {
194
+ L.value = e?.contentRect.width ?? 0, R.value = E(R.value, I.value, B.value, H.value);
195
+ }), $.observe(P.value)), Q(T.src);
196
+ }), _(() => {
197
+ $?.disconnect(), $ = null, Z();
198
+ }), ie(() => T.src, (e) => Q(e));
199
+ async function ge() {
200
+ let e = F.value;
201
+ if (!e || !V.value) return null;
202
+ let t = U(), n = document.createElement("canvas");
203
+ n.width = Math.max(1, Math.round(T.output?.width ?? t.sw)), n.height = Math.max(1, Math.round(T.output?.height ?? t.sh));
204
+ let r = n.getContext("2d");
205
+ if (!r) return null;
206
+ r.drawImage(e, t.sx, t.sy, t.sw, t.sh, 0, 0, n.width, n.height);
207
+ try {
208
+ return await new Promise((e) => {
209
+ n.toBlob(e, T.output?.type ?? "image/png", T.output?.quality);
210
+ });
211
+ } catch (e) {
212
+ return typeof process < "u" && process.env.NODE_ENV !== "production" && console.warn("[granularity-media] GrImageCrop: экспорт кадра запрещён политикой источника. Картинка приехала с другого домена без заголовка `Access-Control-Allow-Origin`, и холст стал непригоден для чтения."), j("error", e), null;
213
+ }
214
+ }
215
+ return C({
216
+ crop: ge,
217
+ reset: q,
218
+ rect: U
219
+ }), (d, _) => (v(), f("div", {
220
+ class: h(S(i)),
221
+ role: "group",
222
+ "aria-label": m.ariaLabel ?? S(M)("grMedia.imageCrop.label", "Image crop")
223
+ }, [p("div", {
224
+ ref_key: "viewportEl",
225
+ ref: P,
226
+ role: "application",
227
+ class: h([S(n), V.value ? "" : [S(r), S(l)[S(N)]]]),
228
+ style: g(V.value ? { aspectRatio: String(m.aspectRatio) } : void 0),
229
+ tabindex: m.disabled || !V.value ? -1 : 0,
230
+ "aria-label": S(M)("grMedia.imageCrop.frame", "Frame: arrow keys move the image, plus and minus zoom"),
231
+ "aria-disabled": m.disabled || void 0,
232
+ onPointerdown: _[0] ||= (...e) => S(X).start && S(X).start(...e),
233
+ onWheel: pe,
234
+ onKeydown: me
235
+ }, [V.value ? (v(), f(u, { key: 0 }, [p("img", {
236
+ src: F.value.src,
237
+ alt: "",
238
+ class: h([S(o), S(X).isDragging.value ? "" : S(c)]),
239
+ style: g(fe.value)
240
+ }, null, 14, de), m.shape === "circle" ? (v(), f("div", {
241
+ key: 0,
242
+ class: h(S(a))
243
+ }, null, 2)) : (v(), f("div", {
244
+ key: 1,
245
+ class: h(S(e))
246
+ }, null, 2))], 64)) : b(d.$slots, "empty", {}, () => [p("span", { class: h(S(t)[S(N)]) }, re(S(M)("grMedia.imageCrop.empty", "No image selected")), 3)], void 0, 1)], 46, ue), V.value ? (v(), f("div", {
247
+ key: 0,
248
+ class: h(S(s))
249
+ }, [b(d.$slots, "controls", {
250
+ zoom: H.value,
251
+ setZoom: K
252
+ }, () => [te(S(ae), {
253
+ "model-value": H.value,
254
+ min: 1,
255
+ max: m.maxZoom,
256
+ step: .01,
257
+ size: S(N),
258
+ disabled: m.disabled,
259
+ "aria-label": S(M)("grMedia.imageCrop.zoom", "Zoom"),
260
+ "onUpdate:modelValue": _[1] ||= (e) => K(Number(e))
261
+ }, null, 8, [
262
+ "model-value",
263
+ "max",
264
+ "size",
265
+ "disabled",
266
+ "aria-label"
267
+ ])])], 2)) : ee("", !0)], 10, le));
268
+ }
269
+ });
270
+ //#endregion
271
+ export { w as a, D as i, E as n, T as o, C as r, O as s, j as t };
@@ -0,0 +1,3 @@
1
+ import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./chunks/GrImageCrop-DYM2vZHV.js";
2
+ import { a as s, t as c } from "../../chunks/config-CrV_FDnM.js";
3
+ export { o as GrImageCrop, o as default, n as clampOffset, i as coverScale, t as cropRect, e as displaySize, c as grImageCropConfig, s as grImageCropSafelist, r as offsetBounds, a as viewportFor };
@@ -0,0 +1,2 @@
1
+ import e, { GRANULARITY_MEDIA_PROVIDER_ID as t, createGranularityMediaProvider as n, granularityMediaComponentConfigs as r } from "./granular-provider.js";
2
+ export { t as GRANULARITY_MEDIA_PROVIDER_ID, n as createGranularityMediaProvider, e as default, e as granularityMediaProvider, r as granularityMediaComponentConfigs };
@@ -0,0 +1,17 @@
1
+ import { t as e } from "./chunks/config-CrV_FDnM.js";
2
+ import { defineGranularProvider as t, resolvePackageBaseUrl as n } from "@feugene/unocss-preset-granular/contract";
3
+ import { granularityProvider as r } from "@feugene/granularity/granular-provider";
4
+ //#region src/granular-provider/shared.ts
5
+ var i = "@feugene/granularity-media", a = { GrImageCrop: e };
6
+ function o(e, n) {
7
+ return t({
8
+ id: i,
9
+ contractVersion: 1,
10
+ packageBaseUrl: n,
11
+ components: Object.values(a),
12
+ dependencies: [e]
13
+ });
14
+ }
15
+ var s = o(r, n(import.meta.url, 0));
16
+ //#endregion
17
+ export { i as GRANULARITY_MEDIA_PROVIDER_ID, o as createGranularityMediaProvider, s as default, s as granularityMediaProvider, a as granularityMediaComponentConfigs };
@@ -0,0 +1,9 @@
1
+ import { n as e, r as t, t as n } from "../chunks/ru-C4IsSzR5.js";
2
+ //#region src/i18n/all.ts
3
+ var r = [
4
+ t,
5
+ n,
6
+ e
7
+ ];
8
+ //#endregion
9
+ export { r as all, r as default };
@@ -0,0 +1,12 @@
1
+ import { t as e } from "../chunks/en-BrjpU7lq.js";
2
+ import { t } from "../chunks/es-BdTkuOLs.js";
3
+ import { t as n } from "../chunks/ru-ByR24Aqk.js";
4
+ import { i as r, n as i, r as a, t as o } from "../chunks/ru-C4IsSzR5.js";
5
+ //#region src/i18n/index.ts
6
+ var s = {
7
+ en: e,
8
+ es: t,
9
+ ru: n
10
+ };
11
+ //#endregion
12
+ export { r as GR_MEDIA_I18N_BLOCK, a as en, i as es, s as grMediaMessages, o as ru };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./components/GrImageCrop/chunks/GrImageCrop-DYM2vZHV.js";
2
+ import { a as s, t as c } from "./chunks/config-CrV_FDnM.js";
3
+ export { o as GrImageCrop, n as clampOffset, i as coverScale, t as cropRect, e as displaySize, c as grImageCropConfig, s as grImageCropSafelist, r as offsetBounds, a as viewportFor };
@@ -0,0 +1,12 @@
1
+ import { createGranularResolver as e } from "@feugene/unplugin-granularity";
2
+ //#region src/componentNames.ts
3
+ var t = ["GrImageCrop"], n = "@feugene/granularity-media";
4
+ function r() {
5
+ return e({
6
+ packageName: n,
7
+ components: t,
8
+ importStyle: !1
9
+ });
10
+ }
11
+ //#endregion
12
+ export { n as GRANULARITY_MEDIA_PACKAGE_NAME, r as GranularityMediaResolver };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Имена компонентов пакета — одним списком без единого импорта.
3
+ *
4
+ * Модуль читают и `resolver.ts` (whitelist авто-импорта), и `vite.config.ts`
5
+ * (раскладка чанков и ассетов). Отдельным файлом именно поэтому: конфиг сборки
6
+ * исполняется до сборки, и тянуть в него `resolver.ts` значило бы грузить
7
+ * `unplugin-vue-components` на этапе чтения конфига.
8
+ *
9
+ * Список генерируется `yarn generate:registry` — руками не писать.
10
+ */
11
+ export declare const GRANULARITY_MEDIA_COMPONENTS: readonly ["GrImageCrop"];
12
+ export type GranularityMediaComponentName = typeof GRANULARITY_MEDIA_COMPONENTS[number];
@@ -0,0 +1,130 @@
1
+ import type { GrCropRect } from './cropGeometry';
2
+ import type { GrImageCropShape, GrImageCropSize } from './grImageCropStyles';
3
+ export interface GrImageCropOutput {
4
+ /** Ширина результата. По умолчанию — ширина захваченной области исходника. */
5
+ width?: number;
6
+ height?: number;
7
+ /** MIME результата: `image/png` (по умолчанию), `image/jpeg`, `image/webp`. */
8
+ type?: string;
9
+ quality?: number;
10
+ }
11
+ export interface GrImageCropProps {
12
+ /** Файл из `GrFileUpload`, blob из камеры или готовый адрес. */
13
+ src?: string | File | Blob | null;
14
+ /** Отношение ширины кадра к высоте: `1` — аватар, `16 / 9` — обложка. */
15
+ aspectRatio?: number;
16
+ shape?: GrImageCropShape;
17
+ /** Увеличение относительно вписанного кадра. Не меньше единицы. */
18
+ zoom?: number;
19
+ maxZoom?: number;
20
+ output?: GrImageCropOutput;
21
+ size?: GrImageCropSize;
22
+ disabled?: boolean;
23
+ ariaLabel?: string;
24
+ }
25
+ export interface GrImageCropEmits {
26
+ (e: 'update:zoom', value: number): void;
27
+ /** Кадр изменился: сдвиг, увеличение или новая картинка. */
28
+ (e: 'change', rect: GrCropRect): void;
29
+ (e: 'load', size: {
30
+ width: number;
31
+ height: number;
32
+ }): void;
33
+ (e: 'error', error: unknown): void;
34
+ }
35
+ type __VLS_Slots = {
36
+ /** Замена встроенного слайдера увеличения. */
37
+ controls?: (props: {
38
+ zoom: number;
39
+ setZoom: (value: number) => void;
40
+ }) => unknown;
41
+ /** Что показать, пока картинки нет. */
42
+ empty?: () => unknown;
43
+ };
44
+ declare function currentRect(): GrCropRect;
45
+ declare function reset(): void;
46
+ /**
47
+ * Кадр в виде картинки.
48
+ *
49
+ * Размер по умолчанию — сама захваченная область в пикселях **исходника**, а не
50
+ * окна на экране: окно вдвое меньше картинки на любом ноутбуке, и вывод по нему
51
+ * молча ополовинил бы разрешение аватара.
52
+ */
53
+ declare function toBlob(): Promise<Blob | null>;
54
+ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPropTypes<__VLS_WithDefaults<__VLS_TypePropsToOption<GrImageCropProps>, {
55
+ src: null;
56
+ aspectRatio: number;
57
+ shape: string;
58
+ zoom: number;
59
+ maxZoom: number;
60
+ output: undefined;
61
+ size: undefined;
62
+ disabled: boolean;
63
+ ariaLabel: undefined;
64
+ }>>, {
65
+ crop: typeof toBlob;
66
+ reset: typeof reset;
67
+ rect: typeof currentRect;
68
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
69
+ "update:zoom": (value: number) => void;
70
+ change: (rect: GrCropRect) => void;
71
+ load: (size: {
72
+ width: number;
73
+ height: number;
74
+ }) => void;
75
+ error: (error: unknown) => void;
76
+ }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<__VLS_WithDefaults<__VLS_TypePropsToOption<GrImageCropProps>, {
77
+ src: null;
78
+ aspectRatio: number;
79
+ shape: string;
80
+ zoom: number;
81
+ maxZoom: number;
82
+ output: undefined;
83
+ size: undefined;
84
+ disabled: boolean;
85
+ ariaLabel: undefined;
86
+ }>>> & Readonly<{
87
+ "onUpdate:zoom"?: ((value: number) => any) | undefined;
88
+ onChange?: ((rect: GrCropRect) => any) | undefined;
89
+ onLoad?: ((size: {
90
+ width: number;
91
+ height: number;
92
+ }) => any) | undefined;
93
+ onError?: ((error: unknown) => any) | undefined;
94
+ }>, {
95
+ src: string | File | Blob | null;
96
+ aspectRatio: number;
97
+ shape: GrImageCropShape;
98
+ zoom: number;
99
+ maxZoom: number;
100
+ output: GrImageCropOutput;
101
+ size: GrImageCropSize;
102
+ disabled: boolean;
103
+ ariaLabel: string;
104
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
105
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
106
+ declare const _default: typeof __VLS_export;
107
+ export default _default;
108
+ type __VLS_TypePropsToOption<T> = {
109
+ [K in keyof T]-?: {} extends Pick<T, K> ? {
110
+ type: import('vue').PropType<Required<T>[K]>;
111
+ } : {
112
+ type: import('vue').PropType<T[K]>;
113
+ required: true;
114
+ };
115
+ };
116
+ type __VLS_WithDefaults<P, D> = {
117
+ [K in keyof Pick<P, keyof P>]: K extends keyof D ? __VLS_PrettifyLocal<P[K] & {
118
+ default: D[K];
119
+ }> : P[K];
120
+ };
121
+ type __VLS_WithSlots<T, S> = T & {
122
+ new (): {
123
+ $slots: S;
124
+ };
125
+ };
126
+ type __VLS_PrettifyLocal<T> = (T extends any ? {
127
+ [K in keyof T]: T[K];
128
+ } : {
129
+ [K in keyof T as K]: T[K];
130
+ }) & {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Слайдер увеличения — `GrSlider` ядра, и ребро объявлено: пресет подмешивает
3
+ * safelist и CSS только тем компонентам, что попали в селекцию, и без графа
4
+ * потребитель, выбравший один `GrImageCrop`, получил бы дорожку без ползунка.
5
+ *
6
+ * Слот `#controls` этого не отменяет: подмену делает потребитель, а по
7
+ * умолчанию слайдер рендерится.
8
+ */
9
+ export declare const grImageCropConfig: import("@feugene/unocss-preset-granular/contract").GranularComponentDescriptor<"GrImageCrop">;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Геометрия кропа: где именно вырезается кадр.
3
+ *
4
+ * Модель — «рамка неподвижна, двигается изображение»: окно кропа занимает весь
5
+ * видимый прямоугольник, а пользователь тянет и масштабирует картинку под ним.
6
+ * Обратная модель (рамка ездит по картинке) требует от пользователя двух
7
+ * жестов вместо одного и на телефоне промахивается по ручкам рамки.
8
+ *
9
+ * Всё здесь — чистые функции над числами: ни DOM, ни canvas. Кроп ошибается
10
+ * незаметно (кадр съезжает на пару процентов, и это видно только на результате),
11
+ * поэтому арифметика вынесена туда, где её можно проверить без рендера.
12
+ */
13
+ export interface GrCropSize {
14
+ width: number;
15
+ height: number;
16
+ }
17
+ export interface GrCropOffset {
18
+ x: number;
19
+ y: number;
20
+ }
21
+ /** Прямоугольник исходного изображения, попадающий в кадр. */
22
+ export interface GrCropRect {
23
+ sx: number;
24
+ sy: number;
25
+ sw: number;
26
+ sh: number;
27
+ }
28
+ /**
29
+ * Масштаб, при котором изображение покрывает окно целиком.
30
+ *
31
+ * Именно `max`, а не `min`: при `min` картинка вписалась бы внутрь окна и по
32
+ * краям остались бы поля, то есть кадр содержал бы пустоту, которой в исходнике
33
+ * нет.
34
+ */
35
+ export declare function coverScale(image: GrCropSize, viewport: GrCropSize): number;
36
+ /** Размер изображения на экране при данном увеличении. */
37
+ export declare function displaySize(image: GrCropSize, viewport: GrCropSize, zoom: number): GrCropSize;
38
+ /**
39
+ * Насколько изображение может съехать, не обнажив край окна.
40
+ *
41
+ * Предел считается от **отображаемого** размера, а не от исходного: при
42
+ * увеличении запас растёт, и жёсткая граница в пикселях исходника заперла бы
43
+ * увеличенную картинку в центре.
44
+ */
45
+ export declare function offsetBounds(image: GrCropSize, viewport: GrCropSize, zoom: number): GrCropOffset;
46
+ /** Смещение, подрезанное до пределов: за ними в кадр попала бы пустота. */
47
+ export declare function clampOffset(offset: GrCropOffset, image: GrCropSize, viewport: GrCropSize, zoom: number): GrCropOffset;
48
+ /**
49
+ * Область исходного изображения под окном кропа.
50
+ *
51
+ * Смещение подрезается здесь же, а не доверяется вызывающему: `zoom` меняется
52
+ * колесом и слайдером, и уменьшение всегда сокращает пределы — смещение,
53
+ * законное миг назад, после него выводит кадр за край.
54
+ */
55
+ export declare function cropRect(image: GrCropSize, viewport: GrCropSize, zoom: number, offset: GrCropOffset): GrCropRect;
56
+ /**
57
+ * Высота окна при заданном соотношении сторон.
58
+ *
59
+ * Соотношение задаётся числом, а не строкой `'16:9'`: строку пришлось бы
60
+ * разбирать в рантайме, а ошибка формата всплыла бы уже на экране.
61
+ */
62
+ export declare function viewportFor(width: number, aspectRatio: number): GrCropSize;
@@ -0,0 +1,16 @@
1
+ import type { GrImageCropSize } from './grImageCropStyles';
2
+ /**
3
+ * Настраиваемые через `GrConfigProvider` пропы.
4
+ *
5
+ * Аугментация реестра идёт по месту объявления — через
6
+ * `@feugene/granularity/composables/useGrComponentConfig`, а не через реэкспорт:
7
+ * иначе слияние отваливается молча.
8
+ */
9
+ export interface GrImageCropConfigurableProps {
10
+ size?: GrImageCropSize;
11
+ }
12
+ declare module '@feugene/granularity/composables/useGrComponentConfig' {
13
+ interface GrComponentDefaultsRegistry {
14
+ GrImageCrop?: GrImageCropConfigurableProps;
15
+ }
16
+ }
@@ -0,0 +1,42 @@
1
+ export type GrImageCropSize = 'xs' | 'sm' | 'md' | 'lg';
2
+ export type GrImageCropShape = 'rect' | 'circle';
3
+ export declare const rootClass = "grid gap-2";
4
+ /**
5
+ * Окно кадра. `touch-none` не косметика: без него браузер уводит вертикальный
6
+ * drag в прокрутку страницы, и картинка тянется только по горизонтали.
7
+ */
8
+ export declare const viewportClass = "relative w-full select-none overflow-hidden rounded-[var(--gr-radius-md)] border border-[var(--gr-brd)] bg-[var(--gr-muted)] touch-none focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--gr-ring)]";
9
+ export declare const imageClass = "pointer-events-none absolute left-1/2 top-1/2 max-w-none origin-center";
10
+ /**
11
+ * Затемнение вне круга — тенью наружу, а не вторым слоем с вырезом: вырез
12
+ * потребовал бы маски, а `mask-image` в presetMini не выражается и уехал бы в
13
+ * собственный CSS-файл ради одного правила.
14
+ *
15
+ * Граница круга обязательна и не декоративна: на тёмной теме затемнение чёрным
16
+ * поверх почти чёрного фона не читается вовсе, и пользователь не видит, что
17
+ * попадёт в кадр. Линия работает в обеих темах.
18
+ */
19
+ export declare const circleMaskClass = "pointer-events-none absolute inset-0 m-auto aspect-square rounded-[var(--gr-radius-full)] border border-[var(--gr-image-crop-guide,var(--gr-brd))] shadow-[0_0_0_9999px_var(--gr-image-crop-mask,rgb(0_0_0/0.55))]";
20
+ export declare const rectGuideClass = "pointer-events-none absolute inset-0 border border-[var(--gr-image-crop-guide,var(--gr-brd))]";
21
+ /**
22
+ * Плавность включается только вне жеста: под пальцем картинка обязана идти
23
+ * след в след, а вот шаг слайдера и клавиши без перехода выглядят рывком.
24
+ */
25
+ export declare const imageTransitionClass = "transition-transform duration-[var(--gr-duration-fast)] ease-[var(--gr-ease-out)]";
26
+ export declare const controlsClass = "flex items-center gap-3";
27
+ export declare const emptyClass = "flex items-center justify-center text-[var(--gr-muted-fg)]";
28
+ /** Кегль подписи и подсказки: контрольная шкала, парой с межстрочным. */
29
+ export declare const sizeTextClass: {
30
+ readonly xs: "text-[length:var(--gr-control-text-xs)] leading-[var(--gr-control-leading-xs)]";
31
+ readonly sm: "text-[length:var(--gr-control-text-sm)] leading-[var(--gr-control-leading-sm)]";
32
+ readonly md: "text-[length:var(--gr-control-text-md)] leading-[var(--gr-control-leading-md)]";
33
+ readonly lg: "text-[length:var(--gr-control-text-lg)] leading-[var(--gr-control-leading-lg)]";
34
+ };
35
+ /** Минимальная высота пустого состояния — по той же шкале, что кегль. */
36
+ export declare const sizeEmptyClass: {
37
+ readonly xs: "min-h-32";
38
+ readonly sm: "min-h-40";
39
+ readonly md: "min-h-48";
40
+ readonly lg: "min-h-56";
41
+ };
42
+ export declare const grImageCropSafelist: string[];
@@ -0,0 +1,9 @@
1
+ export { default } from './GrImageCrop.vue';
2
+ export { default as GrImageCrop } from './GrImageCrop.vue';
3
+ export { grImageCropConfig } from './config';
4
+ export type { GrImageCropConfigurableProps } from './defaults';
5
+ export type { GrCropOffset, GrCropRect, GrCropSize } from './cropGeometry';
6
+ export { clampOffset, coverScale, cropRect, displaySize, offsetBounds, viewportFor } from './cropGeometry';
7
+ export { grImageCropSafelist } from './grImageCropStyles';
8
+ export type { GrImageCropShape, GrImageCropSize } from './grImageCropStyles';
9
+ export type { GrImageCropEmits, GrImageCropOutput, GrImageCropProps } from './GrImageCrop.vue';
@@ -0,0 +1,3 @@
1
+ export * from './shared';
2
+ export declare const granularityMediaProvider: import("@feugene/unocss-preset-granular/contract").GranularProvider;
3
+ export default granularityMediaProvider;
@@ -0,0 +1,2 @@
1
+ export * from './index';
2
+ export { default } from './index';
@@ -0,0 +1,27 @@
1
+ import { type GranularComponentDescriptor, type GranularProvider } from '@feugene/unocss-preset-granular/contract';
2
+ /** Идентификатор провайдера — совпадает с именем пакета. */
3
+ export declare const GRANULARITY_MEDIA_PROVIDER_ID = "@feugene/granularity-media";
4
+ /**
5
+ * Реестр компонентов пакета — именованной мапой, а не инлайн-массивом.
6
+ *
7
+ * Именно по нему гейт реестров сверяет состав с файловой системой, а
8
+ * генератор — раскладывает компонент по четырём спискам.
9
+ */
10
+ export declare const granularityMediaComponentConfigs: {
11
+ GrImageCrop: GranularComponentDescriptor<"GrImageCrop">;
12
+ };
13
+ export type GranularityMediaComponentName = keyof typeof granularityMediaComponentConfigs;
14
+ /**
15
+ * Собирает granular-provider пакета.
16
+ *
17
+ * Принимает `granularityProvider` снаружи — в зависимости от entry это будет
18
+ * browser- или node-вариант провайдера `@feugene/granularity`. Это важно,
19
+ * чтобы у пресета был ровно один инстанс с данным `id`.
20
+ *
21
+ * `packageBaseUrl` тоже приходит снаружи, из самого entry, и это не церемония:
22
+ * он считается от `import.meta.url`, а этот модуль бандлер волен и вынести в
23
+ * общий чанк, и заинлайнить в entry — то есть положить на разную глубину.
24
+ * Промах на уровень даёт `dist/components/<Name>/`, которых нет, пресет молча
25
+ * пропускает скан, и в CSS остаётся только то, что перечислено в safelist.
26
+ */
27
+ export declare function createGranularityMediaProvider(granularityProvider: GranularProvider, packageBaseUrl: string): GranularProvider;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Все локали пакета одним списком.
3
+ *
4
+ * Подключается через subpath `@feugene/granularity-media/i18n/all` и уместен
5
+ * там, где размер бандла не важен: витрина, e2e, инструменты локализации. В
6
+ * приложении лучше импортировать нужные языки поимённо из
7
+ * `@feugene/granularity-media/i18n` — тогда лишние отсеет tree-shaking.
8
+ */
9
+ import type { LocaleLoaderCollection } from '@feugene/fint-i18n/core';
10
+ export declare const all: LocaleLoaderCollection[];
11
+ export default all;
@@ -0,0 +1,28 @@
1
+ export declare const grMediaMessages: {
2
+ readonly en: {
3
+ imageCrop: {
4
+ label: string;
5
+ frame: string;
6
+ empty: string;
7
+ zoom: string;
8
+ };
9
+ };
10
+ readonly es: {
11
+ imageCrop: {
12
+ label: string;
13
+ frame: string;
14
+ empty: string;
15
+ zoom: string;
16
+ };
17
+ };
18
+ readonly ru: {
19
+ imageCrop: {
20
+ label: string;
21
+ frame: string;
22
+ empty: string;
23
+ zoom: string;
24
+ };
25
+ };
26
+ };
27
+ export type GrMediaLocale = keyof typeof grMediaMessages;
28
+ export * from './messages';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Блок словаря пакета.
3
+ *
4
+ * Свой, а не `gr` ядра: `fint-i18n` изолирует ключи по блокам, и словарь
5
+ * companion-пакета, севший на чужой блок, столкнётся с ним при первом же
6
+ * совпадении верхнего ключа — молча, потому что мержатся они по одному имени.
7
+ */
8
+ export declare const GR_MEDIA_I18N_BLOCK = "grMedia";
@@ -0,0 +1,21 @@
1
+ export declare const en: {
2
+ en: {
3
+ grMedia: () => Promise<{
4
+ default: {
5
+ imageCrop: {
6
+ label: string;
7
+ frame: string;
8
+ empty: string;
9
+ zoom: string;
10
+ };
11
+ };
12
+ imageCrop: {
13
+ label: string;
14
+ frame: string;
15
+ empty: string;
16
+ zoom: string;
17
+ };
18
+ }>;
19
+ };
20
+ };
21
+ export default en;
@@ -0,0 +1,21 @@
1
+ export declare const es: {
2
+ es: {
3
+ grMedia: () => Promise<{
4
+ default: {
5
+ imageCrop: {
6
+ label: string;
7
+ frame: string;
8
+ empty: string;
9
+ zoom: string;
10
+ };
11
+ };
12
+ imageCrop: {
13
+ label: string;
14
+ frame: string;
15
+ empty: string;
16
+ zoom: string;
17
+ };
18
+ }>;
19
+ };
20
+ };
21
+ export default es;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Локали пакета в форме, которую понимает `fint-i18n`.
3
+ *
4
+ * Блок — свой (`grMedia`), а не `gr` ядра: чужой блок companion-пакету брать
5
+ * нельзя, иначе словари столкнутся на первом же совпавшем верхнем ключе. Ключи
6
+ * компонентов выглядят как `grMedia.imageCrop.apply`.
7
+ *
8
+ * ```ts
9
+ * import { en, GR_MEDIA_I18N_BLOCK, ru } from '@feugene/granularity-media/i18n'
10
+ * import { en as coreEn, GRANULARITY_I18N_BLOCK, ru as coreRu } from '@feugene/granularity/i18n'
11
+ *
12
+ * const i18n = createFintI18n({ locale: 'ru', loaders: [coreEn, coreRu, en, ru] })
13
+ * i18n.registerBlocks([GRANULARITY_I18N_BLOCK, GR_MEDIA_I18N_BLOCK])
14
+ * ```
15
+ *
16
+ * Нужны все языки разом (демо, e2e, инструменты) — есть агрегат
17
+ * `@feugene/granularity-media/i18n/all`.
18
+ */
19
+ export { GR_MEDIA_I18N_BLOCK } from './const';
20
+ export { en } from './en';
21
+ export { es } from './es';
22
+ export { ru } from './ru';
@@ -0,0 +1,21 @@
1
+ export declare const ru: {
2
+ ru: {
3
+ grMedia: () => Promise<{
4
+ default: {
5
+ imageCrop: {
6
+ label: string;
7
+ frame: string;
8
+ empty: string;
9
+ zoom: string;
10
+ };
11
+ };
12
+ imageCrop: {
13
+ label: string;
14
+ frame: string;
15
+ empty: string;
16
+ zoom: string;
17
+ };
18
+ }>;
19
+ };
20
+ };
21
+ export default ru;
@@ -0,0 +1 @@
1
+ export * from './components/GrImageCrop';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Разбор строки классов на токены для safelist.
3
+ *
4
+ * Своя копия двух строк, а не импорт из ядра: в его публичной поверхности
5
+ * этого хелпера нет, и расширять её ради `split` дороже, чем повторить.
6
+ */
7
+ export declare function splitClassTokens(value: string): string[];
@@ -0,0 +1,38 @@
1
+ import type { ComponentResolver } from 'unplugin-vue-components/types';
2
+ /**
3
+ * Имя npm-пакета — источника компонентов. Совпадает с `package.json#name`.
4
+ */
5
+ export declare const GRANULARITY_MEDIA_PACKAGE_NAME = "@feugene/granularity-media";
6
+ /**
7
+ * Резолвер `unplugin-vue-components` для `@feugene/granularity-media`.
8
+ *
9
+ * Построен на общей фабрике `createGranularResolver` из
10
+ * `@feugene/unplugin-granularity`. Резолвит компоненты пакета на их
11
+ * гранулярные subpath-экспорты. CSS компонента импортируется его же чанком
12
+ * (`libInjectCss` + `sideEffects: ["**\/*.css"]`), поэтому отдельный
13
+ * `styles.css`-side-effect не нужен (`importStyle: false`).
14
+ *
15
+ * ⚠️ Ставьте этот резолвер **перед** жадным `GranularityResolver()` ядра —
16
+ * иначе core-резолвер (совпадает с любым `Gr*`) перехватит `GrImageCrop`
17
+ * и импортирует его из несуществующего пути в `@feugene/granularity`.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * // vite.config.ts
22
+ * import Components from 'unplugin-vue-components/vite'
23
+ * import { GranularityResolver } from '@feugene/unplugin-granularity'
24
+ * import { GranularityMediaResolver } from '@feugene/granularity-media/resolver'
25
+ *
26
+ * export default defineConfig({
27
+ * plugins: [
28
+ * Components({
29
+ * resolvers: [
30
+ * GranularityMediaResolver(), // whitelist — раньше…
31
+ * GranularityResolver(), // …жадного Gr*-резолвера ядра
32
+ * ],
33
+ * }),
34
+ * ],
35
+ * })
36
+ * ```
37
+ */
38
+ export declare function GranularityMediaResolver(): ComponentResolver;
package/package.json ADDED
@@ -0,0 +1,129 @@
1
+ {
2
+ "name": "@feugene/granularity-media",
3
+ "description": "Media components for @feugene/granularity: image cropping, camera capture, code scanning and video playback.",
4
+ "version": "0.1.0",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "author": {
7
+ "name": "Evgeniy Fureev",
8
+ "url": "https://github.com/efureev"
9
+ },
10
+ "homepage": "https://efureev.github.io/granularity",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/efureev/granularity.git",
14
+ "directory": "packages/granularity-media"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/efureev/granularity/issues"
18
+ },
19
+ "keywords": [
20
+ "granularity",
21
+ "design-system",
22
+ "vue",
23
+ "image-crop",
24
+ "avatar",
25
+ "camera",
26
+ "barcode",
27
+ "video"
28
+ ],
29
+ "engines": {
30
+ "node": ">=22"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "provenance": true
35
+ },
36
+ "type": "module",
37
+ "sideEffects": [
38
+ "**/*.css"
39
+ ],
40
+ "main": "./dist/index.js",
41
+ "types": "./dist/types/index.d.ts",
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "exports": {
46
+ ".": {
47
+ "types": "./dist/types/index.d.ts",
48
+ "import": "./dist/index.js"
49
+ },
50
+ "./components/GrImageCrop": {
51
+ "types": "./dist/types/components/GrImageCrop/index.d.ts",
52
+ "import": "./dist/components/GrImageCrop/index.js"
53
+ },
54
+ "./i18n": {
55
+ "types": "./dist/types/i18n/index.d.ts",
56
+ "import": "./dist/i18n/index.js"
57
+ },
58
+ "./i18n/all": {
59
+ "types": "./dist/types/i18n/all.d.ts",
60
+ "import": "./dist/i18n/all.js"
61
+ },
62
+ "./granular-provider": {
63
+ "types": "./dist/types/granular-provider/index.d.ts",
64
+ "import": "./dist/granular-provider.js"
65
+ },
66
+ "./granular-provider/node": {
67
+ "types": "./dist/types/granular-provider/node.d.ts",
68
+ "import": "./dist/granular-provider-node.js"
69
+ },
70
+ "./resolver": {
71
+ "types": "./dist/types/resolver.d.ts",
72
+ "import": "./dist/resolver.js"
73
+ },
74
+ "./package.json": "./package.json"
75
+ },
76
+ "peerDependencies": {
77
+ "@feugene/fint-i18n": "^0.6.0",
78
+ "@feugene/granularity": ">=0.31.0 <1.0.0",
79
+ "@feugene/unocss-preset-granular": "^0.11.0",
80
+ "@feugene/unplugin-granularity": ">=0.4.0 <1.0.0",
81
+ "unplugin-vue-components": ">=0.26.0",
82
+ "vue": "^3.5.0"
83
+ },
84
+ "peerDependenciesMeta": {
85
+ "@feugene/fint-i18n": {
86
+ "optional": true
87
+ },
88
+ "@feugene/unplugin-granularity": {
89
+ "optional": true
90
+ },
91
+ "unplugin-vue-components": {
92
+ "optional": true
93
+ }
94
+ },
95
+ "scripts": {
96
+ "build": "vite build && gr-check-dist-dev-guard && vue-tsc -p tsconfig.build.json",
97
+ "dev": "vite build --watch",
98
+ "doctor": "granular doctor ./granular.options.mjs --strict",
99
+ "generate:registry": "node scripts/generate-registry.mjs",
100
+ "lint": "eslint . --cache",
101
+ "lint:fix": "eslint . --cache --fix",
102
+ "test": "vitest --config vitest.config.ts",
103
+ "test:run": "vitest run --config vitest.config.ts",
104
+ "typecheck": "vue-tsc --noEmit -p tsconfig.json",
105
+ "check:messages": "fint-i18n-check-messages src/i18n/locales"
106
+ },
107
+ "devDependencies": {
108
+ "@antfu/eslint-config": "^9.1.0",
109
+ "@feugene/fint-i18n": "^0.6.0",
110
+ "@feugene/granularity": "^0.31.0",
111
+ "@feugene/granularity-test-kit": "^0.7.2",
112
+ "@feugene/unocss-preset-granular": "^0.11.0",
113
+ "@feugene/unplugin-granularity": "^0.6.0",
114
+ "@types/node": "^26.1.1",
115
+ "@vitejs/plugin-vue": "^6.0.8",
116
+ "@vue/test-utils": "^2.4.11",
117
+ "axe-core": "~4.12.1",
118
+ "eslint": "^10.7.0",
119
+ "globals": "^16.5.0",
120
+ "jsdom": "^29.1.1",
121
+ "typescript": "^6.0.2",
122
+ "unplugin-vue-components": "^32.0.0",
123
+ "vite": "^8.1.5",
124
+ "vite-plugin-lib-inject-css": "^2.2.2",
125
+ "vitest": "^4.1.10",
126
+ "vue": "^3.5.40",
127
+ "vue-tsc": "^3.3.7"
128
+ }
129
+ }