diff --git a/package-lock.json b/package-lock.json index a2210041c53..c437d735b62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2733,6 +2733,37 @@ "version": "0.2.11", "license": "MIT" }, + "node_modules/@formatjs/bigdecimal": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@formatjs/bigdecimal/-/bigdecimal-0.2.3.tgz", + "integrity": "sha512-d7LpumdsbHueHdlVMos2yROIumyisUJNlFAk3PpN/5YcnXhWnkYmeiaQnOjqmmGx8BbaphoIyaF2CPus3cownw==", + "license": "MIT" + }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.4.tgz", + "integrity": "sha512-Lbke1aOrsygKKR09Ux0NrZgbTqpDmiwXOgzyDOJ8Owr1zd5qOKTauf62hH+Seeku3ju77rHWH9I5SfX2CN0vuA==", + "license": "MIT" + }, + "node_modules/@formatjs/intl-durationformat": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@formatjs/intl-durationformat/-/intl-durationformat-0.10.8.tgz", + "integrity": "sha512-1Crir41n1kMTVejBg7tkLfBRSWm7p1y+Bt4Nyny1DtxH41/+q2qZ2vSyTiQEdKbvSvmt0WG/gInFrMsK8lx1/A==", + "license": "MIT", + "dependencies": { + "@formatjs/bigdecimal": "0.2.3", + "@formatjs/intl-localematcher": "0.8.6" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.6.tgz", + "integrity": "sha512-AZRgUxj0q93lyF7Z5lFS85bLINXuBLX4R3tCKicO6fSWo6cvh9GQfoR3B1WlsqQwefZ1QORTivhInx7gM6HUzQ==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.4" + } + }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "6.7.2", "license": "MIT", @@ -5738,6 +5769,13 @@ "@types/node": "*" } }, + "node_modules/@types/humanize-duration": { + "version": "3.27.4", + "resolved": "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.27.4.tgz", + "integrity": "sha512-yaf7kan2Sq0goxpbcwTQ+8E9RP6HutFBPv74T/IA/ojcHKhuKVlk2YFYyHhWZeLvZPzzLE3aatuQB4h0iqyyUA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/inquirer": { "version": "9.0.9", "dev": true, @@ -12260,6 +12298,16 @@ "node": ">=10.17.0" } }, + "node_modules/humanize-duration": { + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.33.2.tgz", + "integrity": "sha512-K7Ny/ULO1hDm2nnhvAY+SJV1skxFb61fd073SG1IWJl+D44ULrruCuTyjHKjBVVcSuTlnY99DKtgEG39CM5QOQ==", + "dev": true, + "license": "Unlicense", + "funding": { + "url": "https://github.com/sponsors/EvanHahn" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "license": "MIT", @@ -22358,6 +22406,7 @@ "license": "MIT", "dependencies": { "@chromatic-com/storybook": "^5.1.2", + "@formatjs/intl-durationformat": "^0.10.8", "@github/relative-time-element": "^5.0.0", "axios": "^1.15.2" }, @@ -22374,6 +22423,7 @@ "@storybook/addon-vitest": "^10.3.6", "@storybook/web-components-vite": "^10.3.6", "@total-typescript/tsconfig": "^1.0.4", + "@types/humanize-duration": "^3.27.4", "@types/jquery": "^4.0.0", "@types/node": "^25.6.0", "@vitest/browser": "^4.1.5", @@ -22384,6 +22434,7 @@ "esbuild": "^0.28.0", "globby": "^16.2.0", "happy-dom": "^20.9.0", + "humanize-duration": "^3.33.2", "lit": "^3.3.2", "ora": "^9.4.0", "playwright": "^1.59.1", diff --git a/packages/craftcms-cp/package.json b/packages/craftcms-cp/package.json index 0629e0a6600..0d7dc6f19ea 100644 --- a/packages/craftcms-cp/package.json +++ b/packages/craftcms-cp/package.json @@ -73,6 +73,7 @@ "@storybook/addon-vitest": "^10.3.6", "@storybook/web-components-vite": "^10.3.6", "@total-typescript/tsconfig": "^1.0.4", + "@types/humanize-duration": "^3.27.4", "@types/jquery": "^4.0.0", "@types/node": "^25.6.0", "@vitest/browser": "^4.1.5", @@ -83,6 +84,7 @@ "esbuild": "^0.28.0", "globby": "^16.2.0", "happy-dom": "^20.9.0", + "humanize-duration": "^3.33.2", "lit": "^3.3.2", "ora": "^9.4.0", "playwright": "^1.59.1", @@ -99,6 +101,7 @@ }, "dependencies": { "@chromatic-com/storybook": "^5.1.2", + "@formatjs/intl-durationformat": "^0.10.8", "@github/relative-time-element": "^5.0.0", "axios": "^1.15.2" }, diff --git a/packages/craftcms-cp/src/utilities/format.ts b/packages/craftcms-cp/src/utilities/format.ts index fdeae1df503..db6a02bb3ac 100644 --- a/packages/craftcms-cp/src/utilities/format.ts +++ b/packages/craftcms-cp/src/utilities/format.ts @@ -1,3 +1,8 @@ +import humanizeDuration from 'humanize-duration'; + +// @TODO grab this from the config +const defaultCpLocale = 'en'; + export function formatNumber(number: number | string, format?: string) { // If d3 is available, use it for compatibility if ( @@ -27,7 +32,7 @@ export function formatNumber(number: number | string, format?: string) { ? parseInt(decimalMatch[1]!, 10) : 0; - return new Intl.NumberFormat('en-US', { + return new Intl.NumberFormat(defaultCpLocale, { useGrouping: hasThousandSeparator, minimumFractionDigits: maximumFractionDigits, maximumFractionDigits: maximumFractionDigits, @@ -35,9 +40,16 @@ export function formatNumber(number: number | string, format?: string) { } // Default: format with thousand separators, no decimal places - return new Intl.NumberFormat('en-US', { + return new Intl.NumberFormat(defaultCpLocale, { useGrouping: true, minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(num); } + +export const cpHumanizer = humanizeDuration.humanizer({ + language: defaultCpLocale, + fallbacks: ['en'], + largest: 2, + round: true, +}); diff --git a/resources/js/bootstrap/cp.ts b/resources/js/bootstrap/cp.ts index d00d006c032..ff656668067 100644 --- a/resources/js/bootstrap/cp.ts +++ b/resources/js/bootstrap/cp.ts @@ -16,6 +16,8 @@ import AssetIndexes from '@/components/utilities/AssetIndexes/AssetIndexes.vue'; import SystemMessages from '@/components/utilities/SystemMessages/SystemMessages.vue'; import DeprecationErrorsToolbar from '@/components/utilities/DeprecationErrors/DeprecationErrorsToolbar.vue'; import {setTranslations} from '@craftcms/cp/utilities/translate.ts.mjs'; +import TotpForm from '@/components/Auth/TotpForm.vue'; +import RecoveryCodesForm from '@/components/Auth/RecoveryCodesForm.vue'; let bootedCallbacks: Array<(instance: any) => void> = []; let bootingCallbacks: Array<(instance: any) => void> = []; @@ -97,6 +99,9 @@ const Cp = { app.component('ProjectConfig', ProjectConfig); app.component('AssetIndexes', AssetIndexes); app.component('SystemMessages', SystemMessages); + + app.component('TotpForm', TotpForm); + app.component('RecoveryCodesForm', RecoveryCodesForm); }, }); diff --git a/resources/js/components/ActionMenu.vue b/resources/js/components/ActionMenu.vue index 1abf7292395..1e85a4564dd 100644 --- a/resources/js/components/ActionMenu.vue +++ b/resources/js/components/ActionMenu.vue @@ -2,6 +2,7 @@ import {t} from '@craftcms/cp/utilities/translate.ts.mjs'; import type {VariantKey} from '@craftcms/cp/types/index.ts'; import {type Component, computed, type VNode} from 'vue'; + import VarDump from '@/components/VarDump.vue'; interface ActionItemHr { type: 'hr'; @@ -73,7 +74,7 @@