@saasicat/ui-vue 0.24.2 → 0.26.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/README.md +32 -8
- package/dist/{catalog-DJcfm5N2.d.cts → catalog-Dch1Ryw0.d.cts} +353 -69
- package/dist/{catalog-DJcfm5N2.d.ts → catalog-Dch1Ryw0.d.ts} +353 -69
- package/dist/{chunk-F7NRM6KI.js → chunk-BPF2BMCQ.js} +748 -24
- package/dist/{chunk-D74FFCCP.js → chunk-NEXTZZRQ.js} +95 -177
- package/dist/{chunk-TQQMKWN6.js → chunk-O3J3ITF2.js} +119 -7
- package/dist/client/index.cjs +758 -83
- package/dist/client/index.d.cts +414 -9
- package/dist/client/index.d.ts +414 -9
- package/dist/client/index.js +84 -12
- package/dist/index.cjs +1325 -294
- package/dist/index.d.cts +205 -15
- package/dist/index.d.ts +205 -15
- package/dist/index.js +566 -240
- package/dist/quasar/index.cjs +574 -62
- package/dist/quasar/index.d.cts +61 -6
- package/dist/quasar/index.d.ts +61 -6
- package/dist/quasar/index.js +83 -19
- package/dist/{use-sa-theme-OUBlaqgl.d.ts → resource-registry-D7Xjs-5f.d.cts} +157 -7
- package/dist/{use-sa-theme-B5t7oXph.d.cts → resource-registry-TaR7rtq0.d.ts} +157 -7
- package/package.json +4 -3
- package/src/client/admin-error.ts +552 -0
- package/src/client/admin-resource-client.ts +9 -12
- package/src/client/batch-column-fetcher.ts +2 -2
- package/src/client/boot-loader.ts +12 -1
- package/src/client/http/axios-http-client.ts +383 -0
- package/src/client/http/fetch-http-client.ts +89 -0
- package/src/client/http/index.ts +6 -0
- package/src/client/http-json.ts +82 -16
- package/src/client/i18n/messages/discovery.ts +0 -6
- package/src/client/i18n/messages/errors.ts +44 -0
- package/src/client/i18n/messages/marketing.ts +0 -6
- package/src/client/i18n/messages/promos.ts +0 -2
- package/src/client/i18n/messages/shell.ts +12 -0
- package/src/client/i18n/messages.ts +3 -0
- package/src/client/index.ts +6 -0
- package/src/client/manifest-loader.ts +65 -15
- package/src/client/resources/audit.resource.ts +25 -0
- package/src/client/resources/define-resource.ts +166 -0
- package/src/client/resources/index.ts +42 -0
- package/src/client/resources/list-resource.ts +176 -0
- package/src/client/resources/plans.resource.ts +169 -0
- package/src/client/resources/resource-request.ts +117 -0
- package/src/client/resources/tenants.resource.ts +27 -0
- package/src/client/types.ts +10 -3
- package/src/components/BundleVersionPublishDialog.vue +1 -1
- package/src/components/MarketingPromotionsTab.vue +263 -241
- package/src/components/ThemeSwitcher.vue +85 -0
- package/src/components/admin-page/AdminAccordion.vue +125 -0
- package/src/components/bundle-editor/BundleCreatePanel.vue +1 -1
- package/src/components/dialogs/PromoCodeDialogFields.vue +20 -20
- package/src/components/plan/PlanCycleToggle.vue +1 -1
- package/src/components/plan-detail/PlanVersionsPanel.vue +5 -2
- package/src/index.ts +5 -0
- package/src/pages-standard/AdminLayout.vue +57 -0
- package/src/pages-standard/AdminManifestErrorPage.vue +4 -3
- package/src/pages-standard/BundlesPage.vue +5 -35
- package/src/pages-standard/DiscoveryPage.vue +8 -1
- package/src/pages-standard/MarketingCatalogPage.vue +14 -3
- package/src/pages-standard/SuperAdminLoginPage.vue +23 -2
- package/src/pages-standard/SuperAdminSetupWizard.vue +37 -3
- package/src/pages-standard/bundles-page/BundleAccordionList.vue +30 -32
- package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +80 -88
- package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +73 -79
- package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +44 -6
- package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +8 -2
- package/src/pages-tenant/PackageSnapshotPanel.vue +31 -2
- package/src/quasar/confirm.ts +83 -0
- package/src/quasar/create-super-admin-app.ts +84 -10
- package/src/quasar/dark-bridge.ts +47 -10
- package/src/quasar/index.ts +1 -0
- package/src/ui/theme/components/accordion.css +118 -0
- package/src/ui/theme/index.css +1 -0
- package/src/vue/create-admin-routes.ts +9 -1
- package/src/vue/platform-loaders.ts +12 -2
- package/src/vue/resource-registry.ts +211 -0
- package/src/vue/super-admin-context.ts +20 -5
- package/src/vue/ui-confirm.ts +76 -0
- package/src/vue/use-api-list.ts +50 -45
- package/src/vue/use-async-action.ts +170 -0
- package/src/vue/use-async-data.ts +83 -0
- package/src/vue/use-bundles.ts +34 -7
- package/src/vue/use-catalog-entries.ts +37 -12
- package/src/vue/use-discovery.ts +27 -6
- package/src/vue/use-marketing-projections.ts +29 -7
- package/src/vue/use-plans.ts +36 -8
- package/src/vue/use-promotions.ts +27 -7
- package/src/vue/use-resource-list.ts +272 -0
- package/src/vue/use-sa-theme.ts +34 -5
- package/src/vue/use-tenant-subscription-bundles.ts +22 -3
|
@@ -132,11 +132,9 @@
|
|
|
132
132
|
v-if="row.liveVersion"
|
|
133
133
|
type="button"
|
|
134
134
|
class="sa-marketing-expand-btn"
|
|
135
|
-
:title="
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
: msg.admin.expandTitle
|
|
139
|
-
"
|
|
135
|
+
:title="msg.admin.expandTitle"
|
|
136
|
+
:aria-expanded="expandedKey === row.plan.planKey"
|
|
137
|
+
:aria-controls="panelId(row)"
|
|
140
138
|
@click="$emit('toggle-expand', row)"
|
|
141
139
|
>
|
|
142
140
|
<span
|
|
@@ -158,7 +156,11 @@
|
|
|
158
156
|
</div>
|
|
159
157
|
</div>
|
|
160
158
|
|
|
161
|
-
<div
|
|
159
|
+
<div
|
|
160
|
+
v-if="expandedKey === row.plan.planKey"
|
|
161
|
+
:id="panelId(row)"
|
|
162
|
+
class="sa-marketing-admin-expand"
|
|
163
|
+
>
|
|
162
164
|
<div class="sa-marketing-expand-grid">
|
|
163
165
|
<div class="sa-marketing-expand-col">
|
|
164
166
|
<div class="sa-marketing-expand-sec">
|
|
@@ -457,6 +459,7 @@
|
|
|
457
459
|
</template>
|
|
458
460
|
|
|
459
461
|
<script setup lang="ts">
|
|
462
|
+
import { useId } from 'vue';
|
|
460
463
|
import type { MarketingTopFeature, PlanRow, PlanVersionRow } from '@saasicat/types';
|
|
461
464
|
import { identityChipStyle } from '../../client/identity-accents.js';
|
|
462
465
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
@@ -495,6 +498,41 @@ defineEmits<{
|
|
|
495
498
|
const msg = useSaMessages('marketing');
|
|
496
499
|
const common = useSaMessages('common');
|
|
497
500
|
|
|
501
|
+
// sa-disclosure-exempt(writes `aria-expanded`):
|
|
502
|
+
// this row is six grid cells, not a header that opens a body
|
|
503
|
+
//
|
|
504
|
+
// Every other disclosure in the package is an `AdminAccordion`. This one cannot
|
|
505
|
+
// be, and the obstacle is structural rather than a matter of taste.
|
|
506
|
+
//
|
|
507
|
+
// The rows here are `display: contents` inside a six-column grid, and the open
|
|
508
|
+
// editor is a sibling at `grid-column: 1 / -1` (both declared in
|
|
509
|
+
// `MarketingCatalogPage.vue`). A self-contained wrapper around either would put
|
|
510
|
+
// a box between the grid and its cells, and the columns would stop lining up.
|
|
511
|
+
// A wrapper-less `AdminAccordion` variant would not rescue it either: the
|
|
512
|
+
// component's recipe is a trigger that FILLS the header, and this header is six
|
|
513
|
+
// cells carrying two checkboxes and two text inputs — interactive content
|
|
514
|
+
// nested in a `<button>` is not valid HTML. What would be left of the component
|
|
515
|
+
// after removing its wrapper, its full-width trigger, its badge and its body
|
|
516
|
+
// padding is a second component wearing the first one's name.
|
|
517
|
+
//
|
|
518
|
+
// So the disclosure stays here, and what it takes from the shared one is the
|
|
519
|
+
// part that has nothing to do with layout: the WAI-ARIA disclosure pattern —
|
|
520
|
+
// a `<button>` (it already was one) that says whether it is expanded and which
|
|
521
|
+
// element it controls. Not the `role="region"` half of `AdminAccordion`'s body:
|
|
522
|
+
// its trigger is named by the row it opens, whereas this one is a bare chevron,
|
|
523
|
+
// and a screenful of regions all named "Edit teaser, trial & top features"
|
|
524
|
+
// would be worse than none.
|
|
525
|
+
//
|
|
526
|
+
// The title no longer flips to "Close" when open. `aria-expanded` says that
|
|
527
|
+
// now, and a control whose NAME changes with its state is announced as a
|
|
528
|
+
// different control each time.
|
|
529
|
+
const uid = useId();
|
|
530
|
+
|
|
531
|
+
/** The panel a row's chevron controls — unique per row, and per instance of this list. */
|
|
532
|
+
function panelId(row: MarketingRow): string {
|
|
533
|
+
return `${uid}-marketing-panel-${row.plan.id}`;
|
|
534
|
+
}
|
|
535
|
+
|
|
498
536
|
/** Hint below the trial toggle — shows the auto CTA text for the set trial days. */
|
|
499
537
|
function trialCtaHint(row: MarketingRow): string {
|
|
500
538
|
const cta = formatMessage(msg.value.cta.trial, { days: row.m.trialDays });
|
|
@@ -64,12 +64,18 @@
|
|
|
64
64
|
|
|
65
65
|
<div class="sa-marketing-card-price">
|
|
66
66
|
<template v-if="row.m.priceTag">
|
|
67
|
-
<span
|
|
67
|
+
<span
|
|
68
|
+
class="sa-marketing-card-price-big"
|
|
69
|
+
style="font-size: var(--sa-text-2xl)"
|
|
70
|
+
>
|
|
68
71
|
{{ row.m.priceTag }}
|
|
69
72
|
</span>
|
|
70
73
|
</template>
|
|
71
74
|
<template v-else-if="!row.liveVersion">
|
|
72
|
-
<span
|
|
75
|
+
<span
|
|
76
|
+
class="sa-marketing-card-price-big"
|
|
77
|
+
style="font-size: var(--sa-text-2xl)"
|
|
78
|
+
>
|
|
73
79
|
{{ msg.preview.priceOnRequest }}
|
|
74
80
|
</span>
|
|
75
81
|
</template>
|
|
@@ -95,16 +95,20 @@
|
|
|
95
95
|
color="primary"
|
|
96
96
|
:label="showRaw ? i18n.packageSnapshotHideRaw : i18n.packageSnapshotShowRaw"
|
|
97
97
|
:icon="showRaw ? 'expand_less' : 'expand_more'"
|
|
98
|
+
:aria-expanded="showRaw"
|
|
99
|
+
:aria-controls="rawId"
|
|
98
100
|
@click="showRaw = !showRaw"
|
|
99
101
|
/>
|
|
100
|
-
<pre v-if="showRaw" class="sp-package-snapshot__raw-body">{{
|
|
102
|
+
<pre v-if="showRaw" :id="rawId" class="sp-package-snapshot__raw-body">{{
|
|
103
|
+
rawJson
|
|
104
|
+
}}</pre>
|
|
101
105
|
</div>
|
|
102
106
|
</q-card-section>
|
|
103
107
|
</q-card>
|
|
104
108
|
</template>
|
|
105
109
|
|
|
106
110
|
<script setup lang="ts">
|
|
107
|
-
import { computed, ref } from 'vue';
|
|
111
|
+
import { computed, ref, useId } from 'vue';
|
|
108
112
|
import type { PackageSnapshotShape } from '../vue/use-tenant-billing.js';
|
|
109
113
|
import type { TenantPlanSectionI18n } from './default-i18n.js';
|
|
110
114
|
|
|
@@ -125,7 +129,32 @@ interface Props {
|
|
|
125
129
|
|
|
126
130
|
const props = defineProps<Props>();
|
|
127
131
|
|
|
132
|
+
// sa-disclosure-exempt(toggles `showRaw`, writes `aria-expanded`):
|
|
133
|
+
// a tenant-facing raw payload toggle, in a directory that is leaving
|
|
134
|
+
//
|
|
135
|
+
// The package has one disclosure, `AdminAccordion`, and every surface inside
|
|
136
|
+
// the admin uses it. This one is outside the admin twice over.
|
|
137
|
+
//
|
|
138
|
+
// It is outside by AUDIENCE: `pages-tenant/*` renders inside the customer's own
|
|
139
|
+
// application, for the paying tenant rather than the operator, and is planned
|
|
140
|
+
// to ship as its own package with its own translations. Reaching into the
|
|
141
|
+
// admin's component layer from here adds a dependency that the split would
|
|
142
|
+
// have to unpick, for a card that is already built from Quasar's own.
|
|
143
|
+
//
|
|
144
|
+
// It is outside by SHAPE: `AdminAccordion` is a row in a list whose header
|
|
145
|
+
// opens its body — the design guide calls the pattern "rows that open". This is
|
|
146
|
+
// a `q-card` with a `<pre>` of raw JSON behind a link-styled button at its foot,
|
|
147
|
+
// and wrapping that in an accordion card would seat a bordered card inside a
|
|
148
|
+
// bordered card to say "this opens".
|
|
149
|
+
//
|
|
150
|
+
// What it does take from the shared one is the half that is not layout. A
|
|
151
|
+
// `q-btn` was already a real `<button>` with a label — the accessibility gap
|
|
152
|
+
// here was never the keyboard, only that nothing said the button expands
|
|
153
|
+
// something or what it expands. `role="region"` is deliberately not copied: the
|
|
154
|
+
// accordion's body is named by a trigger that carries the row's title, and this
|
|
155
|
+
// trigger's name is "Show raw JSON".
|
|
128
156
|
const showRaw = ref(false);
|
|
157
|
+
const rawId = useId();
|
|
129
158
|
|
|
130
159
|
const capturedAtIso = computed(() => {
|
|
131
160
|
if (!props.snapshot) return null;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Quasar implementation of the UI confirm port.
|
|
2
|
+
//
|
|
3
|
+
// The port is wired but not yet consumed by this package's own pages.
|
|
4
|
+
// `createSuperAdminApp()` provides it under `SUPER_ADMIN_CONFIRM_KEY` and
|
|
5
|
+
// `useSuperAdminConfirm()` below resolves it — but nothing under `src/` calls
|
|
6
|
+
// that hook. Its only call site in the repository is
|
|
7
|
+
// `tests-component/ui-confirm-port.test.ts`.
|
|
8
|
+
//
|
|
9
|
+
// The pages that do ask a yes/no question ask it in one of three other ways:
|
|
10
|
+
// Quasar's `q.dialog()`, `window.confirm()`, or a dialog the page mounts
|
|
11
|
+
// itself. Which page uses which shape moves as pages are reworked, so the
|
|
12
|
+
// pages are not listed here — grepping those three shapes under
|
|
13
|
+
// `src/pages-standard/` and `src/pages-tenant/` finds every one of them.
|
|
14
|
+
//
|
|
15
|
+
// Passing `createSuperAdminApp({ confirm })` therefore registers an
|
|
16
|
+
// implementation and nothing more. It reaches exactly those pages that ask
|
|
17
|
+
// through the port — an app's own, once they call `useSuperAdminConfirm()` or
|
|
18
|
+
// inject `SUPER_ADMIN_CONFIRM_KEY` themselves — and none of this package's. By
|
|
19
|
+
// the call-site note above, no page in this repository does either, the example
|
|
20
|
+
// app included, so an integrator who passes the option and changes nothing else
|
|
21
|
+
// replaces no confirmation at all. Moving the pages onto the hook is the page
|
|
22
|
+
// migration; the "Confirm port" section of this package's README carries the
|
|
23
|
+
// same caveat for integrators.
|
|
24
|
+
//
|
|
25
|
+
// The injection default keeps a page working that is mounted without
|
|
26
|
+
// `createSuperAdminApp()` — and it is only safe as a default because it still
|
|
27
|
+
// asks the operator. A default that resolved `{ ok: true }` would silently
|
|
28
|
+
// approve every delete, revoke and deactivation.
|
|
29
|
+
|
|
30
|
+
import { inject } from 'vue';
|
|
31
|
+
import { Dialog, type QDialogOptions } from 'quasar';
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
SUPER_ADMIN_CONFIRM_KEY,
|
|
35
|
+
type UiConfirm,
|
|
36
|
+
type UiConfirmRequest,
|
|
37
|
+
type UiConfirmResult,
|
|
38
|
+
} from '../vue/ui-confirm.js';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Translates a request into Quasar's dialog options.
|
|
42
|
+
*
|
|
43
|
+
* Exported because it is the whole of the mapping and the only part of this
|
|
44
|
+
* file that can be checked without driving a real dialog.
|
|
45
|
+
*/
|
|
46
|
+
export function quasarConfirmOptions(request: UiConfirmRequest): QDialogOptions {
|
|
47
|
+
return {
|
|
48
|
+
title: request.title,
|
|
49
|
+
message: request.message,
|
|
50
|
+
cancel: request.cancelLabel,
|
|
51
|
+
ok: { label: request.confirmLabel, color: request.tone ?? 'primary' },
|
|
52
|
+
...(request.prompt
|
|
53
|
+
? {
|
|
54
|
+
prompt: {
|
|
55
|
+
model: request.prompt.initial ?? '',
|
|
56
|
+
type: request.prompt.type ?? 'text',
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Quasar-backed confirm port.
|
|
65
|
+
*
|
|
66
|
+
* `onOk` and `onCancel` are mutually exclusive and one of them always runs —
|
|
67
|
+
* dismissing with Escape or a backdrop click reaches `onCancel` — so the
|
|
68
|
+
* promise cannot be left hanging.
|
|
69
|
+
*/
|
|
70
|
+
export const quasarConfirm: UiConfirm = (request) =>
|
|
71
|
+
new Promise<UiConfirmResult>((resolve) => {
|
|
72
|
+
Dialog.create(quasarConfirmOptions(request))
|
|
73
|
+
.onOk((value: unknown) => {
|
|
74
|
+
// Without a `prompt` Quasar calls back with no argument at all.
|
|
75
|
+
resolve({ ok: true, value: typeof value === 'string' ? value : undefined });
|
|
76
|
+
})
|
|
77
|
+
.onCancel(() => resolve({ ok: false }));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** Returns the app-provided confirm port, falling back to the Quasar default. */
|
|
81
|
+
export function useSuperAdminConfirm(): UiConfirm {
|
|
82
|
+
return inject(SUPER_ADMIN_CONFIRM_KEY, quasarConfirm);
|
|
83
|
+
}
|
|
@@ -44,6 +44,13 @@ import {
|
|
|
44
44
|
type SuperAdminLoginAdapter,
|
|
45
45
|
} from '../vue/super-admin-context.js';
|
|
46
46
|
import { SUPER_ADMIN_NOTIFY_KEY, type UiNotify } from '../vue/ui-notify.js';
|
|
47
|
+
import { SUPER_ADMIN_CONFIRM_KEY, type UiConfirm } from '../vue/ui-confirm.js';
|
|
48
|
+
import {
|
|
49
|
+
SUPER_ADMIN_RESOURCES_KEY,
|
|
50
|
+
createResourceRegistry,
|
|
51
|
+
type ResourceOverrides,
|
|
52
|
+
} from '../vue/resource-registry.js';
|
|
53
|
+
import { platformResources } from '../client/resources/index.js';
|
|
47
54
|
import {
|
|
48
55
|
SA_THEME_KEY,
|
|
49
56
|
createSaTheme,
|
|
@@ -57,7 +64,9 @@ import {
|
|
|
57
64
|
type SuperAdminI18nOptions,
|
|
58
65
|
} from '../vue/use-super-admin-i18n.js';
|
|
59
66
|
import { bindSaThemeToDocument } from './dark-bridge.js';
|
|
67
|
+
import { resolveSuperAdminEndpoints } from '../vue/platform-loaders.js';
|
|
60
68
|
import { quasarNotify } from './notify.js';
|
|
69
|
+
import { quasarConfirm } from './confirm.js';
|
|
61
70
|
|
|
62
71
|
export interface CreateSuperAdminAppOptions extends SuperAdminGuardOptions {
|
|
63
72
|
/** App root component (`App.vue`). */
|
|
@@ -100,6 +109,22 @@ export interface CreateSuperAdminAppOptions extends SuperAdminGuardOptions {
|
|
|
100
109
|
* center replace it here without touching the pages.
|
|
101
110
|
*/
|
|
102
111
|
notify?: UiNotify;
|
|
112
|
+
/**
|
|
113
|
+
* Optional: confirm port for the standard pages' "are you sure" step.
|
|
114
|
+
* Default is the Quasar `Dialog` implementation — apps with their own
|
|
115
|
+
* modal system replace it here without touching the pages.
|
|
116
|
+
*
|
|
117
|
+
* An implementation must actually ask. Resolving `{ ok: true }` outright
|
|
118
|
+
* turns every guarded action into an unguarded one.
|
|
119
|
+
*/
|
|
120
|
+
confirm?: UiConfirm;
|
|
121
|
+
/**
|
|
122
|
+
* Optional: per-resource adjustments to the platform's own endpoint
|
|
123
|
+
* definitions — a different path for one resource, a different transport,
|
|
124
|
+
* or a single operation wrapped. Everything not named keeps the platform
|
|
125
|
+
* implementation.
|
|
126
|
+
*/
|
|
127
|
+
resourceOverrides?: ResourceOverrides<typeof platformResources>;
|
|
103
128
|
/**
|
|
104
129
|
* Optional: additional Vue plugins (e.g. an app's own NotificationCenter)
|
|
105
130
|
* that are installed after the platform setup, before the mount.
|
|
@@ -123,7 +148,9 @@ export interface CreateSuperAdminAppOptions extends SuperAdminGuardOptions {
|
|
|
123
148
|
* Optional: colour scheme. Default follows the operating system and
|
|
124
149
|
* remembers an explicit pick. The context is returned on the handle
|
|
125
150
|
* (`handle.theme`) — set `handle.theme.scheme.value = 'dark'` to switch at
|
|
126
|
-
* runtime, and read it anywhere via `useSaTheme()`.
|
|
151
|
+
* runtime, and read it anywhere via `useSaTheme()`. The shell chrome
|
|
152
|
+
* renders a three-way switcher for it; `theme: { switcher: false }` drops
|
|
153
|
+
* it, as `i18n: { switcher: false }` drops the language one.
|
|
127
154
|
*
|
|
128
155
|
* Whatever it resolves to is mirrored onto `<html data-sa-theme>` AND into
|
|
129
156
|
* Quasar's `Dark`, so the platform's surfaces and Quasar's own components
|
|
@@ -166,6 +193,31 @@ const DEFAULT_QUASAR_OPTIONS: QuasarPluginOptions = {
|
|
|
166
193
|
config: { notify: { position: 'top-right', timeout: 3000 } },
|
|
167
194
|
};
|
|
168
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Merges an app's Quasar options over the platform's, keeping the plugins the
|
|
198
|
+
* shell's own ports depend on.
|
|
199
|
+
*
|
|
200
|
+
* The bootstrap provides `quasarConfirm`, which calls `Dialog.create`, and the
|
|
201
|
+
* notify port's fallback calls `Notify.create`. Taking the app's options whole
|
|
202
|
+
* — as this did — left those uninstalled for anything passing
|
|
203
|
+
* `{ plugins: {}, … }`, a shape this package's own tests use: the first
|
|
204
|
+
* confirmation threw instead of asking, turning a guarded action into a broken
|
|
205
|
+
* one rather than a stricter one.
|
|
206
|
+
*
|
|
207
|
+
* An app's own entry still wins on a key collision, so its build of a plugin
|
|
208
|
+
* replaces the platform's.
|
|
209
|
+
*
|
|
210
|
+
* Exported because it is the whole of the decision, and the only part of the
|
|
211
|
+
* bootstrap that can be checked without driving a real Quasar install.
|
|
212
|
+
*/
|
|
213
|
+
export function resolveQuasarOptions(own?: Partial<QuasarPluginOptions>): QuasarPluginOptions {
|
|
214
|
+
if (!own) return DEFAULT_QUASAR_OPTIONS;
|
|
215
|
+
return {
|
|
216
|
+
...own,
|
|
217
|
+
plugins: { ...DEFAULT_QUASAR_OPTIONS.plugins, ...own.plugins },
|
|
218
|
+
} as QuasarPluginOptions;
|
|
219
|
+
}
|
|
220
|
+
|
|
169
221
|
/**
|
|
170
222
|
* Universal bootstrap function for SuperAdmin apps. Replaces the `main.ts`
|
|
171
223
|
* boilerplate duplicated per app today (Quasar + Pinia + Router + manifest
|
|
@@ -188,7 +240,7 @@ export function createSuperAdminApp(options: CreateSuperAdminAppOptions): SuperA
|
|
|
188
240
|
// theme already repaints its cards via `.sa-page`. What the class buys is
|
|
189
241
|
// that an app which never calls `createSuperAdminApp` stays untouched —
|
|
190
242
|
// which a bare `.q-dialog .q-card` rule could not promise.
|
|
191
|
-
const quasarOptions = options.quasarOptions
|
|
243
|
+
const quasarOptions = resolveQuasarOptions(options.quasarOptions);
|
|
192
244
|
// `globalNodes` is a documented Quasar config option that its TypeScript
|
|
193
245
|
// types do not declare (see quasar/src/utils/private.config/nodes.js, which
|
|
194
246
|
// reads `globalConfig.globalNodes.class`). The cast is the exception this
|
|
@@ -220,13 +272,8 @@ export function createSuperAdminApp(options: CreateSuperAdminAppOptions): SuperA
|
|
|
220
272
|
|
|
221
273
|
app.use(router);
|
|
222
274
|
|
|
223
|
-
const endpoints
|
|
224
|
-
|
|
225
|
-
publicBootEndpoint:
|
|
226
|
-
options.endpoints.publicBootEndpoint ?? `${options.endpoints.apiBase}/boot`,
|
|
227
|
-
manifestEndpoint:
|
|
228
|
-
options.endpoints.manifestEndpoint ?? `${options.endpoints.apiBase}/manifest`,
|
|
229
|
-
};
|
|
275
|
+
const endpoints = resolveSuperAdminEndpoints(options.endpoints);
|
|
276
|
+
const http = options.http ?? defaultHttpClient();
|
|
230
277
|
|
|
231
278
|
const i18n = createSuperAdminI18n(options.i18n);
|
|
232
279
|
// An app that configured Quasar's dark mode has STATED a preference; the
|
|
@@ -261,6 +308,33 @@ export function createSuperAdminApp(options: CreateSuperAdminAppOptions): SuperA
|
|
|
261
308
|
app.provide(SUPER_ADMIN_EXTENSIONS_KEY, options.extensions ?? {});
|
|
262
309
|
app.provide(SUPER_ADMIN_ACTIONS_KEY, options.actions ?? {});
|
|
263
310
|
app.provide(SUPER_ADMIN_NOTIFY_KEY, options.notify ?? quasarNotify);
|
|
311
|
+
app.provide(SUPER_ADMIN_CONFIRM_KEY, options.confirm ?? quasarConfirm);
|
|
312
|
+
// Only when the app named its client. `createResourceRegistry` refuses to
|
|
313
|
+
// be built without one, because a bare `fetch` sends every request without
|
|
314
|
+
// the app's Authorization header and the failure is silent. Handing it the
|
|
315
|
+
// `defaultHttpClient()` fallback here would have defeated exactly that
|
|
316
|
+
// guarantee from inside the bootstrap: existing pages would keep working
|
|
317
|
+
// through their own `getAuthToken` options while anything reaching for
|
|
318
|
+
// `useResource()` collected 401s.
|
|
319
|
+
//
|
|
320
|
+
// Without a client the registry is simply absent, and `useResource` says
|
|
321
|
+
// so — a named failure at the first call beats a page that renders empty.
|
|
322
|
+
if (options.http) {
|
|
323
|
+
app.provide(
|
|
324
|
+
SUPER_ADMIN_RESOURCES_KEY,
|
|
325
|
+
createResourceRegistry({
|
|
326
|
+
http: options.http,
|
|
327
|
+
// Read per call: the locale changes while the app runs.
|
|
328
|
+
context: () => ({
|
|
329
|
+
apiBase: endpoints.apiBase,
|
|
330
|
+
projectKey: endpoints.projectKey,
|
|
331
|
+
locale: i18n.locale.value,
|
|
332
|
+
}),
|
|
333
|
+
resources: platformResources,
|
|
334
|
+
overrides: options.resourceOverrides,
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
264
338
|
if (options.manifestGuard?.getManifest) {
|
|
265
339
|
app.provide(SUPER_ADMIN_MANIFEST_KEY, options.manifestGuard.getManifest);
|
|
266
340
|
}
|
|
@@ -270,7 +344,7 @@ export function createSuperAdminApp(options: CreateSuperAdminAppOptions): SuperA
|
|
|
270
344
|
if (options.loginAdapter) {
|
|
271
345
|
app.provide(SUPER_ADMIN_LOGIN_ADAPTER_KEY, options.loginAdapter);
|
|
272
346
|
}
|
|
273
|
-
app.provide(SUPER_ADMIN_HTTP_KEY,
|
|
347
|
+
app.provide(SUPER_ADMIN_HTTP_KEY, http);
|
|
274
348
|
|
|
275
349
|
for (const plugin of options.installPlugins ?? []) {
|
|
276
350
|
plugin(app);
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { Dark } from 'quasar';
|
|
15
15
|
import { watch, type WatchStopHandle } from 'vue';
|
|
16
16
|
|
|
17
|
-
import type { SaTheme } from '../vue/use-sa-theme.js';
|
|
17
|
+
import type { SaColorScheme, SaTheme } from '../vue/use-sa-theme.js';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Mirrors a theme context onto the document and Quasar. Returns the stop
|
|
@@ -28,16 +28,27 @@ import type { SaTheme } from '../vue/use-sa-theme.js';
|
|
|
28
28
|
* says is written to Quasar, including `Dark.set(false)`. So a caller that
|
|
29
29
|
* bootstraps with Quasar already dark has to say so when it builds the theme,
|
|
30
30
|
* or its own choice is erased one line after it was applied.
|
|
31
|
-
* `createSuperAdminApp` does exactly that, seeding from
|
|
31
|
+
* `createSuperAdminApp` does exactly that, seeding the scheme from the
|
|
32
|
+
* `quasarOptions.config.dark` it was handed.
|
|
32
33
|
*/
|
|
33
34
|
export function bindSaThemeToDocument(theme: SaTheme): WatchStopHandle {
|
|
35
|
+
// Raised only while this bridge is inside `Dark.set()`. It is what the
|
|
36
|
+
// write-back below reads to tell the bridge's own echo apart from a
|
|
37
|
+
// decision the application made — see the comment on that watcher.
|
|
38
|
+
let mirroring = false;
|
|
39
|
+
|
|
34
40
|
const toQuasar = watch(
|
|
35
41
|
theme.resolved,
|
|
36
42
|
(resolved) => {
|
|
37
43
|
if (typeof document !== 'undefined') {
|
|
38
44
|
document.documentElement.setAttribute('data-sa-theme', resolved);
|
|
39
45
|
}
|
|
40
|
-
|
|
46
|
+
mirroring = true;
|
|
47
|
+
try {
|
|
48
|
+
Dark.set(resolved === 'dark');
|
|
49
|
+
} finally {
|
|
50
|
+
mirroring = false;
|
|
51
|
+
}
|
|
41
52
|
},
|
|
42
53
|
{ immediate: true },
|
|
43
54
|
);
|
|
@@ -48,15 +59,41 @@ export function bindSaThemeToDocument(theme: SaTheme): WatchStopHandle {
|
|
|
48
59
|
// the half-dark screen the bridge exists to prevent, arriving from the
|
|
49
60
|
// other direction.
|
|
50
61
|
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
62
|
+
// `Dark.mode` rather than `Dark.isActive`, because the mode has the three
|
|
63
|
+
// states the scheme has and the flag has two. `Dark.set('auto')` is
|
|
64
|
+
// Quasar's spelling of 'system', and read through the flag it arrives as
|
|
65
|
+
// whatever the machine happens to say at that moment — so an operator who
|
|
66
|
+
// picked 'system' had it silently frozen into a hard 'light' or 'dark' by
|
|
67
|
+
// an app doing nothing wrong, and the tab stopped following the OS. The
|
|
68
|
+
// seed in `createSuperAdminApp` reads the same option for the same reason.
|
|
69
|
+
//
|
|
70
|
+
// No loop, and the question that closes it is "who wrote this?", not "does
|
|
71
|
+
// it look different?". The outbound leg mirrors 'system' into Quasar as the
|
|
72
|
+
// colour it currently resolves to, so a colour comparison cannot tell that
|
|
73
|
+
// echo apart from an application naming the same colour on purpose: moving
|
|
74
|
+
// a control from `set('auto')` to `set(true)` on a dark machine changes
|
|
75
|
+
// `Dark.mode` while the resolved colour stays 'dark'. Dropping that write
|
|
76
|
+
// left the pick on 'system' with nothing visibly wrong — until the next OS
|
|
77
|
+
// change moved a theme somebody had just fixed.
|
|
78
|
+
//
|
|
79
|
+
// `Dark.set()` assigns `Dark.mode` synchronously and this watcher runs on
|
|
80
|
+
// the sync flush, so the flag is still raised when the bridge's own write
|
|
81
|
+
// arrives here, and is down for every write that came from anywhere else.
|
|
82
|
+
//
|
|
83
|
+
// One case stays unanswerable, because Quasar leaves no trace of it: a
|
|
84
|
+
// `set(true)` while the mode is already `true` — an operator on 'system'
|
|
85
|
+
// and a dark machine, whose application then pins dark. Nothing in `Dark`
|
|
86
|
+
// changes, so there is no event to read and the pick stays 'system'. An
|
|
87
|
+
// application that wants that pick to stick sets `theme.scheme` instead.
|
|
54
88
|
const fromQuasar = watch(
|
|
55
|
-
() => Dark.
|
|
56
|
-
(
|
|
57
|
-
|
|
58
|
-
|
|
89
|
+
() => Dark.mode,
|
|
90
|
+
(mode) => {
|
|
91
|
+
if (mirroring) return;
|
|
92
|
+
const picked: SaColorScheme =
|
|
93
|
+
mode === 'auto' ? 'system' : mode === true ? 'dark' : 'light';
|
|
94
|
+
if (theme.scheme.value !== picked) theme.scheme.value = picked;
|
|
59
95
|
},
|
|
96
|
+
{ flush: 'sync' },
|
|
60
97
|
);
|
|
61
98
|
|
|
62
99
|
return () => {
|
package/src/quasar/index.ts
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* Accordion — a header that opens a body (see AdminAccordion).
|
|
2
|
+
*
|
|
3
|
+
* Every value here was one of several. The survey that led to this file found
|
|
4
|
+
* three header paddings across the package (0, 10/12, 12/14), three body
|
|
5
|
+
* paddings (12, 14, 18/20/22), two radii (8 and 10), two colours for the open
|
|
6
|
+
* border (`accent` and `scheduled-border`), two transition durations, and hover
|
|
7
|
+
* feedback on one of six surfaces that all set `cursor: pointer`.
|
|
8
|
+
*
|
|
9
|
+
* The values below are the scale's, not the average of the six: a rung exists
|
|
10
|
+
* for each, so a literal here would be a fourth opinion rather than a decision.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
.sa-accordion {
|
|
14
|
+
background: var(--sa-color-bg-surface);
|
|
15
|
+
border: 1px solid var(--sa-color-border);
|
|
16
|
+
border-radius: var(--sa-radius-head);
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* The open card names itself with the accent rather than with a tone: which
|
|
21
|
+
* row is open is a position, not a status, and a status colour here would
|
|
22
|
+
* compete with the chips the header carries. */
|
|
23
|
+
.sa-accordion--open {
|
|
24
|
+
border-color: var(--sa-color-accent);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.sa-accordion__head {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: stretch;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* The trigger fills the head so the whole row is the target — the affordance
|
|
33
|
+
* the `<div @click>` versions had, without giving up the button. */
|
|
34
|
+
.sa-accordion__trigger {
|
|
35
|
+
flex: 1;
|
|
36
|
+
min-width: 0;
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: var(--sa-space-4);
|
|
40
|
+
padding: var(--sa-space-4) var(--sa-space-5);
|
|
41
|
+
background: none;
|
|
42
|
+
border: 0;
|
|
43
|
+
font: inherit;
|
|
44
|
+
color: inherit;
|
|
45
|
+
text-align: left;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Hover feedback, which five of the six surfaces promised with `cursor` and
|
|
50
|
+
* only one delivered. */
|
|
51
|
+
.sa-accordion__trigger:hover:not(:disabled) {
|
|
52
|
+
background: var(--sa-color-bg-sunken);
|
|
53
|
+
}
|
|
54
|
+
.sa-accordion__trigger:disabled {
|
|
55
|
+
cursor: not-allowed;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* The badge that identifies the row.
|
|
59
|
+
*
|
|
60
|
+
* Three surfaces drew this three ways — a 34px accent-tinted square, a bare
|
|
61
|
+
* 22px glyph, and a bare 20px glyph coloured through a Quasar palette prop — so
|
|
62
|
+
* the same kind of row did not look like the same kind of row from one page to
|
|
63
|
+
* the next. The frame is the component's; only the glyph is the page's.
|
|
64
|
+
*
|
|
65
|
+
* Square-ish rather than round: a circle here would read as an avatar, and none
|
|
66
|
+
* of these rows is about a person. */
|
|
67
|
+
.sa-accordion__mark {
|
|
68
|
+
flex: 0 0 auto;
|
|
69
|
+
width: 34px;
|
|
70
|
+
height: 34px;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
border-radius: var(--sa-radius-field);
|
|
75
|
+
background: var(--sa-color-accent-surface-strong);
|
|
76
|
+
color: var(--sa-color-accent-strong);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Only for a row whose state the badge itself should report. The size, radius
|
|
80
|
+
* and position do not move with it — that is what keeps it the same badge. */
|
|
81
|
+
.sa-accordion__mark--negative {
|
|
82
|
+
background: var(--sa-color-negative-surface-strong);
|
|
83
|
+
color: var(--sa-color-negative-fg);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.sa-accordion__label {
|
|
87
|
+
flex: 1;
|
|
88
|
+
min-width: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Points right when closed, down when open. Not `transform: rotate(90deg)` on a
|
|
92
|
+
* `chevron_down`: the resting state is the one a reader sees most, and a glyph
|
|
93
|
+
* that means "there is more this way" is the honest one to rest on. */
|
|
94
|
+
.sa-accordion__chev {
|
|
95
|
+
flex: 0 0 auto;
|
|
96
|
+
color: var(--sa-color-fg-subtle);
|
|
97
|
+
transition: transform 0.15s;
|
|
98
|
+
}
|
|
99
|
+
.sa-accordion--open .sa-accordion__chev {
|
|
100
|
+
transform: rotate(90deg);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Outside the trigger, so a control here does not toggle the row. Padded on
|
|
104
|
+
* its own rather than by the trigger, or the two would share a hover surface
|
|
105
|
+
* and the row would look pressed while the pointer is on a button that does
|
|
106
|
+
* something else. */
|
|
107
|
+
.sa-accordion__actions {
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
gap: var(--sa-space-3);
|
|
111
|
+
padding-right: var(--sa-space-5);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.sa-accordion__body {
|
|
115
|
+
padding: var(--sa-space-5);
|
|
116
|
+
border-top: 1px solid var(--sa-color-border);
|
|
117
|
+
background: var(--sa-color-bg-sunken);
|
|
118
|
+
}
|
package/src/ui/theme/index.css
CHANGED
|
@@ -17,7 +17,15 @@ export interface CreateAdminRoutesOptions {
|
|
|
17
17
|
/**
|
|
18
18
|
* Loader for the shared login page. The app passes it through as a
|
|
19
19
|
* package-path import (resolved by the app bundler):
|
|
20
|
-
* `() => import('@saasicat/ui-vue/pages
|
|
20
|
+
* `() => import('@saasicat/ui-vue/pages/SuperAdminLoginPage.vue')`.
|
|
21
|
+
*
|
|
22
|
+
* That subpath has two spellings today: the export map maps both
|
|
23
|
+
* `./pages/*` and `./pages-standard/*` onto `src/pages-standard/`, so the
|
|
24
|
+
* two imports are the same file. The scaffolder template
|
|
25
|
+
* (`create-saasicat-admin`) and `examples/notesapp` spell it `/pages/*`,
|
|
26
|
+
* which is why the example above does; the package README lists the SFC
|
|
27
|
+
* entries under `/pages-standard/*`. Collapsing the pair is a breaking
|
|
28
|
+
* change to the export map and is deliberately not done here.
|
|
21
29
|
*/
|
|
22
30
|
loginPage: RouteComponent;
|
|
23
31
|
/** Layout component for `/admin` (e.g. `() => import('@/layouts/AdminLayout.vue')`). */
|
|
@@ -36,11 +36,21 @@ export interface PlatformLoaders {
|
|
|
36
36
|
manifestLoader: ManifestLoader;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Fills in what an app left out.
|
|
41
|
+
*
|
|
42
|
+
* Exported because the bootstrap needs the same answer, and two copies of a
|
|
43
|
+
* default are two places for it to drift — which is how `projectKey` was
|
|
44
|
+
* added to one of them and not the other.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveSuperAdminEndpoints(
|
|
47
|
+
endpoints: SuperAdminEndpoints,
|
|
48
|
+
): Required<SuperAdminEndpoints> {
|
|
40
49
|
return {
|
|
41
50
|
apiBase: endpoints.apiBase,
|
|
42
51
|
publicBootEndpoint: endpoints.publicBootEndpoint ?? `${endpoints.apiBase}/boot`,
|
|
43
52
|
manifestEndpoint: endpoints.manifestEndpoint ?? `${endpoints.apiBase}/manifest`,
|
|
53
|
+
projectKey: endpoints.projectKey ?? '',
|
|
44
54
|
};
|
|
45
55
|
}
|
|
46
56
|
|
|
@@ -49,7 +59,7 @@ function resolveEndpoints(endpoints: SuperAdminEndpoints): Required<SuperAdminEn
|
|
|
49
59
|
* Apps use the same constant for `createSuperAdminApp({ endpoints })`.
|
|
50
60
|
*/
|
|
51
61
|
export function createPlatformLoaders(options: CreatePlatformLoadersOptions): PlatformLoaders {
|
|
52
|
-
const resolved =
|
|
62
|
+
const resolved = resolveSuperAdminEndpoints(options.endpoints);
|
|
53
63
|
|
|
54
64
|
const bootLoader = new BootLoader({
|
|
55
65
|
endpoint: resolved.publicBootEndpoint,
|