@globalbrain/sefirot 4.43.5 → 4.45.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 (41) hide show
  1. package/README.md +5 -5
  2. package/config/vite.js +1 -24
  3. package/lib/blocks/lens/FieldData.ts +46 -0
  4. package/lib/blocks/lens/Rule.ts +6 -0
  5. package/lib/blocks/lens/components/LensCatalog.vue +57 -2
  6. package/lib/blocks/lens/components/LensCatalogControl.vue +9 -0
  7. package/lib/blocks/lens/components/LensCatalogStateFilterCondition.vue +13 -7
  8. package/lib/blocks/lens/components/LensFormFilter.vue +22 -3
  9. package/lib/blocks/lens/components/LensFormFilterCondition.vue +6 -0
  10. package/lib/blocks/lens/components/LensFormOverrideBase.vue +6 -0
  11. package/lib/blocks/lens/components/LensFormOverrideNumber.vue +166 -0
  12. package/lib/blocks/lens/components/LensFormView.vue +31 -4
  13. package/lib/blocks/lens/components/LensTable.vue +18 -5
  14. package/lib/blocks/lens/composables/SetupLens.ts +6 -0
  15. package/lib/blocks/lens/fields/BooleanField.ts +76 -0
  16. package/lib/blocks/lens/fields/DateField.ts +9 -1
  17. package/lib/blocks/lens/fields/DatetimeField.ts +8 -1
  18. package/lib/blocks/lens/fields/DecimalField.ts +60 -0
  19. package/lib/blocks/lens/fields/Field.ts +24 -1
  20. package/lib/blocks/lens/fields/NumberField.ts +25 -4
  21. package/lib/blocks/lens/fields/RelatedManyField.ts +29 -5
  22. package/lib/blocks/lens/fields/RelatedOneField.ts +119 -0
  23. package/lib/blocks/lens/fields/TextField.ts +2 -0
  24. package/lib/blocks/lens/fields/support/Renderers.ts +78 -0
  25. package/lib/blocks/lens/filter-inputs/SelectFilterInput.ts +5 -1
  26. package/lib/blocks/lens/validation/RuleMapper.ts +3 -1
  27. package/lib/components/SInputFileUpload.vue +17 -2
  28. package/lib/components/SInputFileUploadItem.vue +57 -16
  29. package/lib/components/STableCell.vue +1 -0
  30. package/lib/components/STableCellNumber.vue +25 -2
  31. package/lib/composables/Table.ts +6 -0
  32. package/lib/http/Http.ts +8 -13
  33. package/lib/support/Chart.ts +7 -3
  34. package/lib/support/Day.ts +5 -4
  35. package/lib/support/File.ts +25 -0
  36. package/lib/support/Num.ts +3 -2
  37. package/lib/support/Utils.ts +13 -0
  38. package/lib/validation/validators/maxLength.ts +3 -1
  39. package/lib/validation/validators/maxTotalFileSize.ts +10 -2
  40. package/lib/validation/validators/minLength.ts +3 -1
  41. package/package.json +34 -35
@@ -2,7 +2,7 @@
2
2
  import IconFileText from '~icons/ph/file-text'
3
3
  import IconTrash from '~icons/ph/trash'
4
4
  import { type ValidationRuleWithParams } from '@vuelidate/core'
5
- import { type Component, computed } from 'vue'
5
+ import { type Component, computed, watch } from 'vue'
6
6
  import { useValidation } from '../composables/Validation'
7
7
  import { formatSize } from '../support/File'
8
8
  import SButton, { type Mode as ButtonMode } from './SButton.vue'
@@ -27,7 +27,7 @@ export interface Action {
27
27
  }
28
28
 
29
29
  const props = defineProps<{
30
- file: File | FileObject
30
+ file: File | FileObject | string
31
31
  rules?: Record<string, ValidationRuleWithParams>
32
32
  }>()
33
33
 
@@ -35,23 +35,64 @@ defineEmits<{
35
35
  remove: []
36
36
  }>()
37
37
 
38
- const _file = computed(() => ({
39
- name: props.file instanceof File ? props.file.name : props.file.file.name,
40
- file: props.file instanceof File ? props.file : props.file.file,
41
- size: formatSize(props.file instanceof File ? props.file : props.file.file),
42
- indicatorState: props.file instanceof File ? null : props.file.indicatorState,
43
- canRemove: props.file instanceof File ? true : (props.file.canRemove ?? true),
44
- action: props.file instanceof File ? null : props.file.action,
45
- errorMessage: props.file instanceof File ? null : props.file.errorMessage
46
- }))
38
+ const _file = computed(() => {
39
+ const value = props.file
40
+
41
+ if (value instanceof File) {
42
+ return {
43
+ name: value.name,
44
+ file: value as File | null,
45
+ size: formatSize(value) as string | null,
46
+ indicatorState: null as IndicatorState | null,
47
+ canRemove: true,
48
+ action: null as Action | null,
49
+ errorMessage: null as string | null
50
+ }
51
+ }
52
+
53
+ // A plain string references an already-uploaded file: show its basename
54
+ // and skip the size (unknown for server-side files).
55
+ if (typeof value === 'string') {
56
+ return {
57
+ name: value.split('/').pop() || value,
58
+ file: null,
59
+ size: null,
60
+ indicatorState: null,
61
+ canRemove: true,
62
+ action: null,
63
+ errorMessage: null
64
+ }
65
+ }
66
+
67
+ return {
68
+ name: value.file.name,
69
+ file: value.file as File | null,
70
+ size: formatSize(value.file) as string | null,
71
+ indicatorState: value.indicatorState ?? null,
72
+ canRemove: value.canRemove ?? true,
73
+ action: value.action ?? null,
74
+ errorMessage: value.errorMessage ?? null
75
+ }
76
+ })
47
77
 
78
+ // Rules are passed as a getter so validation reacts when the item's
79
+ // identity changes — the parent keys items by index, so a single instance
80
+ // can be reused for a different item after a removal. Per-file rules apply
81
+ // to newly selected `File`s only; an already-uploaded `string` reference
82
+ // has no local file to validate, so it's skipped (the server validates it).
48
83
  const { validation } = useValidation(() => ({
49
84
  file: _file.value.file
50
- }), {
51
- file: props.rules ?? {}
52
- })
85
+ }), () => ({
86
+ file: typeof props.file === 'string' ? {} : (props.rules ?? {})
87
+ }))
53
88
 
54
- validation.value.$touch()
89
+ // Surface validation immediately, and re-touch whenever the item changes:
90
+ // when an index-keyed instance is reused for a different item, switching
91
+ // rules resets the dirty state, so a post-flush re-touch is needed to keep
92
+ // the new item's errors visible.
93
+ watch(() => props.file, () => {
94
+ validation.value.$touch()
95
+ }, { immediate: true, flush: 'post' })
55
96
  </script>
56
97
 
57
98
  <template>
@@ -84,7 +125,7 @@ validation.value.$touch()
84
125
  />
85
126
  </div>
86
127
  <div class="meta">
87
- <div class="size">
128
+ <div v-if="_file.size" class="size">
88
129
  {{ _file.size }}
89
130
  </div>
90
131
  <div class="delete">
@@ -54,6 +54,7 @@ const valueIsImagePath = computed(() => {
54
54
  :icon="computedCell.icon"
55
55
  :number="computedCell.value ?? value"
56
56
  :separator="computedCell.separator"
57
+ :maximum-fraction-digits="computedCell.maximumFractionDigits"
57
58
  :link="computedCell.link"
58
59
  :color="computedCell.color"
59
60
  :icon-color="computedCell.iconColor"
@@ -1,7 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { type Component, computed } from 'vue'
3
3
  import { type TableCellValueColor } from '../composables/Table'
4
- import { format } from '../support/Num'
5
4
  import SLink from './SLink.vue'
6
5
 
7
6
  const props = defineProps<{
@@ -11,6 +10,7 @@ const props = defineProps<{
11
10
  icon?: Component
12
11
  number?: number | null
13
12
  separator?: boolean
13
+ maximumFractionDigits?: number | null
14
14
  color?: TableCellValueColor
15
15
  iconColor?: TableCellValueColor
16
16
  link?: string | null
@@ -25,6 +25,29 @@ const classes = computed(() => [
25
25
  _color,
26
26
  { link: !!(props.link || props.onClick) }
27
27
  ])
28
+
29
+ // We format the value inline (rather than via `Num.format`) so we can
30
+ // thread `useGrouping` and `maximumFractionDigits` through the same
31
+ // `toLocaleString` call. `Num.format` is kept for callers that just
32
+ // want the default separator-on / unbounded-digits behavior.
33
+ //
34
+ // When neither `separator` nor `maximumFractionDigits` is requested we
35
+ // return the raw number and let Vue's template interpolation stringify
36
+ // it. `toLocaleString` would otherwise round very small numbers to
37
+ // `"0"` (e.g. `1e-25` → `"0"` at 20 digits), losing information that
38
+ // the plain `String(number)` form preserves via scientific notation.
39
+ const formatted = computed(() => {
40
+ if (props.number == null) {
41
+ return ''
42
+ }
43
+ if (!props.separator && props.maximumFractionDigits == null) {
44
+ return props.number
45
+ }
46
+ return props.number.toLocaleString('en-US', {
47
+ useGrouping: props.separator === true,
48
+ maximumFractionDigits: props.maximumFractionDigits ?? 20
49
+ })
50
+ })
28
51
  </script>
29
52
 
30
53
  <template>
@@ -40,7 +63,7 @@ const classes = computed(() => [
40
63
  <component :is="icon" class="svg" />
41
64
  </div>
42
65
  <div class="value" :class="_color">
43
- {{ separator ? format(number) : number }}
66
+ {{ formatted }}
44
67
  </div>
45
68
  </SLink>
46
69
  </div>
@@ -115,6 +115,12 @@ export interface TableCellNumber<V = any, R = any> extends TableCellBase {
115
115
  icon?: Component
116
116
  value?: number | null
117
117
  separator?: boolean
118
+ /**
119
+ * Caps the displayed fractional digits using
120
+ * `Intl.NumberFormat`-style "maximum fractional digits" semantics.
121
+ * `null` / `undefined` shows the value as-is (no rounding).
122
+ */
123
+ maximumFractionDigits?: number | null
118
124
  link?: string | null
119
125
  color?: TableCellValueColor
120
126
  iconColor?: TableCellValueColor
package/lib/http/Http.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { parse as parseContentDisposition } from '@tinyhttp/content-disposition'
2
2
  import { parse as parseCookie } from '@tinyhttp/cookie'
3
- import FileSaver from 'file-saver'
4
3
  import { FetchError, type FetchOptions, type FetchResponse } from 'ofetch'
5
4
  import { stringify } from 'qs'
5
+ import { saveAs } from '../support/File'
6
6
  import { objectToFormData } from '../support/Http'
7
7
 
8
8
  type Config = ReturnType<typeof import('../stores/HttpConfig').useHttpConfig>
@@ -116,20 +116,15 @@ export class Http {
116
116
  }
117
117
 
118
118
  async download(url: string, options?: FetchOptions): Promise<void> {
119
- const { _data: blob, headers } = await this.performRequestRaw<Blob>(url, {
120
- method: 'GET',
121
- responseType: 'blob',
122
- ...options
123
- })
124
-
125
- if (!blob) {
126
- throw new Error('No blob')
127
- }
119
+ const { _data: blob, headers } =
120
+ await this.performRequestRaw<Blob>(url, { method: 'GET', responseType: 'blob', ...options })
128
121
 
129
- const { filename = 'download' } =
130
- parseContentDisposition(headers.get('Content-Disposition') || '')?.parameters || {}
122
+ let filename
123
+ try {
124
+ filename = parseContentDisposition(headers.get('Content-Disposition') || '').parameters.filename
125
+ } catch {}
131
126
 
132
- FileSaver.saveAs(blob, filename as string)
127
+ saveAs(blob, filename as string | undefined)
133
128
  }
134
129
  }
135
130
 
@@ -6,8 +6,8 @@
6
6
  * https://github.com/radix-ui/colors/blob/main/LICENSE
7
7
  */
8
8
 
9
- import FileSaver from 'file-saver'
10
9
  import html2canvas from 'html2canvas'
10
+ import { saveAs } from './File'
11
11
 
12
12
  export const c = {
13
13
  text1: 'light-dark(#1c2024, #edeef0)',
@@ -86,6 +86,10 @@ export async function exportAsPng(_el: any, fileName = 'chart.png', delay = 0):
86
86
  }
87
87
  })
88
88
 
89
- const dataUrl = canvas.toDataURL('image/png')
90
- FileSaver.saveAs(dataUrl, fileName)
89
+ const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/png'))
90
+ if (!blob) {
91
+ throw new Error('Failed to export chart as PNG: unable to create blob from canvas')
92
+ }
93
+
94
+ saveAs(blob, fileName)
91
95
  }
@@ -1,7 +1,8 @@
1
- import dayjs, { type ConfigType, type Dayjs } from 'dayjs'
2
- import PluginRelativeTime from 'dayjs/plugin/relativeTime'
3
- import PluginTimezone from 'dayjs/plugin/timezone'
4
- import PluginUtc from 'dayjs/plugin/utc'
1
+ import dayjs, { type ConfigType, type Dayjs } from 'dayjs/esm'
2
+ import PluginRelativeTime from 'dayjs/esm/plugin/relativeTime'
3
+ import PluginTimezone from 'dayjs/esm/plugin/timezone'
4
+ import PluginUtc from 'dayjs/esm/plugin/utc'
5
+ import 'dayjs/esm/locale/ja'
5
6
 
6
7
  dayjs.extend(PluginUtc)
7
8
  dayjs.extend(PluginTimezone)
@@ -13,3 +13,28 @@ export function formatSize(files: File | File[]): string {
13
13
  const i = Math.min(Math.floor(Math.log(size) / Math.log(1000)), units.length - 1)
14
14
  return `${(size / 1000 ** i).toFixed(2)} ${units[i]}`
15
15
  }
16
+
17
+ export function saveAs(blob: Blob | undefined, filename: string | undefined): void {
18
+ if (typeof window === 'undefined') {
19
+ throw new TypeError('saveAs can only be used in a browser environment.')
20
+ }
21
+
22
+ if (!(blob instanceof Blob)) {
23
+ throw new TypeError('The first argument must be a Blob.')
24
+ }
25
+
26
+ const url = URL.createObjectURL(blob)
27
+ const anchor = document.createElement('a')
28
+ anchor.href = url
29
+ anchor.download = filename || 'download'
30
+ anchor.rel = 'noopener'
31
+ anchor.style.display = 'none'
32
+
33
+ document.body.appendChild(anchor)
34
+ anchor.click()
35
+
36
+ setTimeout(() => {
37
+ anchor.remove()
38
+ URL.revokeObjectURL(url)
39
+ }, 4e4)
40
+ }
@@ -2,8 +2,9 @@ export function format(value: number): string {
2
2
  return value.toLocaleString('en-US', { maximumFractionDigits: 20 })
3
3
  }
4
4
 
5
- export function abbreviate(value: number, precision = 0): string {
6
- return value.toLocaleString('en-US', {
5
+ export function abbreviate(value: number, precision = 0, lang: 'en' | 'ja' = 'en'): string {
6
+ const locale = lang === 'ja' ? 'ja-JP' : 'en-US'
7
+ return value.toLocaleString(locale, {
7
8
  notation: 'compact',
8
9
  maximumFractionDigits: precision
9
10
  })
@@ -14,3 +14,16 @@ export function isObject(value: unknown): value is Record<PropertyKey, unknown>
14
14
  export function isString(value: unknown): value is string {
15
15
  return typeof value === 'string'
16
16
  }
17
+
18
+ export function getLength(value: unknown): number {
19
+ if (typeof value === 'string') {
20
+ // Count Unicode code points, not UTF-16 code units (which `String.length`
21
+ // returns). This matches how the database measures column length (MySQL
22
+ // `CHAR_LENGTH`, PostgreSQL `length`) and the backend's PHP `mb_strlen`.
23
+ return [...value].length
24
+ }
25
+ if (Array.isArray(value)) {
26
+ return value.length
27
+ }
28
+ throw new TypeError('Value must be a string or an array')
29
+ }
@@ -1,3 +1,5 @@
1
+ import { getLength } from '../../support/Utils'
2
+
1
3
  export function maxLength(value: unknown, length: number): boolean {
2
- return (typeof value === 'string' || Array.isArray(value)) && value.length <= length
4
+ try { return getLength(value) <= length } catch { return false }
3
5
  }
@@ -1,8 +1,16 @@
1
1
  export function maxTotalFileSize(value: unknown, size: string): boolean {
2
- if (!Array.isArray(value) || !value.every((v) => v instanceof File)) { return false }
2
+ // The model may mix freshly selected `File`s with `string` references to
3
+ // already-uploaded files (see `SInputFileUpload`). Reject anything else,
4
+ // but allow string references through — only `File`s contribute to the
5
+ // total, since an already-uploaded file has no client-side size (the
6
+ // server validates its real size). This means the total under-counts kept
7
+ // files, which is accepted for now.
8
+ if (!Array.isArray(value) || !value.every((v) => v instanceof File || typeof v === 'string')) {
9
+ return false
10
+ }
3
11
 
4
12
  const factor = /gb/i.test(size) ? 1e9 : /mb/i.test(size) ? 1e6 : /kb/i.test(size) ? 1e3 : 1
5
- const total = value.reduce((total, file) => total + file.size, 0)
13
+ const total = value.reduce((total, file) => total + (file instanceof File ? file.size : 0), 0)
6
14
 
7
15
  return total <= factor * Number.parseFloat(size.replace(/[^\d.]/g, ''))
8
16
  }
@@ -1,3 +1,5 @@
1
+ import { getLength } from '../../support/Utils'
2
+
1
3
  export function minLength(value: unknown, length: number): boolean {
2
- return (typeof value === 'string' || Array.isArray(value)) && value.length >= length
4
+ try { return getLength(value) >= length } catch { return false }
3
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globalbrain/sefirot",
3
- "version": "4.43.5",
3
+ "version": "4.45.0",
4
4
  "description": "Vue Components for Global Brain Design System.",
5
5
  "keywords": [
6
6
  "components",
@@ -37,11 +37,11 @@
37
37
  "./*": "./*"
38
38
  },
39
39
  "scripts": {
40
- "docs": "pnpm run docs:dev",
40
+ "docs": "pnpm docs:dev",
41
41
  "docs:dev": "vitepress dev docs --port 4011",
42
42
  "docs:build": "vitepress build docs",
43
43
  "docs:preview": "vitepress serve docs --port 4011",
44
- "story": "pnpm run story:dev",
44
+ "story": "pnpm story:dev",
45
45
  "story:dev": "NODE_NO_WARNINGS=1 VITE_CJS_IGNORE_WARNING=1 histoire dev --port 4010",
46
46
  "story:build": "NODE_NO_WARNINGS=1 VITE_CJS_IGNORE_WARNING=1 histoire build",
47
47
  "story:preview": "NODE_NO_WARNINGS=1 VITE_CJS_IGNORE_WARNING=1 histoire preview --port 4010",
@@ -51,72 +51,71 @@
51
51
  "test": "vitest",
52
52
  "test:fail": "vitest run",
53
53
  "test:coverage": "vitest run --coverage",
54
- "check": "pnpm run type && pnpm run lint:fail && pnpm run test:fail",
54
+ "check": "pnpm run --aggregate-output '/^(type|lint|test:fail)$/'",
55
+ "check:fail": "pnpm run --aggregate-output '/^(type|lint:fail|test:fail)$/'",
55
56
  "release": "pnpm whoami >/dev/null 2>&1 || pnpm login && release-it"
56
57
  },
57
58
  "dependencies": {
58
59
  "@iconify-json/ph": "^1.2.2",
59
60
  "@iconify-json/ri": "^1.2.10",
60
61
  "@popperjs/core": "^2.11.8",
61
- "@sentry/browser": "^10.47.0",
62
- "@sentry/vue": "^10.47.0",
62
+ "@sentry/browser": "^10.55.0",
63
+ "@sentry/vue": "^10.55.0",
63
64
  "@tanstack/vue-virtual": "3.0.0-beta.62",
64
65
  "@tinyhttp/content-disposition": "^2.2.4",
65
66
  "@tinyhttp/cookie": "^2.1.1",
66
67
  "@total-typescript/ts-reset": "^0.6.1",
67
68
  "@types/body-scroll-lock": "^3.1.2",
68
69
  "@types/d3": "^7.4.3",
69
- "@types/file-saver": "^2.0.7",
70
70
  "@types/lodash-es": "^4.17.12",
71
71
  "@types/markdown-it": "^14.1.2",
72
- "@types/qs": "^6.15.0",
73
- "@vitejs/plugin-vue": "^6.0.5",
74
- "@vue/reactivity": "^3.5.32",
72
+ "@types/qs": "^6.15.1",
73
+ "@vitejs/plugin-vue": "^6.0.7",
74
+ "@vue/reactivity": "^3.5.35",
75
75
  "@vuelidate/core": "^2.0.3",
76
76
  "@vuelidate/validators": "^2.0.4",
77
- "@vueuse/core": "^14.2.1",
77
+ "@vueuse/core": "^14.3.0",
78
78
  "body-scroll-lock": "4.0.0-beta.0",
79
79
  "d3": "^7.9.0",
80
- "dayjs": "^1.11.20",
81
- "dompurify": "^3.3.3",
82
- "file-saver": "^2.0.5",
83
- "fuse.js": "^7.2.0",
80
+ "dayjs": "^1.11.21",
81
+ "dompurify": "^3.4.7",
82
+ "fuse.js": "^7.3.0",
84
83
  "html2canvas": "^1.4.1",
85
- "jsdom": "^29.0.1",
84
+ "jsdom": "^29.1.1",
86
85
  "lodash-es": "^4.18.1",
87
86
  "magic-string": "^0.30.21",
88
- "markdown-it": "^14.1.1",
87
+ "markdown-it": "^14.2.0",
89
88
  "normalize.css": "^8.0.1",
90
89
  "ofetch": "^1.5.1",
91
90
  "pinia": "^3.0.4",
92
- "postcss": "^8.5.8",
91
+ "postcss": "^8.5.15",
93
92
  "postcss-nested": "^7.0.2",
94
93
  "punycode": "^2.3.1",
95
- "qs": "^6.15.0",
94
+ "qs": "^6.15.2",
96
95
  "unplugin-icons": "^23.0.1",
97
96
  "v-calendar": "3.0.1",
98
- "vite": "^7.3.1",
99
- "vue": "^3.5.32",
97
+ "vite": "^7.3.3",
98
+ "vue": "^3.5.35",
100
99
  "vue-draggable-plus": "^0.6.1",
101
- "vue-router": "^5.0.4"
100
+ "vue-router": "^5.1.0"
102
101
  },
103
102
  "devDependencies": {
104
- "@globalbrain/eslint-config": "^3.0.1",
103
+ "@globalbrain/eslint-config": "^3.1.0",
105
104
  "@histoire/plugin-vue": "1.0.0-beta.1",
106
- "@release-it/conventional-changelog": "^10.0.6",
107
- "@types/jsdom": "^28.0.1",
108
- "@types/node": "^25.5.1",
109
- "@typescript-eslint/rule-tester": "^8.58.0",
110
- "@vitest/coverage-v8": "^4.1.2",
111
- "@vue/test-utils": "^2.4.6",
105
+ "@release-it/conventional-changelog": "^11.0.1",
106
+ "@types/jsdom": "^28.0.3",
107
+ "@types/node": "^25.9.1",
108
+ "@typescript-eslint/rule-tester": "^8.60.0",
109
+ "@vitest/coverage-v8": "^4.1.7",
110
+ "@vue/test-utils": "^2.4.10",
112
111
  "eslint": "^9.39.4",
113
- "happy-dom": "^20.8.9",
112
+ "happy-dom": "^20.9.0",
114
113
  "histoire": "1.0.0-beta.1",
115
- "release-it": "^19.2.4",
116
- "typescript": "~5.9.3",
114
+ "release-it": "^20.2.0",
115
+ "typescript": "~6.0.3",
117
116
  "vitepress": "^2.0.0-alpha.17",
118
- "vitest": "^4.1.2",
119
- "vue-tsc": "^3.2.6"
117
+ "vitest": "^4.1.7",
118
+ "vue-tsc": "^3.3.3"
120
119
  },
121
- "packageManager": "pnpm@10.33.0"
120
+ "packageManager": "pnpm@11.5.0"
122
121
  }