@saasicat/ui-vue 0.23.0 → 0.24.1
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-D74FFCCP.js} +52 -1
- package/dist/client/index.cjs +58 -0
- package/dist/client/index.d.cts +70 -1
- package/dist/client/index.d.ts +70 -1
- package/dist/client/index.js +15 -1
- package/dist/index.cjs +109 -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 +167 -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 +38 -16
- package/src/components/plan/PromoCodeInput.vue +1 -1
- 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 +9 -26
- package/src/components/plan-review/PlanReview.vue +1 -1
- package/src/components/plan-version-editor/PlanVersionEditor.vue +22 -3
- package/src/pages-standard/AdminManifestErrorPage.vue +4 -1
- package/src/pages-standard/BundlesPage.vue +1 -1
- package/src/pages-standard/DashboardPage.vue +8 -5
- package/src/pages-standard/DiscoveryPage.vue +42 -8
- package/src/pages-standard/MarketingCatalogPage.vue +33 -9
- package/src/pages-standard/PromoCodesPage.vue +3 -1
- package/src/pages-standard/SuperAdminLoginPage.vue +6 -1
- package/src/pages-standard/SuperAdminSetupWizard.vue +1 -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/StatusPill.vue +1 -1
- package/src/pages-standard/tenants/format.ts +11 -12
- package/src/pages-tenant/MySubscriptionBundlesPage.vue +9 -0
- package/src/pages-tenant/OnboardingConfigurator.vue +2 -2
- 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/base.css +13 -1
- package/src/ui/theme/components/card.css +26 -0
- package/src/ui/theme/components/field.css +44 -0
- package/src/ui/theme/components/statistics.css +1 -1
- 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
|
@@ -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());
|
|
@@ -305,7 +305,7 @@ defineExpose({ draft });
|
|
|
305
305
|
font-size: var(--sa-text-xs);
|
|
306
306
|
letter-spacing: 0.1em;
|
|
307
307
|
text-transform: uppercase;
|
|
308
|
-
color: var(--sa-color-accent);
|
|
308
|
+
color: var(--sa-color-accent-strong);
|
|
309
309
|
background: var(--sa-color-accent-surface);
|
|
310
310
|
padding: 6px 11px;
|
|
311
311
|
border-radius: 999px;
|
|
@@ -328,7 +328,7 @@ defineExpose({ draft });
|
|
|
328
328
|
gap: 24px;
|
|
329
329
|
align-items: start;
|
|
330
330
|
}
|
|
331
|
-
@media (max-width:
|
|
331
|
+
@media (max-width: 1023.98px) {
|
|
332
332
|
.sp-onb__layout {
|
|
333
333
|
grid-template-columns: 1fr;
|
|
334
334
|
}
|
|
@@ -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;
|
package/src/ui/theme/base.css
CHANGED
|
@@ -22,7 +22,19 @@
|
|
|
22
22
|
* The font and text colour belong here because the hosting app's stylesheet
|
|
23
23
|
* loads after this one: without a rule on the page itself, admin pages would
|
|
24
24
|
* inherit whatever the consumer sets. Two full-page views used to pin both on
|
|
25
|
-
* their own root, which is exactly the divergence this prevents.
|
|
25
|
+
* their own root, which is exactly the divergence this prevents.
|
|
26
|
+
*
|
|
27
|
+
* `.sa-page` is also the theme's REACH MARKER, and that is the larger job: it
|
|
28
|
+
* and `.sa-portal` are the only two prefixes through which the component layer
|
|
29
|
+
* corrects Quasar's own DOM. A screen without one of them gets Quasar's look —
|
|
30
|
+
* its 4px radii, its transparent outlined control, its neutral #1d1d1d card in
|
|
31
|
+
* dark mode — and nothing announces it. The login and setup screens rendered
|
|
32
|
+
* that way for their whole existence, and they are the first thing a user sees.
|
|
33
|
+
*
|
|
34
|
+
* Which is why the screens OUTSIDE the shell carry it too, even though they
|
|
35
|
+
* bring their own frame: their own root class is more specific than this one,
|
|
36
|
+
* so the min-height and padding below stay theirs and only the reach is shared.
|
|
37
|
+
* `tests/theme-reaches-every-page.test.js` holds every page to it. */
|
|
26
38
|
.sa-page {
|
|
27
39
|
min-height: calc(100vh - 56px);
|
|
28
40
|
padding: 20px 28px 28px;
|
|
@@ -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);
|
|
@@ -36,3 +36,47 @@
|
|
|
36
36
|
flex: 1;
|
|
37
37
|
min-width: 200px;
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/* Quasar's own accent-coloured text, painted from the role that is safe on a
|
|
41
|
+
* dark surface.
|
|
42
|
+
*
|
|
43
|
+
* Quasar colours the focused field's floating label and the selected menu item
|
|
44
|
+
* with `--q-primary` — the brand itself, which does not change between themes.
|
|
45
|
+
* On a light card that is fine; on the dark theme's slate surfaces the brand
|
|
46
|
+
* blue lands around 3.4:1 and the shrunk label is small text, so it reads as
|
|
47
|
+
* decoration rather than as the name of the field you are typing in.
|
|
48
|
+
*
|
|
49
|
+
* `--sa-color-accent-strong` is the same brand mixed toward the theme's own
|
|
50
|
+
* extreme — darker in light, lighter in dark — which is exactly what the guide
|
|
51
|
+
* prescribes for accent text. The colour still follows the host's `$primary`,
|
|
52
|
+
* so nothing about branding changes.
|
|
53
|
+
*
|
|
54
|
+
* `q-field--highlighted` is the whole story and `q-field--focused` would be
|
|
55
|
+
* noise: Quasar's only label-colour rule is
|
|
56
|
+
* `.q-field--highlighted .q-field__label { color: currentColor }`, and it sets
|
|
57
|
+
* that class whenever the field is focused OR invalid.
|
|
58
|
+
*
|
|
59
|
+
* Which is why the invalid half has to be carved back out. Quasar paints an
|
|
60
|
+
* invalid field's red by putting `text-negative` on the CONTROL and letting the
|
|
61
|
+
* label inherit it through that `currentColor`. Naming any colour on the label
|
|
62
|
+
* cuts the inheritance, and `!important` on `.text-negative` cannot defend it,
|
|
63
|
+
* because that declaration sits on a different element. Without the `:not()` a
|
|
64
|
+
* field with `rules` or `:error` — the login, setup and confirm forms — would
|
|
65
|
+
* announce its failure in every indicator except the one naming the field.
|
|
66
|
+
*
|
|
67
|
+
* `.sa-portal` alongside `.sa-page` for the usual reason: Quasar teleports every
|
|
68
|
+
* menu to `<body>`, so a `.sa-page`-prefixed rule never reaches a dropdown. */
|
|
69
|
+
.sa-page .q-field--highlighted:not(.q-field--error) .q-field__label,
|
|
70
|
+
.sa-portal .q-field--highlighted:not(.q-field--error) .q-field__label {
|
|
71
|
+
color: var(--sa-color-accent-strong);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* The chosen entry in an open select. Quasar marks it `q-item--active` and
|
|
75
|
+
* colours it with the brand; on the dark menu surface that is the same 3.4:1,
|
|
76
|
+
* and it is the one line in the list that has to be legible. */
|
|
77
|
+
.sa-page .q-item--active,
|
|
78
|
+
.sa-portal .q-item--active,
|
|
79
|
+
.sa-page .q-item.q-manual-focusable--focused,
|
|
80
|
+
.sa-portal .q-item.q-manual-focusable--focused {
|
|
81
|
+
color: var(--sa-color-accent-strong);
|
|
82
|
+
}
|
|
@@ -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);
|
|
@@ -153,6 +153,36 @@
|
|
|
153
153
|
--sa-color-bundle: var(--sa-amber-500);
|
|
154
154
|
--sa-color-bundle-surface: var(--sa-amber-100);
|
|
155
155
|
|
|
156
|
+
/* ── Identity. A CATEGORICAL ramp: the only job of these seven is to be
|
|
157
|
+
* told apart.
|
|
158
|
+
*
|
|
159
|
+
* Use them when a colour distinguishes one row from the next and means
|
|
160
|
+
* nothing beyond that — a plan's mark, a promotion badge, a capability
|
|
161
|
+
* kind. Use `feature`/`quota`/`bundle`/`scheduled` above when the category
|
|
162
|
+
* carries meaning; a reader should never have to guess which of the two a
|
|
163
|
+
* colour is doing.
|
|
164
|
+
*
|
|
165
|
+
* They exist because the package had FIVE of these ramps, all six colours
|
|
166
|
+
* long, none agreeing: `PlanList` and `PlanMatrix` shared one byte for
|
|
167
|
+
* byte, `tenants/format.ts` had a third that gave the same plan a
|
|
168
|
+
* different colour on the tenants page, and `PromoCodesPage`,
|
|
169
|
+
* `MarketingPromotionsTab` and `discovery-ui.ts` each invented their own.
|
|
170
|
+
* Every one of them was a hex literal in TypeScript, so none followed the
|
|
171
|
+
* theme: the plan mark measured 2.96:1 on a dark surface and was carried
|
|
172
|
+
* as a named contrast exception because there was nowhere to fix it.
|
|
173
|
+
*
|
|
174
|
+
* Each rung is chosen to clear 3:1 as TEXT on this theme's card surface,
|
|
175
|
+
* because that is how they are used — as the label, not only as a dot. */
|
|
176
|
+
--sa-color-identity-1: var(--sa-blue-700);
|
|
177
|
+
--sa-color-identity-2: var(--sa-violet-700);
|
|
178
|
+
--sa-color-identity-3: var(--sa-green-700);
|
|
179
|
+
--sa-color-identity-4: var(--sa-amber-700);
|
|
180
|
+
--sa-color-identity-5: var(--sa-sky-700);
|
|
181
|
+
--sa-color-identity-6: var(--sa-red-700);
|
|
182
|
+
/* The entry tier, and the answer for "no identity yet". Quiet on purpose:
|
|
183
|
+
* a starter plan reading as loud as an enterprise one is a wrong signal. */
|
|
184
|
+
--sa-color-identity-neutral: var(--sa-neutral-500);
|
|
185
|
+
|
|
156
186
|
/* ── Inverse chrome — the dark header, drawer and login backdrop.
|
|
157
187
|
*
|
|
158
188
|
* Declared identically in both themes ON PURPOSE. This surface is dark in
|