@acms-subir/core 0.3.51 → 0.3.53
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/CHANGELOG.md +33 -0
- package/cli/migrations.generated.mjs +16 -1
- package/docs/calendar.md +24 -10
- package/docs/mobile.md +1 -1
- package/integration.mjs +5 -0
- package/mobile-template/recipes/events/files/components/RsvpPanel.tsx +2 -2
- package/mobile-template/recipes/rsvp/files/components/RsvpPanel.tsx +2 -2
- package/package.json +1 -1
- package/src/components/ThemeSections.astro +2 -2
- package/src/components/display/ScreensManager.tsx +481 -0
- package/src/components/editor/PostEditor.tsx +2 -0
- package/src/components/editor/event/EventSheet.tsx +6 -2
- package/src/components/editor/event/RegistrationCard.tsx +3 -3
- package/src/components/editor/event/RegistrationFormEditor.tsx +6 -3
- package/src/components/forms/Canvas.tsx +31 -19
- package/src/components/forms/FormBuildPage.tsx +12 -6
- package/src/components/forms/FormBuilder.tsx +60 -11
- package/src/components/forms/Inspector.tsx +116 -8
- package/src/components/forms/TypeIcon.tsx +1 -0
- package/src/components/forms/meta.ts +71 -9
- package/src/layouts/Admin.astro +2 -0
- package/src/lib/backup/content.ts +10 -1
- package/src/lib/calendar/api.ts +3 -1
- package/src/lib/display/board.ts +4 -3
- package/src/lib/display/screens.ts +111 -0
- package/src/lib/display/store.ts +113 -0
- package/src/lib/display/words.ts +16 -6
- package/src/lib/dynamicBlocks.ts +4 -2
- package/src/lib/forms/fields.ts +14 -9
- package/src/lib/forms/localize.ts +68 -0
- package/src/lib/forms/render.ts +8 -2
- package/src/lib/forms/schema.ts +31 -2
- package/src/lib/forms/service.ts +9 -1
- package/src/lib/forms/validate.ts +16 -4
- package/src/lib/forms/words.ts +32 -0
- package/src/lib/mobile/forms.ts +45 -13
- package/src/lib/mobile/openapi.ts +3 -3
- package/src/lib/mobile/rsvp.ts +4 -2
- package/src/lib/mobile/schemas.ts +3 -1
- package/src/lib/repo/migrations.ts +3 -0
- package/src/lib/repo/sql.ts +5 -0
- package/src/lib/repo/types.ts +5 -0
- package/src/pages/[...slug].astro +1 -1
- package/src/pages/admin/calendar/screens/index.astro +24 -0
- package/src/pages/admin/calendar/screens/preview.astro +36 -0
- package/src/pages/admin/forms/[id].astro +6 -0
- package/src/pages/api/calendar/screens.ts +30 -0
- package/src/pages/blog/[slug].astro +1 -1
- package/src/pages/display/s/[key].astro +48 -0
- package/src/pages/events/[slug].astro +9 -3
- package/src/pages/forms/[id].astro +22 -4
- package/src/pages/index.astro +1 -1
- package/src/styles/display.css +2 -1
- package/src/styles/forms.css +8 -0
- package/src/styles/screens-admin.css +53 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { getRepo } from '../../../lib/repo';
|
|
3
|
+
import { ApiError, apiError, json, readJson } from '../../../lib/api';
|
|
4
|
+
import { requirePermission } from '../../../lib/permissions/access';
|
|
5
|
+
import { CALENDAR } from '../../../lib/calendar/config';
|
|
6
|
+
import { siteOrigin } from '../../../lib/seo';
|
|
7
|
+
import { ScreenAction, screenAction, screensState } from '../../../lib/display/screens';
|
|
8
|
+
|
|
9
|
+
// Admin → Calendar → Screens: saved TV screens, for people who manage rooms
|
|
10
|
+
// and calendars (a screen's link can show private calendars).
|
|
11
|
+
export const GET: APIRoute = async ({ request, cookies, locals, url }) => {
|
|
12
|
+
try {
|
|
13
|
+
if (!CALENDAR.enabled) throw new ApiError(404, 'The calendar is off on this site.');
|
|
14
|
+
const repo = await getRepo(locals);
|
|
15
|
+
const access = await requirePermission(request, cookies, repo, 'schedule.manage');
|
|
16
|
+
return json(await screensState(repo, siteOrigin(await repo.getSettings(), url), { bookings: access.can('schedule.bookings') }));
|
|
17
|
+
} catch (e) { return apiError(e); }
|
|
18
|
+
};
|
|
19
|
+
export const POST: APIRoute = async ({ request, cookies, locals, url }) => {
|
|
20
|
+
try {
|
|
21
|
+
if (!CALENDAR.enabled) throw new ApiError(404, 'The calendar is off on this site.');
|
|
22
|
+
const repo = await getRepo(locals);
|
|
23
|
+
const access = await requirePermission(request, cookies, repo, 'schedule.manage');
|
|
24
|
+
// a screen's key unlocks private calendars: only from this site's admin
|
|
25
|
+
if (request.headers.get('origin') !== url.origin) throw new ApiError(403, 'Change this from this site’s admin.');
|
|
26
|
+
const bookings = access.can('schedule.bookings');
|
|
27
|
+
const result = await screenAction(repo, ScreenAction.parse(await readJson(request, 20_000)), { bookings });
|
|
28
|
+
return json({ ...result, state: await screensState(repo, siteOrigin(await repo.getSettings(), url), { bookings }) });
|
|
29
|
+
} catch (e) { return apiError(e); }
|
|
30
|
+
};
|
|
@@ -63,7 +63,7 @@ const po = design.post;
|
|
|
63
63
|
const PostTemplate = (post.template ? design.theme.postTemplates[post.template] : null) as any;
|
|
64
64
|
const tagSlug = (name: string) =>
|
|
65
65
|
tags.find((t) => t.name.toLowerCase() === name.toLowerCase())?.slug ?? slugify(name);
|
|
66
|
-
const html = highlightCodeHtml(await expandDynamicBlocks(post.bodyHtml || renderMarkdown(post.body), repo));
|
|
66
|
+
const html = highlightCodeHtml(await expandDynamicBlocks(post.bodyHtml || renderMarkdown(post.body), repo, locale));
|
|
67
67
|
const fmt = (ts: number | null) => formatDate(ts, settings);
|
|
68
68
|
const words = (post.bodyHtml || post.body).replace(/<[^>]+>/g, ' ').split(/\s+/).filter(Boolean).length;
|
|
69
69
|
const readMins = Math.max(1, Math.round(words / 220));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
import DisplayPage from '../../../components/display/DisplayPage.astro';
|
|
3
|
+
import { getRepo } from '../../../lib/repo';
|
|
4
|
+
import { CALENDAR } from '../../../lib/calendar/config';
|
|
5
|
+
import { I18N, localePath, requestLocale } from '../../../lib/i18n';
|
|
6
|
+
import { siteOrigin } from '../../../lib/seo';
|
|
7
|
+
import { siteLang } from '../../../lib/dates';
|
|
8
|
+
import { defaultDisplaySettings, readDisplaySettings } from '../../../lib/display/settings';
|
|
9
|
+
import { loadDisplayBoard } from '../../../lib/display/board';
|
|
10
|
+
import { displayHeaders } from '../../../lib/display/http';
|
|
11
|
+
import { MISS_WINDOW, openScreen } from '../../../lib/display/screens';
|
|
12
|
+
|
|
13
|
+
// A saved screen (Admin → Calendar → Screens): its long random key is the
|
|
14
|
+
// whole address. With "Show private calendars and room bookings" on, it shows
|
|
15
|
+
// the private calendars it names and its rooms' bookings, with titles, rooms,
|
|
16
|
+
// hosts and the name on a booking; never emails, phone numbers or notes.
|
|
17
|
+
// The board's 5-minute refresh fetches this same address, so a rotated key
|
|
18
|
+
// stops the TV at once and every fetch counts as the screen's check-in.
|
|
19
|
+
if (!CALENDAR.enabled) return new Response(null, { status: 404 });
|
|
20
|
+
const repo = await getRepo(Astro.locals);
|
|
21
|
+
let address = 'unknown';
|
|
22
|
+
try { address = Astro.clientAddress || address; } catch { /* adapters without a client address share a bucket */ }
|
|
23
|
+
const opened = await openScreen(repo, Astro.params.key ?? '', address);
|
|
24
|
+
if (opened.status === 'limited') {
|
|
25
|
+
return new Response('Too many wrong screen links from this network. Try again in a few minutes.', {
|
|
26
|
+
status: 429,
|
|
27
|
+
headers: { 'Retry-After': String(MISS_WINDOW / 1000), 'Cache-Control': 'private, no-store', 'Referrer-Policy': 'no-referrer', 'Content-Type': 'text/plain; charset=utf-8' },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const site = await repo.getSettings();
|
|
31
|
+
// the address's language prefix (/es/display/s/…) picks the worker's scope
|
|
32
|
+
const pathLang = I18N.enabled ? requestLocale(Astro.url, Astro.locals) : '';
|
|
33
|
+
const prefix = I18N.enabled && pathLang && pathLang !== I18N.defaultLocale ? `/${pathLang}` : '';
|
|
34
|
+
const screen = opened.status === 'ok' ? opened.screen : null;
|
|
35
|
+
const settings = screen ? readDisplaySettings(new URLSearchParams(screen.params)) : defaultDisplaySettings();
|
|
36
|
+
const lang = screen?.locale || (prefix ? pathLang : '') || siteLang(site);
|
|
37
|
+
const board = screen
|
|
38
|
+
? await loadDisplayBoard(repo, site, {
|
|
39
|
+
settings, privateDetails: screen.privateDetails, lang,
|
|
40
|
+
phoneLink: siteOrigin(site, Astro.url) + localePath(CALENDAR.base, lang),
|
|
41
|
+
})
|
|
42
|
+
: null;
|
|
43
|
+
if (screen) await repo.screens.checkIn(screen.id);
|
|
44
|
+
else Astro.response.status = 404;
|
|
45
|
+
displayHeaders(Astro.response.headers, { shared: false });
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
<DisplayPage site={site} board={board} settings={settings} title={screen?.name || site.siteTitle} lang={lang} live sw={`${prefix}/display/sw.js`} />
|
|
@@ -3,6 +3,8 @@ import { ApiError } from '../../lib/api';
|
|
|
3
3
|
import { starter } from '../../lib/forms/schema';
|
|
4
4
|
import { formVersion } from '../../lib/forms/service';
|
|
5
5
|
import { renderFields, hiddenFields } from '../../lib/forms/render';
|
|
6
|
+
import { localizeForm } from '../../lib/forms/localize';
|
|
7
|
+
import { formStrings } from '../../lib/forms/words';
|
|
6
8
|
import { readForm, readAnswers, submissionGuard } from '../../lib/forms/http';
|
|
7
9
|
import { submitEventForm, prepareLegacyEventForm } from '../../lib/forms/events';
|
|
8
10
|
import { I18N, localePath, requestLocale } from '../../lib/i18n';
|
|
@@ -74,22 +76,26 @@ if (Astro.request.method === 'POST' && event.rsvp.enabled) {
|
|
|
74
76
|
}
|
|
75
77
|
}
|
|
76
78
|
const formHtml = hiddenFields(await formVersion(definition), requestId) + `<input type="hidden" name="occurrence" value="${occurrence.start}">`;
|
|
79
|
+
// the questions in the page's language (main-language words where the form has none)
|
|
80
|
+
const fieldsHtml = renderFields(localizeForm(definition, isExtraLocale(locale) ? locale : ''), formValues, 'event-registration', formStrings((I18N.enabled ? locale : '') || siteLang(settings)));
|
|
77
81
|
const taken = event.rsvp.enabled ? await seatsTaken(repo, event.id, occurrence.start) : 0;
|
|
78
82
|
// the event's RSVP deadline for this date (null: open until it ends)
|
|
79
83
|
const closesAt = event.rsvp.enabled ? rsvpDeadline(occurrence, settings) : null;
|
|
80
84
|
// waitlist: a full date offers "Join the waitlist" (the same form, posting waitlist=1)
|
|
81
85
|
const rsvp = event.rsvp.enabled
|
|
82
|
-
? { going: taken, left: event.rsvp.capacity ? Math.max(0, event.rsvp.capacity - taken) : null, maxSeats: event.rsvp.maxSeats, error: rsvpError, duplicate: rsvpDuplicate, formHtml, fieldsHtml
|
|
86
|
+
? { going: taken, left: event.rsvp.capacity ? Math.max(0, event.rsvp.capacity - taken) : null, maxSeats: event.rsvp.maxSeats, error: rsvpError, duplicate: rsvpDuplicate, formHtml, fieldsHtml, values: coreValues,
|
|
83
87
|
open: occurrence.end > now && !occurrence.cancelled && !(closesAt !== null && closesAt <= now), closesAt, waitlist: waitlistOn(event) }
|
|
84
88
|
: null;
|
|
85
89
|
const calendars = event.calendarIds.map((id) => calMap.get(id)).filter((c): c is NonNullable<typeof c> => Boolean(c) && c!.visibility !== 'private');
|
|
86
|
-
const html = highlightCodeHtml(await expandDynamicBlocks(event.bodyHtml, repo));
|
|
90
|
+
const html = highlightCodeHtml(await expandDynamicBlocks(event.bodyHtml, repo, locale));
|
|
87
91
|
const upcoming = (await upcomingEvents(repo, settings, now, { limit: 4 })).filter((u) => u.event.id !== event.id).slice(0, 3);
|
|
88
92
|
const pageUrl = origin + eventUrl(event.slug);
|
|
89
93
|
const jsonLd = eventJsonLd(event, occurrence, settings, origin, pageUrl);
|
|
90
94
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
95
|
const Template = resolveDesign(settings).theme.EventTemplate as any;
|
|
92
|
-
|
|
96
|
+
// templates read the form's title, introduction and button from the event: in the page's language too
|
|
97
|
+
const shownEvent = event.rsvp.form && isExtraLocale(locale) ? { ...event, rsvp: { ...event.rsvp, form: localizeForm(event.rsvp.form, locale) } } : event;
|
|
98
|
+
const props = { event: shownEvent, occurrence, settings, calendars, html, upcoming, origin, preview: event.status !== 'published', rsvp };
|
|
93
99
|
---
|
|
94
100
|
|
|
95
101
|
<Site
|
|
@@ -5,12 +5,24 @@ import { renderForm } from '../../lib/forms/render';
|
|
|
5
5
|
import { collectResponse } from '../../lib/forms/service';
|
|
6
6
|
import { readForm, readAnswers, submissionGuard } from '../../lib/forms/http';
|
|
7
7
|
import { saveResponse } from '../../lib/forms/alerts';
|
|
8
|
+
import { localizeForm } from '../../lib/forms/localize';
|
|
9
|
+
import { formStrings, thanksText } from '../../lib/forms/words';
|
|
8
10
|
import { ApiError } from '../../lib/api';
|
|
11
|
+
import { I18N, localePath, requestLocale } from '../../lib/i18n';
|
|
12
|
+
import { isExtraLocale } from '../../lib/translations';
|
|
13
|
+
import { siteLang } from '../../lib/dates';
|
|
9
14
|
const repo = await getRepo(Astro.locals);
|
|
10
15
|
const form = await repo.forms.get(Astro.params.id || '');
|
|
11
16
|
if (!form || form.status !== 'published' || form.kind !== 'form' || form.purpose !== 'general') return new Response(null, { status: 404 });
|
|
12
17
|
Astro.response.headers.set('Cache-Control', 'private, no-store');
|
|
13
18
|
const embed = Astro.url.searchParams.has('embed');
|
|
19
|
+
// /es/forms/<id>: the form's Spanish words (main-language ones where it has
|
|
20
|
+
// none) and the page's own words in Spanish; answers are stored the same
|
|
21
|
+
const locale = requestLocale(Astro.url, Astro.locals);
|
|
22
|
+
const formLocale = isExtraLocale(locale) ? locale : '';
|
|
23
|
+
const lang = (I18N.enabled ? locale : '') || siteLang(await repo.getSettings());
|
|
24
|
+
const shown = localizeForm(form.definition, formLocale);
|
|
25
|
+
const path = localePath(`/forms/${encodeURIComponent(form.id)}`, locale);
|
|
14
26
|
let error = '', success = false, values: Record<string,string> = {};
|
|
15
27
|
let requestId: string = crypto.randomUUID();
|
|
16
28
|
if (Astro.request.method === 'POST') {
|
|
@@ -18,11 +30,17 @@ if (Astro.request.method === 'POST') {
|
|
|
18
30
|
const data = await readForm(Astro.request);
|
|
19
31
|
requestId = await submissionGuard(repo, data, Astro.clientAddress, `form:${form.id}`);
|
|
20
32
|
values = readAnswers(data, form.definition);
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
// a resubmit of a saved response (refresh, double click) thanks them again,
|
|
34
|
+
// also after the form changed since; the same id with other answers is a 409
|
|
35
|
+
const prior = await repo.forms.receipt(form.id, requestId);
|
|
36
|
+
if (!prior || JSON.stringify((JSON.parse(prior) as { definition: unknown }).definition) === JSON.stringify(form.definition)) {
|
|
37
|
+
const response = await collectResponse(form.definition, values, prior ? undefined : String(data.get('formVersion') || ''));
|
|
38
|
+
await saveResponse(repo, form, response, requestId, { via: 'web', locals: Astro.locals });
|
|
39
|
+
}
|
|
23
40
|
success = true;
|
|
24
41
|
} catch (e) { error = e instanceof ApiError ? e.message : 'Could not save your response. Please try again.'; Astro.response.status = e instanceof ApiError ? e.status : 500; }
|
|
25
42
|
}
|
|
26
|
-
const
|
|
43
|
+
const w = formStrings(lang);
|
|
44
|
+
const html = success ? '' : await renderForm(form.id, form.definition, values, error, requestId, embed, { locale: formLocale, lang, action: path });
|
|
27
45
|
---
|
|
28
|
-
<Site title={
|
|
46
|
+
<Site title={shown.title || form.name} noindex bare={embed}>{success ? <section class="pc-simple-form"><h1>{shown.title || form.name}</h1><p role="status">{thanksText(shown.confirmation, w)}</p><a href={path + (embed ? '?embed=1' : '')}>{w.sendAnother}</a></section> : <div set:html={html}/>}</Site>
|
package/src/pages/index.astro
CHANGED
|
@@ -46,7 +46,7 @@ const [posts, tags] = HomeTemplate
|
|
|
46
46
|
: [[], []];
|
|
47
47
|
// expand dynamic blocks (post lists) in the page body for BOTH paths — a theme
|
|
48
48
|
// template injects the body as its hero, so it must receive expanded HTML too
|
|
49
|
-
const homeHtml = home?.bodyHtml ? await expandDynamicBlocks(home.bodyHtml, repo) : '';
|
|
49
|
+
const homeHtml = home?.bodyHtml ? await expandDynamicBlocks(home.bodyHtml, repo, locale) : '';
|
|
50
50
|
const homeForTemplate = home ? { ...home, bodyHtml: homeHtml } : null;
|
|
51
51
|
---
|
|
52
52
|
|
package/src/styles/display.css
CHANGED
|
@@ -161,7 +161,8 @@ body { background: #000; }
|
|
|
161
161
|
/* list: columns, then pages (the script turns them) */
|
|
162
162
|
.display-list { height: 100%; overflow: hidden; }
|
|
163
163
|
.display-list-cols { height: 100%; column-count: var(--dsp-list-cols, 3); column-gap: calc(3.5 * var(--dsp-u)); column-fill: auto; }
|
|
164
|
-
|
|
164
|
+
/* a day stays in one column when it fits, so a column never starts without its day */
|
|
165
|
+
.display-day { margin: 0 0 calc(2.2 * var(--dsp-u)); break-inside: avoid-column; }
|
|
165
166
|
.display-day-title { margin: 0 0 calc(0.6 * var(--dsp-u)); font-size: calc(2.3 * var(--dsp-u)); font-weight: 700; color: var(--dsp-muted); break-after: avoid; }
|
|
166
167
|
.display-day.is-today .display-day-title { color: var(--dsp-fg); }
|
|
167
168
|
.display-row {
|
package/src/styles/forms.css
CHANGED
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
.fb.is-preview .fb-canvas { border: 0; }
|
|
82
82
|
.fb-wide .fb-canvas:has(> .fb-float) { padding-top: 48px; }
|
|
83
83
|
.fb-float { position: absolute; top: 8px; right: 10px; z-index: 3; display: flex; gap: 2px; padding: 2px; border-radius: 10px; background: color-mix(in srgb, var(--panel-2) 85%, transparent); }
|
|
84
|
+
.fb-float-l { right: auto; left: 10px; }
|
|
84
85
|
.fb-paper {
|
|
85
86
|
/* the public form's tokens, mapped onto the admin palette */
|
|
86
87
|
--ink: var(--text); --line-2: var(--line-strong); --btn: var(--text); --btn-ink: var(--bg); --accent-text: var(--accent-deep);
|
|
@@ -195,6 +196,13 @@
|
|
|
195
196
|
.fb-choice.dragging input { box-shadow: var(--shadow-pop); }
|
|
196
197
|
.fb-choice input { min-height: 34px; }
|
|
197
198
|
.fb-choice-add { margin: 2px 0 0; font-size: 12px; color: var(--muted); }
|
|
199
|
+
/* another language: the main language's words above each box, choices side by side */
|
|
200
|
+
.fb-src { display: block; padding: 6px 9px; border-radius: var(--r-sm); background: var(--panel-2); color: var(--text-2); font-size: 12.5px; line-height: 1.45; white-space: pre-line; overflow-wrap: anywhere; max-height: 7.5em; overflow: auto; }
|
|
201
|
+
.fb-src > span { display: block; font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); margin-bottom: 1px; }
|
|
202
|
+
.fb-tr-choices { display: grid; gap: 6px; }
|
|
203
|
+
.fb-tr-choice { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); gap: 8px; align-items: center; }
|
|
204
|
+
.fb-tr-choice > span { font-size: 12.5px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
205
|
+
.fb-tr-choice input { min-height: 34px; }
|
|
198
206
|
|
|
199
207
|
/* menus */
|
|
200
208
|
.fb-more, .fb-q-more { position: relative; flex: none; }
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* Admin → Calendar → Screens (components/display/ScreensManager.tsx) */
|
|
2
|
+
.sc { display: grid; grid-template-columns: minmax(240px, 300px) minmax(0, 1fr); gap: 16px; align-items: start; }
|
|
3
|
+
.sc .card + .card { margin-top: 0; }
|
|
4
|
+
@media (max-width: 1100px) { .sc { grid-template-columns: minmax(0, 1fr); } }
|
|
5
|
+
.sc-skel { color: var(--muted); }
|
|
6
|
+
.sc-list { overflow: hidden; }
|
|
7
|
+
.sc-list-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
|
|
8
|
+
.sc-list-actions { display: flex; gap: 6px; }
|
|
9
|
+
.sc-empty { padding: 26px 16px; }
|
|
10
|
+
.sc-rows { list-style: none; margin: 0; padding: 0; }
|
|
11
|
+
.sc-rows li + li { border-top: 1px solid var(--line); }
|
|
12
|
+
.sc-row { display: flex; flex-direction: column; gap: 3px; width: 100%; text-align: left; padding: 11px 14px; background: none; border: 0; font: inherit; color: var(--text); cursor: pointer; }
|
|
13
|
+
.sc-row:hover { background: var(--hover); }
|
|
14
|
+
.sc-row.on { background: var(--sel); }
|
|
15
|
+
.sc-row-name { font-weight: 600; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
16
|
+
.sc-row-sum { font-size: 12px; color: var(--muted); }
|
|
17
|
+
.sc-seen { font-size: 12px; color: var(--faint); }
|
|
18
|
+
.sc-seen.ok { color: var(--green); }
|
|
19
|
+
.sc-seen.stale { color: var(--amber); }
|
|
20
|
+
.pill.sc-private { background: var(--amber-soft); color: var(--amber); }
|
|
21
|
+
.sc-intro h2, .sc-h { font-size: 16px; margin: 0 0 8px; }
|
|
22
|
+
.sc-intro p { color: var(--text-2); font-size: 13.5px; line-height: 1.55; margin: 0 0 10px; }
|
|
23
|
+
.sc-steps { margin: 0 0 12px; padding-left: 18px; color: var(--text-2); font-size: 13.5px; line-height: 1.55; }
|
|
24
|
+
.sc-editor { padding: 16px 18px; }
|
|
25
|
+
.sc-editor-grid { display: grid; grid-template-columns: minmax(280px, 1fr) minmax(320px, 1.25fr); gap: 20px; align-items: start; }
|
|
26
|
+
@media (max-width: 1380px) { .sc-editor-grid { grid-template-columns: minmax(0, 1fr); } .sc-side { order: -1; position: static; } }
|
|
27
|
+
.sc-side { position: sticky; top: 16px; display: flex; flex-direction: column; gap: 12px; }
|
|
28
|
+
.sc-fs { border: 0; padding: 0; margin: 0 0 16px; min-width: 0; }
|
|
29
|
+
.sc-fs legend { font-size: 12px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); font-weight: 600; padding: 0; margin-bottom: 8px; }
|
|
30
|
+
.sc-fs > .hint { margin: 0 0 6px; }
|
|
31
|
+
.seg.sc-seg { flex-wrap: wrap; }
|
|
32
|
+
.seg.sc-seg > button { border: 0; background: none; font: inherit; font-size: 12.5px; font-weight: 600; padding: 5px 10px; border-radius: 7px; color: var(--muted); cursor: pointer; }
|
|
33
|
+
.sc-look { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
34
|
+
.sc-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0 10px; }
|
|
35
|
+
.sc-chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border: 1px solid var(--line-strong); border-radius: var(--r-full); font-size: 12.5px; cursor: pointer; }
|
|
36
|
+
.sc-chip input { accent-color: var(--accent); margin: 0; }
|
|
37
|
+
.sc-checks { max-height: 200px; overflow: auto; }
|
|
38
|
+
.sc-checks .check-row { margin: 4px 0; align-items: center; }
|
|
39
|
+
.sc-off { opacity: 0.6; }
|
|
40
|
+
.sc-swatch { display: inline-block; width: 11px; height: 11px; border-radius: 3px; flex: none; }
|
|
41
|
+
.sc-order { display: inline-grid; place-items: center; min-width: 18px; height: 18px; border-radius: 9px; background: var(--accent-soft); color: var(--accent-deep); font-size: 11px; font-weight: 700; }
|
|
42
|
+
.sc-tags-label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 5px; font-weight: 500; }
|
|
43
|
+
.sc-tag-list { border: 1px solid var(--line); border-radius: var(--r-md); padding: 4px 10px; max-height: 150px; }
|
|
44
|
+
.sc-inline { display: flex; gap: 6px; }
|
|
45
|
+
.sc-private { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-md); padding: 10px 12px 6px; }
|
|
46
|
+
.sc-private legend { padding-top: 2px; }
|
|
47
|
+
.sc-private .hint { margin: 0 0 6px 23px; }
|
|
48
|
+
.sc-preview { position: relative; width: 100%; aspect-ratio: 16 / 9; overflow: hidden; border-radius: var(--r-md); background: #000; border: 1px solid var(--line); }
|
|
49
|
+
.sc-preview iframe { position: absolute; top: 0; left: 0; border: 0; transform-origin: 0 0; pointer-events: none; }
|
|
50
|
+
.sc-link .field { margin-bottom: 6px; }
|
|
51
|
+
.sc-link input[readonly] { font-family: ui-monospace, 'SF Mono', monospace; font-size: 12px; }
|
|
52
|
+
.sc-link .form-actions { margin-top: 6px; }
|
|
53
|
+
.sc-save { margin-top: 0; padding-top: 12px; border-top: 1px solid var(--line); }
|