@dumiflex/wp-kit 0.6.0 → 0.7.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.
@@ -0,0 +1,154 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Rich, read-only preview of a published module or bundle payload: the
4
+ * community post page and its embed draw it, and the manager can too.
5
+ *
6
+ * <PayloadPreview :payload="row" :ref-names="names" :ref-groups="groups" />
7
+ *
8
+ * This file only picks the per-type preview (WildcardPreview,
9
+ * FixedValuesPreview, CombinePreview, DerivationPreview, ConstraintPreview,
10
+ * BundlePreview; each one
11
+ * has a stats strip, its own search / paging for big payloads, and routes
12
+ * every value through `TokenizedValue` so `$var` / `@{ref}` / picks render
13
+ * as the chips the editor shows) and owns the Preview / Raw JSON tabs.
14
+ *
15
+ * The community renders the PUBLISHED payload, which has NO `instance`
16
+ * layer — so only LIBRARY exceptions exist (`payload.exceptions`); the
17
+ * instance-only `extra_exceptions` is ignored even if one rides along.
18
+ */
19
+ import { computed, ref } from "vue";
20
+ import WildcardPreview from "./WildcardPreview.vue";
21
+ import FixedValuesPreview from "./FixedValuesPreview.vue";
22
+ import CombinePreview from "./CombinePreview.vue";
23
+ import DerivationPreview from "./DerivationPreview.vue";
24
+ import ConstraintPreview from "./ConstraintPreview.vue";
25
+ import BundlePreview from "./BundlePreview.vue";
26
+ import { innerOf, payloadKind } from "./preview-readers";
27
+ import WpTabs, { type TabItem } from "./WpTabs.vue";
28
+ import CodeBlock from "./CodeBlock.vue";
29
+
30
+ const PREVIEW_TABS: TabItem<"preview" | "raw">[] = [
31
+ { id: "preview", label: "Preview" },
32
+ { id: "raw", label: "Raw JSON" },
33
+ ];
34
+
35
+ defineOptions({ name: "PayloadPreview" });
36
+
37
+ const props = withDefaults(
38
+ defineProps<{
39
+ payload: Record<string, unknown> | null;
40
+ /**
41
+ * `module_id → display-name` map, built by the parent from the post's
42
+ * dependencies. Resolves constraint axis uuids AND any `@{uuid}` ref in
43
+ * a value string to readable names — retroactively, no re-publish.
44
+ */
45
+ refNames?: Record<string, string>;
46
+ /**
47
+ * `module_id → tag_groups` map from the post's dependencies, so a
48
+ * constraint can group its matrix by the wildcards' axes.
49
+ */
50
+ refGroups?: Record<string, Record<string, string[]>>;
51
+ /** Show the Preview / Raw JSON tabs. The post page draws its own. */
52
+ tabs?: boolean;
53
+ /** Rendered inside a bundle's child pane (no tabs). */
54
+ nested?: boolean;
55
+ }>(),
56
+ { refNames: () => ({}), refGroups: () => ({}), tabs: true, nested: false },
57
+ );
58
+
59
+ const tab = ref<"preview" | "raw">("preview");
60
+ const showTabs = computed(() => props.tabs && !props.nested);
61
+
62
+ const kind = computed(() => payloadKind(props.payload));
63
+ const inner = computed(() => innerOf(props.payload));
64
+ // withDefaults fills these, but the optional prop type still reads
65
+ // `| undefined` under exactOptionalPropertyTypes; pin concrete values.
66
+ const names = computed<Record<string, string>>(() => props.refNames ?? {});
67
+ const groups = computed<Record<string, Record<string, string[]>>>(() => props.refGroups ?? {});
68
+ const refMap = computed<ReadonlyMap<string, string>>(
69
+ () => new Map(Object.entries(props.refNames)),
70
+ );
71
+ const rawJson = computed(() => (props.payload ? JSON.stringify(props.payload, null, 2) : ""));
72
+ const KNOWN = new Set(["wildcard", "fixed_values", "combine", "derivation", "constraint", "bundle"]);
73
+ </script>
74
+
75
+ <template>
76
+ <div
77
+ v-if="payload"
78
+ class="wp-payload-preview"
79
+ >
80
+ <WpTabs
81
+ v-if="showTabs"
82
+ v-model="tab"
83
+ :tabs="PREVIEW_TABS"
84
+ aria-label="Preview or raw JSON"
85
+ />
86
+
87
+ <div v-if="tab === 'preview' || !showTabs">
88
+ <WildcardPreview
89
+ v-if="kind === 'wildcard'"
90
+ :inner="inner"
91
+ :ref-map="refMap"
92
+ />
93
+ <FixedValuesPreview
94
+ v-else-if="kind === 'fixed_values'"
95
+ :inner="inner"
96
+ :ref-map="refMap"
97
+ />
98
+ <CombinePreview
99
+ v-else-if="kind === 'combine'"
100
+ :inner="inner"
101
+ :ref-map="refMap"
102
+ />
103
+ <DerivationPreview
104
+ v-else-if="kind === 'derivation'"
105
+ :payload="payload"
106
+ :inner="inner"
107
+ :ref-map="refMap"
108
+ />
109
+ <ConstraintPreview
110
+ v-else-if="kind === 'constraint'"
111
+ :inner="inner"
112
+ :ref-map="refMap"
113
+ :ref-names="names"
114
+ :ref-groups="groups"
115
+ />
116
+ <BundlePreview
117
+ v-else-if="kind === 'bundle'"
118
+ :payload="payload"
119
+ :ref-names="names"
120
+ :ref-groups="groups"
121
+ />
122
+ <p
123
+ v-if="!kind || !KNOWN.has(kind)"
124
+ class="wp-pv-empty"
125
+ >
126
+ Nothing to render in <code>{{ kind ?? "this payload" }}</code>. Switch to Raw JSON.
127
+ </p>
128
+ </div>
129
+ <div
130
+ v-else
131
+ class="wp-payload-raw"
132
+ >
133
+ <CodeBlock
134
+ :code="rawJson"
135
+ language="JSON"
136
+ default-wrap
137
+ />
138
+ </div>
139
+ </div>
140
+ <p
141
+ v-else
142
+ class="wp-pv-empty"
143
+ style="padding: 24px"
144
+ >
145
+ No payload to preview.
146
+ </p>
147
+ </template>
148
+
149
+ <style scoped src="./preview.css"></style>
150
+ <style scoped>
151
+ .wp-payload-raw {
152
+ padding: 18px 24px 22px;
153
+ }
154
+ </style>
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * The shape of a post page while it loads: meta line, title, description,
4
+ * tag chips and the preview card on the left, the download button and its
5
+ * details in the rail (the rail drops under the main column on narrow
6
+ * screens). Shared by the site's post page and the embed's.
7
+ *
8
+ * <PostDetailSkeleton v-if="loading" />
9
+ */
10
+ import Skeleton from "./Skeleton.vue";
11
+
12
+ withDefaults(defineProps<{
13
+ /** What screen readers hear. */
14
+ label?: string | undefined;
15
+ }>(), { label: "Loading post" });
16
+ </script>
17
+
18
+ <template>
19
+ <div class="wp-post-detail-skeleton" data-testid="post-detail-skeleton">
20
+ <div class="wp-post-detail-skeleton__main">
21
+ <div class="wp-post-detail-skeleton__head">
22
+ <Skeleton width="30%" :label="label || undefined" />
23
+ <Skeleton shape="block" width="55%" height="32px" />
24
+ <Skeleton :lines="2" />
25
+ <Skeleton shape="chip" :lines="4" />
26
+ </div>
27
+ <Skeleton shape="block" height="320px" />
28
+ </div>
29
+ <aside class="wp-post-detail-skeleton__aside">
30
+ <Skeleton shape="block" height="40px" />
31
+ <Skeleton :lines="4" />
32
+ </aside>
33
+ </div>
34
+ </template>
35
+
36
+ <style scoped>
37
+ .wp-post-detail-skeleton {
38
+ display: grid;
39
+ grid-template-columns: minmax(0, 1fr) 320px;
40
+ gap: 32px;
41
+ align-items: start;
42
+ container-type: inline-size;
43
+ }
44
+ .wp-post-detail-skeleton__main {
45
+ min-width: 0;
46
+ display: flex;
47
+ flex-direction: column;
48
+ gap: 24px;
49
+ }
50
+ .wp-post-detail-skeleton__head {
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: 18px;
54
+ }
55
+ .wp-post-detail-skeleton__aside {
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: 16px;
59
+ }
60
+ @media (max-width: 900px) {
61
+ .wp-post-detail-skeleton {
62
+ grid-template-columns: minmax(0, 1fr);
63
+ }
64
+ }
65
+ </style>
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * The shape of a post grid while its first page loads: card-sized blocks of
4
+ * mixed heights in the same masonry columns PostCard grids use (340px
5
+ * columns, one column on phones). Used by every post grid (browse, bundles,
6
+ * profiles, libraries, the embed).
7
+ *
8
+ * <PostGridSkeleton v-if="loading" :count="6" />
9
+ *
10
+ * `label` is what screen readers hear once; pass `""` when the grid sits
11
+ * inside another skeleton that already says it (ProfileSkeleton does).
12
+ */
13
+ import Skeleton from "./Skeleton.vue";
14
+
15
+ withDefaults(defineProps<{
16
+ /** How many cards to draw. */
17
+ count?: number | undefined;
18
+ /** What screen readers hear. */
19
+ label?: string | undefined;
20
+ }>(), { count: 6, label: "Loading posts" });
21
+
22
+ const HEIGHTS = ["180px", "240px", "200px", "240px", "180px", "200px"];
23
+ </script>
24
+
25
+ <template>
26
+ <div class="wp-post-grid-skeleton" data-testid="post-grid-skeleton">
27
+ <Skeleton
28
+ v-for="i in count"
29
+ :key="i"
30
+ class="wp-post-grid-skeleton__card"
31
+ shape="block"
32
+ :height="HEIGHTS[(i - 1) % HEIGHTS.length]"
33
+ :label="i === 1 && label ? label : undefined"
34
+ />
35
+ </div>
36
+ </template>
37
+
38
+ <style scoped>
39
+ .wp-post-grid-skeleton {
40
+ column-width: 340px;
41
+ column-gap: 20px;
42
+ }
43
+ .wp-post-grid-skeleton__card {
44
+ margin-bottom: 20px;
45
+ break-inside: avoid;
46
+ }
47
+ @media (max-width: 1200px) {
48
+ .wp-post-grid-skeleton {
49
+ column-width: 320px;
50
+ }
51
+ }
52
+ @media (max-width: 560px) {
53
+ .wp-post-grid-skeleton {
54
+ column-width: auto;
55
+ column-count: 1;
56
+ }
57
+ .wp-post-grid-skeleton__card {
58
+ margin-bottom: 14px;
59
+ }
60
+ }
61
+ </style>
@@ -0,0 +1,75 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * One group of a preview table: a flush kit Disclosure with the group's
4
+ * name, its count and a dim note at the right (a share, a summary). A group
5
+ * with no name (the options outside any group) has no heading and is always
6
+ * open, so the caller writes its body once for both.
7
+ */
8
+ import Disclosure from "./Disclosure.vue";
9
+
10
+ withDefaults(defineProps<{
11
+ name?: string | undefined;
12
+ open: boolean;
13
+ count?: number | string | undefined;
14
+ aside?: string | undefined;
15
+ /** `data-test` on the heading button. */
16
+ testId?: string | undefined;
17
+ }>(), { name: "", count: undefined, aside: undefined, testId: undefined });
18
+
19
+ const emit = defineEmits<{ (e: "update:open", v: boolean): void }>();
20
+ </script>
21
+
22
+ <template>
23
+ <Disclosure
24
+ v-if="name"
25
+ flush
26
+ lazy
27
+ class="wp-pv-group"
28
+ :open="open"
29
+ :trigger-attrs="testId ? { 'data-test': testId } : undefined"
30
+ @update:open="(v: boolean) => emit('update:open', v)"
31
+ >
32
+ <template #summary>
33
+ <span class="wp-pv-group__name">{{ name }}</span>
34
+ <span
35
+ v-if="count !== undefined"
36
+ class="wp-pv-group__count"
37
+ >{{ count }}</span>
38
+ <span
39
+ v-if="aside"
40
+ class="wp-pv-group__aside"
41
+ >{{ aside }}</span>
42
+ </template>
43
+ <slot />
44
+ </Disclosure>
45
+ <div
46
+ v-else
47
+ class="wp-pv-group"
48
+ >
49
+ <slot />
50
+ </div>
51
+ </template>
52
+
53
+ <style scoped>
54
+ .wp-pv-group__name {
55
+ min-width: 0;
56
+ overflow: hidden;
57
+ text-overflow: ellipsis;
58
+ white-space: nowrap;
59
+ }
60
+ .wp-pv-group__count {
61
+ padding: 0 7px;
62
+ border-radius: var(--wp-radius-cap, 999px);
63
+ background: var(--wp-bg-3);
64
+ color: var(--wp-text-dim);
65
+ font-family: var(--wp-font-mono);
66
+ font-size: 11px;
67
+ font-weight: 400;
68
+ }
69
+ .wp-pv-group__aside {
70
+ margin-left: auto;
71
+ color: var(--wp-text-dim);
72
+ font-size: 11.5px;
73
+ font-weight: 400;
74
+ }
75
+ </style>
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * The shape of a public profile while it loads: the head card (avatar,
4
+ * name, meta line), optionally the stat cards, then a post grid
5
+ * (PostGridSkeleton). Shared by the site's profile page and the embed's.
6
+ *
7
+ * <ProfileSkeleton v-if="loading" stats />
8
+ *
9
+ * The root is a column with the page's gaps, so the parts space out like
10
+ * the page they stand in for.
11
+ */
12
+ import Card from "./Card.vue";
13
+ import Skeleton from "./Skeleton.vue";
14
+ import PostGridSkeleton from "./PostGridSkeleton.vue";
15
+
16
+ withDefaults(defineProps<{
17
+ /** Draw the three stat cards under the head (the site's layout). */
18
+ stats?: boolean | undefined;
19
+ /** Avatar size. */
20
+ avatar?: string | undefined;
21
+ /** What screen readers hear. */
22
+ label?: string | undefined;
23
+ }>(), { stats: false, avatar: "64px", label: "Loading profile" });
24
+ </script>
25
+
26
+ <template>
27
+ <div class="wp-profile-skeleton" data-testid="profile-skeleton">
28
+ <Card :padding="false">
29
+ <div class="wp-profile-skeleton__head">
30
+ <Skeleton shape="circle" :width="avatar" :label="label || undefined" />
31
+ <div class="wp-profile-skeleton__name">
32
+ <Skeleton width="30%" />
33
+ <Skeleton width="20%" />
34
+ </div>
35
+ </div>
36
+ </Card>
37
+ <div v-if="stats" class="wp-profile-skeleton__stats">
38
+ <Skeleton v-for="i in 3" :key="i" shape="block" height="72px" />
39
+ </div>
40
+ <PostGridSkeleton :count="3" label="" />
41
+ </div>
42
+ </template>
43
+
44
+ <style scoped>
45
+ .wp-profile-skeleton {
46
+ display: flex;
47
+ flex-direction: column;
48
+ gap: 24px;
49
+ }
50
+ .wp-profile-skeleton__head {
51
+ display: grid;
52
+ grid-template-columns: auto minmax(0, 1fr);
53
+ gap: 24px;
54
+ align-items: center;
55
+ padding: 32px;
56
+ }
57
+ .wp-profile-skeleton__name {
58
+ min-width: 0;
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: 12px;
62
+ }
63
+ .wp-profile-skeleton__stats {
64
+ display: grid;
65
+ grid-template-columns: repeat(3, minmax(0, 1fr));
66
+ gap: 18px;
67
+ }
68
+ @media (max-width: 900px) {
69
+ .wp-profile-skeleton__stats {
70
+ grid-template-columns: minmax(0, 1fr);
71
+ }
72
+ }
73
+ </style>
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Inline rich-text renderer for plain-text prose (post descriptions, notes).
4
+ *
5
+ * <p class="my-description"><RichText :text="post.description" /></p>
6
+ *
7
+ * Turns URLs into links (new tab, `noopener noreferrer`) and `$var` /
8
+ * `@{ref}` mentions into `<code>` chips, WITHOUT `v-html`: every segment from
9
+ * `parseRichText` (linkify.ts) is rendered as a Vue node, so the output is
10
+ * XSS-safe by construction.
11
+ *
12
+ * Prose, not a grammar value: `{a|b}` stays text and a ref's body may be any
13
+ * words. A wildcard value (picks, refs resolved to names) is TokenizedValue.
14
+ *
15
+ * Renders INLINE (no wrapper element), so it drops into the host's own
16
+ * paragraph and inherits its typography.
17
+ */
18
+ import { computed } from "vue";
19
+ import { parseRichText } from "./linkify";
20
+
21
+ const props = defineProps<{ text: string }>();
22
+
23
+ const segments = computed(() => parseRichText(props.text));
24
+ </script>
25
+
26
+ <template>
27
+ <template
28
+ v-for="(seg, idx) in segments"
29
+ :key="idx"
30
+ >
31
+ <a
32
+ v-if="seg.kind === 'link'"
33
+ class="wp-rich-text__link"
34
+ :href="seg.href"
35
+ target="_blank"
36
+ rel="noopener noreferrer"
37
+ >{{ seg.text }}</a>
38
+ <code
39
+ v-else-if="seg.kind === 'code'"
40
+ class="wp-rich-text__code"
41
+ >{{ seg.text }}</code>
42
+ <template v-else>{{ seg.text }}</template>
43
+ </template>
44
+ </template>
45
+
46
+ <style scoped>
47
+ .wp-rich-text__link {
48
+ color: var(--wp-accent-text);
49
+ text-decoration: underline;
50
+ text-underline-offset: 2px;
51
+ }
52
+ .wp-rich-text__code {
53
+ font-family: var(--wp-font-mono);
54
+ font-size: 0.9em;
55
+ background: var(--wp-bg-3);
56
+ color: var(--wp-accent-text);
57
+ padding: 1px 6px;
58
+ border-radius: min(5px, var(--wp-radius-cap, 999px));
59
+ border: 1px solid var(--wp-border);
60
+ }
61
+ </style>
@@ -0,0 +1,90 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * SelectionBar: the bar over a list while rows are ticked. "3 selected", the
4
+ * bulk actions (the default slot) and Clear. One bar for every list: the
5
+ * editors' bulk mode, the moderation queue, a backups list.
6
+ *
7
+ * <SelectionBar v-if="picked.size" :count="picked.size" @clear="picked.clear()">
8
+ * <Button variant="danger" size="sm" icon="trash" @click="del">Delete</Button>
9
+ * </SelectionBar>
10
+ *
11
+ * `#start` sits right after the count (a filter, an option), the default slot
12
+ * on the right before Clear. `sticky` keeps it at the top of its scroll area
13
+ * while the list scrolls under it (`--wp-selection-bar-top` moves it below a
14
+ * fixed header). `noun` says what is selected ("3 options selected"). Escape
15
+ * inside the bar clears, like Clear; the count is read out as it changes.
16
+ */
17
+ import { onBeforeUnmount, onMounted, ref } from "vue";
18
+ import Button from "./Button.vue";
19
+
20
+ const props = withDefaults(defineProps<{
21
+ count: number;
22
+ /** What a row is, plural ("options"); left out it says "3 selected". */
23
+ noun?: string | undefined;
24
+ /** Stick to the top of the scroll area. */
25
+ sticky?: boolean | undefined;
26
+ /** Hide the Clear button (the host clears another way). */
27
+ hideClear?: boolean | undefined;
28
+ clearLabel?: string | undefined;
29
+ /** The toolbar's accessible name. */
30
+ label?: string | undefined;
31
+ }>(), { noun: undefined, clearLabel: "Clear", label: "Bulk actions" });
32
+
33
+ const emit = defineEmits<{ clear: [] }>();
34
+
35
+ function onKeydown(ev: KeyboardEvent): void {
36
+ if (ev.key !== "Escape" || props.hideClear) return;
37
+ ev.stopPropagation();
38
+ emit("clear");
39
+ }
40
+
41
+ // Bound in code, not as @keydown: the bar is a toolbar of buttons, and the
42
+ // keys come from those buttons (a template handler on it reads to the a11y
43
+ // lint as a click target on a plain element).
44
+ const root = ref<HTMLElement | null>(null);
45
+ onMounted(() => root.value?.addEventListener("keydown", onKeydown));
46
+ onBeforeUnmount(() => root.value?.removeEventListener("keydown", onKeydown));
47
+ </script>
48
+
49
+ <template>
50
+ <div
51
+ ref="root"
52
+ class="wp-selbar"
53
+ role="toolbar"
54
+ :aria-label="label"
55
+ :data-sticky="sticky || undefined"
56
+ data-test="selection-bar"
57
+ >
58
+ <span class="wp-selbar__count" aria-live="polite" data-test="selection-count">{{ count }}{{ noun ? ` ${noun}` : "" }} selected</span>
59
+ <slot name="start" />
60
+ <span class="wp-selbar__spacer" />
61
+ <slot />
62
+ <Button v-if="!hideClear" variant="ghost" size="sm" data-test="selection-clear" @click="emit('clear')">{{ clearLabel }}</Button>
63
+ </div>
64
+ </template>
65
+
66
+ <style scoped>
67
+ .wp-selbar {
68
+ display: flex;
69
+ flex-wrap: wrap;
70
+ align-items: center;
71
+ gap: var(--wp-space-4);
72
+ padding: var(--wp-space-3) var(--wp-space-5);
73
+ border: 1px solid color-mix(in oklab, var(--wp-accent) 40%, transparent);
74
+ border-radius: min(8px, var(--wp-radius-cap, 999px));
75
+ background: color-mix(in oklab, var(--wp-accent) 12%, var(--wp-bg-2));
76
+ font-family: var(--wp-font-sans);
77
+ }
78
+ .wp-selbar[data-sticky] {
79
+ position: sticky;
80
+ top: var(--wp-selection-bar-top, 0px);
81
+ z-index: 20;
82
+ }
83
+ .wp-selbar__count {
84
+ font-size: var(--wp-text-base);
85
+ font-weight: 600;
86
+ color: var(--wp-text);
87
+ white-space: nowrap;
88
+ }
89
+ .wp-selbar__spacer { flex: 1; }
90
+ </style>
@@ -0,0 +1,57 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * SettingGroup: a titled block of SettingRows in one bordered panel, the
4
+ * rows split by hairlines, so a long settings page reads as a list rather
5
+ * than a stack of cards.
6
+ *
7
+ * <SettingGroup title="Content" note="Applies to this browser.">
8
+ * <SettingRow label="Show NSFW posts" hint="Off by default."><WpSwitch v-model="nsfw" size="md" /></SettingRow>
9
+ * </SettingGroup>
10
+ *
11
+ * `#title-extra` sits after the title (a Badge, a help link). The panel is a
12
+ * size container: its rows go to one column when it is narrower than 480px.
13
+ */
14
+ defineProps<{
15
+ title?: string | undefined;
16
+ /** A line under the title. */
17
+ note?: string | undefined;
18
+ }>();
19
+ </script>
20
+
21
+ <template>
22
+ <section class="wp-set-group">
23
+ <h3 v-if="title" class="wp-set-group__title">
24
+ {{ title }}<slot name="title-extra" />
25
+ </h3>
26
+ <p v-if="note" class="wp-set-group__note">{{ note }}</p>
27
+ <div class="wp-set-group__panel">
28
+ <slot />
29
+ </div>
30
+ </section>
31
+ </template>
32
+
33
+ <style scoped>
34
+ .wp-set-group { margin-bottom: var(--wp-space-7); }
35
+ .wp-set-group__title {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: var(--wp-space-3);
39
+ margin: 0 0 var(--wp-space-3);
40
+ font-size: var(--wp-text-xs);
41
+ font-weight: 600;
42
+ letter-spacing: 0.08em;
43
+ text-transform: uppercase;
44
+ color: var(--wp-text-dim);
45
+ }
46
+ .wp-set-group__note {
47
+ margin: 0 0 var(--wp-space-3);
48
+ font-size: var(--wp-text-sm);
49
+ color: var(--wp-text-dim);
50
+ }
51
+ .wp-set-group__panel {
52
+ container: wp-set-group / inline-size;
53
+ background: var(--wp-bg-2);
54
+ border: 1px solid var(--wp-border);
55
+ border-radius: var(--wp-radius-lg);
56
+ }
57
+ </style>