@saasicat/ui-vue 0.23.0 → 0.24.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.
- package/dist/{chunk-XMMFN2WJ.js → chunk-VYMTWCTT.js} +51 -1
- package/dist/client/index.cjs +57 -0
- package/dist/client/index.d.cts +58 -1
- package/dist/client/index.d.ts +58 -1
- package/dist/client/index.js +15 -1
- package/dist/index.cjs +108 -0
- package/dist/index.d.cts +65 -2
- package/dist/index.d.ts +65 -2
- package/dist/index.js +65 -1
- package/package.json +2 -2
- package/src/client/identity-accents.ts +129 -0
- package/src/client/index.ts +1 -0
- package/src/components/MarketingPromotionsTab.vue +6 -2
- package/src/components/bundle-editor/BundleVersionInlineEditor.vue +1 -1
- package/src/components/dialogs/PromoCodeDialogFields.vue +3 -2
- package/src/components/plan-create-dialog/PlanCreateDialog.vue +1 -1
- package/src/components/plan-detail/PlanDetail.vue +8 -3
- package/src/components/plan-list/PlanList.vue +18 -23
- package/src/components/plan-matrix/PlanMatrix.vue +8 -25
- package/src/components/plan-review/PlanReview.vue +1 -1
- package/src/components/plan-version-editor/PlanVersionEditor.vue +22 -3
- package/src/pages-standard/BundlesPage.vue +1 -1
- package/src/pages-standard/DashboardPage.vue +5 -2
- package/src/pages-standard/MarketingCatalogPage.vue +19 -3
- package/src/pages-standard/PromoCodesPage.vue +3 -1
- package/src/pages-standard/TenantsPage.vue +5 -4
- package/src/pages-standard/discovery-page/discovery-ui.ts +10 -7
- package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +2 -5
- package/src/pages-standard/plan-versions/PlanDiffCard.vue +11 -4
- package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +3 -1
- package/src/pages-standard/tenants/format.ts +11 -12
- package/src/pages-tenant/MySubscriptionBundlesPage.vue +9 -0
- package/src/pages-tenant/OnboardingConfigurator.vue +1 -1
- package/src/pages-tenant/PlanChangeWizard.vue +2 -49
- package/src/pages-tenant/TenantPlanSection.vue +6 -48
- package/src/pages-tenant/default-i18n.ts +113 -0
- package/src/ui/theme/_breakpoints.scss +8 -2
- package/src/ui/theme/components/card.css +26 -0
- package/src/ui/theme/tokens.primitive.css +4 -0
- package/src/ui/theme/tokens.semantic.dark.css +12 -0
- package/src/ui/theme/tokens.semantic.light.css +30 -0
|
@@ -215,7 +215,10 @@ const cards = reactive<KpiCardState[]>([]);
|
|
|
215
215
|
const loading = ref(false);
|
|
216
216
|
const error = ref<Error | null>(null);
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
// Two stops of the brand accent rather than two fixed blues: a host that sets
|
|
219
|
+
// its own `$primary` had a brand-coloured admin with platform-blue KPI bars.
|
|
220
|
+
const defaultBarColor =
|
|
221
|
+
'linear-gradient(90deg, var(--sa-color-accent), var(--sa-color-accent-strong))';
|
|
219
222
|
|
|
220
223
|
watch(
|
|
221
224
|
() => props.manifest,
|
|
@@ -412,7 +415,7 @@ function barWidth(value: number, max?: number): number {
|
|
|
412
415
|
margin-bottom: 14px;
|
|
413
416
|
align-items: start;
|
|
414
417
|
}
|
|
415
|
-
@media (max-width:
|
|
418
|
+
@media (max-width: 1439.98px) {
|
|
416
419
|
.sa-dashboard__rows {
|
|
417
420
|
grid-template-columns: 1fr;
|
|
418
421
|
}
|
|
@@ -164,7 +164,9 @@ import type {
|
|
|
164
164
|
ResolvedMarketing,
|
|
165
165
|
} from './marketing-catalog/types.js';
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
import { IDENTITY_NEUTRAL, identityAccentAt } from '../client/identity-accents.js';
|
|
168
|
+
|
|
169
|
+
const DEFAULT_ACCENT = IDENTITY_NEUTRAL;
|
|
168
170
|
|
|
169
171
|
const props = defineProps<{
|
|
170
172
|
adminEndpoint: string;
|
|
@@ -650,7 +652,7 @@ function promoFineprintOf(row: MarketingRow): string {
|
|
|
650
652
|
return promo.i18n?.[activeLocale.value]?.fineprint || promo.i18n?.de?.fineprint || '';
|
|
651
653
|
}
|
|
652
654
|
function promoColorOf(row: MarketingRow): string {
|
|
653
|
-
return promoOf(row)?.color ??
|
|
655
|
+
return promoOf(row)?.color ?? identityAccentAt(0);
|
|
654
656
|
}
|
|
655
657
|
|
|
656
658
|
// ─── CTA ───
|
|
@@ -999,12 +1001,22 @@ async function onLocaleChange(loc: string): Promise<void> {
|
|
|
999
1001
|
|
|
1000
1002
|
.sa-marketing-toolbar {
|
|
1001
1003
|
display: flex;
|
|
1004
|
+
/* The tab bar and the meta line do not fit side by side below ~672px, and
|
|
1005
|
+
* without this they simply pushed the page off its own right edge — at every
|
|
1006
|
+
* viewport narrower than that, not just the smallest. Nothing measured it
|
|
1007
|
+
* until the overflow guard started asking at each breakpoint band. */
|
|
1008
|
+
flex-wrap: wrap;
|
|
1002
1009
|
gap: 12px;
|
|
1003
1010
|
align-items: center;
|
|
1004
1011
|
margin: 0px;
|
|
1005
1012
|
}
|
|
1006
1013
|
.sa-marketing-tabbar {
|
|
1007
1014
|
display: inline-flex;
|
|
1015
|
+
/* Scrolls rather than wraps: a row of tabs broken across two lines reads as
|
|
1016
|
+
* two groups. It fits every band except the narrowest phone, where the four
|
|
1017
|
+
* tabs need 389px. */
|
|
1018
|
+
overflow-x: auto;
|
|
1019
|
+
max-width: 100%;
|
|
1008
1020
|
gap: 2px;
|
|
1009
1021
|
background: var(--sa-color-bg-surface);
|
|
1010
1022
|
border: 1px solid var(--sa-color-border);
|
|
@@ -1043,6 +1055,10 @@ async function onLocaleChange(loc: string): Promise<void> {
|
|
|
1043
1055
|
}
|
|
1044
1056
|
.sa-marketing-meta {
|
|
1045
1057
|
margin-left: auto;
|
|
1058
|
+
/* Its own contents wrap too: `catalogVersion <code> · locale <code>` is
|
|
1059
|
+
* wider than a phone on its own, so wrapping only the toolbar would move
|
|
1060
|
+
* the overflow one level down instead of removing it. */
|
|
1061
|
+
flex-wrap: wrap;
|
|
1046
1062
|
font-size: var(--sa-text-sm);
|
|
1047
1063
|
color: var(--sa-color-fg-secondary);
|
|
1048
1064
|
display: flex;
|
|
@@ -1715,7 +1731,7 @@ async function onLocaleChange(loc: string): Promise<void> {
|
|
|
1715
1731
|
cursor: not-allowed;
|
|
1716
1732
|
}
|
|
1717
1733
|
|
|
1718
|
-
@media (max-width:
|
|
1734
|
+
@media (max-width: 1023.98px) {
|
|
1719
1735
|
.sa-marketing-expand-grid {
|
|
1720
1736
|
grid-template-columns: 1fr;
|
|
1721
1737
|
}
|
|
@@ -110,7 +110,9 @@
|
|
|
110
110
|
import AdminTable from '../components/admin-page/AdminTable.vue';
|
|
111
111
|
import type { PromoCodePlanOption } from '../components/dialogs/types.js';
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
import { IDENTITY_ACCENTS } from '../client/identity-accents.js';
|
|
114
|
+
|
|
115
|
+
const PLAN_COLOR_PALETTE = IDENTITY_ACCENTS;
|
|
114
116
|
|
|
115
117
|
/**
|
|
116
118
|
* Heuristic from a consumer wrapper: assigns each plan a stable color so plan
|
|
@@ -176,7 +176,8 @@ import {
|
|
|
176
176
|
import MfaPromptDialog from '../components/MfaPromptDialog.vue';
|
|
177
177
|
import TenantActionConfirmDialog from '../components/TenantActionConfirmDialog.vue';
|
|
178
178
|
import StatusPill, { type PillTone } from './tenants/StatusPill.vue';
|
|
179
|
-
import {
|
|
179
|
+
import { identityChipStyle } from '../client/identity-accents.js';
|
|
180
|
+
import { formatDate, planAccent, tenantInitials } from './tenants/format.js';
|
|
180
181
|
|
|
181
182
|
// Platform standard page: tenant list.
|
|
182
183
|
//
|
|
@@ -323,7 +324,7 @@ const props = withDefaults(
|
|
|
323
324
|
pageSize: 25,
|
|
324
325
|
showPlanColumn: true,
|
|
325
326
|
planLabelField: 'plan',
|
|
326
|
-
planAccents: () =>
|
|
327
|
+
planAccents: () => ({}),
|
|
327
328
|
usageFields: () => [],
|
|
328
329
|
manifest: null,
|
|
329
330
|
},
|
|
@@ -484,8 +485,8 @@ const combinedActions = computed<TenantRowAction[]>(() => {
|
|
|
484
485
|
const hasActions = computed(() => combinedActions.value.length > 0);
|
|
485
486
|
|
|
486
487
|
function avatarStyle(row: TenantRow): Record<string, string> {
|
|
487
|
-
const
|
|
488
|
-
return { background
|
|
488
|
+
const { background, color } = identityChipStyle(planAccentFor(row));
|
|
489
|
+
return { background, color };
|
|
489
490
|
}
|
|
490
491
|
|
|
491
492
|
function planAccentFor(row: TenantRow): string {
|
|
@@ -122,16 +122,19 @@ export function reviewMenuActions(
|
|
|
122
122
|
|
|
123
123
|
// ─── Capability kind styling (read-only code facts, #20) ─────────────────────
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
// A capability kind is a category with no ranking, so it takes the identity
|
|
126
|
+
// ramp rather than a status colour: an endpoint is not "better" than a job.
|
|
127
|
+
import { IDENTITY_NEUTRAL, identityChipStyle } from '../../client/identity-accents.js';
|
|
128
|
+
|
|
129
|
+
const KIND_ACCENTS: Record<string, string> = {
|
|
130
|
+
endpoint: 'var(--sa-color-identity-1)',
|
|
131
|
+
service: 'var(--sa-color-identity-2)',
|
|
132
|
+
job: 'var(--sa-color-identity-5)',
|
|
133
|
+
event: 'var(--sa-color-identity-4)',
|
|
130
134
|
};
|
|
131
135
|
|
|
132
136
|
export function kindStyle(kind: string): Record<string, string> {
|
|
133
|
-
|
|
134
|
-
return { background: `${c}1a`, color: c, borderColor: `${c}33` };
|
|
137
|
+
return identityChipStyle(KIND_ACCENTS[kind] ?? IDENTITY_NEUTRAL);
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
/**
|
|
@@ -26,11 +26,7 @@
|
|
|
26
26
|
<div class="sa-marketing-plan-cell">
|
|
27
27
|
<div
|
|
28
28
|
class="sa-marketing-plan-mark"
|
|
29
|
-
:style="
|
|
30
|
-
background: row.accent + '15',
|
|
31
|
-
color: row.accent,
|
|
32
|
-
borderColor: row.accent + '33',
|
|
33
|
-
}"
|
|
29
|
+
:style="identityChipStyle(row.accent)"
|
|
34
30
|
>
|
|
35
31
|
{{ row.plan.planKey.slice(0, 3) }}
|
|
36
32
|
</div>
|
|
@@ -462,6 +458,7 @@
|
|
|
462
458
|
|
|
463
459
|
<script setup lang="ts">
|
|
464
460
|
import type { MarketingTopFeature, PlanRow, PlanVersionRow } from '@saasicat/types';
|
|
461
|
+
import { identityChipStyle } from '../../client/identity-accents.js';
|
|
465
462
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
466
463
|
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
467
464
|
import type { FeatureSuggestion, MarketingRow, ResolvedMarketing } from './types.js';
|
|
@@ -11,10 +11,7 @@
|
|
|
11
11
|
</div>
|
|
12
12
|
<div v-else class="sa-pv-diff-card">
|
|
13
13
|
<header class="sa-pv-diff-card__head">
|
|
14
|
-
<span
|
|
15
|
-
class="sa-pv-diff-card__accent"
|
|
16
|
-
:style="{ background: `${accent}14`, borderColor: `${accent}40` }"
|
|
17
|
-
>
|
|
14
|
+
<span class="sa-pv-diff-card__accent" :style="accentWashStyle">
|
|
18
15
|
<q-icon name="inventory_2" size="14px" :style="{ color: accent }" />
|
|
19
16
|
</span>
|
|
20
17
|
<span class="sa-pv-diff-card__name">{{ title }}</span>
|
|
@@ -38,6 +35,7 @@
|
|
|
38
35
|
<script setup lang="ts">
|
|
39
36
|
import { computed } from 'vue';
|
|
40
37
|
import type { VersionChange } from '@saasicat/types';
|
|
38
|
+
import { identityChipStyle } from '../../client/identity-accents.js';
|
|
41
39
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
42
40
|
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
43
41
|
import VersionDiffPreview from '../../components/VersionDiffPreview.vue';
|
|
@@ -52,6 +50,15 @@ const props = defineProps<{
|
|
|
52
50
|
}>();
|
|
53
51
|
|
|
54
52
|
const msg = useSaMessages('planVersions');
|
|
53
|
+
|
|
54
|
+
// The wash and the edge this badge always had — deliberately not the chip
|
|
55
|
+
// helper's third property. `identityChipStyle` also sets a text colour, and
|
|
56
|
+
// the icon inside already sets its own; no fixture renders this card, so a
|
|
57
|
+
// change nobody can observe is a change nobody should make.
|
|
58
|
+
const accentWashStyle = computed(() => {
|
|
59
|
+
const { background, borderColor } = identityChipStyle(props.accent);
|
|
60
|
+
return { background, borderColor };
|
|
61
|
+
});
|
|
55
62
|
const common = useSaMessages('common');
|
|
56
63
|
|
|
57
64
|
const changeCountLabel = computed(() => {
|
|
@@ -66,8 +66,10 @@ const props = defineProps<{
|
|
|
66
66
|
|
|
67
67
|
const msg = useSaMessages('planVersions');
|
|
68
68
|
|
|
69
|
+
import { identityAccentFor } from '../../client/identity-accents.js';
|
|
70
|
+
|
|
69
71
|
function planAccent(planId: string): string {
|
|
70
|
-
return props.planAccents
|
|
72
|
+
return identityAccentFor(planId, props.planAccents ?? {});
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
const planDiffs = computed<SnapshotEntityDiff[]>(() => {
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
// Formatting helpers for the tenants page.
|
|
2
2
|
//
|
|
3
3
|
// Shared between TenantsPage.vue and subcomponents (Avatar, Pill).
|
|
4
|
-
// Apps with their own plan accents pass them through via the `planAccents` prop
|
|
5
|
-
//
|
|
4
|
+
// Apps with their own plan accents pass them through via the `planAccents` prop.
|
|
5
|
+
//
|
|
6
|
+
// The default map used to live here as five hex literals, and it disagreed with
|
|
7
|
+
// the two in `PlanList`/`PlanMatrix`: `STANDARD` was the brand blue on this page
|
|
8
|
+
// and a plain blue on the plans page, `ENTERPRISE` near-black here and teal
|
|
9
|
+
// there. Same plan, three colours, depending on which screen you were looking
|
|
10
|
+
// at. It now comes from the shared ramp, so the answer is one.
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
BASIC: '#94a3b8',
|
|
9
|
-
STANDARD: '#3f6bff',
|
|
10
|
-
PROFESSIONAL: '#7c3aed',
|
|
11
|
-
BUSINESS: '#0ea5e9',
|
|
12
|
-
ENTERPRISE: '#0f172a',
|
|
13
|
-
};
|
|
12
|
+
import { IDENTITY_NEUTRAL, identityAccentFor } from '../../client/identity-accents.js';
|
|
14
13
|
|
|
15
14
|
export function tenantInitials(name: string | null | undefined): string {
|
|
16
15
|
if (!name) return '?';
|
|
@@ -22,10 +21,10 @@ export function tenantInitials(name: string | null | undefined): string {
|
|
|
22
21
|
|
|
23
22
|
export function planAccent(
|
|
24
23
|
planId: string | null | undefined,
|
|
25
|
-
accents: Record<string, string> =
|
|
24
|
+
accents: Record<string, string> = {},
|
|
26
25
|
): string {
|
|
27
|
-
if (!planId) return
|
|
28
|
-
return
|
|
26
|
+
if (!planId) return IDENTITY_NEUTRAL;
|
|
27
|
+
return identityAccentFor(planId, accents);
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
/** `locale` is a BCP-47 tag — pages pass `intlLocale.value` of the active UI locale. */
|
|
@@ -188,6 +188,7 @@
|
|
|
188
188
|
<script setup lang="ts">
|
|
189
189
|
import { computed, onMounted, reactive, ref } from 'vue';
|
|
190
190
|
import type { SubscriptionBundleRecord } from '@saasicat/types';
|
|
191
|
+
import type { HttpClient } from '../client/types.js';
|
|
191
192
|
|
|
192
193
|
import { useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
|
|
193
194
|
import { useTenantSubscriptionBundles } from '../vue/use-tenant-subscription-bundles.js';
|
|
@@ -213,6 +214,13 @@ interface BookableBundle {
|
|
|
213
214
|
const props = withDefaults(
|
|
214
215
|
defineProps<{
|
|
215
216
|
billingEndpoint: string;
|
|
217
|
+
/**
|
|
218
|
+
* App-specific HTTP adapter, as every other tenant-facing component
|
|
219
|
+
* takes. Without it this page fell through to `defaultHttpClient()`,
|
|
220
|
+
* so an app using axios with an auth interceptor got a bare `fetch()`
|
|
221
|
+
* here and nowhere else — and no fixture could reach it either.
|
|
222
|
+
*/
|
|
223
|
+
http?: HttpClient;
|
|
216
224
|
/**
|
|
217
225
|
* Mapping `bundleVersionId → BundleKey/Label` (the consumer can
|
|
218
226
|
* preload this from the public catalog). Without a mapping we show the
|
|
@@ -253,6 +261,7 @@ const effectiveI18n = computed<TenantPlanSectionI18n>(() => ({
|
|
|
253
261
|
const { bundles, loading, error, load, add, cancel } = useTenantSubscriptionBundles({
|
|
254
262
|
billingEndpoint: props.billingEndpoint,
|
|
255
263
|
getAuthToken: props.getAuthToken,
|
|
264
|
+
http: props.http,
|
|
256
265
|
});
|
|
257
266
|
|
|
258
267
|
onMounted(() => load());
|
|
@@ -223,6 +223,7 @@ import PlanGrid from '../components/plan/PlanGrid.vue';
|
|
|
223
223
|
import { useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
|
|
224
224
|
import type { BillingCycleStr, PlanChangePreviewShape } from '../vue/use-tenant-billing.js';
|
|
225
225
|
import type { CatalogPlan } from '../vue/use-tenant-billing-catalog.js';
|
|
226
|
+
import type { PlanChangeWizardI18n } from './default-i18n.js';
|
|
226
227
|
|
|
227
228
|
// PlanChangeWizard — 3-step dialog for plan changes.
|
|
228
229
|
//
|
|
@@ -231,54 +232,6 @@ import type { CatalogPlan } from '../vue/use-tenant-billing-catalog.js';
|
|
|
231
232
|
// the limits check + proration + feature diff + blockers, step 3 calls `changePlan`.
|
|
232
233
|
// Data-driven via the passed `catalogQuotaKeys[]` — no hard-coded trinity.
|
|
233
234
|
|
|
234
|
-
interface I18nStrings {
|
|
235
|
-
title: string;
|
|
236
|
-
close: string;
|
|
237
|
-
currentLabel: string;
|
|
238
|
-
cycleMonthly: string;
|
|
239
|
-
cycleYearly: string;
|
|
240
|
-
badgeCurrent: string;
|
|
241
|
-
badgePopular: string;
|
|
242
|
-
priceUnitMonthly: string;
|
|
243
|
-
priceUnitYearly: string;
|
|
244
|
-
priceOnRequest: string;
|
|
245
|
-
stepChoose: string;
|
|
246
|
-
stepChooseIntro: string;
|
|
247
|
-
stepPreview: string;
|
|
248
|
-
stepConfirm: string;
|
|
249
|
-
next: string;
|
|
250
|
-
back: string;
|
|
251
|
-
previewLoading: string;
|
|
252
|
-
effectiveAtLabel: string;
|
|
253
|
-
effectiveImmediate: string;
|
|
254
|
-
prorationTitle: string;
|
|
255
|
-
prorationLine: string;
|
|
256
|
-
prorationDays: string;
|
|
257
|
-
limitsTitle: string;
|
|
258
|
-
limitsUsed: string;
|
|
259
|
-
limitsCurrent: string;
|
|
260
|
-
limitsTarget: string;
|
|
261
|
-
featuresGained: string;
|
|
262
|
-
featuresLost: string;
|
|
263
|
-
blockersTitle: string;
|
|
264
|
-
confirmImmediate: string;
|
|
265
|
-
confirmScheduled: string;
|
|
266
|
-
confirmAction: string;
|
|
267
|
-
confirmInProgress: string;
|
|
268
|
-
confirmPriceTitle: string;
|
|
269
|
-
confirmProratedNow: string;
|
|
270
|
-
confirmRecurringNext: string;
|
|
271
|
-
confirmRecurringFrom: string;
|
|
272
|
-
perCycleMonthly: string;
|
|
273
|
-
perCycleYearly: string;
|
|
274
|
-
confirmTrialNote: string;
|
|
275
|
-
confirmRecurringTrialEnd: string;
|
|
276
|
-
changeTypeUpgrade: string;
|
|
277
|
-
changeTypeDowngrade: string;
|
|
278
|
-
changeTypeCycle: string;
|
|
279
|
-
changeTypeNoop: string;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
235
|
interface Props {
|
|
283
236
|
modelValue: boolean;
|
|
284
237
|
plans: CatalogPlan[];
|
|
@@ -314,7 +267,7 @@ interface Props {
|
|
|
314
267
|
/** Preview caller (passed through from the consumer composable). */
|
|
315
268
|
previewPlanChange: (plan: string, cycle: BillingCycleStr) => Promise<PlanChangePreviewShape>;
|
|
316
269
|
changePlan: (plan: string, cycle: BillingCycleStr, immediate: boolean) => Promise<void>;
|
|
317
|
-
i18n:
|
|
270
|
+
i18n: PlanChangeWizardI18n;
|
|
318
271
|
}
|
|
319
272
|
|
|
320
273
|
const props = defineProps<Props>();
|
|
@@ -198,7 +198,11 @@ import { computed, ref } from 'vue';
|
|
|
198
198
|
import PackageSnapshotPanel from './PackageSnapshotPanel.vue';
|
|
199
199
|
import PendingVersionBanner from './PendingVersionBanner.vue';
|
|
200
200
|
import PlanChangeWizard from './PlanChangeWizard.vue';
|
|
201
|
-
import {
|
|
201
|
+
import {
|
|
202
|
+
defaultTenantPlanSectionI18n,
|
|
203
|
+
planChangeWizardI18n,
|
|
204
|
+
type TenantPlanSectionI18n,
|
|
205
|
+
} from './default-i18n.js';
|
|
202
206
|
import BundlePreviewDialog from './tenant-plan-section/BundlePreviewDialog.vue';
|
|
203
207
|
import TenantBundleStore from './tenant-plan-section/TenantBundleStore.vue';
|
|
204
208
|
import TenantFeatureMatrix from './tenant-plan-section/TenantFeatureMatrix.vue';
|
|
@@ -429,53 +433,7 @@ const statusColor = computed(() => {
|
|
|
429
433
|
}
|
|
430
434
|
});
|
|
431
435
|
|
|
432
|
-
const wizardI18n = computed(() => (
|
|
433
|
-
title: effectiveI18n.value.wizardTitle,
|
|
434
|
-
close: effectiveI18n.value.wizardClose,
|
|
435
|
-
currentLabel: effectiveI18n.value.wizardCurrent,
|
|
436
|
-
cycleMonthly: effectiveI18n.value.cycleMonthly,
|
|
437
|
-
cycleYearly: effectiveI18n.value.cycleYearly,
|
|
438
|
-
badgeCurrent: effectiveI18n.value.wizardBadgeCurrent,
|
|
439
|
-
badgePopular: effectiveI18n.value.wizardBadgePopular,
|
|
440
|
-
priceUnitMonthly: effectiveI18n.value.wizardPriceUnitMonthly,
|
|
441
|
-
priceUnitYearly: effectiveI18n.value.wizardPriceUnitYearly,
|
|
442
|
-
priceOnRequest: effectiveI18n.value.wizardPriceOnRequest,
|
|
443
|
-
stepChoose: effectiveI18n.value.wizardStepChoose,
|
|
444
|
-
stepChooseIntro: effectiveI18n.value.wizardStepChooseIntro,
|
|
445
|
-
stepPreview: effectiveI18n.value.wizardStepPreview,
|
|
446
|
-
stepConfirm: effectiveI18n.value.wizardStepConfirm,
|
|
447
|
-
next: effectiveI18n.value.wizardNext,
|
|
448
|
-
back: effectiveI18n.value.wizardBack,
|
|
449
|
-
previewLoading: effectiveI18n.value.wizardPreviewLoading,
|
|
450
|
-
effectiveAtLabel: effectiveI18n.value.wizardEffectiveAtLabel,
|
|
451
|
-
effectiveImmediate: effectiveI18n.value.wizardEffectiveImmediate,
|
|
452
|
-
prorationTitle: effectiveI18n.value.wizardProrationTitle,
|
|
453
|
-
prorationLine: effectiveI18n.value.wizardProrationLine,
|
|
454
|
-
prorationDays: effectiveI18n.value.wizardProrationDays,
|
|
455
|
-
limitsTitle: effectiveI18n.value.wizardLimitsTitle,
|
|
456
|
-
limitsUsed: effectiveI18n.value.wizardLimitsUsed,
|
|
457
|
-
limitsCurrent: effectiveI18n.value.wizardLimitsCurrent,
|
|
458
|
-
limitsTarget: effectiveI18n.value.wizardLimitsTarget,
|
|
459
|
-
featuresGained: effectiveI18n.value.wizardFeaturesGained,
|
|
460
|
-
featuresLost: effectiveI18n.value.wizardFeaturesLost,
|
|
461
|
-
blockersTitle: effectiveI18n.value.wizardBlockersTitle,
|
|
462
|
-
confirmImmediate: effectiveI18n.value.wizardConfirmImmediate,
|
|
463
|
-
confirmScheduled: effectiveI18n.value.wizardConfirmScheduled,
|
|
464
|
-
confirmAction: effectiveI18n.value.wizardConfirmAction,
|
|
465
|
-
confirmInProgress: effectiveI18n.value.wizardConfirmInProgress,
|
|
466
|
-
confirmPriceTitle: effectiveI18n.value.wizardConfirmPriceTitle,
|
|
467
|
-
confirmProratedNow: effectiveI18n.value.wizardConfirmProratedNow,
|
|
468
|
-
confirmRecurringNext: effectiveI18n.value.wizardConfirmRecurringNext,
|
|
469
|
-
confirmRecurringFrom: effectiveI18n.value.wizardConfirmRecurringFrom,
|
|
470
|
-
perCycleMonthly: effectiveI18n.value.wizardConfirmPerCycleMonthly,
|
|
471
|
-
perCycleYearly: effectiveI18n.value.wizardConfirmPerCycleYearly,
|
|
472
|
-
confirmTrialNote: effectiveI18n.value.wizardConfirmTrialNote,
|
|
473
|
-
confirmRecurringTrialEnd: effectiveI18n.value.wizardConfirmRecurringTrialEnd,
|
|
474
|
-
changeTypeUpgrade: effectiveI18n.value.wizardChangeTypeUpgrade,
|
|
475
|
-
changeTypeDowngrade: effectiveI18n.value.wizardChangeTypeDowngrade,
|
|
476
|
-
changeTypeCycle: effectiveI18n.value.wizardChangeTypeCycle,
|
|
477
|
-
changeTypeNoop: effectiveI18n.value.wizardChangeTypeNoop,
|
|
478
|
-
}));
|
|
436
|
+
const wizardI18n = computed(() => planChangeWizardI18n(effectiveI18n.value));
|
|
479
437
|
|
|
480
438
|
// Helper hooks with defaults
|
|
481
439
|
function quotaLabelResolved(key: string): string {
|
|
@@ -494,3 +494,116 @@ export const DEFAULT_I18N_EN: TenantPlanSectionI18n = {
|
|
|
494
494
|
export function defaultTenantPlanSectionI18n(locale: SaLocale): TenantPlanSectionI18n {
|
|
495
495
|
return locale === 'en' ? DEFAULT_I18N_EN : DEFAULT_I18N_DE;
|
|
496
496
|
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* The strings `PlanChangeWizard` takes.
|
|
500
|
+
*
|
|
501
|
+
* A projection of `TenantPlanSectionI18n` with the `wizard` prefix dropped, so
|
|
502
|
+
* the wizard's own template reads `i18n.stepChoose` rather than
|
|
503
|
+
* `i18n.wizardStepChoose`.
|
|
504
|
+
*/
|
|
505
|
+
export interface PlanChangeWizardI18n {
|
|
506
|
+
title: string;
|
|
507
|
+
close: string;
|
|
508
|
+
currentLabel: string;
|
|
509
|
+
cycleMonthly: string;
|
|
510
|
+
cycleYearly: string;
|
|
511
|
+
badgeCurrent: string;
|
|
512
|
+
badgePopular: string;
|
|
513
|
+
priceUnitMonthly: string;
|
|
514
|
+
priceUnitYearly: string;
|
|
515
|
+
priceOnRequest: string;
|
|
516
|
+
stepChoose: string;
|
|
517
|
+
stepChooseIntro: string;
|
|
518
|
+
stepPreview: string;
|
|
519
|
+
stepConfirm: string;
|
|
520
|
+
next: string;
|
|
521
|
+
back: string;
|
|
522
|
+
previewLoading: string;
|
|
523
|
+
effectiveAtLabel: string;
|
|
524
|
+
effectiveImmediate: string;
|
|
525
|
+
prorationTitle: string;
|
|
526
|
+
prorationLine: string;
|
|
527
|
+
prorationDays: string;
|
|
528
|
+
limitsTitle: string;
|
|
529
|
+
limitsUsed: string;
|
|
530
|
+
limitsCurrent: string;
|
|
531
|
+
limitsTarget: string;
|
|
532
|
+
featuresGained: string;
|
|
533
|
+
featuresLost: string;
|
|
534
|
+
blockersTitle: string;
|
|
535
|
+
confirmImmediate: string;
|
|
536
|
+
confirmScheduled: string;
|
|
537
|
+
confirmAction: string;
|
|
538
|
+
confirmInProgress: string;
|
|
539
|
+
confirmPriceTitle: string;
|
|
540
|
+
confirmProratedNow: string;
|
|
541
|
+
confirmRecurringNext: string;
|
|
542
|
+
confirmRecurringFrom: string;
|
|
543
|
+
perCycleMonthly: string;
|
|
544
|
+
perCycleYearly: string;
|
|
545
|
+
confirmTrialNote: string;
|
|
546
|
+
confirmRecurringTrialEnd: string;
|
|
547
|
+
changeTypeUpgrade: string;
|
|
548
|
+
changeTypeDowngrade: string;
|
|
549
|
+
changeTypeCycle: string;
|
|
550
|
+
changeTypeNoop: string;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Builds the wizard's strings from the section's.
|
|
555
|
+
*
|
|
556
|
+
* Here rather than inline in `TenantPlanSection.vue`, because the wizard is an
|
|
557
|
+
* exported component: a consumer who mounts it directly would otherwise have to
|
|
558
|
+
* reproduce all 44 keys of this mapping by hand, and a visual fixture had to do
|
|
559
|
+
* exactly that before it moved.
|
|
560
|
+
*/
|
|
561
|
+
export function planChangeWizardI18n(i18n: TenantPlanSectionI18n): PlanChangeWizardI18n {
|
|
562
|
+
return {
|
|
563
|
+
title: i18n.wizardTitle,
|
|
564
|
+
close: i18n.wizardClose,
|
|
565
|
+
currentLabel: i18n.wizardCurrent,
|
|
566
|
+
cycleMonthly: i18n.cycleMonthly,
|
|
567
|
+
cycleYearly: i18n.cycleYearly,
|
|
568
|
+
badgeCurrent: i18n.wizardBadgeCurrent,
|
|
569
|
+
badgePopular: i18n.wizardBadgePopular,
|
|
570
|
+
priceUnitMonthly: i18n.wizardPriceUnitMonthly,
|
|
571
|
+
priceUnitYearly: i18n.wizardPriceUnitYearly,
|
|
572
|
+
priceOnRequest: i18n.wizardPriceOnRequest,
|
|
573
|
+
stepChoose: i18n.wizardStepChoose,
|
|
574
|
+
stepChooseIntro: i18n.wizardStepChooseIntro,
|
|
575
|
+
stepPreview: i18n.wizardStepPreview,
|
|
576
|
+
stepConfirm: i18n.wizardStepConfirm,
|
|
577
|
+
next: i18n.wizardNext,
|
|
578
|
+
back: i18n.wizardBack,
|
|
579
|
+
previewLoading: i18n.wizardPreviewLoading,
|
|
580
|
+
effectiveAtLabel: i18n.wizardEffectiveAtLabel,
|
|
581
|
+
effectiveImmediate: i18n.wizardEffectiveImmediate,
|
|
582
|
+
prorationTitle: i18n.wizardProrationTitle,
|
|
583
|
+
prorationLine: i18n.wizardProrationLine,
|
|
584
|
+
prorationDays: i18n.wizardProrationDays,
|
|
585
|
+
limitsTitle: i18n.wizardLimitsTitle,
|
|
586
|
+
limitsUsed: i18n.wizardLimitsUsed,
|
|
587
|
+
limitsCurrent: i18n.wizardLimitsCurrent,
|
|
588
|
+
limitsTarget: i18n.wizardLimitsTarget,
|
|
589
|
+
featuresGained: i18n.wizardFeaturesGained,
|
|
590
|
+
featuresLost: i18n.wizardFeaturesLost,
|
|
591
|
+
blockersTitle: i18n.wizardBlockersTitle,
|
|
592
|
+
confirmImmediate: i18n.wizardConfirmImmediate,
|
|
593
|
+
confirmScheduled: i18n.wizardConfirmScheduled,
|
|
594
|
+
confirmAction: i18n.wizardConfirmAction,
|
|
595
|
+
confirmInProgress: i18n.wizardConfirmInProgress,
|
|
596
|
+
confirmPriceTitle: i18n.wizardConfirmPriceTitle,
|
|
597
|
+
confirmProratedNow: i18n.wizardConfirmProratedNow,
|
|
598
|
+
confirmRecurringNext: i18n.wizardConfirmRecurringNext,
|
|
599
|
+
confirmRecurringFrom: i18n.wizardConfirmRecurringFrom,
|
|
600
|
+
perCycleMonthly: i18n.wizardConfirmPerCycleMonthly,
|
|
601
|
+
perCycleYearly: i18n.wizardConfirmPerCycleYearly,
|
|
602
|
+
confirmTrialNote: i18n.wizardConfirmTrialNote,
|
|
603
|
+
confirmRecurringTrialEnd: i18n.wizardConfirmRecurringTrialEnd,
|
|
604
|
+
changeTypeUpgrade: i18n.wizardChangeTypeUpgrade,
|
|
605
|
+
changeTypeDowngrade: i18n.wizardChangeTypeDowngrade,
|
|
606
|
+
changeTypeCycle: i18n.wizardChangeTypeCycle,
|
|
607
|
+
changeTypeNoop: i18n.wizardChangeTypeNoop,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
@@ -7,8 +7,14 @@
|
|
|
7
7
|
//
|
|
8
8
|
// They are Quasar's own breakpoints, deliberately. A component that changes
|
|
9
9
|
// layout at 980px inside an app whose grid changes at 1024px produces a band
|
|
10
|
-
// 44px wide where the two disagree — and the admin surface had six such
|
|
11
|
-
//
|
|
10
|
+
// 44px wide where the two disagree — and the admin surface had six such values
|
|
11
|
+
// (540, 600, 980, 1100, 1180, 1280), so it had several such bands. All six are
|
|
12
|
+
// now on the bounds below; the audit fails on a seventh.
|
|
13
|
+
//
|
|
14
|
+
// Moving 1100 and 1180 DOWN to 1023.98 keeps the wide layout for another 76 and
|
|
15
|
+
// 156 pixels respectively, which is the one direction of this change that could
|
|
16
|
+
// push content off the page. `visual-baseline.spec.ts` measures horizontal
|
|
17
|
+
// overflow at both edges of every band, on every page, for exactly that reason.
|
|
12
18
|
//
|
|
13
19
|
// Usage:
|
|
14
20
|
// @use '@saasicat/ui-vue/theme/breakpoints' as bp;
|
|
@@ -32,6 +32,32 @@
|
|
|
32
32
|
color: var(--sa-color-fg-body);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/* Quasar's stepper, for the same reason and one component further along.
|
|
36
|
+
*
|
|
37
|
+
* Only the steps you are NOT on. Their titles are a hard-coded grey —
|
|
38
|
+
* `rgb(158,158,158)`, 2.68:1 on a white card — which is below the floor for
|
|
39
|
+
* text whose whole job is to say which steps exist. The ACTIVE step is left
|
|
40
|
+
* alone: Quasar marks it `text-primary`, and that accent is the "you are here"
|
|
41
|
+
* affordance. A first attempt here painted every title with the heading role
|
|
42
|
+
* and quietly took that affordance away; the baseline diff showed it.
|
|
43
|
+
*
|
|
44
|
+
* The theme could not see any of this until the contrast checker learned to
|
|
45
|
+
* look inside teleported nodes: the only stepper in the package is in a dialog.
|
|
46
|
+
*/
|
|
47
|
+
.sa-page .q-stepper,
|
|
48
|
+
.sa-portal .q-stepper {
|
|
49
|
+
background: var(--sa-color-bg-surface);
|
|
50
|
+
color: var(--sa-color-fg-body);
|
|
51
|
+
}
|
|
52
|
+
.sa-page .q-stepper__tab:not(.q-stepper__tab--active) .q-stepper__title,
|
|
53
|
+
.sa-portal .q-stepper__tab:not(.q-stepper__tab--active) .q-stepper__title {
|
|
54
|
+
color: var(--sa-color-fg-muted);
|
|
55
|
+
}
|
|
56
|
+
.sa-page .q-stepper__caption,
|
|
57
|
+
.sa-portal .q-stepper__caption {
|
|
58
|
+
color: var(--sa-color-fg-subtle);
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
.sa-card {
|
|
36
62
|
background: var(--sa-color-bg-surface);
|
|
37
63
|
border: 1px solid var(--sa-color-border);
|
|
@@ -100,6 +100,10 @@
|
|
|
100
100
|
--sa-sky-50: #f0f9ff;
|
|
101
101
|
--sa-sky-200: #bae6fd;
|
|
102
102
|
--sa-sky-500: #0ea5e9;
|
|
103
|
+
/* Sky-500 is 2.6:1 on a white card — fine as a dot or a border, too pale to
|
|
104
|
+
* carry a label. The identity ramp needs a readable sky, so here is the
|
|
105
|
+
* rung below it. */
|
|
106
|
+
--sa-sky-700: #0369a1;
|
|
103
107
|
|
|
104
108
|
/* ── Type families. ─────────────────────────────────────────────────── */
|
|
105
109
|
--sa-font-head: 'Plus Jakarta Sans', 'Inter', system-ui, sans-serif;
|
|
@@ -125,6 +125,18 @@ body.body--dark {
|
|
|
125
125
|
--sa-color-bundle: var(--sa-amber-500);
|
|
126
126
|
--sa-color-bundle-surface: color-mix(in srgb, var(--sa-amber-500) 16%, transparent);
|
|
127
127
|
|
|
128
|
+
/* The identity ramp, one rung lighter throughout — see the light file for
|
|
129
|
+
* what it is for. This is the half that did not exist while the ramps lived
|
|
130
|
+
* as hex literals in TypeScript, and its absence is the whole reason the
|
|
131
|
+
* plan mark sat at 2.96:1 here. */
|
|
132
|
+
--sa-color-identity-1: var(--sa-blue-300);
|
|
133
|
+
--sa-color-identity-2: var(--sa-violet-200);
|
|
134
|
+
--sa-color-identity-3: var(--sa-green-200);
|
|
135
|
+
--sa-color-identity-4: var(--sa-amber-300);
|
|
136
|
+
--sa-color-identity-5: var(--sa-sky-200);
|
|
137
|
+
--sa-color-identity-6: var(--sa-red-300);
|
|
138
|
+
--sa-color-identity-neutral: var(--sa-neutral-400);
|
|
139
|
+
|
|
128
140
|
/* Identical to the light theme, and that is the point — see there. */
|
|
129
141
|
--sa-color-inverse-bg: var(--sa-neutral-950);
|
|
130
142
|
--sa-color-inverse-surface: var(--sa-neutral-800);
|