@saasicat/ui-vue 0.18.0 → 0.18.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/package.json +2 -2
- package/src/components/dialogs/PilotCreateDialog.vue +15 -188
- package/src/components/dialogs/PilotEditDialog.vue +51 -233
- package/src/components/dialogs/PromoCodeCreateDialog.vue +19 -560
- package/src/components/dialogs/PromoCodeDialogFields.vue +666 -0
- package/src/components/dialogs/PromoCodeEditDialog.vue +22 -570
- package/src/components/dialogs/pilot-dialog.css +222 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Section: Code & Discount -->
|
|
3
|
+
<div class="pc-section">
|
|
4
|
+
<div class="pc-section__title">{{ msg.form.sectionCodeDiscount }}</div>
|
|
5
|
+
<div class="pc-grid pc-grid--2">
|
|
6
|
+
<div class="pc-field">
|
|
7
|
+
<div class="pc-field__label">{{ msg.form.codeLabel }}</div>
|
|
8
|
+
<div v-if="mode === 'create'" class="pc-code-input">
|
|
9
|
+
<input
|
|
10
|
+
:value="code"
|
|
11
|
+
class="pc-input pc-input--code"
|
|
12
|
+
:placeholder="msg.form.codePlaceholder"
|
|
13
|
+
@input="onCodeInput"
|
|
14
|
+
/>
|
|
15
|
+
<button type="button" class="pc-btn-mini" @click="onRandomCode">
|
|
16
|
+
{{ msg.form.codeRandom }}
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
<input v-else :value="code" class="pc-input pc-input--code" disabled />
|
|
20
|
+
<div class="pc-field__hint">
|
|
21
|
+
{{ mode === 'create' ? msg.form.codeHint : msg.form.codeStableHint }}
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="pc-field">
|
|
26
|
+
<div class="pc-field__label">{{ msg.form.valueTypeLabel }}</div>
|
|
27
|
+
<div class="pc-type-grid">
|
|
28
|
+
<button
|
|
29
|
+
v-for="o in typeOptions"
|
|
30
|
+
:key="o.k"
|
|
31
|
+
type="button"
|
|
32
|
+
class="pc-type-opt"
|
|
33
|
+
:class="{ 'pc-type-opt--active': form.valueType === o.k }"
|
|
34
|
+
@click="form.valueType = o.k"
|
|
35
|
+
>
|
|
36
|
+
<div class="pc-type-opt__label">{{ o.label }}</div>
|
|
37
|
+
<div class="pc-type-opt__sub">{{ o.sub }}</div>
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="pc-field" style="max-width: 280px">
|
|
44
|
+
<div class="pc-field__label">
|
|
45
|
+
{{
|
|
46
|
+
form.valueType === 'PERCENT'
|
|
47
|
+
? msg.form.valuePercentLabel
|
|
48
|
+
: msg.form.valueAbsoluteLabel
|
|
49
|
+
}}
|
|
50
|
+
</div>
|
|
51
|
+
<input
|
|
52
|
+
v-model.number="form.value"
|
|
53
|
+
class="pc-input"
|
|
54
|
+
type="number"
|
|
55
|
+
min="1"
|
|
56
|
+
:max="form.valueType === 'PERCENT' ? 100 : undefined"
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- Section: Validity & Duration -->
|
|
62
|
+
<div class="pc-section">
|
|
63
|
+
<div class="pc-section__title">{{ msg.form.sectionValidity }}</div>
|
|
64
|
+
|
|
65
|
+
<div v-if="plans.length > 0" class="pc-field">
|
|
66
|
+
<div class="pc-field__label">{{ msg.form.plansLabel }}</div>
|
|
67
|
+
<div class="pc-plan-pick">
|
|
68
|
+
<button
|
|
69
|
+
v-for="p in plans"
|
|
70
|
+
:key="p.key"
|
|
71
|
+
type="button"
|
|
72
|
+
class="pc-plan-opt"
|
|
73
|
+
:class="{ 'pc-plan-opt--on': isPlanSelected(p.key) }"
|
|
74
|
+
:style="planChipStyle(p)"
|
|
75
|
+
@click="togglePlan(p.key)"
|
|
76
|
+
>
|
|
77
|
+
<span class="pc-plan-opt__mark" :style="{ background: p.color ?? '#64748b' }" />
|
|
78
|
+
{{ p.label }}
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="pc-field__hint">
|
|
82
|
+
{{
|
|
83
|
+
formatMessage(msg.form.plansHint, {
|
|
84
|
+
count: form.appliesToPlans.length,
|
|
85
|
+
})
|
|
86
|
+
}}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="pc-grid pc-grid--2">
|
|
91
|
+
<div class="pc-field">
|
|
92
|
+
<div class="pc-field__label">{{ msg.form.durationLabel }}</div>
|
|
93
|
+
<div class="pc-dur">
|
|
94
|
+
<button
|
|
95
|
+
v-for="o in durationOptions"
|
|
96
|
+
:key="o.k"
|
|
97
|
+
type="button"
|
|
98
|
+
class="pc-dur-opt"
|
|
99
|
+
:class="{ 'pc-dur-opt--active': form.durationType === o.k }"
|
|
100
|
+
@click="form.durationType = o.k"
|
|
101
|
+
>
|
|
102
|
+
{{ o.label }}
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
<input
|
|
106
|
+
v-if="form.durationType !== 'ONCE'"
|
|
107
|
+
v-model.number="form.durationValue"
|
|
108
|
+
class="pc-input"
|
|
109
|
+
type="number"
|
|
110
|
+
min="1"
|
|
111
|
+
style="margin-top: 8px; max-width: 120px"
|
|
112
|
+
:placeholder="
|
|
113
|
+
form.durationType === 'MONTHS'
|
|
114
|
+
? msg.form.durationMonthsPlaceholder
|
|
115
|
+
: msg.form.durationCyclesPlaceholder
|
|
116
|
+
"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div class="pc-field">
|
|
121
|
+
<div class="pc-field__label">{{ msg.form.maxRedemptionsLabel }}</div>
|
|
122
|
+
<input
|
|
123
|
+
v-model.number="form.maxRedemptions"
|
|
124
|
+
class="pc-input"
|
|
125
|
+
type="number"
|
|
126
|
+
min="1"
|
|
127
|
+
:placeholder="msg.form.maxRedemptionsPlaceholder"
|
|
128
|
+
/>
|
|
129
|
+
<div class="pc-field__hint">
|
|
130
|
+
{{
|
|
131
|
+
mode === 'create'
|
|
132
|
+
? msg.form.maxRedemptionsHintCreate
|
|
133
|
+
: msg.form.maxRedemptionsHintEdit
|
|
134
|
+
}}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div class="pc-grid pc-grid--2">
|
|
140
|
+
<div class="pc-field">
|
|
141
|
+
<div class="pc-field__label">{{ msg.form.validFromLabel }}</div>
|
|
142
|
+
<input v-model="form.validFrom" class="pc-input" type="date" />
|
|
143
|
+
</div>
|
|
144
|
+
<div class="pc-field">
|
|
145
|
+
<div class="pc-field__label">{{ msg.form.validUntilLabel }}</div>
|
|
146
|
+
<input v-model="form.validUntil" class="pc-input" type="date" />
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div v-if="mode === 'edit'" class="pc-grid pc-grid--2">
|
|
151
|
+
<div class="pc-field">
|
|
152
|
+
<div class="pc-field__label">{{ common.status }}</div>
|
|
153
|
+
<div class="pc-status">
|
|
154
|
+
<button
|
|
155
|
+
v-for="o in statusOptions"
|
|
156
|
+
:key="o.k"
|
|
157
|
+
type="button"
|
|
158
|
+
class="pc-status-opt"
|
|
159
|
+
:class="{ 'pc-status-opt--active': form.status === o.k }"
|
|
160
|
+
@click="form.status = o.k"
|
|
161
|
+
>
|
|
162
|
+
<q-icon :name="o.icon" size="14px" />
|
|
163
|
+
{{ o.label }}
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="pc-field__hint">{{ msg.form.statusHint }}</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<!-- Section: Campaign & Note -->
|
|
172
|
+
<div class="pc-section">
|
|
173
|
+
<div class="pc-section__title">{{ msg.form.sectionCampaign }}</div>
|
|
174
|
+
<div v-if="showCampaignTag" class="pc-field">
|
|
175
|
+
<div class="pc-field__label">{{ msg.form.campaignLabel }}</div>
|
|
176
|
+
<input
|
|
177
|
+
v-model="form.campaignTag"
|
|
178
|
+
class="pc-input"
|
|
179
|
+
:placeholder="msg.form.campaignPlaceholder"
|
|
180
|
+
/>
|
|
181
|
+
<div class="pc-field__hint">{{ msg.form.campaignHint }}</div>
|
|
182
|
+
</div>
|
|
183
|
+
<div class="pc-field">
|
|
184
|
+
<div class="pc-field__label">{{ msg.form.noteLabel }}</div>
|
|
185
|
+
<textarea
|
|
186
|
+
v-model="form.description"
|
|
187
|
+
class="pc-input"
|
|
188
|
+
rows="2"
|
|
189
|
+
:placeholder="msg.form.notePlaceholder"
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<!-- Section: Advanced (backend-only fields, collapsed) -->
|
|
195
|
+
<div class="pc-section">
|
|
196
|
+
<button type="button" class="pc-section__toggle" @click="advancedOpen = !advancedOpen">
|
|
197
|
+
<q-icon :name="advancedOpen ? 'expand_more' : 'chevron_right'" size="16px" />
|
|
198
|
+
{{ msg.form.advancedToggle }}
|
|
199
|
+
</button>
|
|
200
|
+
<div v-if="advancedOpen" class="pc-advanced">
|
|
201
|
+
<div class="pc-grid pc-grid--2">
|
|
202
|
+
<div class="pc-field">
|
|
203
|
+
<div class="pc-field__label">{{ msg.form.billingCycleLabel }}</div>
|
|
204
|
+
<select v-model="form.appliesToBilling" class="pc-input">
|
|
205
|
+
<option :value="mode === 'create' ? undefined : null">
|
|
206
|
+
{{ common.both }}
|
|
207
|
+
</option>
|
|
208
|
+
<option value="MONTHLY">{{ common.monthly }}</option>
|
|
209
|
+
<option value="YEARLY">{{ common.yearly }}</option>
|
|
210
|
+
</select>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="pc-field">
|
|
213
|
+
<div class="pc-field__label">{{ msg.form.minAmountLabel }}</div>
|
|
214
|
+
<input
|
|
215
|
+
v-model.number="form.minimumPlanAmountGross"
|
|
216
|
+
class="pc-input"
|
|
217
|
+
type="number"
|
|
218
|
+
min="0"
|
|
219
|
+
:placeholder="msg.form.minAmountPlaceholder"
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="pc-grid pc-grid--2">
|
|
224
|
+
<label class="pc-check">
|
|
225
|
+
<input v-model="form.firstTimeCustomersOnly" type="checkbox" />
|
|
226
|
+
<span>{{ msg.form.firstTimeOnly }}</span>
|
|
227
|
+
</label>
|
|
228
|
+
<label class="pc-check">
|
|
229
|
+
<input v-model="form.allowZeroInvoice" type="checkbox" />
|
|
230
|
+
<span>{{ msg.form.allowZeroInvoice }}</span>
|
|
231
|
+
</label>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="pc-field">
|
|
234
|
+
<div class="pc-field__label">{{ msg.form.revenueAccountLabel }}</div>
|
|
235
|
+
<input
|
|
236
|
+
v-model="form.revenueDeductionAccount"
|
|
237
|
+
class="pc-input"
|
|
238
|
+
:placeholder="msg.form.revenueAccountPlaceholder"
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<!-- Live preview -->
|
|
245
|
+
<div class="pc-preview">
|
|
246
|
+
<div class="pc-preview__eyebrow">{{ msg.form.previewEyebrow }}</div>
|
|
247
|
+
<div class="pc-preview__body">
|
|
248
|
+
<code class="pc-preview__code">{{ code || 'CODE' }}</code>
|
|
249
|
+
<span class="pc-preview__disc">{{ previewValue }}</span>
|
|
250
|
+
<span class="pc-preview__meta">{{ previewMeta }}</span>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</template>
|
|
254
|
+
|
|
255
|
+
<script setup lang="ts">
|
|
256
|
+
import { computed } from 'vue';
|
|
257
|
+
import { formatMessage } from '../../client/i18n/format.js';
|
|
258
|
+
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
259
|
+
import type { PromoCodeDurationType, PromoCodePlanOption, PromoCodeValueType } from './types.js';
|
|
260
|
+
|
|
261
|
+
// The form body shared by both promo-code dialogs.
|
|
262
|
+
//
|
|
263
|
+
// Create and edit show the same sections and differ in a handful of
|
|
264
|
+
// enumerable points: the code is only editable on create, the status only on
|
|
265
|
+
// edit, two hint texts swap, and an empty billing cycle is `undefined` on
|
|
266
|
+
// create (omit the field) but `null` on edit (clear the field). Hence a `mode`
|
|
267
|
+
// rather than slots: slot content carries the parent's scope, so the shared
|
|
268
|
+
// `.pc-*` rules would never reach it.
|
|
269
|
+
//
|
|
270
|
+
// This component is internal to the two dialogs and not part of the public
|
|
271
|
+
// surface.
|
|
272
|
+
|
|
273
|
+
/** The fields both dialogs share. `status` exists on edit only. */
|
|
274
|
+
export interface PromoCodeSharedForm {
|
|
275
|
+
valueType: PromoCodeValueType;
|
|
276
|
+
value: number;
|
|
277
|
+
durationType: PromoCodeDurationType;
|
|
278
|
+
durationValue: number | null;
|
|
279
|
+
maxRedemptions: number | null;
|
|
280
|
+
validFrom: string;
|
|
281
|
+
validUntil: string;
|
|
282
|
+
appliesToPlans: string[];
|
|
283
|
+
appliesToBilling?: 'MONTHLY' | 'YEARLY' | null;
|
|
284
|
+
firstTimeCustomersOnly: boolean;
|
|
285
|
+
minimumPlanAmountGross: number | null;
|
|
286
|
+
allowZeroInvoice: boolean;
|
|
287
|
+
campaignTag: string;
|
|
288
|
+
revenueDeductionAccount: string;
|
|
289
|
+
description: string;
|
|
290
|
+
status?: 'ACTIVE' | 'PAUSED';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const props = withDefaults(
|
|
294
|
+
defineProps<{
|
|
295
|
+
mode: 'create' | 'edit';
|
|
296
|
+
form: PromoCodeSharedForm;
|
|
297
|
+
/** Editable on create, display-only on edit. */
|
|
298
|
+
code: string;
|
|
299
|
+
showCampaignTag?: boolean;
|
|
300
|
+
plans?: readonly PromoCodePlanOption[];
|
|
301
|
+
}>(),
|
|
302
|
+
{ showCampaignTag: true, plans: () => [] },
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const emit = defineEmits<{ (e: 'update:code', v: string): void }>();
|
|
306
|
+
|
|
307
|
+
const msg = useSaMessages('promos');
|
|
308
|
+
const common = useSaMessages('common');
|
|
309
|
+
|
|
310
|
+
const typeOptions = computed<ReadonlyArray<{ k: PromoCodeValueType; label: string; sub: string }>>(
|
|
311
|
+
() => [
|
|
312
|
+
{
|
|
313
|
+
k: 'PERCENT',
|
|
314
|
+
label: msg.value.form.valueTypePercent,
|
|
315
|
+
sub: msg.value.form.valueTypePercentSub,
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
k: 'ABSOLUTE',
|
|
319
|
+
label: msg.value.form.valueTypeAbsolute,
|
|
320
|
+
sub: msg.value.form.valueTypeAbsoluteSub,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const durationOptions = computed<ReadonlyArray<{ k: PromoCodeDurationType; label: string }>>(() => [
|
|
326
|
+
{ k: 'ONCE', label: msg.value.form.durationOnce },
|
|
327
|
+
{ k: 'MONTHS', label: msg.value.form.durationMonths },
|
|
328
|
+
{ k: 'BILLING_CYCLES', label: msg.value.form.durationBillingCycles },
|
|
329
|
+
]);
|
|
330
|
+
|
|
331
|
+
const statusOptions = computed<
|
|
332
|
+
ReadonlyArray<{ k: 'ACTIVE' | 'PAUSED'; label: string; icon: string }>
|
|
333
|
+
>(() => [
|
|
334
|
+
{ k: 'ACTIVE', label: common.value.active, icon: 'play_arrow' },
|
|
335
|
+
{ k: 'PAUSED', label: msg.value.form.statusPaused, icon: 'pause' },
|
|
336
|
+
]);
|
|
337
|
+
|
|
338
|
+
/** The code round-trips through the caller so its form stays the source of truth. */
|
|
339
|
+
function onCodeInput(event: Event): void {
|
|
340
|
+
const raw = (event.target as HTMLInputElement).value;
|
|
341
|
+
emit('update:code', raw.toUpperCase().replace(/[^A-Z0-9_-]/g, ''));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function onRandomCode(): void {
|
|
345
|
+
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
346
|
+
let s = '';
|
|
347
|
+
for (let i = 0; i < 8; i++) s += chars[Math.floor(Math.random() * chars.length)];
|
|
348
|
+
emit('update:code', s);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function isPlanSelected(key: string): boolean {
|
|
352
|
+
return props.form.appliesToPlans.includes(key);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function togglePlan(key: string): void {
|
|
356
|
+
if (props.form.appliesToPlans.includes(key)) {
|
|
357
|
+
props.form.appliesToPlans = props.form.appliesToPlans.filter((k) => k !== key);
|
|
358
|
+
} else {
|
|
359
|
+
props.form.appliesToPlans = [...props.form.appliesToPlans, key];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function planChipStyle(p: PromoCodePlanOption): Record<string, string> {
|
|
364
|
+
if (!isPlanSelected(p.key) || !p.color) return {};
|
|
365
|
+
return { borderColor: p.color, background: `${p.color}12`, color: p.color };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const previewValue = computed(() => {
|
|
369
|
+
if (props.form.valueType === 'PERCENT') return `\u2212${props.form.value || 0}%`;
|
|
370
|
+
return `\u2212${props.form.value || 0} \u20ac`;
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const previewMeta = computed(() => {
|
|
374
|
+
const parts: string[] = [];
|
|
375
|
+
const count = props.form.durationValue || 0;
|
|
376
|
+
if (props.form.durationType === 'ONCE') parts.push(msg.value.form.previewOnce);
|
|
377
|
+
else if (props.form.durationType === 'MONTHS')
|
|
378
|
+
parts.push(formatMessage(msg.value.form.previewMonths, { count }));
|
|
379
|
+
else parts.push(formatMessage(msg.value.form.previewCycles, { count }));
|
|
380
|
+
parts.push(
|
|
381
|
+
props.form.appliesToPlans.length > 0
|
|
382
|
+
? props.form.appliesToPlans.join(', ')
|
|
383
|
+
: props.plans.length > 0
|
|
384
|
+
? msg.value.form.previewAllPlans
|
|
385
|
+
: msg.value.form.previewNoPlanFilter,
|
|
386
|
+
);
|
|
387
|
+
if (props.form.maxRedemptions)
|
|
388
|
+
parts.push(formatMessage(msg.value.form.previewMax, { count: props.form.maxRedemptions }));
|
|
389
|
+
return parts.join(' \u00b7 ');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const advancedOpen = defineModel<boolean>('advancedOpen', { default: false });
|
|
393
|
+
</script>
|
|
394
|
+
|
|
395
|
+
<style scoped>
|
|
396
|
+
.pc-section {
|
|
397
|
+
border: 1px solid var(--sa-border, #e2e8f0);
|
|
398
|
+
border-radius: 10px;
|
|
399
|
+
padding: 14px 16px;
|
|
400
|
+
background: #fafbfc;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.pc-section__title {
|
|
404
|
+
font-family: var(--sa-font-head, system-ui, sans-serif);
|
|
405
|
+
font-weight: 700;
|
|
406
|
+
font-size: 13px;
|
|
407
|
+
color: var(--sa-heading, #0f172a);
|
|
408
|
+
margin-bottom: 12px;
|
|
409
|
+
letter-spacing: -0.005em;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.pc-section__toggle {
|
|
413
|
+
border: 0;
|
|
414
|
+
background: transparent;
|
|
415
|
+
cursor: pointer;
|
|
416
|
+
font: 600 12.5px var(--sa-font-body, system-ui, sans-serif);
|
|
417
|
+
color: var(--sa-muted-dark, #475569);
|
|
418
|
+
display: inline-flex;
|
|
419
|
+
align-items: center;
|
|
420
|
+
gap: 4px;
|
|
421
|
+
padding: 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.pc-advanced {
|
|
425
|
+
display: flex;
|
|
426
|
+
flex-direction: column;
|
|
427
|
+
gap: 12px;
|
|
428
|
+
margin-top: 12px;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.pc-grid {
|
|
432
|
+
display: grid;
|
|
433
|
+
gap: 12px;
|
|
434
|
+
margin-bottom: 12px;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.pc-grid--2 {
|
|
438
|
+
grid-template-columns: 1fr 1fr;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
@media (max-width: 600px) {
|
|
442
|
+
.pc-grid--2 {
|
|
443
|
+
grid-template-columns: 1fr;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.pc-grid:last-child {
|
|
448
|
+
margin-bottom: 0;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.pc-field {
|
|
452
|
+
display: flex;
|
|
453
|
+
flex-direction: column;
|
|
454
|
+
gap: 4px;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.pc-field__label {
|
|
458
|
+
font-size: 11.5px;
|
|
459
|
+
font-weight: 600;
|
|
460
|
+
color: var(--sa-muted, #64748b);
|
|
461
|
+
text-transform: uppercase;
|
|
462
|
+
letter-spacing: 0.04em;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.pc-field__hint {
|
|
466
|
+
font-size: 11.5px;
|
|
467
|
+
color: #94a3b8;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.pc-input {
|
|
471
|
+
width: 100%;
|
|
472
|
+
border: 1px solid var(--sa-border, #e2e8f0);
|
|
473
|
+
background: #fff;
|
|
474
|
+
border-radius: 7px;
|
|
475
|
+
padding: 8px 10px;
|
|
476
|
+
font: 13.5px var(--sa-font-body, system-ui, sans-serif);
|
|
477
|
+
color: var(--sa-body, #1e293b);
|
|
478
|
+
outline: 0;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.pc-input:focus {
|
|
482
|
+
border-color: var(--sa-primary, #3f6bff);
|
|
483
|
+
box-shadow: 0 0 0 3px rgba(63, 107, 255, 0.12);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.pc-input--code {
|
|
487
|
+
font: 600 14px var(--sa-font-mono, ui-monospace, monospace);
|
|
488
|
+
letter-spacing: 0.04em;
|
|
489
|
+
text-transform: uppercase;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
textarea.pc-input {
|
|
493
|
+
font: 13.5px var(--sa-font-body, system-ui, sans-serif);
|
|
494
|
+
resize: vertical;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.pc-code-input {
|
|
498
|
+
display: flex;
|
|
499
|
+
gap: 8px;
|
|
500
|
+
align-items: center;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.pc-btn-mini {
|
|
504
|
+
border: 1px solid var(--sa-border, #e2e8f0);
|
|
505
|
+
background: #fff;
|
|
506
|
+
border-radius: 7px;
|
|
507
|
+
padding: 6px 10px;
|
|
508
|
+
font: 500 12px var(--sa-font-body, system-ui, sans-serif);
|
|
509
|
+
cursor: pointer;
|
|
510
|
+
color: var(--sa-muted-dark, #475569);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.pc-btn-mini:hover {
|
|
514
|
+
background: #f1f5f9;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.pc-type-grid {
|
|
518
|
+
display: grid;
|
|
519
|
+
grid-template-columns: 1fr 1fr;
|
|
520
|
+
gap: 6px;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.pc-type-opt {
|
|
524
|
+
border: 1px solid var(--sa-border, #e2e8f0);
|
|
525
|
+
background: #fff;
|
|
526
|
+
border-radius: 8px;
|
|
527
|
+
padding: 8px 10px;
|
|
528
|
+
text-align: left;
|
|
529
|
+
cursor: pointer;
|
|
530
|
+
transition:
|
|
531
|
+
border-color 0.1s,
|
|
532
|
+
background 0.1s;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.pc-type-opt:hover {
|
|
536
|
+
border-color: #cbd5e1;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.pc-type-opt--active {
|
|
540
|
+
border-color: var(--sa-primary, #3f6bff);
|
|
541
|
+
background: var(--sa-primary-soft, rgba(63, 107, 255, 0.08));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.pc-type-opt__label {
|
|
545
|
+
font: 600 12.5px var(--sa-font-body, system-ui, sans-serif);
|
|
546
|
+
color: var(--sa-heading, #0f172a);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.pc-type-opt__sub {
|
|
550
|
+
font: 11.5px var(--sa-font-mono, ui-monospace, monospace);
|
|
551
|
+
color: var(--sa-muted, #64748b);
|
|
552
|
+
margin-top: 1px;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.pc-plan-pick {
|
|
556
|
+
display: flex;
|
|
557
|
+
flex-wrap: wrap;
|
|
558
|
+
gap: 6px;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.pc-plan-opt {
|
|
562
|
+
display: inline-flex;
|
|
563
|
+
align-items: center;
|
|
564
|
+
gap: 8px;
|
|
565
|
+
background: #fff;
|
|
566
|
+
border: 1px solid var(--sa-border, #e2e8f0);
|
|
567
|
+
border-radius: 999px;
|
|
568
|
+
padding: 5px 12px 5px 8px;
|
|
569
|
+
font: 600 12px var(--sa-font-body, system-ui, sans-serif);
|
|
570
|
+
cursor: pointer;
|
|
571
|
+
color: var(--sa-muted-dark, #475569);
|
|
572
|
+
transition:
|
|
573
|
+
border-color 0.1s,
|
|
574
|
+
background 0.1s;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.pc-plan-opt:hover {
|
|
578
|
+
border-color: #cbd5e1;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.pc-plan-opt--on {
|
|
582
|
+
border-color: var(--sa-primary, #3f6bff);
|
|
583
|
+
background: var(--sa-primary-soft, rgba(63, 107, 255, 0.08));
|
|
584
|
+
color: var(--sa-primary, #3f6bff);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.pc-plan-opt__mark {
|
|
588
|
+
width: 8px;
|
|
589
|
+
height: 8px;
|
|
590
|
+
border-radius: 50%;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.pc-dur {
|
|
594
|
+
display: flex;
|
|
595
|
+
gap: 4px;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.pc-dur-opt {
|
|
599
|
+
flex: 1;
|
|
600
|
+
border: 1px solid var(--sa-border, #e2e8f0);
|
|
601
|
+
background: #fff;
|
|
602
|
+
border-radius: 7px;
|
|
603
|
+
padding: 6px 10px;
|
|
604
|
+
font: 500 12px var(--sa-font-body, system-ui, sans-serif);
|
|
605
|
+
cursor: pointer;
|
|
606
|
+
color: var(--sa-muted-dark, #475569);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.pc-dur-opt--active {
|
|
610
|
+
border-color: var(--sa-primary, #3f6bff);
|
|
611
|
+
background: var(--sa-primary-soft, rgba(63, 107, 255, 0.08));
|
|
612
|
+
color: var(--sa-primary, #3f6bff);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.pc-check {
|
|
616
|
+
display: inline-flex;
|
|
617
|
+
align-items: center;
|
|
618
|
+
gap: 8px;
|
|
619
|
+
font: 13px var(--sa-font-body, system-ui, sans-serif);
|
|
620
|
+
color: var(--sa-body, #1e293b);
|
|
621
|
+
cursor: pointer;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.pc-preview {
|
|
625
|
+
background: var(--sa-primary-soft, rgba(63, 107, 255, 0.06));
|
|
626
|
+
border: 1px solid var(--sa-primary-border, rgba(63, 107, 255, 0.18));
|
|
627
|
+
border-radius: 10px;
|
|
628
|
+
padding: 12px 14px;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.pc-preview__eyebrow {
|
|
632
|
+
font-size: 11px;
|
|
633
|
+
font-weight: 700;
|
|
634
|
+
color: var(--sa-primary, #3f6bff);
|
|
635
|
+
text-transform: uppercase;
|
|
636
|
+
letter-spacing: 0.06em;
|
|
637
|
+
margin-bottom: 6px;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.pc-preview__body {
|
|
641
|
+
display: flex;
|
|
642
|
+
align-items: center;
|
|
643
|
+
gap: 10px;
|
|
644
|
+
flex-wrap: wrap;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.pc-preview__code {
|
|
648
|
+
background: #fff;
|
|
649
|
+
border: 1px solid var(--sa-border, #e2e8f0);
|
|
650
|
+
border-radius: 6px;
|
|
651
|
+
padding: 3px 8px;
|
|
652
|
+
font: 600 13px var(--sa-font-mono, ui-monospace, monospace);
|
|
653
|
+
letter-spacing: 0.04em;
|
|
654
|
+
color: var(--sa-heading, #0f172a);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.pc-preview__disc {
|
|
658
|
+
font: 700 13px var(--sa-font-body, system-ui, sans-serif);
|
|
659
|
+
color: var(--sa-positive, #047857);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.pc-preview__meta {
|
|
663
|
+
font-size: 12px;
|
|
664
|
+
color: var(--sa-muted, #64748b);
|
|
665
|
+
}
|
|
666
|
+
</style>
|